Added ability to read registers in a sperate collection class

- multiple bugfixes
This commit is contained in:
Felix Weiß
2022-06-17 17:46:47 +02:00
parent e114c4b8c2
commit a43756f00c
22 changed files with 1313 additions and 639 deletions

View File

@@ -1,44 +1,101 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Linq;
using System.Text.Json; using System.Text.Json;
using MewtocolNet; using MewtocolNet;
using MewtocolNet.Responses; using MewtocolNet.RegisterAttributes;
using System.Collections;
using MewtocolNet.Logging;
namespace Examples { namespace Examples {
class Program {
static void Main(string[] args) {
Console.WriteLine("Starting test");
public class TestRegisters : RegisterCollectionBase {
//corresponds to a R100 boolean register in the PLC
[Register(100, RegisterType.R)]
public bool TestBool1 { get; private set; }
//corresponds to a R100 boolean register in the PLC
[Register(RegisterType.X, SpecialAddress.D)]
public bool TestBoolInputXD { get; private set; }
//corresponds to a DT1101 - DT1104 string register in the PLC with (STRING[4])
[Register(1101, 4)]
public string TestString1 { get; private set; }
//corresponds to a DT7000 16 bit int register in the PLC
[Register(7000)]
public short TestInt16 { get; private set; }
//corresponds to a DTD7001 - DTD7002 32 bit int register in the PLC
[Register(7001)]
public int TestInt32 { get; private set; }
//corresponds to a DTD7001 - DTD7002 32 bit float register in the PLC (REAL)
[Register(7003)]
public float TestFloat32 { get; private set; }
//corresponds to a DT7005 - DT7009 string register in the PLC with (STRING[5])
[Register(7005, 5)]
public string TestString2 { get; private set; }
//corresponds to a DT7010 as a 16bit word/int and parses the word as single bits
[Register(7010)]
public BitArray TestBitRegister { get; private set; }
//corresponds to a DT1204 as a 16bit word/int takes the bit at index 9 and writes it back as a boolean
[Register(1204, 9, BitCount.B16)]
public bool BitValue { get; private set; }
}
class Program {
static void Main(string[] args) {
Task.Factory.StartNew(async () => { Task.Factory.StartNew(async () => {
//attaching the logger
Logger.LogLevel = LogLevel.Critical;
Logger.OnNewLogMessage((date, msg) => {
Console.WriteLine($"{date.ToString("HH:mm:ss")} {msg}");
});
//setting up a new PLC interface and register collection
MewtocolInterface interf = new MewtocolInterface("10.237.191.3"); MewtocolInterface interf = new MewtocolInterface("10.237.191.3");
TestRegisters registers = new TestRegisters();
interf.AddRegister<short>("Cooler Status",1204); //attaching the register collection and an automatic poller
interf.AddRegister<string>(1101, 4); interf.WithRegisterCollection(registers).WithPoller();
interf.WithPoller();
interf.RegisterChanged += (o) => {
Console.WriteLine($"DT{o.MemoryAdress} {(o.Name != null ? $"({o.Name}) " : "")}changed to {o.GetValueString()}");
};
await interf.ConnectAsync( await interf.ConnectAsync(
(plcinf) => { (plcinf) => {
Console.WriteLine("Connected to PLC:\n" + plcinf.ToString()); //reading a value from the register collection
Console.WriteLine($"BitValue is: {registers.BitValue}");
//read back a register value //writing a value to the registers
var statusNum = (NRegister<short>)interf.Registers[1204]; Task.Factory.StartNew(async () => {
Console.WriteLine($"Status num is: {statusNum.Value}");
await Task.Delay(2000);
//inverts the boolean register
interf.SetRegister(nameof(registers.TestBool1), !registers.TestBool1);
//adds 10 each time the plc connects to the PLCs INT regíster
interf.SetRegister(nameof(registers.TestInt16), (short)(registers.TestInt16 + 10));
//adds 1 each time the plc connects to the PLCs DINT regíster
interf.SetRegister(nameof(registers.TestInt32), (registers.TestInt32 + 1));
//adds 11.11 each time the plc connects to the PLCs REAL regíster
interf.SetRegister(nameof(registers.TestFloat32), (float)(registers.TestFloat32 + 11.11));
interf.SetRegister(nameof(registers.TestString2), new Random().Next(0, 99999).ToString());
});
},
() => {
Console.WriteLine("Failed connection");
} }
); );
}); });
Console.ReadLine(); Console.ReadLine();

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MewtocolNet.Logging;
using MewtocolNet.Responses; using MewtocolNet.Responses;
namespace MewtocolNet { namespace MewtocolNet {
@@ -15,7 +16,6 @@ namespace MewtocolNet {
internal event Action PolledCycle; internal event Action PolledCycle;
internal CancellationTokenSource cTokenAutoUpdater; internal CancellationTokenSource cTokenAutoUpdater;
internal bool isWriting;
internal bool ContinousReaderRunning; internal bool ContinousReaderRunning;
internal bool usePoller = false; internal bool usePoller = false;
@@ -30,30 +30,49 @@ namespace MewtocolNet {
cTokenAutoUpdater = new CancellationTokenSource(); cTokenAutoUpdater = new CancellationTokenSource();
Console.WriteLine("Attaching cont reader"); Logger.Log("Poller is attaching", LogLevel.Info, this);
Task.Factory.StartNew(async () => { Task.Factory.StartNew(async () => {
var plcinf = await GetPLCInfoAsync(); var plcinf = await GetPLCInfoAsync();
if (plcinf == null) { if (plcinf == null) {
Console.WriteLine("PLC is not reachable"); Logger.Log("PLC not reachable, stopping logger", LogLevel.Info, this);
throw new Exception("PLC is not reachable"); return;
} }
if (!plcinf.OperationMode.RunMode) {
Console.WriteLine("PLC is not running"); PolledCycle += MewtocolInterface_PolledCycle;
throw new Exception("PLC is not running"); void MewtocolInterface_PolledCycle () {
StringBuilder stringBuilder = new StringBuilder();
foreach (var reg in GetAllRegisters()) {
string address = $"{reg.GetRegisterString()}{reg.GetStartingMemoryArea()}".PadRight(8, (char)32);
stringBuilder.AppendLine($"{address}{(reg.Name != null ? $" ({reg.Name})" : "")}: {reg.GetValueString()}");
}
Logger.Log($"Registers loaded are: \n" +
$"--------------------\n" +
$"{stringBuilder.ToString()}" +
$"--------------------",
LogLevel.Verbose, this);
Logger.Log("Logger did its first cycle successfully", LogLevel.Info, this);
PolledCycle -= MewtocolInterface_PolledCycle;
} }
ContinousReaderRunning = true; ContinousReaderRunning = true;
while (true) { while (true) {
//dont update when currently writing a var //do priority tasks first
if (isWriting) { if(PriorityTasks.Count > 0) {
continue;
await PriorityTasks.FirstOrDefault(x => !x.IsCompleted);
} }
await Task.Delay(pollingDelayMs); //await Task.Delay(pollingDelayMs);
foreach (var registerPair in Registers) { foreach (var registerPair in Registers) {
var reg = registerPair.Value; var reg = registerPair.Value;
@@ -102,7 +121,17 @@ namespace MewtocolNet {
InvokeRegisterChanged(floatReg); InvokeRegisterChanged(floatReg);
floatReg.TriggerNotifyChange(); floatReg.TriggerNotifyChange();
} }
} else if (reg is SRegister stringReg) { }
if (reg is BRegister boolReg) {
var lastVal = boolReg.Value;
var readout = (await ReadBoolRegister(boolReg, stationNumber)).Register.Value;
if (lastVal != readout) {
boolReg.LastValue = readout;
InvokeRegisterChanged(boolReg);
boolReg.TriggerNotifyChange();
}
}
if (reg is SRegister stringReg) {
var lastVal = stringReg.Value; var lastVal = stringReg.Value;
var readout = (await ReadStringRegister(stringReg, stationNumber)).Register.Value; var readout = (await ReadStringRegister(stringReg, stationNumber)).Register.Value;
if (lastVal != readout) { if (lastVal != readout) {
@@ -115,7 +144,7 @@ namespace MewtocolNet {
} }
//invoke cycle polled event //invoke cycle polled event
InvokePolledCycleDone(); InvokePolledCycleDone();
} }
@@ -127,42 +156,68 @@ namespace MewtocolNet {
#region Register Adding #region Register Adding
/// <summary> /// <summary>
/// Adds a PLC memory register to the watchlist <para/> /// Adds a PLC memory register to the watchlist <para/>
/// The registers can be read back by attaching <see cref="WithPoller"/> /// The registers can be read back by attaching <see cref="WithPoller"/>
/// </summary> /// </summary>
/// <typeparam name="T">
/// The type of the register translated from C# to IEC 61131-3 types
/// <para>C# ------ IEC</para>
/// <para>short => INT/WORD</para>
/// <para>ushort => UINT</para>
/// <para>int => DOUBLE</para>
/// <para>uint => UDOUBLE</para>
/// <para>float => REAL</para>
/// <para>string => STRING</para>
/// </typeparam>
/// <param name="_address">The address of the register in the PLCs memory</param> /// <param name="_address">The address of the register in the PLCs memory</param>
/// <param name="_length">The length of the string (Can be ignored for other types)</param> /// <param name="_type">
public void AddRegister<T> (int _address, int _length = 1) { /// The memory area type
/// <para>X = Physical input area (bool)</para>
/// <para>Y = Physical input area (bool)</para>
/// <para>R = Internal relay area (bool)</para>
/// <para>DT = Internal data area (short/ushort)</para>
/// <para>DDT = Internal relay area (int/uint)</para>
/// </param>
/// <param name="_name">A naming definition for QOL, doesn't effect PLC and is optional</param>
public void AddRegister (int _address, RegisterType _type, string _name = null) {
Type regType = typeof(T); //as number registers
if (_type == RegisterType.DT_short) {
if (regType == typeof(short)) { Registers.Add(_address, new NRegister<short>(_address, _name));
Registers.Add(_address, new NRegister<short>(_address)); return;
} else if (regType == typeof(ushort)) {
Registers.Add(_address, new NRegister<ushort>(_address));
} else if (regType == typeof(int)) {
Registers.Add(_address, new NRegister<int>(_address));
} else if (regType == typeof(uint)) {
Registers.Add(_address, new NRegister<uint>(_address));
} else if (regType == typeof(float)) {
Registers.Add(_address, new NRegister<float>(_address));
} else if (regType == typeof(string)) {
Registers.Add(_address, new SRegister(_address, _length));
} else {
throw new NotSupportedException($"The type {regType} is not allowed for Registers \n" +
$"Allowed are: short, ushort, int, uint, float and string");
} }
if (_type == RegisterType.DT_ushort) {
Registers.Add(_address, new NRegister<ushort>(_address, _name));
return;
}
if (_type == RegisterType.DDT_int) {
Registers.Add(_address, new NRegister<int>(_address, _name));
return;
}
if (_type == RegisterType.DDT_uint) {
Registers.Add(_address, new NRegister<uint>(_address, _name));
return;
}
if (_type == RegisterType.DDT_float) {
Registers.Add(_address, new NRegister<float>(_address, _name));
return;
}
//as bool registers
Registers.Add(_address, new BRegister(_address, _type, _name));
}
/// <summary>
/// Adds a PLC memory register to the watchlist <para/>
/// The registers can be read back by attaching <see cref="WithPoller"/>
/// </summary>
/// <param name="_spAddress">The special address of the register in the PLCs memory</param>
/// <param name="_type">
/// The memory area type
/// <para>X = Physical input area (bool)</para>
/// <para>Y = Physical input area (bool)</para>
/// <para>R = Internal relay area (bool)</para>
/// <para>DT = Internal data area (short/ushort)</para>
/// <para>DDT = Internal relay area (int/uint)</para>
/// </param>
/// <param name="_name">A naming definition for QOL, doesn't effect PLC and is optional</param>
public void AddRegister (SpecialAddress _spAddress, RegisterType _type, string _name = null) {
//as bool registers
Registers.Add((int)_spAddress, new BRegister(_spAddress, _type, _name));
} }
@@ -183,22 +238,35 @@ namespace MewtocolNet {
/// <param name="_name">A naming definition for QOL, doesn't effect PLC and is optional</param> /// <param name="_name">A naming definition for QOL, doesn't effect PLC and is optional</param>
/// <param name="_address">The address of the register in the PLCs memory</param> /// <param name="_address">The address of the register in the PLCs memory</param>
/// <param name="_length">The length of the string (Can be ignored for other types)</param> /// <param name="_length">The length of the string (Can be ignored for other types)</param>
public void AddRegister<T>(string _name, int _address, int _length = 1) { public void AddRegister<T>(int _address, int _length = 1, string _name = null, bool _isBitwise = false) {
Type regType = typeof(T); Type regType = typeof(T);
if (regType != typeof(string) && _length != 1) {
throw new NotSupportedException($"_lenght parameter only allowed for register of type string");
}
if (Registers.Any(x => x.Key == _address)) {
throw new NotSupportedException($"Cannot add a register multiple times, " +
$"make sure that all register attributes or AddRegister assignments have different adresses.");
}
if (regType == typeof(short)) { if (regType == typeof(short)) {
Registers.Add(_address, new NRegister<short>(_address, _name)); Registers.Add(_address, new NRegister<short>(_address, _name, _isBitwise));
} else if (regType == typeof(ushort)) { } else if (regType == typeof(ushort)) {
Registers.Add(_address, new NRegister<ushort>(_address, _name)); Registers.Add(_address, new NRegister<ushort>(_address, _name));
} else if (regType == typeof(int)) { } else if (regType == typeof(int)) {
Registers.Add(_address, new NRegister<int>(_address, _name)); Registers.Add(_address, new NRegister<int>(_address, _name, _isBitwise));
} else if (regType == typeof(uint)) { } else if (regType == typeof(uint)) {
Registers.Add(_address, new NRegister<uint>(_address, _name)); Registers.Add(_address, new NRegister<uint>(_address, _name));
} else if (regType == typeof(float)) { } else if (regType == typeof(float)) {
Registers.Add(_address, new NRegister<float>(_address, _name)); Registers.Add(_address, new NRegister<float>(_address, _name));
} else if (regType == typeof(string)) { } else if (regType == typeof(string)) {
Registers.Add(_address, new SRegister(_address, _length, _name)); Registers.Add(_address, new SRegister(_address, _length, _name));
} else if (regType == typeof(bool)) {
Registers.Add(_address, new BRegister(_address, RegisterType.R, _name));
} else { } else {
throw new NotSupportedException($"The type {regType} is not allowed for Registers \n" + throw new NotSupportedException($"The type {regType} is not allowed for Registers \n" +
$"Allowed are: short, ushort, int, uint, float and string"); $"Allowed are: short, ushort, int, uint, float and string");
@@ -208,6 +276,21 @@ namespace MewtocolNet {
#endregion #endregion
#region Register Reading
/// <summary>
/// Gets a list of all added registers
/// </summary>
public List<Register> GetAllRegisters () {
return Registers.Values.ToList();
}
#endregion
#region Event Invoking
internal void InvokeRegisterChanged (Register reg) { internal void InvokeRegisterChanged (Register reg) {
RegisterChanged?.Invoke(reg); RegisterChanged?.Invoke(reg);
@@ -220,5 +303,7 @@ namespace MewtocolNet {
} }
#endregion
} }
} }

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MewtocolNet.Logging {
/// <summary>
/// Logging module for all PLCs
/// </summary>
public static class Logger {
/// <summary>
/// Sets the loglevel for the logger module
/// </summary>
public static LogLevel LogLevel { get; set; }
internal static Action<DateTime, string> LogInvoked;
/// <summary>
/// Gets invoked whenever a new log message is ready
/// </summary>
public static void OnNewLogMessage (Action<DateTime, string> onMsg) {
LogInvoked += (t, m) => {
onMsg(t, m);
};
}
internal static void Log (string message, LogLevel loglevel, MewtocolInterface sender = null) {
if ((int)loglevel <= (int)LogLevel) {
if (sender == null) {
LogInvoked?.Invoke(DateTime.Now, message);
} else {
LogInvoked?.Invoke(DateTime.Now, $"[{sender.GetConnectionPortInfo()}] {message}");
}
}
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MewtocolNet.Logging {
/// <summary>
/// The loglevel of the logging module
/// </summary>
public enum LogLevel {
/// <summary>
/// Logs only errors
/// </summary>
Error = 0,
/// <summary>
/// Logs info like connection establish and loss
/// </summary>
Info = 1,
/// <summary>
/// Logs only state changes
/// </summary>
Change = 2,
/// <summary>
/// Logs all errors, state changes, and messages
/// </summary>
Verbose = 3,
/// <summary>
/// Logs all types including network traffic
/// </summary>
Critical = 4,
}
}

View File

@@ -1,48 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using MewtocolNet.Responses;
namespace MewtocolNet.Events {
public class MewtocolContactListener : IDisposable {
/// <summary>
/// Gets fired whenever a contact of the observed list changes its value
/// </summary>
public event Action<List<IBoolContact>> ContactsChangedValue;
//privates
private List<IBoolContact> lastContacts = new List<IBoolContact>();
private CancellationTokenSource cToken = new CancellationTokenSource();
public static MewtocolContactListener ListenContactChanges (MewtocolInterface _interFace, List<Contact> _observeContacts, int _refreshMS = 100, int _stationNumber = 1) {
MewtocolContactListener listener = new MewtocolContactListener();
_ = Task.Factory.StartNew( async () => {
//get contacts first time
listener.lastContacts = (List<IBoolContact>) await _interFace.ReadBoolContacts(_observeContacts, _stationNumber);
while(!listener.cToken.Token.IsCancellationRequested) {
//compare and update
var newContactData = (List<IBoolContact>) await _interFace.ReadBoolContacts(_observeContacts, _stationNumber);
var difference = newContactData.Where(p => listener.lastContacts.Any(l => p.Value != l.Value && p.Identifier == l.Identifier));
if(difference.Count() > 0) {
listener.ContactsChangedValue?.Invoke(difference.ToList());
listener.lastContacts = newContactData;
} else {
}
await Task.Delay(_refreshMS);
}
});
return listener;
}
public void Dispose() {
cToken.Cancel();
}
}
}

View File

@@ -3,19 +3,30 @@ using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using MewtocolNet.Responses; using MewtocolNet.Responses;
using System.Collections;
namespace MewtocolNet { namespace MewtocolNet {
public static class MewtocolHelpers { public static class MewtocolHelpers {
public static Byte[] ToHexASCIIBytes (this string _str) {
/// <summary>
/// Turns a bit array into a 0 and 1 string
/// </summary>
public static string ToBitString (this BitArray arr) {
var bits = new bool[arr.Length];
arr.CopyTo(bits, 0);
return string.Join("", bits.Select(x => x ? "1" : "0"));
}
public static byte[] ToHexASCIIBytes (this string _str) {
ASCIIEncoding ascii = new ASCIIEncoding(); ASCIIEncoding ascii = new ASCIIEncoding();
Byte[] bytes = ascii.GetBytes(_str.ToUpper()); byte[] bytes = ascii.GetBytes(_str.ToUpper());
return bytes; return bytes;
} }
public static string BuildBCCFrame (this string asciiArr) { internal static string BuildBCCFrame (this string asciiArr) {
Encoding ae = Encoding.ASCII; Encoding ae = Encoding.ASCII;
byte[] b = ae.GetBytes(asciiArr); byte[] b = ae.GetBytes(asciiArr);
byte xorTotalByte = 0; byte xorTotalByte = 0;
@@ -24,7 +35,7 @@ namespace MewtocolNet {
return asciiArr.Insert(asciiArr.Length, xorTotalByte.ToString("X2")); return asciiArr.Insert(asciiArr.Length, xorTotalByte.ToString("X2"));
} }
public static byte[] ParseDTBytes (this string _onString ,int _blockSize = 4) { internal static byte[] ParseDTBytes (this string _onString ,int _blockSize = 4) {
var res = new Regex(@"\%([0-9]{2})\$RD(.{"+_blockSize+"})").Match(_onString); var res = new Regex(@"\%([0-9]{2})\$RD(.{"+_blockSize+"})").Match(_onString);
if(res.Success) { if(res.Success) {
string val = res.Groups[2].Value; string val = res.Groups[2].Value;
@@ -33,7 +44,7 @@ namespace MewtocolNet {
return null; return null;
} }
public static string ParseDTByteString (this string _onString, int _blockSize = 4) { internal static string ParseDTByteString (this string _onString, int _blockSize = 4) {
var res = new Regex(@"\%([0-9]{2})\$RD(.{" + _blockSize + "})").Match(_onString); var res = new Regex(@"\%([0-9]{2})\$RD(.{" + _blockSize + "})").Match(_onString);
if (res.Success) { if (res.Success) {
string val = res.Groups[2].Value; string val = res.Groups[2].Value;
@@ -42,16 +53,46 @@ namespace MewtocolNet {
return null; return null;
} }
public static string ParseDTString (this string _onString) { internal static bool? ParseRCSingleBit (this string _onString, int _blockSize = 4) {
var res = new Regex(@"\%([0-9]{2})\$RD.{8}(.*)...").Match(_onString); var res = new Regex(@"\%([0-9]{2})\$RC(.)").Match(_onString);
if(res.Success) { if (res.Success) {
string val = res.Groups[2].Value; string val = res.Groups[2].Value;
return val.GetStringFromAsciiHex(); return val == "1";
} }
return null; return null;
} }
public static string BuildDTString (this string _inString, short _stringReservedSize) { internal static string ParseDTString (this string _onString) {
var res = new Regex(@"\%([0-9]{2})\$RD.{8}(.*)...").Match(_onString);
if(res.Success) {
string val = res.Groups[2].Value;
return val.GetStringFromAsciiHex().Trim();
}
return null;
}
internal static string ReverseByteOrder (this string _onString) {
//split into 2 chars
var stringBytes = _onString.SplitInParts(2).ToList();
stringBytes.Reverse();
return string.Join("", stringBytes);
}
internal static IEnumerable<String> SplitInParts (this string s, int partLength) {
if (s == null)
throw new ArgumentNullException(nameof(s));
if (partLength <= 0)
throw new ArgumentException("Part length has to be positive.", nameof(partLength));
for (var i = 0; i < s.Length; i += partLength)
yield return s.Substring(i, Math.Min(partLength, s.Length - i));
}
internal static string BuildDTString (this string _inString, short _stringReservedSize) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
//06000600 //06000600
short stringSize = (short)_inString.Length; short stringSize = (short)_inString.Length;
@@ -62,13 +103,13 @@ namespace MewtocolNet {
//string count actual bytes //string count actual bytes
sb.Append(sizeBytes); sb.Append(sizeBytes);
//actual string content //actual string content
sb.Append(_inString.GetAsciiHexFromString().PadRight(_stringReservedSize * 2, '0')); sb.Append(_inString.GetAsciiHexFromString());
return sb.ToString(); return sb.ToString();
} }
public static string GetStringFromAsciiHex (this string input) { internal static string GetStringFromAsciiHex (this string input) {
if (input.Length % 2 != 0) if (input.Length % 2 != 0)
throw new ArgumentException("input not a hex string"); throw new ArgumentException("input not a hex string");
byte[] bytes = new byte[input.Length / 2]; byte[] bytes = new byte[input.Length / 2];
@@ -79,19 +120,19 @@ namespace MewtocolNet {
return Encoding.ASCII.GetString(bytes); return Encoding.ASCII.GetString(bytes);
} }
public static string GetAsciiHexFromString (this string input) { internal static string GetAsciiHexFromString (this string input) {
var bytes = new ASCIIEncoding().GetBytes(input); var bytes = new ASCIIEncoding().GetBytes(input);
return bytes.ToHexString(); return bytes.ToHexString();
} }
public static byte[] HexStringToByteArray(this string hex) { internal static byte[] HexStringToByteArray(this string hex) {
return Enumerable.Range(0, hex.Length) return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0) .Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) .Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray(); .ToArray();
} }
public static string ToHexString (this byte[] arr) { internal static string ToHexString (this byte[] arr) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
foreach (var b in arr) { foreach (var b in arr) {
sb.Append(b.ToString("X2")); sb.Append(b.ToString("X2"));
@@ -99,23 +140,6 @@ namespace MewtocolNet {
return sb.ToString(); return sb.ToString();
} }
public static string ToJsonString (this IEnumerable<IBoolContact> _contacts, bool formatPretty = false) {
return JsonSerializer.Serialize(_contacts, new JsonSerializerOptions {
WriteIndented = formatPretty,
});
}
public static bool IsSubclassOfRawGeneric(Type generic, Type toCheck) {
while (toCheck != null && toCheck != typeof(object)) {
var cur = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck;
if (generic == cur) {
return true;
}
toCheck = toCheck.BaseType;
}
return false;
}
} }
} }

View File

@@ -6,7 +6,14 @@ using System.Text.RegularExpressions;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Linq;
using MewtocolNet.Responses; using MewtocolNet.Responses;
using MewtocolNet.RegisterAttributes;
using MewtocolNet.Logging;
using System.Collections;
using System.Reflection;
using MewtocolNet.Logging;
using System.Diagnostics;
namespace MewtocolNet { namespace MewtocolNet {
@@ -33,14 +40,11 @@ namespace MewtocolNet {
/// <summary> /// <summary>
/// The registered data registers of the PLC /// The registered data registers of the PLC
/// </summary> /// </summary>
public Dictionary<int, Register> Registers { get; set; } = new(); public Dictionary<int, Register> Registers { get; set; } = new Dictionary<int, Register>();
private CancellationTokenSource tokenSource;
private string ip {get;set;} private string ip {get;set;}
private int port {get;set;} private int port {get;set;}
private int stationNumber {get;set;} private int stationNumber {get;set;}
private int pollingDelayMs {get;set;}
/// <summary> /// <summary>
/// The current IP of the PLC connection /// The current IP of the PLC connection
@@ -55,6 +59,7 @@ namespace MewtocolNet {
/// </summary> /// </summary>
public int StationNumber => stationNumber; public int StationNumber => stationNumber;
internal List<Task> PriorityTasks { get; set; } = new List<Task>();
#region Initialization #region Initialization
@@ -79,6 +84,15 @@ namespace MewtocolNet {
} }
RegisterChanged += (o) => {
string address = $"{o.GetRegisterString()}{o.MemoryAdress}".PadRight(5, (char)32); ;
Logger.Log($"{address} " +
$"{(o.Name != null ? $"({o.Name}) " : "")}" +
$"changed to \"{o.GetValueString()}\"", LogLevel.Change, this);
};
} }
#endregion #endregion
@@ -99,7 +113,10 @@ namespace MewtocolNet {
var plcinf = await GetPLCInfoAsync(); var plcinf = await GetPLCInfoAsync();
if (plcinf is not null) { if (plcinf != null) {
Logger.Log("Connected", LogLevel.Info, this);
Logger.Log($"\n\n{plcinf.ToString()}\n\n", LogLevel.Verbose, this);
Connected?.Invoke(plcinf); Connected?.Invoke(plcinf);
@@ -119,8 +136,10 @@ namespace MewtocolNet {
} else { } else {
if (OnFailed != null) if (OnFailed != null) {
OnFailed(); OnFailed();
Logger.Log("Initial connection failed", LogLevel.Info, this);
}
} }
@@ -132,9 +151,8 @@ namespace MewtocolNet {
/// Attaches a poller to the interface that continously /// Attaches a poller to the interface that continously
/// polls the registered data registers and writes the values to them /// polls the registered data registers and writes the values to them
/// </summary> /// </summary>
public MewtocolInterface WithPoller (int pollerDelayMs = 50) { public MewtocolInterface WithPoller () {
pollingDelayMs = pollerDelayMs;
usePoller = true; usePoller = true;
return this; return this;
@@ -143,6 +161,258 @@ namespace MewtocolNet {
#endregion #endregion
#region Register Collection
/// <summary>
/// Attaches a register collection object to
/// the interface that can be updated automatically.
/// <para/>
/// Just create a class inheriting from <see cref="RegisterCollectionBase"/>
/// and assert some propertys with the custom <see cref="RegisterAttribute"/>.
/// </summary>
/// <param name="collection">A collection inherting the <see cref="RegisterCollectionBase"/> class</param>
public MewtocolInterface WithRegisterCollection (RegisterCollectionBase collection) {
collection.PLCInterface = this;
var props = collection.GetType().GetProperties();
foreach (var prop in props) {
var attributes = prop.GetCustomAttributes(true);
string propName = prop.Name;
foreach (var attr in attributes) {
if(attr is RegisterAttribute cAttribute) {
if (prop.PropertyType == typeof(bool) && cAttribute.AssignedBitIndex == -1) {
if (cAttribute.SpecialAddress == SpecialAddress.None) {
AddRegister(cAttribute.MemoryArea, cAttribute.RegisterType, _name: propName);
} else {
AddRegister(cAttribute.SpecialAddress, cAttribute.RegisterType, _name: propName);
}
}
if (prop.PropertyType == typeof(short)) {
AddRegister<short>(cAttribute.MemoryArea, _name: propName);
}
if (prop.PropertyType == typeof(ushort)) {
AddRegister<ushort>(cAttribute.MemoryArea, _name: propName);
}
if (prop.PropertyType == typeof(int)) {
AddRegister<int>(cAttribute.MemoryArea, _name: propName);
}
if (prop.PropertyType == typeof(uint)) {
AddRegister<uint>(cAttribute.MemoryArea, _name: propName);
}
if (prop.PropertyType == typeof(float)) {
AddRegister<float>(cAttribute.MemoryArea, _name: propName);
}
if (prop.PropertyType == typeof(string)) {
AddRegister<string>(cAttribute.MemoryArea, cAttribute.StringLength, _name: propName);
}
//read number as bit array
if (prop.PropertyType == typeof(BitArray)) {
if(cAttribute.BitCount == BitCount.B16) {
AddRegister<short>(cAttribute.MemoryArea, _name: propName, _isBitwise: true);
} else {
AddRegister<int>(cAttribute.MemoryArea, _name: propName, _isBitwise: true);
}
}
//read number as bit array by invdividual properties
if (prop.PropertyType == typeof(bool) && cAttribute.AssignedBitIndex != -1) {
if (cAttribute.BitCount == BitCount.B16) {
AddRegister<short>(cAttribute.MemoryArea, _name: propName, _isBitwise: true);
} else {
AddRegister<int>(cAttribute.MemoryArea, _name: propName, _isBitwise: true);
}
}
}
}
}
RegisterChanged += (reg) => {
var foundToUpdate = props.FirstOrDefault(x => x.Name == reg.Name);
if (foundToUpdate != null) {
var foundAttributes = foundToUpdate.GetCustomAttributes(true);
var foundAttr = foundAttributes.FirstOrDefault(x => x.GetType() == typeof(RegisterAttribute));
if (foundAttr == null)
return;
var registerAttr = (RegisterAttribute)foundAttr;
//check if bit parse mode
if (registerAttr.AssignedBitIndex == -1) {
//setting back booleans
if (foundToUpdate.PropertyType == typeof(bool)) {
foundToUpdate.SetValue(collection, ((BRegister)reg).Value);
}
//setting back numbers
if (foundToUpdate.PropertyType == typeof(short)) {
foundToUpdate.SetValue(collection, ((NRegister<short>)reg).Value);
}
if (foundToUpdate.PropertyType == typeof(ushort)) {
foundToUpdate.SetValue(collection, ((NRegister<ushort>)reg).Value);
}
if (foundToUpdate.PropertyType == typeof(int)) {
foundToUpdate.SetValue(collection, ((NRegister<int>)reg).Value);
}
if (foundToUpdate.PropertyType == typeof(uint)) {
foundToUpdate.SetValue(collection, ((NRegister<uint>)reg).Value);
}
if (foundToUpdate.PropertyType == typeof(float)) {
foundToUpdate.SetValue(collection, ((NRegister<float>)reg).Value);
}
//setting back strings
if (foundToUpdate.PropertyType == typeof(string)) {
foundToUpdate.SetValue(collection, ((SRegister)reg).Value);
}
}
if (foundToUpdate.PropertyType == typeof(bool) && registerAttr.AssignedBitIndex >= 0) {
//setting back bit registers to individual properties
if (reg is NRegister<short> shortReg) {
var bytes = BitConverter.GetBytes(shortReg.Value);
BitArray bitAr = new BitArray(bytes);
foundToUpdate.SetValue(collection, bitAr[registerAttr.AssignedBitIndex]);
}
if (reg is NRegister<int> intReg) {
var bytes = BitConverter.GetBytes(intReg.Value);
BitArray bitAr = new BitArray(bytes);
foundToUpdate.SetValue(collection, bitAr[registerAttr.AssignedBitIndex]);
}
} else if(foundToUpdate.PropertyType == typeof(BitArray)) {
//setting back bit registers
if (reg is NRegister<short> shortReg) {
var bytes = BitConverter.GetBytes(shortReg.Value);
BitArray bitAr = new BitArray(bytes);
foundToUpdate.SetValue(collection, bitAr);
}
if (reg is NRegister<int> intReg) {
var bytes = BitConverter.GetBytes(intReg.Value);
BitArray bitAr = new BitArray(bytes);
foundToUpdate.SetValue(collection, bitAr);
}
}
collection.TriggerPropertyChanged(foundToUpdate.Name);
}
};
return this;
}
#endregion
#region Register Writing
/// <summary>
/// Sets a register in the PLCs memory
/// </summary>
/// <param name="registerName">The name the register was given to or a property name from the RegisterCollection class</param>
/// <param name="value">The value to write to the register</param>
public void SetRegister (string registerName, object value) {
var foundRegister = GetAllRegisters().FirstOrDefault(x => x.Name == registerName);
if (foundRegister == null) {
throw new Exception($"Register with the name {registerName} was not found");
}
if (foundRegister.GetType() == typeof(BRegister)) {
_ = WriteBoolRegister((BRegister)foundRegister, (bool)value, StationNumber);
}
if (foundRegister.GetType() == typeof(NRegister<short>)) {
_ = WriteNumRegister((NRegister<short>)foundRegister, (short)value, StationNumber);
}
if (foundRegister.GetType() == typeof(NRegister<ushort>)) {
_ = WriteNumRegister((NRegister<ushort>)foundRegister, (ushort)value, StationNumber);
}
if (foundRegister.GetType() == typeof(NRegister<int>)) {
_ = WriteNumRegister((NRegister<int>)foundRegister, (int)value, StationNumber);
}
if (foundRegister.GetType() == typeof(NRegister<uint>)) {
_ = WriteNumRegister((NRegister<uint>)foundRegister, (uint)value, StationNumber);
}
if (foundRegister.GetType() == typeof(NRegister<float>)) {
_ = WriteNumRegister((NRegister<float>)foundRegister, (float)value, StationNumber);
}
if (foundRegister.GetType() == typeof(SRegister)) {
_ = WriteStringRegister((SRegister)foundRegister, (string)value, StationNumber);
}
}
#endregion
#region Low level command handling #region Low level command handling
@@ -158,7 +428,27 @@ namespace MewtocolNet {
_msg += "\r"; _msg += "\r";
//send request //send request
var response = await SendSingleBlock(_msg);
string response = null;
if(ContinousReaderRunning) {
//if the poller is active then add all messages to a qeueue
var awaittask = SendSingleBlock(_msg);
PriorityTasks.Add(awaittask);
awaittask.Wait();
PriorityTasks.Remove(awaittask);
response = awaittask.Result;
} else {
//poller not active let the user manage message timing
response = await SendSingleBlock(_msg);
}
if(response == null) { if(response == null) {
return new CommandResult { return new CommandResult {
@@ -195,16 +485,12 @@ namespace MewtocolNet {
private async Task<string> SendSingleBlock (string _blockString) { private async Task<string> SendSingleBlock (string _blockString) {
if(isWriting) { Stopwatch sw = Stopwatch.StartNew();
return null;
}
tokenSource = new CancellationTokenSource();
using (TcpClient client = new TcpClient() { ReceiveBufferSize = 64, NoDelay = true, ExclusiveAddressUse = true }) { using (TcpClient client = new TcpClient() { ReceiveBufferSize = 64, NoDelay = true, ExclusiveAddressUse = true }) {
try { try {
await client.ConnectAsync(ip, port, tokenSource.Token); await client.ConnectAsync(ip, port);
} catch(SocketException) { } catch(SocketException) {
return null; return null;
} }
@@ -213,9 +499,9 @@ namespace MewtocolNet {
var message = _blockString.ToHexASCIIBytes(); var message = _blockString.ToHexASCIIBytes();
var messageAscii = BitConverter.ToString(message).Replace("-", " "); var messageAscii = BitConverter.ToString(message).Replace("-", " ");
//send request //send request
isWriting = true;
using (var sendStream = new MemoryStream(message)) { using (var sendStream = new MemoryStream(message)) {
await sendStream.CopyToAsync(stream); await sendStream.CopyToAsync(stream);
Logger.Log($"OUT MSG: {_blockString}", LogLevel.Critical, this);
//log message sent //log message sent
ASCIIEncoding enc = new ASCIIEncoding(); ASCIIEncoding enc = new ASCIIEncoding();
string characters = enc.GetString(message); string characters = enc.GetString(message);
@@ -228,7 +514,8 @@ namespace MewtocolNet {
response.Append(Encoding.UTF8.GetString(responseBuffer, 0, bytes)); response.Append(Encoding.UTF8.GetString(responseBuffer, 0, bytes));
} }
while (stream.DataAvailable); while (stream.DataAvailable);
isWriting = false; sw.Stop();
Logger.Log($"IN MSG ({(int)sw.Elapsed.TotalMilliseconds}ms): {_blockString}", LogLevel.Critical, this);
return response.ToString(); return response.ToString();
} }
@@ -238,6 +525,20 @@ namespace MewtocolNet {
#endregion #endregion
#region Accessing Info
/// <summary>
/// Gets the connection info string
/// </summary>
public string GetConnectionPortInfo () {
return $"{IpAddress}:{Port}";
}
#endregion
} }
} }

View File

@@ -1,16 +0,0 @@
using MewtocolNet.Responses;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace MewtocolNet {
public static class MewtocolInterfaceExtensions {
}
}

View File

@@ -13,7 +13,7 @@ namespace MewtocolNet {
public partial class MewtocolInterface { public partial class MewtocolInterface {
#region High level command handling #region PLC info getters
/// <summary> /// <summary>
/// Gets generic information about the PLC /// Gets generic information about the PLC
@@ -49,91 +49,50 @@ namespace MewtocolNet {
return null; return null;
} }
/// <summary> #endregion
/// Reads bool values from the plc by the given <c>Contact</c> List
/// </summary>
/// <param name="_contactsToRead">A list of contacts</param>
/// <param name="_stationNumber">The PLCs station number</param>
/// <returns>List of IBoolContact with unique copys of the given contacts</returns>
public async Task<IEnumerable<IBoolContact>> ReadBoolContacts (List<Contact> _contactsToRead, int _stationNumber = 1) {
//re order by contact pfx for faster querying
_contactsToRead = _contactsToRead.OrderBy(x=>x.Prefix).ToList();
//return list #region Bool register reading / writing
List<IBoolContact> returnContacts = new List<IBoolContact>();
//grouped by 8 each public async Task<BRegisterResult> ReadBoolRegister (BRegister _toRead, int _stationNumber = 1) {
List<List<Contact>> nestedContacts = new List<List<Contact>>();
//group into max 8 contacts list string requeststring = $"%{_stationNumber.ToString().PadLeft(2, '0')}#RCS{_toRead.BuildMewtocolIdent()}";
List<Contact> tempGroup = new List<Contact>(); var result = await SendCommandAsync(requeststring);
for (int i = 0; i < _contactsToRead.Count; i++) {
tempGroup.Add(_contactsToRead[i]);
//each 8 contacts make a new list
if(i % 7 == 0 && i != 0 && i != _contactsToRead.Count) {
nestedContacts.Add(tempGroup);
tempGroup = new List<Contact>();
}
//if end of list and contacts cannot be broke down to 8 each group
if(i == _contactsToRead.Count - 1 && _contactsToRead.Count % 8 != 0) {
nestedContacts.Add(tempGroup);
tempGroup = new List<Contact>();
}
}
//make task for each group if(!result.Success) {
foreach (var group in nestedContacts) { return new BRegisterResult {
//regex for getting values Result = result,
StringBuilder regexString = new StringBuilder(@"\%..\$RC"); Register = _toRead
//append start %01#RCP2 };
StringBuilder messageString = new StringBuilder();
messageString.Append($"%{_stationNumber.ToString().PadLeft(2, '0')}#RCP");
messageString.Append($"{group.Count}");
//append each contact of group Y0000 Y0001 etc
foreach (var cont in group) {
messageString.Append(cont.BuildMewtocolIdent());
regexString.Append(@"([0-9])");
}
regexString.Append(@"(..)");
//parse the result
var result = await SendCommandAsync(messageString.ToString());
Regex regCheck = new Regex(regexString.ToString(), RegexOptions.IgnoreCase);
if(result.Success && regCheck.IsMatch(result.Response)) {
//parse result string
Match regMatch = regCheck.Match(result.Response);
// add to return list
for (int i = 0; i < group.Count; i++) {
Contact cont = group[i].ShallowCopy();
Contact toadd = cont;
if( regMatch.Groups[i + 1].Value == "1" ) {
toadd.Value = true;
} else if( regMatch.Groups[i + 1].Value == "0" ) {
toadd.Value = false;
}
returnContacts.Add(toadd);
}
}
} }
return returnContacts;
var resultBool = result.Response.ParseRCSingleBit();
if(resultBool != null) {
_toRead.LastValue = resultBool.Value;
}
var finalRes = new BRegisterResult {
Result = result,
Register = _toRead
};
return finalRes;
} }
/// <summary> public async Task<bool> WriteBoolRegister (BRegister _toWrite, bool value, int _stationNumber = 1) {
/// Writes a boolen value to the given contact
/// </summary> string requeststring = $"%{_stationNumber.ToString().PadLeft(2, '0')}#WCS{_toWrite.BuildMewtocolIdent()}{(value ? "1" : "0")}";
/// <param name="_contact">The contact to write</param>
/// <param name="_value">The boolean state to write</param> var result = await SendCommandAsync(requeststring);
/// <param name="_stationNumber">Station Number (optional)</param>
/// <returns>A result struct</returns> return result.Success && result.Response.StartsWith($"%{ _stationNumber.ToString().PadLeft(2, '0')}#WC");
public async Task<CommandResult> WriteContact (Contact _contact, bool _value, int _stationNumber = 1) {
string stationNum = _stationNumber.ToString().PadLeft(2, '0');
string dataArea = _contact.BuildMewtocolIdent();
string dataString = _value ? "1" : "0";
string requeststring = $"%{stationNum}#WCS{dataArea}{dataString}";
var res = await SendCommandAsync(requeststring);
return res;
} }
#endregion
#region Number register reading / writing
/// <summary> /// <summary>
/// Reads the given numeric register from PLC /// Reads the given numeric register from PLC
/// </summary> /// </summary>
@@ -150,26 +109,39 @@ namespace MewtocolNet {
if (numType == typeof(short)) { if (numType == typeof(short)) {
var resultBytes = result.Response.ParseDTByteString(4); var resultBytes = result.Response.ParseDTByteString(4).ReverseByteOrder();
var val = short.Parse(resultBytes, NumberStyles.HexNumber); var val = short.Parse(resultBytes, NumberStyles.HexNumber);
(_toRead as NRegister<short>).LastValue = val; (_toRead as NRegister<short>).LastValue = val;
} else if (numType == typeof(ushort)) { } else if (numType == typeof(ushort)) {
var resultBytes = result.Response.ParseDTBytes(4);
var val = BitConverter.ToInt16(resultBytes); var resultBytes = result.Response.ParseDTByteString(4).ReverseByteOrder();
_toRead.Value = (T)Convert.ChangeType(val, typeof(T)); var val = ushort.Parse(resultBytes, NumberStyles.HexNumber);
(_toRead as NRegister<ushort>).LastValue = val;
} else if (numType == typeof(int)) { } else if (numType == typeof(int)) {
var resultBytes = result.Response.ParseDTBytes(8);
var val = BitConverter.ToInt16(resultBytes); var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder();
_toRead.Value = (T)Convert.ChangeType(val, typeof(T)); var val = int.Parse(resultBytes, NumberStyles.HexNumber);
(_toRead as NRegister<int>).LastValue = val;
} else if (numType == typeof(uint)) { } else if (numType == typeof(uint)) {
var resultBytes = result.Response.ParseDTBytes(8);
var val = BitConverter.ToInt16(resultBytes); var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder();
_toRead.Value = (T)Convert.ChangeType(val, typeof(T)); var val = uint.Parse(resultBytes, NumberStyles.HexNumber);
(_toRead as NRegister<uint>).LastValue = val;
} else if (numType == typeof(float)) { } else if (numType == typeof(float)) {
var resultBytes = result.Response.ParseDTBytes(8);
var val = BitConverter.ToSingle(resultBytes); var resultBytes = result.Response.ParseDTByteString(8).ReverseByteOrder();
_toRead.Value = (T)Convert.ChangeType(val, typeof(T)); //convert to unsigned int first
var val = uint.Parse(resultBytes, NumberStyles.HexNumber);
byte[] floatVals = BitConverter.GetBytes(val);
float finalFloat = BitConverter.ToSingle(floatVals, 0);
(_toRead as NRegister<float>).LastValue = finalFloat;
} }
var finalRes = new NRegisterResult<T> { var finalRes = new NRegisterResult<T> {
@@ -187,7 +159,7 @@ namespace MewtocolNet {
/// <param name="_toWrite">The register to write</param> /// <param name="_toWrite">The register to write</param>
/// <param name="_stationNumber">Station number to access</param> /// <param name="_stationNumber">Station number to access</param>
/// <returns>A result with the given NumberRegister and a result struct</returns> /// <returns>A result with the given NumberRegister and a result struct</returns>
public async Task<NRegisterResult<T>> WriteNumRegister<T>(NRegister<T> _toWrite, T _value, int _stationNumber = 1) { public async Task<bool> WriteNumRegister<T> (NRegister<T> _toWrite, T _value, int _stationNumber = 1) {
byte[] toWriteVal; byte[] toWriteVal;
Type numType = typeof(T); Type numType = typeof(T);
@@ -201,22 +173,38 @@ namespace MewtocolNet {
} else if (numType == typeof(uint)) { } else if (numType == typeof(uint)) {
toWriteVal = BitConverter.GetBytes(Convert.ToUInt32(_value)); toWriteVal = BitConverter.GetBytes(Convert.ToUInt32(_value));
} else if (numType == typeof(float)) { } else if (numType == typeof(float)) {
toWriteVal = BitConverter.GetBytes(Convert.ToUInt32(_value));
var fl = _value as float?;
if (fl == null)
throw new NullReferenceException("Float cannot be null");
toWriteVal = BitConverter.GetBytes(fl.Value);
} else { } else {
toWriteVal = null; toWriteVal = null;
} }
string requeststring = $"%{_stationNumber.ToString().PadLeft(2, '0')}#WD{_toWrite.BuildMewtocolIdent()}{toWriteVal.ToHexString()}"; string requeststring = $"%{_stationNumber.ToString().PadLeft(2, '0')}#WD{_toWrite.BuildMewtocolIdent()}{toWriteVal.ToHexString()}";
var result = await SendCommandAsync(requeststring); var result = await SendCommandAsync(requeststring);
return new NRegisterResult<T> { return result.Success && result.Response.StartsWith($"%{ _stationNumber.ToString().PadLeft(2, '0')}#WD");
Result = result,
Register = _toWrite
};
} }
#endregion
#region String register reading / writing
public async Task<SRegisterResult> ReadStringRegister (SRegister _toRead, int _stationNumber = 1) { public async Task<SRegisterResult> ReadStringRegister (SRegister _toRead, int _stationNumber = 1) {
//string is build up like this
//04 00 04 00 53 50 33 35 13
//0, 1 = reserved size
//1, 2 = current size
//3,4,5,6 = ASCII encoded chars (SP35)
//7,8 = checksum
string requeststring = $"%{_stationNumber.ToString().PadLeft(2, '0')}#RD{_toRead.BuildMewtocolIdent()}"; string requeststring = $"%{_stationNumber.ToString().PadLeft(2, '0')}#RD{_toRead.BuildMewtocolIdent()}";
var result = await SendCommandAsync(requeststring); var result = await SendCommandAsync(requeststring);
if (result.Success) if (result.Success)
@@ -227,7 +215,7 @@ namespace MewtocolNet {
}; };
} }
public async Task<SRegisterResult> WriteStringRegister(SRegister _toWrite, string _value, int _stationNumber = 1) { public async Task<bool> WriteStringRegister(SRegister _toWrite, string _value, int _stationNumber = 1) {
if (_value == null) _value = ""; if (_value == null) _value = "";
if(_value.Length > _toWrite.ReservedSize) { if(_value.Length > _toWrite.ReservedSize) {
@@ -242,10 +230,7 @@ namespace MewtocolNet {
Console.WriteLine($"reserved: {_toWrite.MemoryLength}, size: {_value.Length}"); Console.WriteLine($"reserved: {_toWrite.MemoryLength}, size: {_value.Length}");
var result = await SendCommandAsync(requeststring); var result = await SendCommandAsync(requeststring);
return new SRegisterResult { return result.Success && result.Response.StartsWith($"%{ _stationNumber.ToString().PadLeft(2, '0')}#WD");
Result = result,
Register = _toWrite
};
} }
#endregion #endregion

View File

@@ -1,183 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MewtocolNet.Responses {
/// <summary>
/// A class describing a register
/// </summary>
public abstract class Register : INotifyPropertyChanged {
/// <summary>
/// Gets called whenever the value was changed
/// </summary>
public event Action<object> ValueChanged;
public event PropertyChangedEventHandler PropertyChanged;
public string Name { get; set; }
public int MemoryAdress { get; set; }
public int MemoryLength { get; set; }
public virtual string BuildMewtocolIdent() {
StringBuilder asciistring = new StringBuilder("D");
asciistring.Append(MemoryAdress.ToString().PadLeft(5, '0'));
asciistring.Append((MemoryAdress + MemoryLength).ToString().PadLeft(5, '0'));
return asciistring.ToString();
}
protected void TriggerChangedEvnt(object changed) {
ValueChanged?.Invoke(changed);
}
public void TriggerNotifyChange () {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value"));
}
public string GetValueString () {
if (this is NRegister<short> shortReg) {
return shortReg.Value.ToString();
}
if (this is NRegister<ushort> ushortReg) {
return ushortReg.Value.ToString();
}
if (this is NRegister<int> intReg) {
return intReg.Value.ToString();
}
if (this is NRegister<uint> uintReg) {
return uintReg.Value.ToString();
}
if (this is NRegister<float> floatReg) {
return floatReg.Value.ToString();
}
else if (this is SRegister stringReg) {
return stringReg.Value.ToString();
}
return "Type of the register is not supported.";
}
}
/// <summary>
/// Defines a register containing a number
/// </summary>
/// <typeparam name="T">The type of the numeric value</typeparam>
public class NRegister<T> : Register {
public T NeedValue;
public T LastValue;
/// <summary>
/// The value of the register
/// </summary>
public T Value {
get => LastValue;
set {
NeedValue = value;
TriggerChangedEvnt(this);
}
}
/// <summary>
/// Defines a register containing a number
/// </summary>
/// <param name="_adress">Memory start adress max 99999</param>
/// <param name="_format">The format in which the variable is stored</param>
public NRegister(int _adress, string _name = null) {
if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999");
MemoryAdress = _adress;
Name = _name;
Type numType = typeof(T);
if (numType == typeof(short)) {
MemoryLength = 0;
} else if (numType == typeof(ushort)) {
MemoryLength = 0;
} else if (numType == typeof(int)) {
MemoryLength = 1;
} else if (numType == typeof(uint)) {
MemoryLength = 1;
} else if (numType == typeof(float)) {
MemoryLength = 1;
} else {
throw new NotSupportedException($"The type {numType} is not allowed for Number Registers");
}
}
public override string ToString() {
return $"Adress: {MemoryAdress} Val: {Value}";
}
}
/// <summary>
/// Result for a read/write operation
/// </summary>
/// <typeparam name="T">The type of the numeric value</typeparam>
public class NRegisterResult<T> {
public CommandResult Result { get; set; }
public NRegister<T> Register { get; set; }
public override string ToString() {
string errmsg = Result.Success ? "" : $", Error [{Result.ErrorDescription}]";
return $"Result [{Result.Success}], Register [{Register.ToString()}]{errmsg}";
}
}
/// <summary>
/// Defines a register containing a string
/// </summary>
public class SRegister : Register {
private string lastVal = "";
public string Value {
get => lastVal;
}
public short ReservedSize { get; set; }
/// <summary>
/// Defines a register containing a string
/// </summary>
public SRegister(int _adress, int _reservedStringSize, string _name = null) {
if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999");
Name = _name;
MemoryAdress = _adress;
ReservedSize = (short)_reservedStringSize;
MemoryLength = 1 + (_reservedStringSize) / 2;
}
public override string ToString() {
return $"Adress: {MemoryAdress} Val: {Value}";
}
public override string BuildMewtocolIdent() {
StringBuilder asciistring = new StringBuilder("D");
asciistring.Append(MemoryAdress.ToString().PadLeft(5, '0'));
asciistring.Append((MemoryAdress + MemoryLength).ToString().PadLeft(5, '0'));
return asciistring.ToString();
}
public void SetValueFromPLC (string val) {
lastVal = val;
TriggerChangedEvnt(this);
TriggerNotifyChange();
}
}
public class SRegisterResult {
public CommandResult Result { get; set; }
public SRegister Register { get; set; }
public override string ToString() {
string errmsg = Result.Success ? "" : $", Error [{Result.ErrorDescription}]";
return $"Result [{Result.Success}], Register [{Register.ToString()}]{errmsg}";
}
}
}

View File

@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MewtocolNet.RegisterAttributes {
public enum BitCount {
B16,
B32
}
[AttributeUsage(AttributeTargets.Property)]
public class RegisterAttribute : Attribute {
public int MemoryArea;
public int StringLength;
public RegisterType RegisterType;
public SpecialAddress SpecialAddress = SpecialAddress.None;
public BitCount BitCount;
public int AssignedBitIndex = -1;
public RegisterAttribute (int memoryArea, int stringLength = 1) {
MemoryArea = memoryArea;
StringLength = stringLength;
}
public RegisterAttribute (int memoryArea, RegisterType type) {
if (type.ToString().StartsWith("DT"))
throw new NotSupportedException("DT types are not supported for attribute register setups because the number type is casted automatically");
MemoryArea = memoryArea;
RegisterType = type;
SpecialAddress = SpecialAddress.None;
}
public RegisterAttribute (RegisterType type, SpecialAddress spAdress) {
if (type.ToString().StartsWith("DT"))
throw new NotSupportedException("DT types are not supported for attribute register setups because the number type is casted automatically");
RegisterType = type;
SpecialAddress = spAdress;
}
public RegisterAttribute (int memoryArea, BitCount bitcount) {
MemoryArea = memoryArea;
StringLength = 0;
BitCount = bitcount;
}
public RegisterAttribute (int memoryArea, uint assignBit, BitCount bitcount) {
if(assignBit > 15 && bitcount == BitCount.B16) {
throw new NotSupportedException("The assignBit parameter cannot be greater than 15 in a 16 bit var");
}
if (assignBit > 31 && bitcount == BitCount.B32) {
throw new NotSupportedException("The assignBit parameter cannot be greater than 31 in a 32 bit var");
}
MemoryArea = memoryArea;
StringLength = 0;
BitCount = bitcount;
AssignedBitIndex = (int)assignBit;
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace MewtocolNet.RegisterAttributes {
public class RegisterCollectionBase : INotifyPropertyChanged {
public MewtocolInterface PLCInterface { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
internal void TriggerPropertyChanged (string propertyName = null) {
var handler = PropertyChanged;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MewtocolNet {
public enum RegisterType {
/// <summary>
/// Physical input as a bool (Relay)
/// </summary>
X,
/// <summary>
/// Physical output as a bool (Relay)
/// </summary>
Y,
/// <summary>
/// Internal as a bool (Relay)
/// </summary>
R,
/// <summary>
/// Data area as a short (Register)
/// </summary>
DT_short,
/// <summary>
/// Data area as an unsigned short (Register)
/// </summary>
DT_ushort,
/// <summary>
/// Double data area as an integer (Register)
/// </summary>
DDT_int,
/// <summary>
/// Double data area as an unsigned integer (Register)
/// </summary>
DDT_uint,
/// <summary>
/// Double data area as an floating point number (Register)
/// </summary>
DDT_float,
}
public enum SpecialAddress {
None,
A = -10,
B = -11,
C = -12,
D = -13,
E = -14,
F = -15,
}
}

View File

@@ -167,177 +167,4 @@ namespace MewtocolNet.Responses {
} }
/// <summary>
/// Contact as bool contact
/// </summary>
public interface IBoolContact {
public string Name {get;set;}
public string Identifier {get;}
public bool? Value {get;set;}
}
/// <summary>
/// A class describing a PLC contact
/// </summary>
public class Contact : IBoolContact {
public string Name {get;set;}
public PFX Prefix {get;set;}
public int Number {get;set;}
public ContactType Type {get;set;}
public string Endprefix {get;set;}
public string Asciistring {get => BuildMewtocolIdent();}
public string Identifier {get => Asciistring;}
public bool? Value {get;set;} = null;
public enum ContactType {
Unknown,
Input,
Output,
}
public enum PFX {
X,
Y,
R
}
/// <summary>
/// Creates a new base Contact
/// </summary>
/// <param name="_prefix">A prefix identifier eg. X,Y,R,L</param>
/// <param name="_number">The number of the PLC contact</param>
public Contact (PFX _prefix, int _number, string _name = "unknown") {
switch (_prefix) {
case PFX.X:
Type = ContactType.Input;
break;
case PFX.Y:
case PFX.R:
Type = ContactType.Output;
break;
}
Prefix = _prefix;
Number = _number;
Name = _name;
}
/// <summary>
/// Creates a new base Contact
/// </summary>
/// <param name="_prefix">A prefix identifier eg. X,Y,R,L</param>
/// <param name="_number">The number of the PLC contact</param>
public Contact (string _prefix, int _number, string _name = "unknown") {
PFX parsedPFX;
if(Enum.TryParse<PFX>(_prefix, true, out parsedPFX)) {
switch (parsedPFX) {
case PFX.X:
Type = ContactType.Input;
break;
case PFX.Y:
case PFX.R:
Type = ContactType.Output;
break;
}
Prefix = parsedPFX;
Number = _number;
Name = _name;
} else {
throw new ArgumentException($"The prefix {_prefix} is no valid contact prefix");
}
}
/// <summary>
/// Build contact from complete contact name
/// </summary>
/// <param name="_contactName">Complete contact name e.g. Y1C, Y3D or X1</param>
public Contact (string _contactName, string _name = "unknown") {
string prefix = "";
int number = 0;
string endpfx = null;
Match regcheck = new Regex(@"(Y|X|R|L)([0-9]{1,3})?(.)?", RegexOptions.IgnoreCase).Match(_contactName);
if(regcheck.Success) {
/* for (int i = 0; i < regcheck.Groups.Count; i++) {
var item = regcheck.Groups[i].Value;
Console.WriteLine(item);
} */
prefix = regcheck.Groups[1].Value;
number = regcheck.Groups[2]?.Value != string.Empty ? Convert.ToInt32(regcheck.Groups[2].Value) : -1;
endpfx = regcheck.Groups[3]?.Value;
} else {
throw new ArgumentException($"The contact {_contactName} is no valid contact");
}
PFX parsedPFX;
if(Enum.TryParse<PFX>(prefix, true, out parsedPFX)) {
switch (parsedPFX) {
case PFX.X:
Type = ContactType.Input;
break;
case PFX.Y:
case PFX.R:
Type = ContactType.Output;
break;
}
Prefix = parsedPFX;
Number = number;
Endprefix = endpfx;
Name = _name;
} else {
throw new ArgumentException($"The prefix {prefix} is no valid contact prefix");
}
Console.WriteLine(BuildMewtocolIdent());
}
/// <summary>
/// Builds the mewtocol ascii contact identifier
/// </summary>
/// <returns>The identifier e.g. Y0001 or Y000A or X001C</returns>
public string BuildMewtocolIdent () {
string contactstring = "";
if(Endprefix == null) {
contactstring += Prefix;
contactstring += Number.ToString().PadLeft(4, '0');
} else {
contactstring += Prefix;
if(Number == -1) {
contactstring += "000" + Endprefix;
} else {
contactstring += (Number.ToString() + Endprefix).PadLeft(4, '0');
}
}
if(string.IsNullOrEmpty(contactstring)) {
return null;
}
return contactstring;
}
/// <summary>
/// Converts the class to a generic json compatible object
/// </summary>
/// <returns></returns>
public object ToGenericObject () {
return new {
Name = this.Name,
Identifier = this.Asciistring,
Value = this.Value
};
}
/// <summary>
/// Creates a copy of the contact
/// </summary>
public Contact ShallowCopy() {
return (Contact) this.MemberwiseClone();
}
}
} }

View File

@@ -0,0 +1,82 @@
using System;
using System.Text;
using MewtocolNet;
namespace MewtocolNet.Responses {
/// <summary>
/// Defines a register containing a boolean
/// </summary>
public class BRegister : Register {
internal RegisterType RegType { get; set; }
internal SpecialAddress SpecialAddress { get; set; }
public bool NeedValue;
public bool LastValue;
/// <summary>
/// The value of the register
/// </summary>
public bool Value {
get => LastValue;
set {
NeedValue = value;
TriggerChangedEvnt(this);
}
}
/// <summary>
/// Defines a register containing a number
/// </summary>
/// <param name="_address">Memory start adress max 99999</param>
/// <param name="_type">Type of boolean register</param>
/// <param name="_name">Name of the register</param>
public BRegister (int _address, RegisterType _type = RegisterType.R, string _name = null) {
if (_address > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999");
MemoryAdress = _address;
Name = _name;
RegType = _type;
}
/// <summary>
/// Defines a register containing a number
/// </summary>
/// <param name="_address">Memory start adress max 99999</param>
/// <param name="_type">Type of boolean register</param>
/// <param name="_name">Name of the register</param>
public BRegister (SpecialAddress _address, RegisterType _type = RegisterType.R, string _name = null) {
if (_address == SpecialAddress.None)
throw new NotSupportedException("Special adress cant be none");
SpecialAddress = _address;
Name = _name;
RegType = _type;
}
public override string BuildMewtocolIdent () {
//build area code from register type
StringBuilder asciistring = new StringBuilder(RegType.ToString());
if(SpecialAddress == SpecialAddress.None) {
asciistring.Append(MemoryAdress.ToString().PadLeft(4, '0'));
} else {
asciistring.Append(SpecialAddress.ToString().PadLeft(4, '0'));
}
return asciistring.ToString();
}
public override string ToString() {
return $"Adress: {MemoryAdress} Val: {Value}";
}
}
}

View File

@@ -0,0 +1,13 @@
namespace MewtocolNet.Responses {
public class BRegisterResult {
public CommandResult Result { get; set; }
public BRegister Register { get; set; }
public override string ToString() {
string errmsg = Result.Success ? "" : $", Error [{Result.ErrorDescription}]";
return $"Result [{Result.Success}], Register [{Register.ToString()}]{errmsg}";
}
}
}

View File

@@ -0,0 +1,61 @@
using System;
namespace MewtocolNet.Responses {
/// <summary>
/// Defines a register containing a number
/// </summary>
/// <typeparam name="T">The type of the numeric value</typeparam>
public class NRegister<T> : Register {
public T NeedValue;
public T LastValue;
/// <summary>
/// The value of the register
/// </summary>
public T Value {
get => LastValue;
set {
NeedValue = value;
TriggerChangedEvnt(this);
}
}
/// <summary>
/// Defines a register containing a number
/// </summary>
/// <param name="_adress">Memory start adress max 99999</param>
/// <param name="_format">The format in which the variable is stored</param>
public NRegister(int _adress, string _name = null, bool isBitwise = false) {
if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999");
MemoryAdress = _adress;
Name = _name;
Type numType = typeof(T);
if (numType == typeof(short)) {
MemoryLength = 0;
} else if (numType == typeof(ushort)) {
MemoryLength = 0;
} else if (numType == typeof(int)) {
MemoryLength = 1;
} else if (numType == typeof(uint)) {
MemoryLength = 1;
} else if (numType == typeof(float)) {
MemoryLength = 1;
} else {
throw new NotSupportedException($"The type {numType} is not allowed for Number Registers");
}
isUsedBitwise = isBitwise;
}
public override string ToString() {
return $"Adress: {MemoryAdress} Val: {Value}";
}
}
}

View File

@@ -0,0 +1,19 @@
namespace MewtocolNet.Responses {
/// <summary>
/// Result for a read/write operation
/// </summary>
/// <typeparam name="T">The type of the numeric value</typeparam>
public class NRegisterResult<T> {
public CommandResult Result { get; set; }
public NRegister<T> Register { get; set; }
public override string ToString() {
string errmsg = Result.Success ? "" : $", Error [{Result.ErrorDescription}]";
return $"Result [{Result.Success}], Register [{Register.ToString()}]{errmsg}";
}
}
}

View File

@@ -0,0 +1,153 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MewtocolNet.Responses {
/// <summary>
/// A class describing a register
/// </summary>
public abstract class Register : INotifyPropertyChanged {
/// <summary>
/// Gets called whenever the value was changed
/// </summary>
public event Action<object> ValueChanged;
public event PropertyChangedEventHandler PropertyChanged;
public string Name { get; set; }
public int MemoryAdress { get; set; }
public int MemoryLength { get; set; }
internal bool isUsedBitwise { get; set; }
public virtual string BuildMewtocolIdent() {
StringBuilder asciistring = new StringBuilder("D");
asciistring.Append(MemoryAdress.ToString().PadLeft(5, '0'));
asciistring.Append((MemoryAdress + MemoryLength).ToString().PadLeft(5, '0'));
return asciistring.ToString();
}
internal void TriggerChangedEvnt(object changed) {
ValueChanged?.Invoke(changed);
}
internal void TriggerNotifyChange () {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Value"));
}
/// <summary>
/// Gets the starting memory are either numeric or A,B,C,D etc for special areas like inputs
/// </summary>
/// <returns></returns>
public string GetStartingMemoryArea () {
if (this is BRegister bReg && bReg.SpecialAddress != SpecialAddress.None) {
return bReg.SpecialAddress.ToString();
}
return this.MemoryAdress.ToString();
}
/// <summary>
/// Gets the current value in the adress as a string
/// </summary>
/// <returns></returns>
public string GetValueString () {
if (this is NRegister<short> shortReg) {
return $"{shortReg.Value}{(isUsedBitwise ? $" [{shortReg.GetBitwise().ToBitString()}]" : "")}";
}
if (this is NRegister<ushort> ushortReg) {
return ushortReg.Value.ToString();
}
if (this is NRegister<int> intReg) {
return $"{intReg.Value}{(isUsedBitwise ? $" [{intReg.GetBitwise().ToBitString()}]" : "")}";
}
if (this is NRegister<uint> uintReg) {
return uintReg.Value.ToString();
}
if (this is NRegister<float> floatReg) {
return floatReg.Value.ToString();
}
if (this is BRegister boolReg) {
return boolReg.Value.ToString();
}
if (this is SRegister stringReg) {
return stringReg.Value.ToString();
}
return "Type of the register is not supported.";
}
/// <summary>
/// Gets the register bitwise if its a 16 or 32 bit int
/// </summary>
/// <returns>A bitarray</returns>
public BitArray GetBitwise () {
if (this is NRegister<short> shortReg) {
var bytes = BitConverter.GetBytes(shortReg.Value);
BitArray bitAr = new BitArray(bytes);
return bitAr;
}
if (this is NRegister<int> intReg) {
var bytes = BitConverter.GetBytes(intReg.Value);
BitArray bitAr = new BitArray(bytes);
return bitAr;
}
return null;
}
public string GetRegisterString () {
if (this is NRegister<short> shortReg) {
return "DT";
}
if (this is NRegister<ushort> ushortReg) {
return "DT";
}
if (this is NRegister<int> intReg) {
return "DDT";
}
if (this is NRegister<uint> uintReg) {
return "DDT";
}
if (this is NRegister<float> floatReg) {
return "DDT";
}
if (this is BRegister boolReg) {
return boolReg.RegType.ToString();
}
if (this is SRegister stringReg) {
return "DT";
}
return "Type of the register is not supported.";
}
public string GetRegisterPLCName () {
return $"{GetRegisterString()}{MemoryAdress}";
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Text;
namespace MewtocolNet.Responses {
/// <summary>
/// Defines a register containing a string
/// </summary>
public class SRegister : Register {
private string lastVal = "";
public string Value {
get => lastVal;
}
public short ReservedSize { get; set; }
/// <summary>
/// Defines a register containing a string
/// </summary>
public SRegister(int _adress, int _reservedStringSize, string _name = null) {
if (_adress > 99999) throw new NotSupportedException("Memory adresses cant be greater than 99999");
Name = _name;
MemoryAdress = _adress;
ReservedSize = (short)_reservedStringSize;
//calc mem length
var wordsize = (double)_reservedStringSize / 2;
if (wordsize % 2 != 0) {
wordsize++;
}
MemoryLength = (int)Math.Round(wordsize + 1);
}
public override string ToString() {
return $"Adress: {MemoryAdress} Val: {Value}";
}
public override string BuildMewtocolIdent() {
StringBuilder asciistring = new StringBuilder("D");
asciistring.Append(MemoryAdress.ToString().PadLeft(5, '0'));
asciistring.Append((MemoryAdress + MemoryLength).ToString().PadLeft(5, '0'));
return asciistring.ToString();
}
public void SetValueFromPLC (string val) {
lastVal = val;
TriggerChangedEvnt(this);
TriggerNotifyChange();
}
}
}

View File

@@ -0,0 +1,15 @@
namespace MewtocolNet.Responses {
public class SRegisterResult {
public CommandResult Result { get; set; }
public SRegister Register { get; set; }
public override string ToString() {
string errmsg = Result.Success ? "" : $", Error [{Result.ErrorDescription}]";
return $"Result [{Result.Success}], Register [{Register.ToString()}]{errmsg}";
}
}
}

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<PackageId>AppLogger</PackageId> <PackageId>AppLogger</PackageId>
<Version>0.1.5</Version> <Version>0.1.5</Version>
<Authors>Felix Weiss</Authors> <Authors>Felix Weiss</Authors>