diff --git a/Sharp7.Rx/Extensions/S7VariableExtensions.cs b/Sharp7.Rx/Extensions/S7VariableExtensions.cs index fa1c87f..f125bb6 100644 --- a/Sharp7.Rx/Extensions/S7VariableExtensions.cs +++ b/Sharp7.Rx/Extensions/S7VariableExtensions.cs @@ -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)) + }; + } diff --git a/Sharp7.Rx/Interfaces/IPlc.cs b/Sharp7.Rx/Interfaces/IPlc.cs index 296e830..31be636 100644 --- a/Sharp7.Rx/Interfaces/IPlc.cs +++ b/Sharp7.Rx/Interfaces/IPlc.cs @@ -8,8 +8,11 @@ namespace Sharp7.Rx.Interfaces; public interface IPlc : IDisposable { IObservable CreateNotification(string variableName, TransmissionMode transmissionMode); - Task SetValue(string variableName, TValue value); - Task GetValue(string variableName); + Task SetValue(string variableName, TValue value, CancellationToken token = default); + Task GetValue(string variableName, CancellationToken token = default); IObservable ConnectionState { get; } + + Task GetValue(string variableName, CancellationToken token = default); + ILogger Logger { get; } } diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index f250ad8..032cdbb 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -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 arrayPool = ArrayPool.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 multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase); private readonly List 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 arrayPool = ArrayPool.Shared; - /// /// @@ -75,6 +80,14 @@ public class Sharp7Plc : IPlc GC.SuppressFinalize(this); } + /// + /// Create an Observable for a given variable. Multiple notifications are automatically combined into a multi-variable subscription to + /// reduce network trafic and PLC workload. + /// + /// + /// + /// + /// public IObservable CreateNotification(string variableName, TransmissionMode transmissionMode) { return Observable.Create(observer => @@ -104,19 +117,14 @@ public class Sharp7Plc : IPlc }); } - public Task GetValue(string variableName) - { - return GetValue(variableName, CancellationToken.None); - } - - - public Task SetValue(string variableName, TValue value) - { - return SetValue(variableName, value, CancellationToken.None); - } - - - public async Task GetValue(string variableName, CancellationToken token) + /// + /// Read PLC variable as generic variable. + /// + /// + /// + /// + /// + public async Task GetValue(string variableName, CancellationToken token = default) { var address = ParseAndVerify(variableName, typeof(TValue)); @@ -124,31 +132,38 @@ public class Sharp7Plc : IPlc return ValueConverter.ReadFromBuffer(data, address); } - public async Task InitializeAsync() + /// + /// Read PLC variable as object. + /// + /// + /// + /// + public async Task 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.Result)); + var result = propertyInfo!.GetValue(task); + + return result; } - public async Task SetValue(string variableName, TValue value, CancellationToken token) + /// + /// Write value to the PLC. + /// + /// + /// + /// + /// + /// + public async Task SetValue(string variableName, TValue value, CancellationToken token = default) { var address = ParseAndVerify(variableName, typeof(TValue)); @@ -170,11 +185,105 @@ public class Sharp7Plc : IPlc } finally { - ArrayPool.Shared.Return(buffer); + ArrayPool.Shared.Return(buffer); } } } + /// + /// Trigger PLC connection and start notification loop. + /// + /// This method returns immediately and does not wait for the connection to be established. + /// + /// + /// Always true + [Obsolete("Use InitializeConnection.")] + public async Task 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; + } + + /// + /// Initialize PLC connection and wait for connection to be established. + /// + /// + /// + 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(); + } + + + /// + /// Initialize PLC connection and wait for connection to be established. + /// + /// + /// + 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()