Add non-generic GetValue and improve documentation

This commit is contained in:
Peter Butzhammer
2024-04-26 09:23:51 +02:00
parent 99cf9cbc12
commit f5a51c074f
3 changed files with 175 additions and 43 deletions

View File

@@ -22,4 +22,23 @@ internal static class VariableAddressExtensions
public static bool MatchesType(this VariableAddress address, Type type) =>
supportedTypeMap.TryGetValue(type, out var map) && map(address);
public static Type GetClrType(this VariableAddress address) =>
address.Type switch
{
DbType.Bit => typeof(bool),
DbType.String => typeof(string),
DbType.WString => typeof(string),
DbType.Byte => address.Length == 1 ? typeof(byte) : typeof(byte[]),
DbType.Int => typeof(short),
DbType.UInt => typeof(ushort),
DbType.DInt => typeof(int),
DbType.UDInt => typeof(uint),
DbType.LInt => typeof(long),
DbType.ULInt => typeof(ulong),
DbType.Single => typeof(float),
DbType.Double => typeof(double),
_ => throw new ArgumentOutOfRangeException(nameof(address))
};
}

View File

@@ -8,8 +8,11 @@ namespace Sharp7.Rx.Interfaces;
public interface IPlc : IDisposable
{
IObservable<TValue> CreateNotification<TValue>(string variableName, TransmissionMode transmissionMode);
Task SetValue<TValue>(string variableName, TValue value);
Task<TValue> GetValue<TValue>(string variableName);
Task SetValue<TValue>(string variableName, TValue value, CancellationToken token = default);
Task<TValue> GetValue<TValue>(string variableName, CancellationToken token = default);
IObservable<ConnectionState> ConnectionState { get; }
Task<object> GetValue(string variableName, CancellationToken token = default);
ILogger Logger { get; }
}

View File

