From aa8441fa3374b9566dc8a3f8d2e31144b1e54ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Wei=C3=9F?= <72068105+Sandoun@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:59:19 +0200 Subject: [PATCH] Gone from async queue --- Examples.WPF/Views/ConnectView.xaml.cs | 11 + Examples.WPF/Views/PlcDataView.xaml | 1 + MewtocolNet/Helpers/AsyncQueue.cs | 45 +++- MewtocolNet/IPlc.cs | 2 +- MewtocolNet/Logging/Logger.cs | 28 ++- MewtocolNet/Mewtocol.cs | 5 +- MewtocolNet/MewtocolFrameResponse.cs | 2 + MewtocolNet/MewtocolInterface.cs | 223 +++++++++++++++--- .../MewtocolInterfaceRegisterHandling.cs | 55 ++++- MewtocolNet/MewtocolInterfaceRequests.cs | 16 +- MewtocolNet/MewtocolInterfaceTcp.cs | 141 ++++++++--- MewtocolNet/PLCInfo.cs | 22 -- MewtocolNet/Registers/Base/IRegister.cs | 5 + MewtocolNet/Registers/Base/Register.cs | 9 +- .../Registers/Classes/ArrayRegister.cs | 4 +- MewtocolNet/SetupClasses/InterfaceSettings.cs | 19 +- 16 files changed, 465 insertions(+), 123 deletions(-) diff --git a/Examples.WPF/Views/ConnectView.xaml.cs b/Examples.WPF/Views/ConnectView.xaml.cs index ceba6a7..2ade4ac 100644 --- a/Examples.WPF/Views/ConnectView.xaml.cs +++ b/Examples.WPF/Views/ConnectView.xaml.cs @@ -55,9 +55,14 @@ public partial class ConnectView : UserControl { App.ViewModel.Plc = Mewtocol.Ethernet(viewModel.SelectedIP, parsedInt) .WithPoller() + .WithInterfaceSettings(s => { + s.TryReconnectAttempts = 30; + s.TryReconnectDelayMs = 2000; + }) .WithRegisters(b => { b.Struct("DT0").Build(); b.Struct("DT0").AsArray(30).Build(); + b.Struct("DT1002").Build(); }) .Build(); @@ -67,6 +72,12 @@ public partial class ConnectView : UserControl { App.MainWindow.mainContent.Content = new PlcDataView(); + //for (int i = 0; i < 300000; i++) { + + // _ = Task.Run(async () => await App.ViewModel.Plc.SendCommandAsync("%EE#RT")); + + //} + } viewModel.IsConnecting = false; diff --git a/Examples.WPF/Views/PlcDataView.xaml b/Examples.WPF/Views/PlcDataView.xaml index 0de5a41..b7be31f 100644 --- a/Examples.WPF/Views/PlcDataView.xaml +++ b/Examples.WPF/Views/PlcDataView.xaml @@ -79,6 +79,7 @@ + diff --git a/MewtocolNet/Helpers/AsyncQueue.cs b/MewtocolNet/Helpers/AsyncQueue.cs index 42fd8c9..6ca45fe 100644 --- a/MewtocolNet/Helpers/AsyncQueue.cs +++ b/MewtocolNet/Helpers/AsyncQueue.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace MewtocolNet.Helpers { @@ -8,16 +10,46 @@ namespace MewtocolNet.Helpers { readonly object _locker = new object(); readonly WeakReference _lastTask = new WeakReference(null); + internal CancellationTokenSource tSource = new CancellationTokenSource(); + + private List queuedTasks = new List(); + + //internal Task Enqueue(Func> asyncFunction) { + + // lock (_locker) { + + // var token = tSource.Token; + + // Task lastTask; + // Task resultTask; + + // if (_lastTask.TryGetTarget(out lastTask)) { + // resultTask = lastTask.ContinueWith(_ => asyncFunction(), token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Current).Unwrap(); + // } else { + // resultTask = Task.Run(asyncFunction, token); + // } + + // _lastTask.SetTarget(resultTask); + + // return resultTask; + + // } + + //} + internal Task Enqueue(Func> asyncFunction) { + lock (_locker) { + var token = tSource.Token; + Task lastTask; Task resultTask; if (_lastTask.TryGetTarget(out lastTask)) { - resultTask = lastTask.ContinueWith(_ => asyncFunction(), TaskContinuationOptions.ExecuteSynchronously).Unwrap(); + resultTask = lastTask.ContinueWith(_ => asyncFunction(), token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Current).Unwrap(); } else { - resultTask = Task.Run(asyncFunction); + resultTask = Task.Run(asyncFunction, token); } _lastTask.SetTarget(resultTask); @@ -25,6 +57,15 @@ namespace MewtocolNet.Helpers { return resultTask; } + + } + + internal void CancelAll () { + + tSource.Cancel(); + + Console.WriteLine(); + } } diff --git a/MewtocolNet/IPlc.cs b/MewtocolNet/IPlc.cs index 9416f9a..95d65c7 100644 --- a/MewtocolNet/IPlc.cs +++ b/MewtocolNet/IPlc.cs @@ -95,7 +95,7 @@ namespace MewtocolNet { /// 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, Action onReceiveProgress = null); + Task SendCommandAsync(string _msg, Action onReceiveProgress = null); /// /// Changes the PLCs operation mode to the given one diff --git a/MewtocolNet/Logging/Logger.cs b/MewtocolNet/Logging/Logger.cs index 6dbcc9b..63b99e8 100644 --- a/MewtocolNet/Logging/Logger.cs +++ b/MewtocolNet/Logging/Logger.cs @@ -9,7 +9,7 @@ namespace MewtocolNet.Logging { public static class Logger { /// - /// Sets the loglevel for the logger module + /// Sets the loglevel for the global logging module /// public static LogLevel LogLevel { get; set; } @@ -54,13 +54,17 @@ namespace MewtocolNet.Logging { } + }); + + LogInvoked += (d, l, m) => { + if (DefaultTargets.HasFlag(LoggerTargets.Trace)) { Trace.WriteLine($"{d:hh:mm:ss:ff} {m}"); } - }); + }; } @@ -70,22 +74,26 @@ namespace MewtocolNet.Logging { /// /// Gets invoked whenever a new log message is ready /// - public static void OnNewLogMessage(Action onMsg) { + public static void OnNewLogMessage(Action onMsg, LogLevel? maxLevel = null) { + + if (maxLevel == null) maxLevel = LogLevel; LogInvoked += (t, l, m) => { - onMsg(t, l, m); + + if ((int)l <= (int)maxLevel) { + onMsg(t, l, m); + } + }; } internal static void Log(string message, LogLevel loglevel, MewtocolInterface sender = null) { - if ((int)loglevel <= (int)LogLevel) { - if (sender == null) { - LogInvoked?.Invoke(DateTime.Now, loglevel, message); - } else { - LogInvoked?.Invoke(DateTime.Now, loglevel, $"[{sender.GetConnectionInfo()}] {message}"); - } + if (sender == null) { + LogInvoked?.Invoke(DateTime.Now, loglevel, message); + } else { + LogInvoked?.Invoke(DateTime.Now, loglevel, $"[{sender.GetConnectionInfo()}] {message}"); } } diff --git a/MewtocolNet/Mewtocol.cs b/MewtocolNet/Mewtocol.cs index 4a3c7eb..0932910 100644 --- a/MewtocolNet/Mewtocol.cs +++ b/MewtocolNet/Mewtocol.cs @@ -320,7 +320,10 @@ namespace MewtocolNet imew.memoryManager.maxOptimizationDistance = res.MaxOptimizationDistance; imew.memoryManager.pollLevelOrMode = res.PollLevelOverwriteMode; - imew.maxDataBlocksPerWrite = res.MaxDataBlocksPerWrite; + imew.maxDataBlocksPerWrite = res.MaxDataBlocksPerWrite; + imew.sendReceiveTimeoutMs = res.SendReceiveTimeoutMs; + imew.tryReconnectAttempts = res.TryReconnectAttempts; + imew.tryReconnectDelayMs = res.TryReconnectDelayMs; } diff --git a/MewtocolNet/MewtocolFrameResponse.cs b/MewtocolNet/MewtocolFrameResponse.cs index db91232..9533d33 100644 --- a/MewtocolNet/MewtocolFrameResponse.cs +++ b/MewtocolNet/MewtocolFrameResponse.cs @@ -17,6 +17,8 @@ namespace MewtocolNet public static MewtocolFrameResponse NotIntialized => new MewtocolFrameResponse(405, "PLC was not initialized"); + public static MewtocolFrameResponse Canceled => new MewtocolFrameResponse(500, "Op was canceled by the library"); + public MewtocolFrameResponse(string response) { Success = true; diff --git a/MewtocolNet/MewtocolInterface.cs b/MewtocolNet/MewtocolInterface.cs index 90323e8..93019a7 100644 --- a/MewtocolNet/MewtocolInterface.cs +++ b/MewtocolNet/MewtocolInterface.cs @@ -9,9 +9,11 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; namespace MewtocolNet { @@ -53,13 +55,18 @@ namespace MewtocolNet { private protected int bytesPerSecondDownstream = 0; private protected AsyncQueue queue = new AsyncQueue(); + private protected Stopwatch speedStopwatchUpstr; private protected Stopwatch speedStopwatchDownstr; private protected Task firstPollTask; + private Task reconnectTask; private protected bool wasInitialStatusReceived; private protected MewtocolVersion mewtocolVersion; + //private protected string[] lastMsgs = new string[10]; + private protected List lastMsgs = new List(); + #endregion #region Internal fields @@ -72,6 +79,10 @@ namespace MewtocolNet { private volatile bool isReceiving; private volatile bool isSending; + internal int sendReceiveTimeoutMs = 1000; + internal int tryReconnectAttempts = 0; + internal int tryReconnectDelayMs = 1000; + #endregion #region Public Read Only Properties / Fields @@ -171,13 +182,20 @@ namespace MewtocolNet { WatchPollerDemand(); Connected += MewtocolInterface_Connected; + Disconnected += MewtocolInterface_Disconnected; RegisterChanged += OnRegisterChanged; - void MewtocolInterface_Connected(object sender, PlcConnectionArgs args) { + } - IsConnected = true; + private void MewtocolInterface_Connected(object sender, PlcConnectionArgs args) { - } + IsConnected = true; + + } + + private void MewtocolInterface_Disconnected(object sender, PlcConnectionArgs e) { + + Logger.LogVerbose("Disconnected", this); } @@ -213,13 +231,13 @@ namespace MewtocolNet { await memoryManager.OnPlcConnected(); - Logger.Log($"PLC: {PlcInfo.TypeName}", LogLevel.Verbose, this); + Logger.Log($"PLC: {PlcInfo.TypeName}", LogLevel.Verbose, this); Logger.Log($"TYPE CODE: {PlcInfo.TypeCode.ToString("X")}", LogLevel.Verbose, this); - Logger.Log($"OP MODE: {PlcInfo.OperationMode}", LogLevel.Verbose, this); - Logger.Log($"PROG CAP: {PlcInfo.ProgramCapacity}k", LogLevel.Verbose, this); - Logger.Log($"HW INFO: {PlcInfo.HardwareInformation}", LogLevel.Verbose, this); - Logger.Log($"DIAG ERR: {PlcInfo.SelfDiagnosticError}", LogLevel.Verbose, this); - Logger.Log($"CPU VER: {PlcInfo.CpuVersion}", LogLevel.Verbose, this); + Logger.Log($"OP MODE: {PlcInfo.OperationMode}", LogLevel.Verbose, this); + Logger.Log($"PROG CAP: {PlcInfo.ProgramCapacity}k", LogLevel.Verbose, this); + Logger.Log($"HW INFO: {PlcInfo.HardwareInformation}", LogLevel.Verbose, this); + Logger.Log($"DIAG ERR: {PlcInfo.SelfDiagnosticError}", LogLevel.Verbose, this); + Logger.Log($"CPU VER: {PlcInfo.CpuVersion}", LogLevel.Verbose, this); Logger.Log($">> Intial connection end <<", LogLevel.Verbose, this); @@ -233,6 +251,9 @@ namespace MewtocolNet { } + /// + protected virtual Task ReconnectAsync(int conTimeout) => throw new NotImplementedException(); + /// public async Task AwaitFirstDataCycleAsync() => await firstPollTask; @@ -271,8 +292,71 @@ namespace MewtocolNet { /// public virtual string GetConnectionInfo() => throw new NotImplementedException(); + internal void StartReconnectTask() { + + if (reconnectTask == null) { + + reconnectTask = Task.Run(async () => { + + int retryCount = 1; + + if (!IsConnected) { + + if(this is MewtocolInterfaceTcp tcpI) { + + tcpI.client.Close(); + tcpI.client = null; + + } + + while (!IsConnected && tryReconnectAttempts > 0 && retryCount < tryReconnectAttempts + 1) { + + Logger.Log($"Reconnecting {retryCount}/{tryReconnectAttempts} ...", this); + + //kill the poller + KillPoller(); + + //stop the heartbeat timer for the time of retries + StopHeartBeat(); + + await ReconnectAsync(tryReconnectDelayMs); + await Task.Delay(2000); + + retryCount++; + + + } + + //still not connected + if (!IsConnected) { + + //invoke the dc evnt + OnMajorSocketExceptionAfterRetries(); + + } + + } + + }); + + } + + } + + internal async Task AwaitReconnectTaskAsync () { + + if (reconnectTask != null && !reconnectTask.IsCompleted) await reconnectTask; + + await Task.CompletedTask; + + } + + private Task regularSendTask; + /// - public async Task SendCommandAsync (string _msg, bool withTerminator = true, int timeoutMs = -1, Action onReceiveProgress = null) { + public async Task SendCommandAsync(string _msg, Action onReceiveProgress = null) { + + await AwaitReconnectTaskAsync(); if (!IsConnected && !isConnectingStage) throw new NotSupportedException("The device must be connected to send a message"); @@ -280,32 +364,51 @@ namespace MewtocolNet { //send request queuedMessages++; - var tempResponse = queue.Enqueue(async () => await SendFrameAsync(_msg, withTerminator, withTerminator, onReceiveProgress)); + //wait for the last send task to complete + if (regularSendTask != null && !regularSendTask.IsCompleted) await regularSendTask; + + regularSendTask = SendFrameAsync(_msg, onReceiveProgress); + + if (await Task.WhenAny(regularSendTask, Task.Delay(2000)) != regularSendTask) { - if (await Task.WhenAny(tempResponse, Task.Delay(timeoutMs)) != tempResponse) { // timeout logic return MewtocolFrameResponse.Timeout; + } + //canceled + if (regularSendTask.IsCanceled) return MewtocolFrameResponse.Canceled; + tcpMessagesSentThisCycle++; queuedMessages--; - return tempResponse.Result; + //success + if (regularSendTask.Result.Success) return regularSendTask.Result; + + //no success + if(reconnectTask == null) StartReconnectTask(); + + //await the single reconnect task + await AwaitReconnectTaskAsync(); + + //re-send the command + if (IsConnected) { + + return await SendCommandAsync(_msg, onReceiveProgress); + + } + + return MewtocolFrameResponse.Timeout; } - private protected async Task SendFrameAsync(string frame, bool useBcc = true, bool useCr = true, Action onReceiveProgress = null) { + private protected async Task SendFrameAsync(string frame, Action onReceiveProgress = null) { try { if (stream == null) return MewtocolFrameResponse.NotIntialized; - if (useBcc) - frame = $"{frame.BCC_Mew()}"; - - if (useCr) - frame = $"{frame}\r"; - + frame = $"{frame.BCC_Mew()}\r"; SetUpstreamStopWatchStart(); @@ -334,8 +437,7 @@ namespace MewtocolNet { //calc upstream speed CalcUpstreamSpeed(writeBuffer.Length); - Logger.Log($"[---------CMD START--------]", LogLevel.Critical, this); - Logger.Log($"--> OUT MSG: {frame.Replace("\r", "(CR)")}", LogLevel.Critical, this); + OnOutMsg(frame); var readResult = await ReadCommandAsync(wordsCountRequested, onReceiveProgress); @@ -381,9 +483,7 @@ namespace MewtocolNet { } - 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); + OnInMsg(resString); return new MewtocolFrameResponse(resString); @@ -415,7 +515,7 @@ namespace MewtocolNet { byte[] buffer = new byte[RecBufferSize]; IsReceiving = true; - int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, queue.tSource.Token); IsReceiving = false; CalcDownstreamSpeed(bytesRead); @@ -426,8 +526,7 @@ namespace MewtocolNet { 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); + OnInMsgPart(Encoding.UTF8.GetString(received)); //add complete response to collector without empty bytes totalResponse.AddRange(received.Where(x => x != (byte)0x0)); @@ -437,7 +536,7 @@ namespace MewtocolNet { //request next frame var writeBuffer = Encoding.UTF8.GetBytes($"%{GetStationNumber()}**&\r"); IsSending = true; - await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length); + await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length, queue.tSource.Token); IsSending = false; Logger.Log($">> Requested next frame", LogLevel.Critical, this); wasMultiFramedResponse = true; @@ -463,7 +562,16 @@ namespace MewtocolNet { } while (needsRead); - } catch (OperationCanceledException) { } + } + catch (OperationCanceledException) { } + catch (IOException ex) { + + Logger.LogError($"Socket exception encountered: {ex.Message.ToString()}", this); + + //socket io exception + OnSocketExceptionWhileConnected(); + + } return (totalResponse.ToArray(), wasMultiFramedResponse); @@ -508,10 +616,47 @@ namespace MewtocolNet { } + private protected void OnOutMsg(string outMsg) { + + Logger.Log($"[---------CMD START--------]", LogLevel.Critical, this); + var formatted = $"S -> : {outMsg.Replace("\r", "(CR)")}"; + AddToLastMsgs(formatted); + Logger.Log(formatted, LogLevel.Critical, this); + + } + + private protected void OnInMsgPart(string inPart) { + + var formatted = $"<< IN PART: {inPart.Replace("\r", "(CR)")}"; + AddToLastMsgs(formatted); + Logger.Log(formatted, LogLevel.Critical, this); + + } + + private protected void OnInMsg(string inMsg) { + + var formatted = $"R <- : {inMsg.Replace("\r", "(CR)")}"; + AddToLastMsgs(formatted); + Logger.Log(formatted, LogLevel.Critical, this); + Logger.Log($"[---------CMD END----------]", LogLevel.Critical, this); + + } + + private protected void AddToLastMsgs (string msgTxt) { + + lastMsgs.Add(msgTxt); + if(lastMsgs.Count >= 51) { + lastMsgs.RemoveAt(0); + } + + } + private protected void OnMajorSocketExceptionWhileConnecting() { if (IsConnected) { + queue.CancelAll(); + Logger.Log("The PLC connection timed out", LogLevel.Error, this); OnDisconnect(); @@ -523,6 +668,8 @@ namespace MewtocolNet { if (IsConnected) { + queue.CancelAll(); + Logger.Log("The PLC connection was closed", LogLevel.Error, this); OnDisconnect(); @@ -530,6 +677,22 @@ namespace MewtocolNet { } + private protected void OnMajorSocketExceptionAfterRetries() { + + Logger.LogError($"Failed to re-connect, closing PLC", this); + + OnDisconnect(); + + } + + private protected void OnSocketExceptionWhileConnected () { + + queue.CancelAll(); + + IsConnected = false; + + } + private protected virtual void OnConnected(PLCInfo plcinf) { Logger.Log("Connected to PLC", LogLevel.Info, this); diff --git a/MewtocolNet/MewtocolInterfaceRegisterHandling.cs b/MewtocolNet/MewtocolInterfaceRegisterHandling.cs index 6df6d2b..43a740e 100644 --- a/MewtocolNet/MewtocolInterfaceRegisterHandling.cs +++ b/MewtocolNet/MewtocolInterfaceRegisterHandling.cs @@ -41,7 +41,7 @@ namespace MewtocolNet { } } - private System.Timers.Timer heartBeatTimer = new System.Timers.Timer(); + private System.Timers.Timer heartBeatTimer; #region Register Polling @@ -53,17 +53,24 @@ namespace MewtocolNet { Disconnected += (s, e) => { - heartBeatTimer.Elapsed -= PollTimerTick; - heartBeatTimer.Stop(); + StopHeartBeat(); }; } + private void StopHeartBeat () { + + heartBeatTimer.Elapsed -= PollTimerTick; + heartBeatTimer.Dispose(); + + } + private void TestPollerStartNeeded () { if (!IsConnected) return; + heartBeatTimer = new System.Timers.Timer(); heartBeatTimer.Interval = 3000; heartBeatTimer.Elapsed += PollTimerTick; heartBeatTimer.Start(); @@ -102,10 +109,28 @@ namespace MewtocolNet { private void PollTimerTick(object sender, System.Timers.ElapsedEventArgs e) { - heartbeatTask = Task.Run(async () => { - Logger.LogVerbose("Sending heartbeat", this); - await GetPLCInfoAsync(); - }); + if(!IsConnected || isConnectingStage) return; + + heartbeatNeedsRun = true; + + } + + private bool heartbeatNeedsRun = false; + + private async Task HeartbeatTickTask () { + + if (regularSendTask != null && !regularSendTask.IsCompleted) await regularSendTask; + + Logger.LogVerbose("Sending heartbeat", this); + + if (await GetPLCInfoAsync(2000) == null) { + + Logger.LogError("Heartbeat timed out", this); + + OnSocketExceptionWhileConnected(); + StartReconnectTask(); + + } } @@ -125,6 +150,8 @@ namespace MewtocolNet { pollCycleTask = OnMultiFrameCycle(); await pollCycleTask; + if (!memoryManager.HasCyclicPollableRegisters()) KillPoller(); + return tcpMessagesSentThisCycle; } @@ -144,6 +171,8 @@ namespace MewtocolNet { pollCycleTask = OnMultiFrameCycle(); await pollCycleTask; + if (!memoryManager.HasCyclicPollableRegisters()) KillPoller(); + InvokePolledCycleDone(); if (!IsConnected) { @@ -160,7 +189,12 @@ namespace MewtocolNet { private async Task OnMultiFrameCycle() { //await the timed task before starting a new poller cycle - if (!heartbeatTask.IsCompleted) await heartbeatTask; + if (heartbeatNeedsRun) { + + await HeartbeatTickTask(); + heartbeatNeedsRun = false; + + } var sw = Stopwatch.StartNew(); @@ -169,8 +203,6 @@ namespace MewtocolNet { sw.Stop(); PollerCycleDurationMs = (int)sw.ElapsedMilliseconds; - if (!memoryManager.HasCyclicPollableRegisters()) KillPoller(); - } #endregion @@ -417,8 +449,9 @@ namespace MewtocolNet { for (int i = 0; i < internals.Count; i++) { - var reg = (Register)internals[i]; + var reg = internals[i]; reg.ClearValue(); + //reg.TriggerNotifyChange(); } diff --git a/MewtocolNet/MewtocolInterfaceRequests.cs b/MewtocolNet/MewtocolInterfaceRequests.cs index 8630dd8..78b343c 100644 --- a/MewtocolNet/MewtocolInterfaceRequests.cs +++ b/MewtocolNet/MewtocolInterfaceRequests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace MewtocolNet { @@ -15,6 +16,8 @@ namespace MewtocolNet { internal int maxDataBlocksPerWrite = 8; + private CancellationTokenSource tTaskCancelSource = new CancellationTokenSource(); + #region PLC info getters /// @@ -23,22 +26,15 @@ namespace MewtocolNet { /// A PLCInfo class public async Task GetPLCInfoAsync(int timeout = -1) { - MewtocolFrameResponse resRT = await SendCommandAsync("%EE#RT", timeoutMs: timeout); + MewtocolFrameResponse resRT = await SendCommandAsync("%EE#RT"); - if (!resRT.Success) { - - //timeouts are ok and don't throw - if (resRT == MewtocolFrameResponse.Timeout) return null; - - throw new Exception(resRT.Error); - - } + if (!resRT.Success) return null; MewtocolFrameResponse? resEXRT = null; if(isConnectingStage) { - resEXRT = await SendCommandAsync("%EE#EX00RT00", timeoutMs: timeout); + resEXRT = await SendCommandAsync("%EE#EX00RT00"); } diff --git a/MewtocolNet/MewtocolInterfaceTcp.cs b/MewtocolNet/MewtocolInterfaceTcp.cs index fa00aa8..59c4184 100644 --- a/MewtocolNet/MewtocolInterfaceTcp.cs +++ b/MewtocolNet/MewtocolInterfaceTcp.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using System.Net; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; namespace MewtocolNet { @@ -68,7 +69,45 @@ namespace MewtocolNet { } /// - public override async Task ConnectAsync(Func callBack = null) { + public override async Task ConnectAsync(Func callBack = null) => await ConnectAsyncPriv(callBack); + + private void BuildTcpClient () { + + if (HostEndpoint != null) { + + var hasEndpoint = Mewtocol + .GetSourceEndpoints() + .Any(x => x.Address.ToString() == HostEndpoint.Address.ToString()); + + if (!hasEndpoint) + throw new NotSupportedException($"The specified source endpoint: " + + $"{HostEndpoint}, doesn't exist on the device, " + + $"use 'Mewtocol.GetSourceEndpoints()' to find applicable ones"); + + client = new TcpClient(HostEndpoint) { + ReceiveBufferSize = RecBufferSize, + NoDelay = false, + ReceiveTimeout = sendReceiveTimeoutMs, + SendTimeout = sendReceiveTimeoutMs, + }; + + var ep = (IPEndPoint)client.Client.LocalEndPoint; + Logger.Log($"Connecting [MAN] endpoint: {ep.Address}:{ep.Port}", LogLevel.Info, this); + + } else { + + client = new TcpClient() { + ReceiveBufferSize = RecBufferSize, + NoDelay = false, + ReceiveTimeout = sendReceiveTimeoutMs, + SendTimeout = sendReceiveTimeoutMs, + }; + + } + + } + + private async Task ConnectAsyncPriv(Func callBack = null) { try { @@ -77,33 +116,7 @@ namespace MewtocolNet { Logger.Log($">> Intial connection start <<", LogLevel.Verbose, this); isConnectingStage = true; - if (HostEndpoint != null) { - - var hasEndpoint = Mewtocol - .GetSourceEndpoints() - .Any(x => x.Address.ToString() == HostEndpoint.Address.ToString()); - - if (!hasEndpoint) - throw new NotSupportedException($"The specified source endpoint: " + - $"{HostEndpoint}, doesn't exist on the device, " + - $"use 'Mewtocol.GetSourceEndpoints()' to find applicable ones"); - - client = new TcpClient(HostEndpoint) { - ReceiveBufferSize = RecBufferSize, - NoDelay = false, - }; - var ep = (IPEndPoint)client.Client.LocalEndPoint; - Logger.Log($"Connecting [MAN] endpoint: {ep.Address}:{ep.Port}", LogLevel.Info, this); - - } else { - - client = new TcpClient() { - ReceiveBufferSize = RecBufferSize, - NoDelay = false, - //ExclusiveAddressUse = true, - }; - - } + BuildTcpClient(); var result = client.BeginConnect(ipAddr, Port, null, null); var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(ConnectTimeout)); @@ -125,7 +138,7 @@ namespace MewtocolNet { stream.ReadTimeout = 1000; //get plc info - var plcinf = await GetPLCInfoAsync(); + var plcinf = await GetPLCInfoAsync(ConnectTimeout); if (plcinf != null) { @@ -147,7 +160,71 @@ namespace MewtocolNet { OnMajorSocketExceptionWhileConnecting(); isConnectingStage = false; - } + } + + } + + protected override async Task ReconnectAsync (int conTimeout) { + + try { + + firstPollTask = new Task(() => { }); + + Logger.Log($">> Reconnect start <<", LogLevel.Verbose, this); + + isConnectingStage = true; + + BuildTcpClient(); + + var result = client.BeginConnect(ipAddr, Port, null, null); + var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(conTimeout)); + + if (client.Connected) + Logger.LogVerbose("TCP/IP Client connected", this); + + if (!success || !client.Connected) { + + Logger.Log("The PLC connection timed out", LogLevel.Error, this); + OnMajorSocketExceptionWhileConnecting(); + return; + } + + if (HostEndpoint == null) { + var ep = (IPEndPoint)client.Client.LocalEndPoint; + Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Info, this); + } + + //get the stream + stream = client.GetStream(); + stream.ReadTimeout = 1000; + + Logger.LogVerbose("Attached stream, getting PLC info", this); + + //get plc info + var plcinf = await GetPLCInfoAsync(ConnectTimeout); + + if (plcinf != null) { + + IsConnected = true; + await base.ConnectAsync(); + + Logger.LogVerbose("Connection re-established", this); + OnConnected(plcinf); + + } else { + + Logger.Log("Initial connection failed", LogLevel.Error, this); + OnDisconnect(); + + } + + await Task.CompletedTask; + + } catch (Exception ex) { + + Logger.LogError($"Reconnect exception: {ex.Message}"); + + } } @@ -162,9 +239,9 @@ namespace MewtocolNet { private protected override void OnDisconnect() { - if (IsConnected) { + base.OnDisconnect(); - base.OnDisconnect(); + if (client != null && client.Connected) { client.Close(); diff --git a/MewtocolNet/PLCInfo.cs b/MewtocolNet/PLCInfo.cs index b39f93a..e01f299 100644 --- a/MewtocolNet/PLCInfo.cs +++ b/MewtocolNet/PLCInfo.cs @@ -210,34 +210,12 @@ namespace MewtocolNet { }; - /// - public static bool operator ==(PLCInfo c1, PLCInfo c2) { - return c1.Equals(c2); - } - - /// - public static bool operator !=(PLCInfo c1, PLCInfo c2) { - return !c1.Equals(c2); - } - public override string ToString() { return $"{TypeName}, OP: {OperationMode}"; } - public override bool Equals(object obj) { - - if ((obj == null) || !this.GetType().Equals(obj.GetType())) { - return false; - } else { - return (PLCInfo)obj == this; - } - - } - - public override int GetHashCode() => base.GetHashCode(); - private protected void OnPropChange([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); diff --git a/MewtocolNet/Registers/Base/IRegister.cs b/MewtocolNet/Registers/Base/IRegister.cs index 9c78d5f..9ebd047 100644 --- a/MewtocolNet/Registers/Base/IRegister.cs +++ b/MewtocolNet/Registers/Base/IRegister.cs @@ -24,6 +24,11 @@ namespace MewtocolNet.Registers { /// string Name { get; } + /// + /// The poll level this register is attached to + /// + int PollLevel { get; } + /// /// Gets the register address name as in the plc /// diff --git a/MewtocolNet/Registers/Base/Register.cs b/MewtocolNet/Registers/Base/Register.cs index 3edc0aa..9a50d39 100644 --- a/MewtocolNet/Registers/Base/Register.cs +++ b/MewtocolNet/Registers/Base/Register.cs @@ -65,6 +65,9 @@ namespace MewtocolNet.Registers { /// public uint MemoryAddress => memoryAddress; + /// + int IRegister.PollLevel => pollLevel; + #region Trigger update notify public event PropertyChangedEventHandler PropertyChanged; @@ -82,7 +85,11 @@ namespace MewtocolNet.Registers { internal virtual void UpdateHoldingValue(object val) { - if (lastValue?.ToString() != val?.ToString()) { + bool nullDiff = false; + if (val == null && lastValue != null) nullDiff = true; + if (val != null && lastValue == null) nullDiff = true; + + if (lastValue?.ToString() != val?.ToString() || nullDiff) { var beforeVal = lastValue; var beforeValStr = GetValueString(); diff --git a/MewtocolNet/Registers/Classes/ArrayRegister.cs b/MewtocolNet/Registers/Classes/ArrayRegister.cs index 33f4eba..0d8c2b4 100644 --- a/MewtocolNet/Registers/Classes/ArrayRegister.cs +++ b/MewtocolNet/Registers/Classes/ArrayRegister.cs @@ -66,9 +66,9 @@ namespace MewtocolNet.Registers { lastValue = null; } - public IEnumerator GetEnumerator() => ((Array)ValueObj).OfType().GetEnumerator(); + public IEnumerator GetEnumerator() => ((Array)ValueObj)?.OfType()?.GetEnumerator() ?? Enumerable.Empty().GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => ((Array)ValueObj).OfType().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((Array)ValueObj)?.OfType()?.GetEnumerator() ?? Enumerable.Empty().GetEnumerator(); async Task IArrayRegister.ReadAsync() => (T[])(object)await ReadAsync(); diff --git a/MewtocolNet/SetupClasses/InterfaceSettings.cs b/MewtocolNet/SetupClasses/InterfaceSettings.cs index aa19277..8db7485 100644 --- a/MewtocolNet/SetupClasses/InterfaceSettings.cs +++ b/MewtocolNet/SetupClasses/InterfaceSettings.cs @@ -1,4 +1,6 @@ -namespace MewtocolNet.SetupClasses { +using System; + +namespace MewtocolNet.SetupClasses { public class InterfaceSettings { @@ -34,6 +36,21 @@ /// public int MaxDataBlocksPerWrite { get; set; } = 8; + /// + /// The send and receive timout for messages in milliseconds + /// + public int SendReceiveTimeoutMs { get; set; } = 1000; + + /// + /// Number of attempts to try and reconnect to the plc, 0 for none + /// + public int TryReconnectAttempts { get; set; } = 5; + + /// + /// The delay between reconnect trys + /// + public int TryReconnectDelayMs { get; set; } = 2000; + } }