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] 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 | 647 ++++++------------ ...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, 1135 insertions(+), 564 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 Microsoft.Win32; 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 class ExampleScenarios { 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 class ExampleScenarios { 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 class ExampleScenarios { 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 @@ public class ExampleScenarios { } - [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 class ExampleScenarios { //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 class ExampleScenarios { 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 class ExampleScenarios { } - 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 @@ class Program { 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 @@ class Program { 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 @@ class Program { 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; - task.Wait(); + 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"); 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 @@ namespace MewtocolNet { } } + 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 @@ namespace MewtocolNet.Queue { _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 @@ namespace MewtocolNet.Logging { 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 @@ using System.Threading.Tasks; 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; } - } + /// + public bool Disposed { get; private set; } - private volatile int queuedMessages; - /// - /// Currently queued Messages - /// - public int QueuedMessages { - get => queuedMessages; - } + /// + public int QueuedMessages => queuedMessages; - /// - /// The host ip endpoint, leave it null to use an automatic interface - /// - public IPEndPoint HostEndpoint { get; set; } - - 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 @@ namespace MewtocolNet { }; } + + /// + public virtual Task ConnectAsync () => throw new NotImplementedException(); - #endregion + /// + public async Task AwaitFirstDataAsync() => await firstPollTask; - #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; - - } - - /// - /// Closes the connection all cyclic polling - /// + /// public void Disconnect() { - if (!IsConnected) - return; + if (!IsConnected) return; + + pollCycleTask.Wait(); OnMajorSocketExceptionWhileConnected(); } - /// - /// Attaches a poller to the interface that continously - /// polls the registered data registers and writes the values to them - /// - public MewtocolInterface WithPoller() { + /// + public void Dispose() { - usePoller = true; - return this; + if (Disposed) return; + Disconnect(); + //GC.SuppressFinalize(this); + Disposed = true; } - #endregion + /// + public virtual string GetConnectionInfo() => throw new NotImplementedException(); - #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(); - - } - - } - - #endregion - - #region Low level command handling - - /// - /// 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,82 +214,32 @@ namespace MewtocolNet { //write inital command byte[] writeBuffer = Encoding.UTF8.GetBytes(frame); - await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length); - - //calc upstream speed - bytesTotalCountedUpstream += writeBuffer.Length; - - var perSecUpstream = (double)((bytesTotalCountedUpstream / speedStopwatchUpstr.Elapsed.TotalMilliseconds) * 1000); - if (perSecUpstream <= 10000) - BytesPerSecondUpstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); - + stream.Write(writeBuffer, 0, writeBuffer.Length); Logger.Log($"[---------CMD START--------]", LogLevel.Critical, this); Logger.Log($"--> OUT MSG: {frame.Replace("\r", "(CR)")}", LogLevel.Critical, this); - //read - List totalResponse = new List(); - byte[] responseBuffer = new byte[512]; + var readResult = await ReadCommandAsync(); - bool wasMultiFramedResponse = false; - CommandState cmdState = CommandState.Intial; + //did not receive bytes but no errors, the com port was not configured right + if (readResult.Item1.Length == 0) { - //read until command complete - while (cmdState != CommandState.Complete) { - - //time measuring - if (speedStopwatchDownstr == null) { - speedStopwatchDownstr = Stopwatch.StartNew(); - } - - if (speedStopwatchDownstr.Elapsed.TotalSeconds >= 1) { - speedStopwatchDownstr.Restart(); - bytesTotalCountedDownstream = 0; - } - - responseBuffer = new byte[128]; - - 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 }); - - 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); - - //error response - int errorCode = CheckForErrorMsg(tempMsg); - if (errorCode != 0) return new MewtocolFrameResponse(errorCode); - - //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); - - wasMultiFramedResponse = true; - writeBuffer = Encoding.UTF8.GetBytes("%01**&\r"); - await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length); - - } + return new MewtocolFrameResponse(402, "Receive buffer was empty"); } //build final result - string resString = Encoding.UTF8.GetString(totalResponse.ToArray()); + string resString = Encoding.UTF8.GetString(readResult.Item1); - if (wasMultiFramedResponse) { + //check if the message had errors + //error response + var gotErrorcode = CheckForErrorMsg(resString); + if (gotErrorcode != 0) { + return new MewtocolFrameResponse(gotErrorcode); + } + + //was multiframed response + if (readResult.Item2) { var split = resString.Split('&'); @@ -519,13 +255,6 @@ namespace MewtocolNet { } - bytesTotalCountedDownstream += Encoding.ASCII.GetByteCount(resString); - - var perSecDownstream = (double)((bytesTotalCountedDownstream / speedStopwatchDownstr.Elapsed.TotalMilliseconds) * 1000); - - if (perSecUpstream <= 10000) - BytesPerSecondDownstream = (int)Math.Round(perSecUpstream, MidpointRounding.AwayFromZero); - 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); @@ -540,7 +269,73 @@ namespace MewtocolNet { } - private int CheckForErrorMsg (string msg) { + private protected async Task<(byte[], bool)> ReadCommandAsync () { + + //read total + List totalResponse = new List(); + bool wasMultiFramedResponse = false; + + try { + + bool needsRead = false; + + do { + + byte[] buffer = new byte[128]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + + byte[] received = new byte[bytesRead]; + Buffer.BlockCopy(buffer, 0, received, 0, bytesRead); + + var commandRes = ParseBufferFrame(received); + needsRead = commandRes == CommandState.LineFeed || commandRes == CommandState.RequestedNextFrame; + + 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)); + + if (commandRes == CommandState.RequestedNextFrame) { + + //request next frame + var writeBuffer = Encoding.UTF8.GetBytes("%01**&\r"); + await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length); + wasMultiFramedResponse = true; + + } + + } while (needsRead); + + } catch (OperationCanceledException) { } + + return (totalResponse.ToArray(), wasMultiFramedResponse); + + } + + private protected CommandState ParseBufferFrame(byte[] received) { + + const char CR = '\r'; + const char DELIMITER = '&'; + + 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 protected int CheckForErrorMsg (string msg) { //error catching Regex errorcheck = new Regex(@"\%[0-9]{2}\!([0-9]{2})", RegexOptions.IgnoreCase); @@ -557,11 +352,7 @@ namespace MewtocolNet { } - #endregion - - #region Disposing - - private void OnMajorSocketExceptionWhileConnecting() { + private protected void OnMajorSocketExceptionWhileConnecting() { if (IsConnected) { @@ -572,7 +363,7 @@ namespace MewtocolNet { } - private void OnMajorSocketExceptionWhileConnected() { + private protected void OnMajorSocketExceptionWhileConnected() { if (IsConnected) { @@ -583,43 +374,44 @@ namespace MewtocolNet { } + 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() { - } - - private void OnDisconnect () { - - if (IsConnected) { - - BytesPerSecondDownstream = 0; - BytesPerSecondUpstream = 0; - CycleTimeMs = 0; - - IsConnected = false; - ClearRegisterVals(); - - Disconnected?.Invoke(); - KillPoller(); - client.Close(); + firstPollTask.RunSynchronously(); + PolledCycle -= OnPollCycleDone; } } + private protected virtual void OnDisconnect () { - private void ClearRegisterVals() { + BytesPerSecondDownstream = 0; + BytesPerSecondUpstream = 0; + CycleTimeMs = 0; + + IsConnected = false; + ClearRegisterVals(); + + Disconnected?.Invoke(); + KillPoller(); + + } + + private protected void ClearRegisterVals() { for (int i = 0; i < RegistersUnderlying.Count; i++) { @@ -630,28 +422,7 @@ namespace MewtocolNet { } - #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 @@ namespace MewtocolNet { } -} \ 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.Text; 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 @@ namespace MewtocolNet tcpMessagesSentThisCycle = 0; - await OnMultiFrameCycle(); + pollCycleTask = OnMultiFrameCycle(); + await pollCycleTask; return tcpMessagesSentThisCycle; @@ -104,9 +96,10 @@ namespace MewtocolNet 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 @@ namespace MewtocolNet { /// 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 @@ 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/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 @@ namespace MewtocolNet.RegisterBuilding { }; - 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; 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 @@ namespace MewtocolTests { [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 @@ namespace MewtocolTests { [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 @@ namespace MewtocolTests { [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 @@ namespace MewtocolTests { [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 @@ namespace MewtocolTests { [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 @@ namespace MewtocolTests { [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 @@ namespace MewtocolTests { [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 @@ namespace MewtocolTests { [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 @@ namespace MewtocolTests 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 @@ namespace MewtocolTests 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");