@@ -3,6 +3,8 @@ using System.Diagnostics;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Sharp7.Rx.Basics;
using Sharp7.Rx.Enums;
@@ -14,6 +16,11 @@ namespace Sharp7.Rx;
public class Sharp7Plc : IPlc
{
private static readonly ArrayPool<byte> arrayPool = ArrayPool<byte>.Shared;
private static readonly MethodInfo getValueMethod = typeof(Sharp7Plc).GetMethods()
.Single(m => m.Name == nameof(GetValue) && m.GetGenericArguments().Length == 1);
private readonly CompositeDisposable disposables = new();
private readonly ConcurrentSubjectDictionary<string, byte[]> multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase);
private readonly List<long> performanceCounter = new(1000);
@@ -21,8 +28,6 @@ public class Sharp7Plc : IPlc
private readonly CacheVariableNameParser variableNameParser = new CacheVariableNameParser(new VariableNameParser());
private bool disposed;
private Sharp7Connector s7Connector;
private static readonly ArrayPool<byte> arrayPool = ArrayPool<byte>.Shared;
/// <summary>
/// </summary>
@@ -75,6 +80,14 @@ public class Sharp7Plc : IPlc
GC.SuppressFinalize(this);
}
/// <summary>
/// Create an Observable for a given variable. Multiple notifications are automatically combined into a multi-variable subscription to
/// reduce network trafic and PLC workload.
/// </summary>
/// <typeparam name="TValue"></typeparam>
/// <param name="variableName"></param>
/// <param name="transmissionMode"></param>
/// <returns></returns>
public IObservable<TValue> CreateNotification<TValue>(string variableName, TransmissionMode transmissionMode)
{
return Observable.Create<TValue>(observer =>
@@ -104,19 +117,14 @@ public class Sharp7Plc : IPlc
});
}
public Task<TValue> GetValue<TValue>(string variableName)
{
return GetValue<TValue>(variableName, CancellationToken.None);
}
public Task SetValue<TValue>(string variableName, TValue value)
{
return SetValue(variableName, value, CancellationToken.None);
}
public async Task<TValue> GetValue<TValue>(string variableName, CancellationToken token)
/// <summary>
/// Read PLC variable as generic variable.
/// </summary>
/// <typeparam name="TValue"></typeparam>
/// <param name="variableName"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<TValue> GetValue<TValue>(string variableName, CancellationToken token = default)
{
var address = ParseAndVerify(variableName, typeof(TValue));
@@ -124,31 +132,38 @@ public class Sharp7Plc : IPlc
return ValueConverter.ReadFromBuffer<TValue>(data, address);
}
public async Task<bool> InitializeAsync()
/// <summary>
/// Read PLC variable as object.
/// </summary>
/// <param name="variableName"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<object> GetValue(string variableName, CancellationToken token = default)
{
await s7Connector.InitializeAsync();
var address = variableNameParser.Parse(variableName);
var clrType = address.GetClrType();
#pragma warning disable 4014
Task.Run(async () =>
{
try
{
await s7Connector.Connect();
}
catch (Exception e)
{
Logger?.LogError(e, "Error while connecting to PLC");
}
});
#pragma warning restore 4014
var genericGetValue = getValueMethod!.MakeGenericMethod(clrType);
RunNotifications(s7Connector, MultiVarRequestCycleTime)
.AddDisposableTo(disposables);
var task = genericGetValue.Invoke(this, [variableName, token]) as Task;
return true;
await task!;
var taskType = typeof(Task<>).MakeGenericType(clrType);
var propertyInfo = taskType.GetProperty(nameof(Task<object>.Result));
var result = propertyInfo!.GetValue(task);
return result;
}
public async Task SetValue<TValue>(string variableName, TValue value, CancellationToken token)
/// <summary>
/// Write value to the PLC.
/// </summary>
/// <typeparam name="TValue"></typeparam>
/// <param name="variableName"></param>
/// <param name="value"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task SetValue<TValue>(string variableName, TValue value, CancellationToken token = default)
{
var address = ParseAndVerify(variableName, typeof(TValue));
@@ -170,11 +185,105 @@ public class Sharp7Plc : IPlc
}
finally
{
ArrayPool<Byte>.Shared.Return(buffer);
ArrayPool<byte>.Shared.Return(buffer);
}
}
}
/// <summary>
/// Trigger PLC connection and start notification loop.
/// <para>
/// This method returns immediately and does not wait for the connection to be established.
/// </para>
/// </summary>
/// <returns>Always true</returns>
[Obsolete("Use InitializeConnection.")]
public async Task<bool> InitializeAsync()
{
await s7Connector.InitializeAsync();
#pragma warning disable 4014
Task.Run(async () =>
{
try
{
await s7Connector.Connect();
}
catch (Exception e)
{
Logger?.LogError(e, "Error while connecting to PLC");
}
});
#pragma warning restore 4014
RunNotifications();
return true;
}
/// <summary>
/// Initialize PLC connection and wait for connection to be established.
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public async Task TriggerInitialize(CancellationToken token = default)
{
await s7Connector.InitializeAsync();
// Triger connection.
// The initial connection might fail. In this case a reconnect is initiated.
// So we ignore any errors and wait for ConnectionState Connected afterward.
_ = Task.Run(async () =>
{
try
{
await s7Connector.Connect();
}
catch (Exception e)
{
Logger?.LogError(e, "Error while connecting to PLC");
}
}, token);
await s7Connector.ConnectionState
.FirstAsync(c => c == Enums.ConnectionState.Connected)
.ToTask(token);
RunNotifications();
}
/// <summary>
/// Initialize PLC connection and wait for connection to be established.
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public async Task InitializeConnection(CancellationToken token = default)
{
await s7Connector.InitializeAsync();
// Triger connection.
// The initial connection might fail. In this case a reconnect is initiated.
// So we ignore any errors and wait for ConnectionState Connected afterward.
_ = Task.Run(async () =>
{
try
{
await s7Connector.Connect();
}
catch (Exception e)
{
Logger?.LogError(e, "Error while connecting to PLC");
}
}, token);
await s7Connector.ConnectionState
.FirstAsync(c => c == Enums.ConnectionState.Connected)
.ToTask(token);
RunNotifications();
}
protected virtual void Dispose(bool disposing)
{
if (disposed) return;
@@ -240,21 +349,22 @@ public class Sharp7Plc : IPlc
Logger?.LogTrace("PLC {Plc} notification perf: {Elements} calls, min {Min}, max {Max}, avg {Avg}, variables {Vars}, batch size {BatchSize}",
plcConnectionSettings.IpAddress,
performanceCounter.Capacity, min, max, average,
performanceCounter.Capacity, min, max, average,
multiVariableSubscriptions.ExistingKeys.Count(),
MultiVarRequestMaxItems);
performanceCounter.Clear();
}
}
private IDisposable RunNotifications(IS7Connector connector, TimeSpan cycle)
private void RunNotifications()
{
return ConnectionState.FirstAsync()
ConnectionState.FirstAsync()
.Select(states => states == Enums.ConnectionState.Connected)
.SelectMany(connected => GetAllValues(connected, connector))
.SelectMany(connected => GetAllValues(connected, s7Connector))
.RepeatAfterDelay(MultiVarRequestCycleTime)
.LogAndRetryAfterDelay(Logger, cycle, "Error while getting batch notifications from plc")
.Subscribe();
.LogAndRetryAfterDelay(Logger, MultiVarRequestCycleTime, "Error while getting batch notifications from plc")
.Subscribe()
.AddDisposableTo(disposables);
}
~Sharp7Plc()