Fixed canceling messages

This commit is contained in:
Felix Weiß
2023-08-02 17:35:09 +02:00
parent e5020253c9
commit 74cb3bd59f
21 changed files with 353 additions and 98 deletions

View File

@@ -2,8 +2,9 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Examples.WPF" xmlns:local="clr-namespace:Examples.WPF"
xmlns:conv="clr-namespace:Examples.WPF.Converters"
StartupUri="MainWindow.xaml"> StartupUri="MainWindow.xaml">
<Application.Resources> <Application.Resources>
<conv:NegationConverter x:Key="bInv"/>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@@ -0,0 +1,14 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace Examples.WPF.Converters;
[ValueConversion(typeof(bool), typeof(bool))]
public class NegationConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => value != null ? !(bool)value : false;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => value != null ? !(bool)value : false;
}

View File

@@ -59,10 +59,30 @@ public partial class ConnectView : UserControl {
s.TryReconnectAttempts = 30; s.TryReconnectAttempts = 30;
s.TryReconnectDelayMs = 2000; s.TryReconnectDelayMs = 2000;
}) })
.WithCustomPollLevels(l => {
l.SetLevel(2, 3);
l.SetLevel(3, TimeSpan.FromSeconds(5));
l.SetLevel(4, TimeSpan.FromSeconds(10));
})
.WithRegisters(b => { .WithRegisters(b => {
b.Struct<short>("DT0").Build();
b.Struct<short>("DT0").AsArray(30).Build(); //b.Struct<short>("DT0").Build();
b.Struct<Word>("DT1002").Build(); //b.Struct<short>("DT0").AsArray(30).Build();
b.Struct<short>("DT1000").Build();
//b.Struct<Word>("DT1000").Build();
b.Struct<ushort>("DT1001").PollLevel(2).Build();
b.Struct<Word>("DT1002").PollLevel(2).Build();
b.Struct<int>("DDT1010").PollLevel(2).Build();
b.Struct<uint>("DDT1012").PollLevel(2).Build();
b.Struct<DWord>("DDT1014").PollLevel(2).Build();
b.Struct<float>("DDT1016").PollLevel(2).Build();
b.Struct<TimeSpan>("DDT1018").PollLevel(2).Build();
b.String("DT1024", 32).PollLevel(3).Build();
b.String("DT1042", 5).PollLevel(4).Build();
}) })
.Build(); .Build();

View File

@@ -9,14 +9,25 @@
d:DataContext="{d:DesignInstance vm:PlcDataViewViewModel, IsDesignTimeCreatable=True}" d:DataContext="{d:DesignInstance vm:PlcDataViewViewModel, IsDesignTimeCreatable=True}"
d:DesignHeight="450" d:DesignHeight="450"
d:DesignWidth="800"> d:DesignWidth="800">
<Grid Margin="10"> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/> <RowDefinition Height="auto"/>
<RowDefinition/> <RowDefinition/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel> <Menu>
<MenuItem Header="PLC">
<MenuItem Header="Disconnect" IsEnabled="{Binding Plc.IsConnected}"
Click="ClickedDisconnect"/>
<MenuItem Header="Connect" IsEnabled="{Binding Plc.IsConnected, Converter={StaticResource bInv}}"
Click="ClickedConnect"/>
</MenuItem>
</Menu>
<StackPanel Margin="10"
Grid.Row="1">
<TextBlock> <TextBlock>
@@ -71,15 +82,16 @@
</StackPanel> </StackPanel>
<DataGrid Grid.Row="1" <DataGrid Grid.Row="2"
AutoGenerateColumns="False" AutoGenerateColumns="False"
IsReadOnly="True" IsReadOnly="True"
ItemsSource="{Binding Plc.Registers, Mode=OneWay}"> ItemsSource="{Binding Plc.Registers, Mode=OneWay}">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Address" Binding="{Binding PLCAddressName}"/> <DataGridTextColumn Header="FP Address" Binding="{Binding PLCAddressName}"/>
<DataGridTextColumn Header="Name" Binding="{Binding UnderlyingSystemType.Name}"/> <DataGridTextColumn Header="Type" Binding="{Binding UnderlyingSystemType.Name}"/>
<DataGridTextColumn Header="Value" Binding="{Binding ValueStr}"/> <DataGridTextColumn Header="Value" Binding="{Binding ValueStr}"/>
<!--<DataGridTextColumn Header="Value" Binding="{Binding PollLevel, Mode=OneWay}"/>--> <DataGridTextColumn Header="Poll Level" Binding="{Binding PollLevel, Mode=OneWay}"/>
<DataGridTextColumn Header="Update Frequency" Binding="{Binding UpdateFreqHz, StringFormat='{}{0} Hz',Mode=OneWay}"/>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>

