Fix queue and dc issues

This commit is contained in:
Felix Weiß
2023-08-07 18:05:02 +02:00
parent 82d9aa8b16
commit c41b0cef16
21 changed files with 564 additions and 213 deletions

View File

@@ -26,7 +26,6 @@ internal class Program {
//connect async to the plc
await plc.ConnectAsync();
await plc.SendCommandAsync($"%EE#RP0000000004");
//check if the connection was established
if (!plc.IsConnected) {

View File

@@ -8,7 +8,7 @@
mc:Ignorable="d"
MinWidth="500"
MinHeight="400"
Height="600"
Height="850"
Width="800"
Title="MewtocolNet WPF Demo">
<Grid>
@@ -101,6 +101,13 @@
<TextBlock Text="{Binding AppViewModel.Plc.BytesPerSecondUpstream, Mode=OneWay}"
VerticalAlignment="Center"/>
<Border Width="1"
Margin="5"
Background="Gray"/>
<TextBlock Text="{Binding AppViewModel.Plc.QueuedMessages, StringFormat='{}Q: {0}', Mode=OneWay}"
VerticalAlignment="Center"/>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Style.Triggers>

View File

@@ -1,6 +1,7 @@
using Examples.WPF.ViewModels;
using MewtocolNet;
using MewtocolNet.ComCassette;
using MewtocolNet.Registers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -53,24 +54,28 @@ public partial class ConnectView : UserControl {
var parsedInt = int.Parse(viewModel.SelectedPort);
IRegister<short> heartbeatSetter = null!;
App.ViewModel.Plc = Mewtocol.Ethernet(viewModel.SelectedIP, parsedInt)
.WithPoller()
.WithInterfaceSettings(s => {
s.TryReconnectAttempts = 30;
s.TryReconnectDelayMs = 2000;
.WithInterfaceSettings(setting => {
setting.TryReconnectAttempts = 30;
setting.TryReconnectDelayMs = 2000;
setting.HeartbeatIntervalMs = 3000;
})
.WithCustomPollLevels(l => {
l.SetLevel(2, 3);
l.SetLevel(3, TimeSpan.FromSeconds(5));
l.SetLevel(4, TimeSpan.FromSeconds(10));
.WithCustomPollLevels(lvl => {
lvl.SetLevel(2, 3);
lvl.SetLevel(3, TimeSpan.FromSeconds(5));
lvl.SetLevel(4, TimeSpan.FromSeconds(10));
})
.WithRegisters(b => {
//b.Struct<short>("DT0").Build();
//b.Struct<short>("DT0").AsArray(30).Build();
b.Struct<short>("DT1000").Build();
//b.Struct<Word>("DT1000").Build();
b.Struct<short>("DT1000").Build(out heartbeatSetter);
b.Struct<Word>("DT1000").Build();
b.Struct<ushort>("DT1001").PollLevel(2).Build();
b.Struct<Word>("DT1002").PollLevel(2).Build();
@@ -83,6 +88,11 @@ public partial class ConnectView : UserControl {
b.String("DT1024", 32).PollLevel(3).Build();
b.String("DT1042", 5).PollLevel(4).Build();
})
.WithHeartbeatTask(async () => {
await heartbeatSetter.WriteAsync((short)new Random().Next(short.MinValue, short.MaxValue));
})
.Build();
@@ -92,12 +102,6 @@ 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;

View File

@@ -23,6 +23,10 @@
Click="ClickedDisconnect"/>
<MenuItem Header="Connect" IsEnabled="{Binding Plc.IsConnected, Converter={StaticResource bInv}}"
Click="ClickedConnect"/>
<MenuItem Header="Set Random DT1000" IsEnabled="{Binding Plc.IsConnected}"
Click="ClickedSetRandom"/>
<MenuItem Header="Toggle OP mode" IsEnabled="{Binding Plc.IsConnected}"
Click="ClickedToggleRunMode"/>
</MenuItem>
</Menu>

View File

@@ -1,4 +1,5 @@
using Examples.WPF.ViewModels;
using MewtocolNet.Registers;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -41,4 +42,22 @@ public partial class PlcDataView : UserControl {
}
private async void ClickedSetRandom(object sender, RoutedEventArgs e) {
var reg = (IRegister<ushort>?)viewModel.Plc.GetAllRegisters()?.FirstOrDefault(x => x.PLCAddressName == "DT1001");
if(reg != null) {
await reg.WriteAsync((ushort)new Random().Next(ushort.MinValue, ushort.MaxValue));
}
}
private async void ClickedToggleRunMode(object sender, RoutedEventArgs e) {
await viewModel.Plc.ToggleOperationModeAsync();
}
}

View File

@@ -3,6 +3,7 @@ using MewtocolNet.Registers;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MewtocolNet.Helpers {
internal static class PlcBitConverter {
internal static string ToVersionNumber(IEnumerable<byte> inBytes, int startIndex = 0) {
return string.Join(".", inBytes.Skip(startIndex).Take(4).Reverse().Select(x => x.ToString()));
}
internal static DateTime ToDateTime(IEnumerable<byte> inBytes, int startIndex = 0) {
var offDate = new DateTime(2001, 01, 01);
var secondsOff = BitConverter.ToUInt32(inBytes.ToArray(), startIndex);
return offDate + TimeSpan.FromSeconds(secondsOff);
}
}
}

View File

@@ -94,24 +94,21 @@ namespace MewtocolNet {
/// </summary>
/// <param name="_msg">MEWTOCOL Formatted request string ex: %01#RT</param>
/// <returns>Returns the result</returns>
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);
//Task<MewtocolFrameResponse> SendCommandAsync(string _msg, Action<double> onReceiveProgress = null);
/// <summary>
/// Changes the PLCs operation mode to the given one
/// </summary>
/// <param name="setRun">True for run mode, false for prog mode</param>
/// <param name="setRun">True for RUN mode, false for PROG mode</param>
/// <returns>The success state of the write operation</returns>
Task<bool> SetOperationModeAsync(bool setRun);
/// <summary>
/// Toggles between RUN and PROG mode
/// </summary>
/// <returns>The success state of the write operation</returns>
Task<bool> ToggleOperationModeAsync();
/// <summary>
/// Restarts the plc program
/// </summary>

View File

@@ -321,10 +321,12 @@ namespace MewtocolNet
imew.memoryManager.pollLevelOrMode = res.PollLevelOverwriteMode;
imew.maxDataBlocksPerWrite = res.MaxDataBlocksPerWrite;
imew.sendReceiveTimeoutMs = res.SendReceiveTimeoutMs;
imew.heartbeatIntervalMs = res.HeartbeatIntervalMs;
imew.tryReconnectAttempts = res.TryReconnectAttempts;
imew.tryReconnectDelayMs = res.TryReconnectDelayMs;
imew.alwaysGetMetadata = res.AlwaysGetMetadata;
}
return this;
@@ -352,7 +354,7 @@ namespace MewtocolNet
/// <summary>
/// A builder for attaching register collections
/// </summary>
public EndInit<T> WithRegisterCollections(Action<RegCollector> collector) {
public PostRegisterSetup<T> WithRegisterCollections(Action<RegCollector> collector) {
try {
@@ -363,7 +365,7 @@ namespace MewtocolNet
imew.WithRegisterCollections(res.collections);
}
return new EndInit<T> {
return new PostRegisterSetup<T> {
postInit = this
};
@@ -378,7 +380,7 @@ namespace MewtocolNet
/// <summary>
/// A builder for attaching register collections
/// </summary>
public PostInit<T> WithRegisters(Action<RBuild> builder) {
public PostRegisterSetup<T> WithRegisters(Action<RBuild> builder) {
try {
@@ -389,7 +391,9 @@ namespace MewtocolNet
plc.AddRegisters(regBuilder.assembler.assembled.ToArray());
return this;
return new PostRegisterSetup<T> {
postInit = this,
};
} catch {
@@ -408,12 +412,53 @@ namespace MewtocolNet
#endregion
#region Interface building step 3
public class EndInit<T> {
public class PostRegisterSetup<T> {
internal PostInit<T> postInit;
/// <summary>
/// Repeats the passed method each time the hearbeat is triggered,
/// use
/// </summary>
/// <param name="heartBeatAsync"></param>
/// <returns></returns>
public EndInitSetup<T> WithHeartbeatTask(Func<Task> heartBeatAsync, bool executeInProg = false) {
try {
var plc = (MewtocolInterface)(object)postInit.intf;
plc.heartbeatCallbackTask = heartBeatAsync;
plc.execHeartBeatCallbackTaskInProg = executeInProg;
return new EndInitSetup<T> {
postInit = this.postInit,
postRegSetupInit = this
};
} catch {
throw;
}
}
/// <summary>
/// Builds and returns the final plc interface
/// </summary>
public T Build() => postInit.intf;
}
#region Interface building step 4
public class EndInitSetup<T> {
internal PostInit<T> postInit;
internal PostRegisterSetup<T> postRegSetupInit;
/// <summary>
/// Builds and returns the final plc interface
/// </summary>

View File

@@ -10,6 +10,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
@@ -25,6 +26,9 @@ namespace MewtocolNet {
/// <inheritdoc/>
public event PlcConnectionEventHandler Connected;
/// <inheritdoc/>
public event PlcConnectionEventHandler Reconnected;
/// <inheritdoc/>
public event PlcConnectionEventHandler Disconnected;
@@ -45,7 +49,6 @@ namespace MewtocolNet {
private int tcpMessagesSentThisCycle = 0;
private int pollerCycleDurationMs;
private volatile int queuedMessages;
private bool isConnected;
private PLCInfo plcInfo;
private protected int stationNumber;
@@ -57,12 +60,14 @@ namespace MewtocolNet {
private protected int bytesPerSecondUpstream = 0;
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;
protected Task firstPollTask;
protected Task reconnectTask;
protected Task<MewtocolFrameResponse> regularSendTask;
protected Queue<Task<MewtocolFrameResponse>> userInputSendTasks = new Queue<Task<MewtocolFrameResponse>>();
private protected bool wasInitialStatusReceived;
private protected MewtocolVersion mewtocolVersion;
@@ -77,17 +82,22 @@ namespace MewtocolNet {
internal event Action PolledCycle;
internal volatile bool pollerTaskStopped = true;
internal volatile bool pollerFirstCycle;
internal bool usePoller = false;
internal MemoryAreaManager memoryManager;
private volatile bool isMessageLocked;
//configuration
private protected bool isMessageLocked;
private volatile bool isReceiving;
private volatile bool isSending;
internal int sendReceiveTimeoutMs = 1000;
internal int heartbeatIntervalMs = 3000;
internal int tryReconnectAttempts = 0;
internal int tryReconnectDelayMs = 1000;
internal bool usePoller = false;
internal bool alwaysGetMetadata = true;
#endregion
#region Public Read Only Properties / Fields
@@ -96,7 +106,7 @@ namespace MewtocolNet {
public bool Disposed { get; private set; }
/// <inheritdoc/>
public int QueuedMessages => queuedMessages;
public int QueuedMessages => userInputSendTasks.Count;
/// <inheritdoc/>
public bool IsConnected {
@@ -244,6 +254,20 @@ namespace MewtocolNet {
Logger.Log($"DIAG ERR: {PlcInfo.SelfDiagnosticError}", LogLevel.Verbose, this);
Logger.Log($"CPU VER: {PlcInfo.CpuVersion}", LogLevel.Verbose, this);
if(alwaysGetMetadata) {
Logger.LogVerbose($"METADATA: {PlcInfo.Metadata.MetaDataVersion}", this);
Logger.LogVerbose($"FP-WIN VERSION: {PlcInfo.Metadata.FPWinVersion}", this);
Logger.LogVerbose($"Project VERSION: {PlcInfo.Metadata.ProjectVersion}", this);
Logger.LogVerbose($"Company ID: {PlcInfo.Metadata.CompanyID}", this);
Logger.LogVerbose($"Application ID: {PlcInfo.Metadata.ApplicationID}", this);
Logger.LogVerbose($"Project ID: {PlcInfo.Metadata.ProjectID}", this);
Logger.LogVerbose($"LAST CONF CHNG: {PlcInfo.Metadata.LastConfigChangeDate}", this);
Logger.LogVerbose($"LAST POU CHNG: {PlcInfo.Metadata.LastPouChangeDate}", this);
Logger.LogVerbose($"LAST USR-LIB CHNG: {PlcInfo.Metadata.LastUserLibChangeDate}", this);
}
Logger.Log($">> Intial connection end <<", LogLevel.Verbose, this);
if (callBack != null) {
@@ -348,131 +372,120 @@ namespace MewtocolNet {
}
internal async Task AwaitReconnectTaskAsync () {
//internally used send task
internal async Task<MewtocolFrameResponse> SendCommandInternalAsync(string _msg, Action<double> onReceiveProgress = null) {
if (reconnectTask != null && !reconnectTask.IsCompleted) await reconnectTask;
if (regularSendTask != null && !regularSendTask.IsCompleted) {
await Task.CompletedTask;
//queue self
var t = new Task<MewtocolFrameResponse>(() => SendCommandInternalAsync(_msg, onReceiveProgress).Result);
userInputSendTasks.Enqueue(t);
OnPropChange(nameof(QueuedMessages));
await t;
OnPropChange(nameof(QueuedMessages));
return t.Result;
}
private Task<MewtocolFrameResponse> regularSendTask;
if (tSource.Token.IsCancellationRequested) return MewtocolFrameResponse.Canceled;
/// <inheritdoc/>
public async Task<MewtocolFrameResponse> SendCommandAsync(string _msg, Action<double> onReceiveProgress = null) {
if (isMessageLocked)
throw new NotSupportedException("Can't send multiple messages in parallel");
if (!IsConnected && !isConnectingStage && !isReconnectingStage)
throw new NotSupportedException("The device must be connected to send a message");
isMessageLocked = true;
await AwaitReconnectTaskAsync();
if (!IsConnected && !isConnectingStage)
throw new NotSupportedException("The device must be connected to send a message");
//wait for the last send task to complete
//if (regularSendTask != null && !regularSendTask.IsCompleted) await regularSendTask;
//send request
queuedMessages++;
regularSendTask = SendTwoDirectionalFrameAsync(_msg, onReceiveProgress);
//wait for the last send task to complete
if (regularSendTask != null && !regularSendTask.IsCompleted) await regularSendTask;
//if (regularSendTask == null) return MewtocolFrameResponse.Canceled;
regularSendTask = SendFrameAsync(_msg, onReceiveProgress, false);
try {
if (await Task.WhenAny(regularSendTask, Task.Delay(2000)) != regularSendTask) {
var timeoutAwaiter = await Task.WhenAny(regularSendTask, Task.Delay(2000, tSource.Token));
if (timeoutAwaiter != regularSendTask) {
isMessageLocked = false;
regularSendTask = null;
// timeout logic
return MewtocolFrameResponse.Timeout;
}
//canceled
if (regularSendTask.IsCanceled) {
} catch (OperationCanceledException) {
isMessageLocked = false;
return MewtocolFrameResponse.Canceled;
}
tcpMessagesSentThisCycle++;
queuedMessages--;
//success
if (regularSendTask.Result.Success) {
isMessageLocked = false;
return regularSendTask.Result;
}
//no success
if(reconnectTask == null) StartReconnectTask();
//await the single reconnect task
await AwaitReconnectTaskAsync();
//re-send the command
if (IsConnected) {
isMessageLocked = false;
return await SendCommandAsync(_msg, onReceiveProgress);
}
isMessageLocked = false;
return regularSendTask.Result;
}
/// <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;
regularSendTask = null;
return MewtocolFrameResponse.Canceled;
}
MewtocolFrameResponse responseData = regularSendTask.Result;
tcpMessagesSentThisCycle++;
queuedMessages--;
isMessageLocked = false;
return true;
regularSendTask = null;
return responseData;
}
private protected async Task<MewtocolFrameResponse> SendFrameAsync(string frame, Action<double> onReceiveProgress, bool noResponse) {
private protected async Task<MewtocolFrameResponse> SendOneDirectionalFrameAsync (string frame) {
try {
if (stream == null) return MewtocolFrameResponse.NotIntialized;
frame = $"{frame.BCC_Mew()}\r";
SetUpstreamStopWatchStart();
IsSending = true;
if (tSource.Token.IsCancellationRequested) return MewtocolFrameResponse.Canceled;
//write inital command
byte[] writeBuffer = Encoding.UTF8.GetBytes(frame);
await stream.WriteAsync(writeBuffer, 0, writeBuffer.Length, tSource.Token);
IsSending = false;
//calc upstream speed
CalcUpstreamSpeed(writeBuffer.Length);
OnOutMsg(frame);
OnEndMsg();
} catch (Exception ex) {
IsSending = false;
return new MewtocolFrameResponse(400, ex.Message.ToString());
}
return MewtocolFrameResponse.EmptySuccess;
}
private protected async Task<MewtocolFrameResponse> SendTwoDirectionalFrameAsync(string frame, Action<double> onReceiveProgress = null) {
try {
@@ -511,13 +524,6 @@ namespace MewtocolNet {
OnOutMsg(frame);
if(noResponse) {
Logger.Log($"[---------CMD END----------]", LogLevel.Critical, this);
return MewtocolFrameResponse.EmptySuccess;
}
var readResult = await ReadCommandAsync(wordsCountRequested, onReceiveProgress);
//did not receive bytes but no errors, the com port was not configured right
@@ -724,6 +730,12 @@ namespace MewtocolNet {
var formatted = $"R <- : {inMsg.Replace("\r", "(CR)")}";
AddToLastMsgs(formatted);
Logger.Log(formatted, LogLevel.Critical, this);
OnEndMsg();
}
private protected void OnEndMsg () {
Logger.Log($"[---------CMD END----------]", LogLevel.Critical, this);
}
@@ -742,6 +754,7 @@ namespace MewtocolNet {
if (IsConnected) {
tSource.Cancel();
isMessageLocked = false;
Logger.Log("The PLC connection timed out", LogLevel.Error, this);
OnDisconnect();
@@ -755,6 +768,7 @@ namespace MewtocolNet {
if (IsConnected) {
tSource.Cancel();
isMessageLocked = false;
Logger.Log("The PLC connection was closed", LogLevel.Error, this);
OnDisconnect();
@@ -774,9 +788,11 @@ namespace MewtocolNet {
private protected void OnSocketExceptionWhileConnected () {
tSource.Cancel();
isMessageLocked = false;
IsConnected = false;
if (reconnectTask == null) StartReconnectTask();
}
private protected virtual void OnConnected(PLCInfo plcinf) {
@@ -809,10 +825,30 @@ namespace MewtocolNet {
}
private void OnGenericUpdateTimerTick(object sender, System.Timers.ElapsedEventArgs e) {
private protected void OnReconnected () {
GetAllRegisters().Cast<Register>()
.ToList().ForEach(x => x.OnInterfaceCyclicTimerUpdate((int)cyclicGenericUpdateCounter.Interval));
IsReceiving = false;
IsSending = false;
BytesPerSecondDownstream = 0;
BytesPerSecondUpstream = 0;
PollerCycleDurationMs = 0;
isMessageLocked = false;
ClearRegisterVals();
KillPoller();
//GetAllRegisters().Cast<Register>().ToList().ForEach(x => x.OnPlcDisconnected());
//generate a new cancellation token source
tSource = new CancellationTokenSource();
IsConnected = true;
isReconnectingStage = false;
isConnectingStage = false;
reconnectTask = null;
Reconnected?.Invoke(this, new PlcConnectionArgs());
}
@@ -825,6 +861,7 @@ namespace MewtocolNet {
PollerCycleDurationMs = 0;
PlcInfo = null;
isMessageLocked = false;
IsConnected = false;
ClearRegisterVals();
@@ -845,6 +882,13 @@ namespace MewtocolNet {
}
private void OnGenericUpdateTimerTick(object sender, System.Timers.ElapsedEventArgs e) {
GetAllRegisters().Cast<Register>()
.ToList().ForEach(x => x.OnInterfaceCyclicTimerUpdate((int)cyclicGenericUpdateCounter.Interval));
}
private void SetUpstreamStopWatchStart() {
if (speedStopwatchUpstr == null) {

View File

@@ -19,6 +19,9 @@ namespace MewtocolNet {
internal Task heartbeatTask = Task.CompletedTask;
internal Func<Task> heartbeatCallbackTask;
internal bool execHeartBeatCallbackTaskInProg = false;
internal Task pollCycleTask;
private List<RegisterCollection> registerCollections = new List<RegisterCollection>();
@@ -43,6 +46,8 @@ namespace MewtocolNet {
private System.Timers.Timer heartBeatTimer;
internal volatile bool pollerFirstCycleCompleted;
#region Register Polling
internal void WatchPollerDemand() {
@@ -50,6 +55,7 @@ namespace MewtocolNet {
memoryManager.MemoryLayoutChanged += () => TestPollerStartNeeded();
Connected += (s, e) => TestPollerStartNeeded();
Reconnected += (s, e) => TestPollerStartNeeded();
Disconnected += (s, e) => {
@@ -93,6 +99,7 @@ namespace MewtocolNet {
/// </summary>
internal void KillPoller() {
pollerFirstCycleCompleted = false;
pollerTaskStopped = true;
}
@@ -104,6 +111,7 @@ namespace MewtocolNet {
if (!pollerTaskStopped) return;
pollerFirstCycleCompleted = false;
PollerCycleDurationMs = 0;
pollerFirstCycle = true;
@@ -113,7 +121,9 @@ namespace MewtocolNet {
private void PollTimerTick(object sender, System.Timers.ElapsedEventArgs e) {
if(!IsConnected || isConnectingStage) return;
if(!IsConnected || isConnectingStage || isReconnectingStage) return;
heartBeatTimer.Stop();
heartbeatNeedsRun = true;
@@ -127,15 +137,24 @@ namespace MewtocolNet {
Logger.LogVerbose("Sending heartbeat", this);
if (await GetPLCInfoAsync(2000) == null) {
if (await GetInfoAsync() == null) {
Logger.LogError("Heartbeat timed out", this);
OnSocketExceptionWhileConnected();
StartReconnectTask();
//OnSocketExceptionWhileConnected();
//StartReconnectTask();
return;
}
if(heartbeatCallbackTask != null && (plcInfo.IsRunMode || execHeartBeatCallbackTaskInProg))
await heartbeatCallbackTask();
Logger.LogVerbose("End heartbeat", this);
heartBeatTimer.Start();
}
/// <summary>
@@ -196,15 +215,30 @@ namespace MewtocolNet {
if (heartbeatNeedsRun) {
await HeartbeatTickTask();
heartbeatNeedsRun = false;
}
var sw = Stopwatch.StartNew();
await memoryManager.PollAllAreasAsync();
await memoryManager.PollAllAreasAsync(async () => {
if (userInputSendTasks != null && userInputSendTasks.Count > 0) {
var t = userInputSendTasks.Dequeue();
t.Start();
await t;
}
});
sw.Stop();
pollerFirstCycleCompleted = true;
PollerCycleDurationMs = (int)sw.ElapsedMilliseconds;
}

View File

@@ -7,6 +7,8 @@ using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MewtocolNet.Helpers;
using System.Reflection;
namespace MewtocolNet {
@@ -14,6 +16,8 @@ namespace MewtocolNet {
internal bool isConnectingStage = false;
internal protected bool isReconnectingStage = false;
internal int maxDataBlocksPerWrite = 8;
private CancellationTokenSource tTaskCancelSource = new CancellationTokenSource();
@@ -24,17 +28,17 @@ namespace MewtocolNet {
/// Gets generic information about the PLC
/// </summary>
/// <returns>A PLCInfo class</returns>
public async Task<PLCInfo> GetPLCInfoAsync(int timeout = -1) {
public async Task<PLCInfo> GetInfoAsync(bool detailed = true) {
MewtocolFrameResponse resRT = await SendCommandAsync("%EE#RT");
MewtocolFrameResponse resRT = await SendCommandInternalAsync("%EE#RT");
if (!resRT.Success) return null;
if (!resRT.Success || tSource.Token.IsCancellationRequested) return null;
MewtocolFrameResponse? resEXRT = null;
if(isConnectingStage) {
if (isConnectingStage && detailed) {
resEXRT = await SendCommandAsync("%EE#EX00RT00");
resEXRT = await SendCommandInternalAsync("%EE#EX00RT00");
}
@@ -50,6 +54,8 @@ namespace MewtocolNet {
}
if(!detailed) return plcInf;
//overwrite first with EXRT only on connecting stage
if (isConnectingStage && resEXRT != null && resEXRT.Value.Success && !plcInf.TryExtendFromEXRT(resEXRT.Value.Response)) {
@@ -57,7 +63,7 @@ namespace MewtocolNet {
}
if(isConnectingStage) {
if (isConnectingStage) {
//set the intial obj
PlcInfo = plcInf;
} else {
@@ -70,6 +76,55 @@ namespace MewtocolNet {
}
/// <summary>
/// Gets the metadata information for the PLC program
/// </summary>
/// <returns>The metadata or null of it isn't used by the PLC program</returns>
/// <exception cref="NotSupportedException"></exception>
public async Task<PlcMetadata> GetMetadataAsync() {
//if the prog capacity and plc type are unknown retrieve them first
if (PlcInfo == null) await GetInfoAsync();
//still 0
if (PlcInfo.ProgramCapacity == 0) throw new NotSupportedException("Unable to access the program capacity of the PLC");
//meta data is always at last dt addresses of the plc
//so we take the capacity in k and multiply by 1024 to get the last register index and sub 3
//to get the last readable registers
var endAddress = (int)PlcInfo.ProgramCapacity * 1024 - 3; //32765 for 32k
var readBytes = 42;
var metaMarker = new byte[] { 0x4D, 0x65, 0x74, 0x41 };
var data = await ReadByteRangeNonBlocking(endAddress - 2 - (readBytes / 2), readBytes);
if (data != null && data.SearchBytePattern(metaMarker) == readBytes - 4) {
var meta = new PlcMetadata {
LastUserLibChangeDate = PlcBitConverter.ToDateTime(data, 0),
LastPouChangeDate = PlcBitConverter.ToDateTime(data, 4),
LastConfigChangeDate = PlcBitConverter.ToDateTime(data, 8),
FPWinVersion = PlcBitConverter.ToVersionNumber(data, 12),
ProjectVersion = PlcBitConverter.ToVersionNumber(data, 16),
ProjectID = BitConverter.ToUInt32(data, 20),
ApplicationID = BitConverter.ToUInt32(data, 24),
CompanyID = BitConverter.ToUInt32(data, 28),
MetaDataVersion = PlcBitConverter.ToVersionNumber(data, 32),
};
PlcInfo.Metadata = meta;
return meta;
}
return null;
}
#endregion
#region Operation mode changing
@@ -80,7 +135,7 @@ namespace MewtocolNet {
string modeChar = setRun ? "R" : "P";
string requeststring = $"%{GetStationNumber()}#RM{modeChar}";
var result = await SendCommandAsync(requeststring);
var result = await SendCommandInternalAsync(requeststring);
if (result.Success) {
Logger.Log($"Operation mode was changed to {(setRun ? "Run" : "Prog")}", LogLevel.Info, this);
@@ -93,7 +148,7 @@ namespace MewtocolNet {
}
/// <inheritdoc/>
public async Task<bool> RestartProgramAsync () {
public async Task<bool> RestartProgramAsync() {
return await SetOperationModeAsync(false) &&
await SetOperationModeAsync(true);
@@ -101,14 +156,23 @@ namespace MewtocolNet {
}
/// <inheritdoc/>
public async Task FactoryResetAsync () {
public async Task<bool> ToggleOperationModeAsync() {
var currMode = await GetInfoAsync(false);
return await SetOperationModeAsync(!currMode.IsRunMode);
}
/// <inheritdoc/>
public async Task FactoryResetAsync() {
//set to prog mode
await SetOperationModeAsync(false);
//reset plc
await SendCommandAsync($"%{GetStationNumber()}#0F");
await SendCommandAsync($"%{GetStationNumber()}#21");
await SendCommandInternalAsync($"%{GetStationNumber()}#0F");
await SendCommandInternalAsync($"%{GetStationNumber()}#21");
}
@@ -116,13 +180,13 @@ namespace MewtocolNet {
#region Program Read / Write
public async Task<PlcBinaryProgram> ReadProgramAsync () {
public async Task<PlcBinaryProgram> ReadProgramAsync() {
var steps = new List<byte[]>();
int i = 0;
int stepsPerReq = 50;
while(i < int.MaxValue) {
while (i < int.MaxValue) {
var sb = new StringBuilder($"%{GetStationNumber()}#RP");
var stp1 = (i * stepsPerReq);
@@ -131,7 +195,7 @@ namespace MewtocolNet {
sb.Append(stp1.ToString().PadLeft(5, '0'));
sb.Append(stp2.ToString().PadLeft(5, '0'));
var res = await SendCommandAsync(sb.ToString());
var res = await SendCommandInternalAsync(sb.ToString());
if (res.Success) {
@@ -188,7 +252,7 @@ namespace MewtocolNet {
string endStr = (start + wordLength - 1).ToString().PadLeft(5, '0');
string requeststring = $"%{GetStationNumber()}#WDD{startStr}{endStr}{byteString}";
var result = await SendCommandAsync(requeststring);
var result = await SendCommandInternalAsync(requeststring);
return result.Success;
@@ -218,7 +282,7 @@ namespace MewtocolNet {
List<byte> readBytes = new List<byte>();
async Task ReadBlock (int wordStart, int wordEnd, Action<double> readProg) {
async Task ReadBlock(int wordStart, int wordEnd, Action<double> readProg) {
int blockSize = wordEnd - wordStart + 1;
string startStr = wordStart.ToString().PadLeft(5, '0');
@@ -226,13 +290,13 @@ namespace MewtocolNet {
string requeststring = $"%{GetStationNumber()}#RDD{startStr}{endStr}";
var result = await SendCommandAsync(requeststring, onReceiveProgress: readProg);
var result = await SendCommandInternalAsync(requeststring, onReceiveProgress: readProg);
if (result.Success && !string.IsNullOrEmpty(result.Response)) {
var bytes = result.Response.ParseDTRawStringAsBytes();
if(bytes != null)
if (bytes != null)
readBytes.AddRange(bytes);
}
@@ -265,7 +329,7 @@ namespace MewtocolNet {
curWordStart = start + ((i + 1) * maxReadBlockSize);
curWordEnd = curWordStart + blocksOverflow - 1;
await ReadBlock(curWordStart, curWordEnd, (p) => {});
await ReadBlock(curWordStart, curWordEnd, (p) => { });
}

View File

@@ -252,10 +252,12 @@ namespace MewtocolNet {
Logger.Log($"Opened [SERIAL]: {GetConnectionInfo()}", LogLevel.Critical, this);
var plcinf = await GetPLCInfoAsync(100);
var plcinf = await GetInfoAsync();
if (plcinf == null) CloseClient();
if (alwaysGetMetadata) await GetMetadataAsync();
return plcinf;
} catch (UnauthorizedAccessException) {

View File

@@ -140,14 +140,13 @@ namespace MewtocolNet {
stream = client.GetStream();
stream.ReadTimeout = 1000;
//try to abort any non read message
//await SendNoResponseCommandAsync($"%{GetStationNumber()}#AB");
//get plc info
var plcinf = await GetPLCInfoAsync(ConnectTimeout);
var plcinf = await GetInfoAsync();
if (plcinf != null) {
if(alwaysGetMetadata) await GetMetadataAsync();
IsConnected = true;
await base.ConnectAsync(callBack);
OnConnected(plcinf);
@@ -178,56 +177,72 @@ namespace MewtocolNet {
Logger.Log($">> Reconnect start <<", LogLevel.Verbose, this);
isReconnectingStage = true;
isConnectingStage = true;
IsConnected = false;
BuildTcpClient();
var result = client.BeginConnect(ipAddr, Port, null, null);
var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(conTimeout));
var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(ConnectTimeout));
if (!success || !client.Connected) {
Logger.Log("The PLC connection timed out", LogLevel.Error, this);
OnMajorSocketExceptionWhileConnecting();
return;
}
Logger.LogVerbose("TCP/IP Client connected", this);
if (HostEndpoint == null) {
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
stream = client.GetStream();
stream.ReadTimeout = 1000;
Logger.LogVerbose("Attached stream, getting PLC info", this);
isMessageLocked = false;
//get plc info
var plcinf = await GetPLCInfoAsync(ConnectTimeout);
//null ongoing tasks
regularSendTask = null;
reconnectTask = Task.CompletedTask;
if (plcinf != null) {
//try to abort any non read message
//await SendNoResponseCommandAsync($"%{GetStationNumber()}#AB");
IsConnected = true;
await base.ConnectAsync();
//get plc info 2 times to clear old stuff from the buffer
Logger.LogVerbose("Connection re-established", this);
OnConnected(plcinf);
OnReconnected();
} else {
//var plcinf = await SendCommandAsync($"%{GetStationNumber()}#RT");
Logger.Log("Initial connection failed", LogLevel.Error, this);
OnDisconnect();
//if (plcinf != null) {
}
// Logger.Log("Reconnect successfull");
// OnReconnected();
// //await base.ConnectAsync();
// //OnConnected(plcinf);
//} else {
// Logger.Log("Initial connection failed", LogLevel.Error, this);
// OnDisconnect();
//}
await Task.CompletedTask;
} catch (Exception ex) {
} catch (SocketException) {
Logger.LogError($"Reconnect exception: {ex.Message}");
OnMajorSocketExceptionWhileConnecting();
isConnectingStage = false;
isReconnectingStage = false;
}

View File

@@ -18,6 +18,7 @@ namespace MewtocolNet {
private OPMode operationMode;
private HWInformation hardwareInformation;
private string selfDiagnosticError;
private PlcMetadata metadata;
/// <summary>
/// The type of the PLC named by Panasonic
@@ -103,6 +104,17 @@ namespace MewtocolNet {
/// </summary>
public bool IsRunMode => OperationMode.HasFlag(OPMode.RunMode);
/// <summary>
/// Contains useful information about the PLC program and metadata
/// </summary>
public PlcMetadata Metadata {
get => metadata;
internal set {
metadata = value;
OnPropChange();
}
}
public event PropertyChangedEventHandler PropertyChanged;
internal bool TryExtendFromEXRT(string msg) {

View File

@@ -0,0 +1,56 @@
using System;
namespace MewtocolNet {
/// <summary>
/// Contains useful information about the PLC program and metadata
/// </summary>
public class PlcMetadata {
/// <summary>
/// The last date the used user librarys were changed
/// </summary>
public DateTime LastUserLibChangeDate { get; internal set; }
/// <summary>
/// The last date the program Pou's were changed
/// </summary>
public DateTime LastPouChangeDate { get; internal set; }
/// <summary>
/// The last date the PLC configuration was changed
/// </summary>
public DateTime LastConfigChangeDate { get; internal set; }
/// <summary>
/// The used FP-Win version to create the PLC program
/// </summary>
public string FPWinVersion { get; internal set; }
/// <summary>
/// The custom project version of the PLC program
/// </summary>
public string ProjectVersion { get; internal set; }
/// <summary>
/// Metadata format version
/// </summary>
public string MetaDataVersion { get; internal set; }
/// <summary>
/// The project ID of the PLC program
/// </summary>
public uint ProjectID { get; internal set; }
/// <summary>
/// The application / machine specific ID for the PLC program
/// </summary>
public uint ApplicationID { get; internal set; }
/// <summary>
/// The company ID of the PLC program creator
/// </summary>
public uint CompanyID { get; internal set; }
}
}

View File

@@ -134,6 +134,8 @@ namespace MewtocolNet.Registers {
protected void TriggerUpdateReceived () {
if (timeSinceLastUpdate == null) return;
updateCountTimerCycle++;
if(updateCountTimerCycle >= 1) {

View File

@@ -123,7 +123,9 @@ namespace MewtocolNet.Registers {
//if string correct the sizing of the byte hint was wrong
var reservedSize = BitConverter.ToInt16(bytes, 0);
if (reservedStringLength != reservedSize)
if (reservedStringLength != reservedSize &&
attachedInterface.PlcInfo.IsRunMode &&
(attachedInterface.pollerTaskStopped || attachedInterface.pollerFirstCycleCompleted))
throw new NotSupportedException(
$"The STRING register at {GetMewName()} is not correctly sized, " +
$"the size should be STRING[{reservedSize}] instead of STRING[{reservedStringLength}]"

View File

@@ -41,6 +41,11 @@ namespace MewtocolNet.SetupClasses {
/// </summary>
public int SendReceiveTimeoutMs { get; set; } = 1000;
/// <summary>
/// The heartbeat interval in milliseconds, it is recommended to use 3000ms
/// </summary>
public int HeartbeatIntervalMs { get; set; } = 3000;
/// <summary>
/// Number of attempts to try and reconnect to the plc, 0 for none
/// </summary>
@@ -51,6 +56,11 @@ namespace MewtocolNet.SetupClasses {
/// </summary>
public int TryReconnectDelayMs { get; set; } = 2000;
/// <summary>
/// Sets wether or not the interface should always retrieve metadata on connection start
/// </summary>
public bool AlwaysGetMetadata { get; set; } = true;
}
}

View File

@@ -106,12 +106,16 @@ namespace MewtocolNet.UnderlyingRegisters {
private void SetUnderlyingBytes(byte[] bytes, ulong addStart) {
int copyOffset = (int)((addStart - addressStart) * 2);
bytes.CopyTo(underlyingBytes, copyOffset);
if(bytes.Length + copyOffset <= underlyingBytes.Length) {
bytes.CopyTo(underlyingBytes, copyOffset);
UpdateAreaRegisterValues();
}
}
private string GetMewtocolIdent() {
StringBuilder asciistring = new StringBuilder("D");

View File

@@ -314,7 +314,7 @@ namespace MewtocolNet.UnderlyingRegisters {
}
internal async Task PollAllAreasAsync() {
internal async Task PollAllAreasAsync(Func<Task> inbetweenCallback) {
foreach (var pollLevel in pollLevels) {
@@ -363,6 +363,8 @@ namespace MewtocolNet.UnderlyingRegisters {
//set the whole memory area at once
await dtArea.RequestByteReadAsync(dtArea.AddressStart, dtArea.AddressEnd);
await inbetweenCallback();
}
pollLevel.lastReadTimeMs = (int)sw.Elapsed.TotalMilliseconds;