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) => public static bool MatchesType(this VariableAddress address, Type type) =>
supportedTypeMap.TryGetValue(type, out var map) && map(address); 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 public interface IPlc : IDisposable
{ {
IObservable<TValue> CreateNotification<TValue>(string variableName, TransmissionMode transmissionMode); IObservable<TValue> CreateNotification<TValue>(string variableName, TransmissionMode transmissionMode);
Task SetValue<TValue>(string variableName, TValue value); Task SetValue<TValue>(string variableName, TValue value, CancellationToken token = default);
Task<TValue> GetValue<TValue>(string variableName); Task<TValue> GetValue<TValue>(string variableName, CancellationToken token = default);
IObservable<ConnectionState> ConnectionState { get; } IObservable<ConnectionState> ConnectionState { get; }
Task<object> GetValue(string variableName, CancellationToken token = default);
ILogger Logger { get; } ILogger Logger { get; }
} }

View File

@@ -3,6 +3,8 @@ using System.Diagnostics;
using System.Reactive; using System.Reactive;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Reflection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Sharp7.Rx.Basics; using Sharp7.Rx.Basics;
using Sharp7.Rx.Enums; using Sharp7.Rx.Enums;
@@ -14,6 +16,11 @@ namespace Sharp7.Rx;
public class Sharp7Plc : IPlc 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 CompositeDisposable disposables = new();
private readonly ConcurrentSubjectDictionary<string, byte[]> multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase); private readonly ConcurrentSubjectDictionary<string, byte[]> multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase);
private readonly List<long> performanceCounter = new(1000); private readonly List<long> performanceCounter = new(1000);
@@ -21,8 +28,6 @@ public class Sharp7Plc : IPlc
private readonly CacheVariableNameParser variableNameParser = new CacheVariableNameParser(new VariableNameParser()); private readonly CacheVariableNameParser variableNameParser = new CacheVariableNameParser(new VariableNameParser());
private bool disposed; private bool disposed;
private Sharp7Connector s7Connector; private Sharp7Connector s7Connector;
private static readonly ArrayPool<byte> arrayPool = ArrayPool<byte>.Shared;
/// <summary> /// <summary>
/// </summary> /// </summary>
@@ -75,6 +80,14 @@ public class Sharp7Plc : IPlc
GC.SuppressFinalize(this); 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) public IObservable<TValue> CreateNotification<TValue>(string variableName, TransmissionMode transmissionMode)
{ {
return Observable.Create<TValue>(observer => return Observable.Create<TValue>(observer =>
@@ -104,19 +117,14 @@ public class Sharp7Plc : IPlc
}); });
} }
public Task<TValue> GetValue<TValue>(string variableName) /// <summary>
{ /// Read PLC variable as generic variable.
return GetValue<TValue>(variableName, CancellationToken.None); /// </summary>
} /// <typeparam name="TValue"></typeparam>
/// <param name="variableName"></param>
/// <param name="token"></param>
public Task SetValue<TValue>(string variableName, TValue value) /// <returns></returns>
{ public async Task<TValue> GetValue<TValue>(string variableName, CancellationToken token = default)
return SetValue(variableName, value, CancellationToken.None);
}
public async Task<TValue> GetValue<TValue>(string variableName, CancellationToken token)
{ {
var address = ParseAndVerify(variableName, typeof(TValue)); var address = ParseAndVerify(variableName, typeof(TValue));
@@ -124,31 +132,38 @@ public class Sharp7Plc : IPlc
return ValueConverter.ReadFromBuffer<TValue>(data, address); 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 var genericGetValue = getValueMethod!.MakeGenericMethod(clrType);
Task.Run(async () =>
{
try
{
await s7Connector.Connect();
}
catch (Exception e)
{
Logger?.LogError(e, "Error while connecting to PLC");
}
});
#pragma warning restore 4014
RunNotifications(s7Connector, MultiVarRequestCycleTime) var task = genericGetValue.Invoke(this, [variableName, token]) as Task;
.AddDisposableTo(disposables);
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)); var address = ParseAndVerify(variableName, typeof(TValue));
@@ -170,11 +185,105 @@ public class Sharp7Plc : IPlc
} }
finally 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) protected virtual void Dispose(bool disposing)
{ {
if (disposed) return; if (disposed) return;
@@ -247,14 +356,15 @@ public class Sharp7Plc : IPlc
} }
} }
private IDisposable RunNotifications(IS7Connector connector, TimeSpan cycle) private void RunNotifications()
{ {
return ConnectionState.FirstAsync() ConnectionState.FirstAsync()
.Select(states => states == Enums.ConnectionState.Connected) .Select(states => states == Enums.ConnectionState.Connected)
.SelectMany(connected => GetAllValues(connected, connector)) .SelectMany(connected => GetAllValues(connected, s7Connector))
.RepeatAfterDelay(MultiVarRequestCycleTime) .RepeatAfterDelay(MultiVarRequestCycleTime)
.LogAndRetryAfterDelay(Logger, cycle, "Error while getting batch notifications from plc") .LogAndRetryAfterDelay(Logger, MultiVarRequestCycleTime, "Error while getting batch notifications from plc")
.Subscribe(); .Subscribe()
.AddDisposableTo(disposables);
} }
~Sharp7Plc() ~Sharp7Plc()