View File

@@ -29,4 +29,16 @@ public partial class PlcDataView : UserControl {
} }
private void ClickedDisconnect(object sender, RoutedEventArgs e) {
viewModel.Plc.Disconnect();
}
private async void ClickedConnect(object sender, RoutedEventArgs e) {
await viewModel.Plc.ConnectAsync();
}
} }

View File

@@ -113,6 +113,8 @@ namespace MewtocolNet.ComCassette {
while (!tSource.Token.IsCancellationRequested) { while (!tSource.Token.IsCancellationRequested) {
if (tSource.Token.IsCancellationRequested) break;
var res = await udpClient.ReceiveAsync().WithCancellation(tSource.Token); var res = await udpClient.ReceiveAsync().WithCancellation(tSource.Token);
if (res.Buffer == null) break; if (res.Buffer == null) break;
@@ -131,7 +133,9 @@ namespace MewtocolNet.ComCassette {
} }
} catch (OperationCanceledException) { } catch (SocketException) { } }
catch (OperationCanceledException) { }
catch (SocketException) { }
} }

View File

@@ -11,7 +11,7 @@ namespace MewtocolNet {
var tcs = new TaskCompletionSource<bool>(); var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs)) { using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs)) {
if (task != await Task.WhenAny(task, tcs.Task)) { if (task != await Task.WhenAny(task, tcs.Task)) {
throw new OperationCanceledException(cancellationToken); return default(T);
} }
} }

View File

@@ -10,7 +10,6 @@ namespace MewtocolNet.Helpers {
readonly object _locker = new object(); readonly object _locker = new object();
readonly WeakReference<Task> _lastTask = new WeakReference<Task>(null); readonly WeakReference<Task> _lastTask = new WeakReference<Task>(null);
internal CancellationTokenSource tSource = new CancellationTokenSource();
private List<Task> queuedTasks = new List<Task>(); private List<Task> queuedTasks = new List<Task>();
@@ -37,37 +36,6 @@ namespace MewtocolNet.Helpers {
//} //}
internal Task<T> Enqueue<T>(Func<Task<T>> asyncFunction) {
lock (_locker) {
var token = tSource.Token;
Task lastTask;
Task<T> 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 void CancelAll () {
tSource.Cancel();
Console.WriteLine();
}
} }
} }

View File

@@ -89,14 +89,22 @@ namespace MewtocolNet {
void Disconnect(); void Disconnect();
/// <summary> /// <summary>
/// Calculates the checksum automatically and sends a command to the PLC then awaits results /// Sends a command to the PLC then awaits results<br/>
/// The checksum and BCC are appended automatically
/// </summary> /// </summary>
/// <param name="_msg">MEWTOCOL Formatted request string ex: %01#RT</param> /// <param name="_msg">MEWTOCOL Formatted request string ex: %01#RT</param>
/// <param name="withTerminator">Append the checksum and bcc automatically</param>
/// <param name="timeoutMs">Timout to wait for a response</param>
/// <returns>Returns the result</returns> /// <returns>Returns the result</returns>
Task<MewtocolFrameResponse> SendCommandAsync(string _msg, Action<double> onReceiveProgress = null); Task<MewtocolFrameResponse> SendCommandAsync(string _msg, Action<double> onReceiveProgress = null);
/// <summary>
/// Sends a command to the PLC and awaits its arrival<br/>
/// The checksum and BCC are appended automatically.<br/>
/// <b>Warning!</b> this command is only sent one directional, no error checking or other features included
/// </summary>
/// <param name="_msg">MEWTOCOL Formatted request string ex: %01#RT</param>
/// <returns>Returns true if the message was received</returns>
Task<bool> SendNoResponseCommandAsync(string _msg);
/// <summary> /// <summary>
/// Changes the PLCs operation mode to the given one /// Changes the PLCs operation mode to the given one
/// </summary> /// </summary>

View File

@@ -19,6 +19,8 @@ namespace MewtocolNet
public static MewtocolFrameResponse Canceled => new MewtocolFrameResponse(500, "Op was canceled by the library"); public static MewtocolFrameResponse Canceled => new MewtocolFrameResponse(500, "Op was canceled by the library");
public static MewtocolFrameResponse EmptySuccess => new MewtocolFrameResponse() { Success = true };
public MewtocolFrameResponse(string response) { public MewtocolFrameResponse(string response) {
Success = true; Success = true;

View File

@@ -38,6 +38,9 @@ namespace MewtocolNet {
#region Private fields #region Private fields
//cancellation token for the messages
internal CancellationTokenSource tSource = new CancellationTokenSource();
private protected Stream stream; private protected Stream stream;
private int tcpMessagesSentThisCycle = 0; private int tcpMessagesSentThisCycle = 0;
@@ -64,18 +67,20 @@ namespace MewtocolNet {
private protected bool wasInitialStatusReceived; private protected bool wasInitialStatusReceived;
private protected MewtocolVersion mewtocolVersion; private protected MewtocolVersion mewtocolVersion;
//private protected string[] lastMsgs = new string[10];
private protected List<string> lastMsgs = new List<string>(); private protected List<string> lastMsgs = new List<string>();
#endregion #endregion
#region Internal fields #region Internal fields
internal protected System.Timers.Timer cyclicGenericUpdateCounter;
internal event Action PolledCycle; internal event Action PolledCycle;
internal volatile bool pollerTaskStopped = true; internal volatile bool pollerTaskStopped = true;
internal volatile bool pollerFirstCycle; internal volatile bool pollerFirstCycle;
internal bool usePoller = false; internal bool usePoller = false;
internal MemoryAreaManager memoryManager; internal MemoryAreaManager memoryManager;
private volatile bool isMessageLocked;
private volatile bool isReceiving; private volatile bool isReceiving;
private volatile bool isSending; private volatile bool isSending;
@@ -356,6 +361,11 @@ namespace MewtocolNet {
/// <inheritdoc/> /// <inheritdoc/>
public async Task<MewtocolFrameResponse> SendCommandAsync(string _msg, Action<double> onReceiveProgress = null) { public async Task<MewtocolFrameResponse> SendCommandAsync(string _msg, Action<double> onReceiveProgress = null) {
if (isMessageLocked)
throw new NotSupportedException("Can't send multiple messages in parallel");
isMessageLocked = true;
await AwaitReconnectTaskAsync(); await AwaitReconnectTaskAsync();
if (!IsConnected && !isConnectingStage) if (!IsConnected && !isConnectingStage)
@@ -367,23 +377,35 @@ namespace MewtocolNet {
//wait for the last send task to complete //wait for the last send task to complete
if (regularSendTask != null && !regularSendTask.IsCompleted) await regularSendTask; if (regularSendTask != null && !regularSendTask.IsCompleted) await regularSendTask;
regularSendTask = SendFrameAsync(_msg, onReceiveProgress); regularSendTask = SendFrameAsync(_msg, onReceiveProgress, false);
if (await Task.WhenAny(regularSendTask, Task.Delay(2000)) != regularSendTask) { if (await Task.WhenAny(regularSendTask, Task.Delay(2000)) != regularSendTask) {
isMessageLocked = false;
// timeout logic // timeout logic
return MewtocolFrameResponse.Timeout; return MewtocolFrameResponse.Timeout;
} }
//canceled //canceled
if (regularSendTask.IsCanceled) return MewtocolFrameResponse.Canceled; if (regularSendTask.IsCanceled) {
isMessageLocked = false;
return MewtocolFrameResponse.Canceled;
}
tcpMessagesSentThisCycle++; tcpMessagesSentThisCycle++;
queuedMessages--; queuedMessages--;
//success //success
if (regularSendTask.Result.Success) return regularSendTask.Result; if (regularSendTask.Result.Success) {
isMessageLocked = false;
return regularSendTask.Result;
}
//no success //no success
if(reconnectTask == null) StartReconnectTask(); if(reconnectTask == null) StartReconnectTask();
@@ -394,15 +416,63 @@ namespace MewtocolNet {
//re-send the command //re-send the command
if (IsConnected) { if (IsConnected) {
isMessageLocked = false;
return await SendCommandAsync(_msg, onReceiveProgress); return await SendCommandAsync(_msg, onReceiveProgress);
} }
return MewtocolFrameResponse.Timeout; isMessageLocked = false;
return regularSendTask.Result;
} }
private protected async Task<MewtocolFrameResponse> SendFrameAsync(string frame, Action<double> onReceiveProgress = null) { /// <inheritdoc/>
public async Task<bool> SendNoResponseCommandAsync(string _msg) {
if (isMessageLocked)
throw new NotSupportedException("Can't send multiple messages in parallel");
isMessageLocked = true;
await AwaitReconnectTaskAsync();
if (!IsConnected && !isConnectingStage)
throw new NotSupportedException("The device must be connected to send a message");
//send request
queuedMessages++;
//wait for the last send task to complete
if (regularSendTask != null && !regularSendTask.IsCompleted) await regularSendTask;
regularSendTask = SendFrameAsync(_msg, null, true);
if (await Task.WhenAny(regularSendTask, Task.Delay(2000)) != regularSendTask) {
isMessageLocked = false;
// timeout logic
return false;
}
//canceled
if (regularSendTask.IsCanceled) {
isMessageLocked = false;
return false;
}
tcpMessagesSentThisCycle++;
queuedMessages--;
isMessageLocked = false;
return true;
}
private protected async Task<MewtocolFrameResponse> SendFrameAsync(string frame, Action<double> onReceiveProgress, bool noResponse) {
try { try {
@@ -414,9 +484,11 @@ namespace MewtocolNet {
IsSending = true; IsSending = true;
if (tSource.Token.IsCancellationRequested) return MewtocolFrameResponse.Canceled;
//write inital command //write inital command
byte[] writeBuffer = Encoding.UTF8.GetBytes(frame); byte[] writeBuffer = Encoding.UTF8.GetBytes(frame);
stream.Write(writeBuffer, 0, writeBuffer.Length); await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length, tSource.Token);
IsSending = false; IsSending = false;
@@ -439,6 +511,13 @@ namespace MewtocolNet {
OnOutMsg(frame); OnOutMsg(frame);
if(noResponse) {
Logger.Log($"[---------CMD END----------]", LogLevel.Critical, this);
return MewtocolFrameResponse.EmptySuccess;
}
var readResult = await ReadCommandAsync(wordsCountRequested, onReceiveProgress); var readResult = await ReadCommandAsync(wordsCountRequested, onReceiveProgress);
//did not receive bytes but no errors, the com port was not configured right //did not receive bytes but no errors, the com port was not configured right
@@ -487,6 +566,10 @@ namespace MewtocolNet {
return new MewtocolFrameResponse(resString); return new MewtocolFrameResponse(resString);
} catch (OperationCanceledException) {
return MewtocolFrameResponse.Canceled;
} catch (Exception ex) { } catch (Exception ex) {
return new MewtocolFrameResponse(400, ex.Message.ToString(System.Globalization.CultureInfo.InvariantCulture)); return new MewtocolFrameResponse(400, ex.Message.ToString(System.Globalization.CultureInfo.InvariantCulture));
@@ -515,7 +598,10 @@ namespace MewtocolNet {
byte[] buffer = new byte[RecBufferSize]; byte[] buffer = new byte[RecBufferSize];
IsReceiving = true; IsReceiving = true;
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, queue.tSource.Token);
if (tSource.Token.IsCancellationRequested) break;
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, tSource.Token);
IsReceiving = false; IsReceiving = false;
CalcDownstreamSpeed(bytesRead); CalcDownstreamSpeed(bytesRead);
@@ -536,7 +622,7 @@ namespace MewtocolNet {
//request next frame //request next frame
var writeBuffer = Encoding.UTF8.GetBytes($"%{GetStationNumber()}**&\r"); var writeBuffer = Encoding.UTF8.GetBytes($"%{GetStationNumber()}**&\r");
IsSending = true; IsSending = true;
await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length, queue.tSource.Token); await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length, tSource.Token);
IsSending = false; IsSending = false;
Logger.Log($">> Requested next frame", LogLevel.Critical, this); Logger.Log($">> Requested next frame", LogLevel.Critical, this);
wasMultiFramedResponse = true; wasMultiFramedResponse = true;
@@ -655,7 +741,7 @@ namespace MewtocolNet {
if (IsConnected) { if (IsConnected) {
queue.CancelAll(); tSource.Cancel();
Logger.Log("The PLC connection timed out", LogLevel.Error, this); Logger.Log("The PLC connection timed out", LogLevel.Error, this);
OnDisconnect(); OnDisconnect();
@@ -668,7 +754,7 @@ namespace MewtocolNet {
if (IsConnected) { if (IsConnected) {
queue.CancelAll(); tSource.Cancel();
Logger.Log("The PLC connection was closed", LogLevel.Error, this); Logger.Log("The PLC connection was closed", LogLevel.Error, this);
OnDisconnect(); OnDisconnect();
@@ -687,7 +773,7 @@ namespace MewtocolNet {
private protected void OnSocketExceptionWhileConnected () { private protected void OnSocketExceptionWhileConnected () {
queue.CancelAll(); tSource.Cancel();
IsConnected = false; IsConnected = false;
@@ -697,6 +783,14 @@ namespace MewtocolNet {
Logger.Log("Connected to PLC", LogLevel.Info, this); Logger.Log("Connected to PLC", LogLevel.Info, this);
//start timer for register update data
cyclicGenericUpdateCounter = new System.Timers.Timer(1000);
cyclicGenericUpdateCounter.Elapsed += OnGenericUpdateTimerTick;
cyclicGenericUpdateCounter.Start();
//notify the registers
GetAllRegisters().Cast<Register>().ToList().ForEach(x => x.OnPlcConnected());
IsConnected = true; IsConnected = true;
Connected?.Invoke(this, new PlcConnectionArgs()); Connected?.Invoke(this, new PlcConnectionArgs());
@@ -715,6 +809,13 @@ namespace MewtocolNet {
} }
private void OnGenericUpdateTimerTick(object sender, System.Timers.ElapsedEventArgs e) {
GetAllRegisters().Cast<Register>()
.ToList().ForEach(x => x.OnInterfaceCyclicTimerUpdate((int)cyclicGenericUpdateCounter.Interval));
}
private protected virtual void OnDisconnect() { private protected virtual void OnDisconnect() {
IsReceiving = false; IsReceiving = false;
@@ -730,6 +831,18 @@ namespace MewtocolNet {
Disconnected?.Invoke(this, new PlcConnectionArgs()); Disconnected?.Invoke(this, new PlcConnectionArgs());
KillPoller(); KillPoller();
if(cyclicGenericUpdateCounter != null) {
cyclicGenericUpdateCounter.Elapsed -= OnGenericUpdateTimerTick;
cyclicGenericUpdateCounter.Dispose();
}
GetAllRegisters().Cast<Register>().ToList().ForEach(x => x.OnPlcDisconnected());
//generate a new cancellation token source
tSource = new CancellationTokenSource();
} }
private void SetUpstreamStopWatchStart() { private void SetUpstreamStopWatchStart() {

View File

@@ -61,8 +61,12 @@ namespace MewtocolNet {
private void StopHeartBeat () { private void StopHeartBeat () {
heartBeatTimer.Elapsed -= PollTimerTick; if(heartBeatTimer != null) {
heartBeatTimer.Dispose();
heartBeatTimer.Elapsed -= PollTimerTick;
heartBeatTimer.Dispose();
}
} }

View File

@@ -231,7 +231,9 @@ namespace MewtocolNet {
if (result.Success && !string.IsNullOrEmpty(result.Response)) { if (result.Success && !string.IsNullOrEmpty(result.Response)) {
var bytes = result.Response.ParseDTRawStringAsBytes(); var bytes = result.Response.ParseDTRawStringAsBytes();
readBytes.AddRange(bytes);
if(bytes != null)
readBytes.AddRange(bytes);
} }

View File

@@ -126,17 +126,23 @@ namespace MewtocolNet {
Logger.Log("The PLC connection timed out", LogLevel.Error, this); Logger.Log("The PLC connection timed out", LogLevel.Error, this);
OnMajorSocketExceptionWhileConnecting(); OnMajorSocketExceptionWhileConnecting();
return; return;
} }
Logger.LogVerbose("TCP/IP Client connected", this);
if (HostEndpoint == null) { if (HostEndpoint == null) {
var ep = (IPEndPoint)client.Client.LocalEndPoint; var ep = (IPEndPoint)client.Client.LocalEndPoint;
Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Info, this); Logger.Log($"Connecting [AUTO] from: {ep.Address.MapToIPv4()}:{ep.Port} to {GetConnectionInfo()}", LogLevel.Info, this);
} }
//get the stream //get the stream
stream = client.GetStream(); stream = client.GetStream();
stream.ReadTimeout = 1000; stream.ReadTimeout = 1000;
//try to abort any non read message
//await SendNoResponseCommandAsync($"%{GetStationNumber()}#AB");
//get plc info //get plc info
var plcinf = await GetPLCInfoAsync(ConnectTimeout); var plcinf = await GetPLCInfoAsync(ConnectTimeout);
@@ -179,9 +185,6 @@ namespace MewtocolNet {
var result = client.BeginConnect(ipAddr, Port, null, null); var result = client.BeginConnect(ipAddr, Port, null, null);
var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(conTimeout)); var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(conTimeout));
if (client.Connected)
Logger.LogVerbose("TCP/IP Client connected", this);
if (!success || !client.Connected) { if (!success || !client.Connected) {
Logger.Log("The PLC connection timed out", LogLevel.Error, this); Logger.Log("The PLC connection timed out", LogLevel.Error, this);
@@ -189,6 +192,8 @@ namespace MewtocolNet {
return; return;
} }
Logger.LogVerbose("TCP/IP Client connected", this);
if (HostEndpoint == null) { if (HostEndpoint == null) {
var ep = (IPEndPoint)client.Client.LocalEndPoint; var ep = (IPEndPoint)client.Client.LocalEndPoint;
Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Info, this); Logger.Log($"Connecting [AUTO] endpoint: {ep.Address.MapToIPv4()}:{ep.Port}", LogLevel.Info, this);
@@ -243,7 +248,7 @@ namespace MewtocolNet {
if (client != null && client.Connected) { if (client != null && client.Connected) {
client.Close(); client.Dispose();
} }

View File

@@ -29,6 +29,11 @@ namespace MewtocolNet.Registers {
/// </summary> /// </summary>
int PollLevel { get; } int PollLevel { get; }
/// <summary>
/// The update frequency of the register in Hz
/// </summary>
float UpdateFreqHz { get; }
/// <summary> /// <summary>
/// Gets the register address name as in the plc /// Gets the register address name as in the plc
/// </summary> /// </summary>

View File

@@ -4,6 +4,7 @@ using MewtocolNet.UnderlyingRegisters;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -17,6 +18,8 @@ namespace MewtocolNet.Registers {
/// </summary> /// </summary>
public event RegisterChangedEventHandler ValueChanged; public event RegisterChangedEventHandler ValueChanged;
public event PropertyChangedEventHandler PropertyChanged;
//links to //links to
internal RegisterCollection containedCollection; internal RegisterCollection containedCollection;
internal MewtocolInterface attachedInterface; internal MewtocolInterface attachedInterface;
@@ -36,8 +39,16 @@ namespace MewtocolNet.Registers {
internal uint successfulReads = 0; internal uint successfulReads = 0;
internal uint successfulWrites = 0; internal uint successfulWrites = 0;
internal int updateCountTimerCycle;
internal float updateFreqHz;
internal bool wasOverlapFitted = false; internal bool wasOverlapFitted = false;
private Stopwatch timeSinceLastUpdate;
private float updateFreqFastCount;
private float[] updateFreqAvgList;
/// <inheritdoc/> /// <inheritdoc/>
internal RegisterCollection ContainedCollection => containedCollection; internal RegisterCollection ContainedCollection => containedCollection;
@@ -66,25 +77,110 @@ namespace MewtocolNet.Registers {
public uint MemoryAddress => memoryAddress; public uint MemoryAddress => memoryAddress;
/// <inheritdoc/> /// <inheritdoc/>
int IRegister.PollLevel => pollLevel; public int PollLevel {
get => pollLevel;
internal set {
PollLevel = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PollLevel)));
}
}
/// <inheritdoc/>
public float UpdateFreqHz {
get => updateFreqHz;
private set {
updateFreqHz = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(UpdateFreqHz)));
}
}
internal Register() { }
internal virtual void OnPlcConnected () {
timeSinceLastUpdate = Stopwatch.StartNew();
updateFreqAvgListIteration = 0;
updateFreqAvgList = new float[10];
}
internal virtual void OnPlcDisconnected () {
timeSinceLastUpdate?.Stop();
UpdateFreqHz = default;
updateFreqAvgList = new float[10];
updateFreqFastCount = default;
}
internal virtual void OnInterfaceCyclicTimerUpdate (int cyclicTimerRateMs) {
UpdateCountTimerTick(cyclicTimerRateMs);
}
#region Trigger update notify #region Trigger update notify
public event PropertyChangedEventHandler PropertyChanged; public void TriggerValueChange() {
public void TriggerNotifyChange() {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ValueObj))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ValueObj)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ValueStr))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ValueStr)));
} }
private int updateFreqAvgListIteration = 0;
protected void TriggerUpdateReceived () {
updateCountTimerCycle++;
if(updateCountTimerCycle >= 1) {
var updateRateMultiplicator = (float)timeSinceLastUpdate.ElapsedMilliseconds / 1000;
updateFreqFastCount = ((float)updateCountTimerCycle) / updateRateMultiplicator;
//populate with first value
if(updateFreqAvgListIteration == 0) {
updateFreqAvgList = Enumerable.Repeat<float>(0, updateFreqAvgList.Length).ToArray();
updateFreqAvgListIteration = 1;
} else if(updateFreqAvgListIteration == updateFreqAvgList.Length - 1) {
updateFreqAvgList = Enumerable.Repeat<float>(updateFreqAvgList.Average(), updateFreqAvgList.Length).ToArray();
updateFreqAvgListIteration = 1;
} else {
updateFreqAvgList[updateFreqAvgListIteration] = updateFreqFastCount;
updateFreqAvgListIteration++;
}
//Reset
updateCountTimerCycle = 0;
timeSinceLastUpdate.Restart();
}
}
private void UpdateCountTimerTick(int cyclicTimerRateMs) {
UpdateFreqHz = (float)Math.Round(updateFreqAvgList.Average(), 2);
}
#endregion #endregion
public virtual void ClearValue() => UpdateHoldingValue(null); public virtual void ClearValue() => UpdateHoldingValue(null);
internal virtual void UpdateHoldingValue(object val) { internal virtual void UpdateHoldingValue(object val) {
TriggerUpdateReceived();
bool nullDiff = false; bool nullDiff = false;
if (val == null && lastValue != null) nullDiff = true; if (val == null && lastValue != null) nullDiff = true;
if (val != null && lastValue == null) nullDiff = true; if (val != null && lastValue == null) nullDiff = true;
@@ -96,7 +192,7 @@ namespace MewtocolNet.Registers {
lastValue = val; lastValue = val;
TriggerNotifyChange(); TriggerValueChange();
attachedInterface.InvokeRegisterChanged(this, beforeVal, beforeValStr); attachedInterface.InvokeRegisterChanged(this, beforeVal, beforeValStr);
} }

View File

@@ -43,7 +43,7 @@ namespace MewtocolNet.Registers {
public ArrayRegister() => public ArrayRegister() =>
throw new NotSupportedException("Direct register instancing is not supported, use the builder pattern"); throw new NotSupportedException("Direct register instancing is not supported, use the builder pattern");
internal ArrayRegister(uint _address, uint _reservedByteSize, int[] _indices, string _name = null) { internal ArrayRegister(uint _address, uint _reservedByteSize, int[] _indices, string _name = null) : base() {
if (_reservedByteSize % 2 != 0) if (_reservedByteSize % 2 != 0)
throw new ArgumentException(nameof(_reservedByteSize), "Reserved byte size must be even"); throw new ArgumentException(nameof(_reservedByteSize), "Reserved byte size must be even");
@@ -174,6 +174,8 @@ namespace MewtocolNet.Registers {
/// <inheritdoc/> /// <inheritdoc/>
internal override void UpdateHoldingValue(object val) { internal override void UpdateHoldingValue(object val) {
TriggerUpdateReceived();
if (val == null && lastValue == null) return; if (val == null && lastValue == null) return;
bool sequenceDifference = false; bool sequenceDifference = false;
@@ -196,7 +198,7 @@ namespace MewtocolNet.Registers {
lastValue = val; lastValue = val;
TriggerNotifyChange(); TriggerValueChange();
attachedInterface.InvokeRegisterChanged(this, beforeVal, beforeValStr); attachedInterface.InvokeRegisterChanged(this, beforeVal, beforeValStr);
} }

View File

@@ -18,7 +18,7 @@ namespace MewtocolNet.Registers {
public BoolRegister() => public BoolRegister() =>
throw new NotSupportedException("Direct register instancing is not supported, use the builder pattern"); throw new NotSupportedException("Direct register instancing is not supported, use the builder pattern");
internal BoolRegister(SingleBitPrefix _io, byte _spAddress = 0x0, uint _areaAdress = 0, string _name = null) { internal BoolRegister(SingleBitPrefix _io, byte _spAddress = 0x0, uint _areaAdress = 0, string _name = null) : base() {
lastValue = null; lastValue = null;

View File

@@ -31,7 +31,7 @@ namespace MewtocolNet.Registers {
public StringRegister() => public StringRegister() =>
throw new NotSupportedException("Direct register instancing is not supported, use the builder pattern"); throw new NotSupportedException("Direct register instancing is not supported, use the builder pattern");
internal StringRegister(uint _address, uint _reservedByteSize, string _name = null) { internal StringRegister(uint _address, uint _reservedByteSize, string _name = null) : base() {
memoryAddress = _address; memoryAddress = _address;
name = _name; name = _name;
@@ -122,10 +122,11 @@ namespace MewtocolNet.Registers {
//if string correct the sizing of the byte hint was wrong //if string correct the sizing of the byte hint was wrong
var reservedSize = BitConverter.ToInt16(bytes, 0); var reservedSize = BitConverter.ToInt16(bytes, 0);
if (reservedSize != byteLength - 4)
if (reservedStringLength != reservedSize)
throw new NotSupportedException( throw new NotSupportedException(
$"The STRING register at {GetMewName()} is not correctly sized, " + $"The STRING register at {GetMewName()} is not correctly sized, " +
$"the size should be STRING[{reservedSize}] instead of STRING[{byteLength - 4}]" $"the size should be STRING[{reservedSize}] instead of STRING[{reservedStringLength}]"
); );
AddSuccessRead(); AddSuccessRead();
@@ -139,6 +140,8 @@ namespace MewtocolNet.Registers {
internal override void UpdateHoldingValue(object val) { internal override void UpdateHoldingValue(object val) {
TriggerUpdateReceived();
if (lastValue?.ToString() != val?.ToString()) { if (lastValue?.ToString() != val?.ToString()) {
var beforeVal = lastValue; var beforeVal = lastValue;
@@ -146,7 +149,7 @@ namespace MewtocolNet.Registers {
lastValue = val; lastValue = val;
TriggerNotifyChange(); TriggerValueChange();
attachedInterface.InvokeRegisterChanged(this, beforeVal, beforeValStr); attachedInterface.InvokeRegisterChanged(this, beforeVal, beforeValStr);
} }

View File

@@ -36,7 +36,7 @@ namespace MewtocolNet.Registers {
throw new NotSupportedException("Direct register instancing is not supported, use the builder pattern"); throw new NotSupportedException("Direct register instancing is not supported, use the builder pattern");
//struct for 16-32bit registers //struct for 16-32bit registers
internal StructRegister(uint _address, uint _reservedByteSize, string _name = null) { internal StructRegister(uint _address, uint _reservedByteSize, string _name = null) : base() {
memoryAddress = _address; memoryAddress = _address;
specialAddress = 0x0; specialAddress = 0x0;
@@ -56,7 +56,7 @@ namespace MewtocolNet.Registers {
} }
//struct for one bit registers //struct for one bit registers
internal StructRegister(SingleBitPrefix _io, byte _spAddress = 0x0, uint _areaAdress = 0, string _name = null) { internal StructRegister(SingleBitPrefix _io, byte _spAddress = 0x0, uint _areaAdress = 0, string _name = null) : base() {
lastValue = null; lastValue = null;
@@ -173,22 +173,6 @@ namespace MewtocolNet.Registers {
} }
internal override void UpdateHoldingValue(object val) {
if (lastValue?.ToString() != val?.ToString()) {
var beforeVal = lastValue;
var beforeValStr = GetValueString();
lastValue = val;
TriggerNotifyChange();
attachedInterface.InvokeRegisterChanged(this, beforeVal, beforeValStr);
}
}
} }
} }

View File

@@ -498,7 +498,7 @@ namespace MewtocolNet.UnderlyingRegisters {
} }
return registers; return registers.OrderBy(x => x.MemoryAddress).ThenByDescending(x => x.GetRegisterString());
} }