diff --git a/Sharp7.Rx.sln b/Sharp7.Rx.sln new file mode 100644 index 0000000..faec062 --- /dev/null +++ b/Sharp7.Rx.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28010.2041 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sharp7.Rx", "Sharp7.Rx\Sharp7.Rx.csproj", "{690A7E0E-BE95-49AC-AF2F-7FEA2F63204A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {690A7E0E-BE95-49AC-AF2F-7FEA2F63204A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {690A7E0E-BE95-49AC-AF2F-7FEA2F63204A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {690A7E0E-BE95-49AC-AF2F-7FEA2F63204A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {690A7E0E-BE95-49AC-AF2F-7FEA2F63204A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {ABA1FD47-15EE-4588-9BA7-0116C635BFC4} + EndGlobalSection +EndGlobal diff --git a/Sharp7.Rx/Enums/ConnectionState.cs b/Sharp7.Rx/Enums/ConnectionState.cs new file mode 100644 index 0000000..acf93b6 --- /dev/null +++ b/Sharp7.Rx/Enums/ConnectionState.cs @@ -0,0 +1,10 @@ +namespace Sharp7.Rx.Enums +{ + public enum ConnectionState + { + Initial, + Connected, + DisconnectedByUser, + ConnectionLost + } +} diff --git a/Sharp7.Rx/Enums/CpuType.cs b/Sharp7.Rx/Enums/CpuType.cs new file mode 100644 index 0000000..28cd172 --- /dev/null +++ b/Sharp7.Rx/Enums/CpuType.cs @@ -0,0 +1,10 @@ +namespace Sharp7.Rx.Enums +{ + internal enum CpuType + { + S7_300, + S7_400, + S7_1200, + S7_1500 + } +} diff --git a/Sharp7.Rx/Enums/DbType.cs b/Sharp7.Rx/Enums/DbType.cs new file mode 100644 index 0000000..66116c8 --- /dev/null +++ b/Sharp7.Rx/Enums/DbType.cs @@ -0,0 +1,13 @@ +namespace Sharp7.Rx.Enums +{ + internal enum DbType + { + Bit, + String, + Byte, + Double, + Integer, + DInteger, + ULong + } +} diff --git a/Sharp7.Rx/Enums/Operand.cs b/Sharp7.Rx/Enums/Operand.cs new file mode 100644 index 0000000..79ed488 --- /dev/null +++ b/Sharp7.Rx/Enums/Operand.cs @@ -0,0 +1,10 @@ +namespace Sharp7.Rx.Enums +{ + internal enum Operand : byte + { + Input = 69, + Output = 65, + Marker = 77, + Db = 68, + } +} diff --git a/Sharp7.Rx/Enums/TransmissionMode.cs b/Sharp7.Rx/Enums/TransmissionMode.cs new file mode 100644 index 0000000..2e9304a --- /dev/null +++ b/Sharp7.Rx/Enums/TransmissionMode.cs @@ -0,0 +1,8 @@ +namespace Sharp7.Rx.Enums +{ + public enum TransmissionMode + { + Cyclic = 3, + OnChange = 4, + } +} diff --git a/Sharp7.Rx/Extensions/DisposableExtensions.cs b/Sharp7.Rx/Extensions/DisposableExtensions.cs new file mode 100644 index 0000000..89f3c75 --- /dev/null +++ b/Sharp7.Rx/Extensions/DisposableExtensions.cs @@ -0,0 +1,13 @@ +using System; +using System.Reactive.Disposables; + +namespace Sharp7.Rx.Extensions +{ + internal static class DisposableExtensions + { + public static void AddDisposableTo(this IDisposable disposable, CompositeDisposable compositeDisposable) + { + compositeDisposable.Add(disposable); + } + } +} diff --git a/Sharp7.Rx/Extensions/ObservableExtensions.cs b/Sharp7.Rx/Extensions/ObservableExtensions.cs new file mode 100644 index 0000000..b93038f --- /dev/null +++ b/Sharp7.Rx/Extensions/ObservableExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; + +namespace Sharp7.Rx.Extensions +{ + internal static class ObservableExtensions + { + public static IObservable Select(this IObservable source, Func selector) + { + return source + .Select(x => Observable.FromAsync(async () => await selector(x))) + .Concat(); + } + + public static IObservable Select(this IObservable source, Func> selector) + { + return source + .Select(x => Observable.FromAsync(async () => await selector(x))) + .Concat(); + } + } +} diff --git a/Sharp7.Rx/Extensions/PlcExtensions.cs b/Sharp7.Rx/Extensions/PlcExtensions.cs new file mode 100644 index 0000000..23cbcf5 --- /dev/null +++ b/Sharp7.Rx/Extensions/PlcExtensions.cs @@ -0,0 +1,67 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Sharp7.Rx.Enums; +using Sharp7.Rx.Interfaces; + +namespace Sharp7.Rx.Extensions +{ + public static class PlcExtensions + { + public static IObservable CreateDatatransferWithHandshake(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func> readData, bool initialTransfer) + { + return Observable.Create(async observer => + { + var subscriptions = new CompositeDisposable(); + + var notification = plc + .CreateNotification(triggerAddress, TransmissionMode.OnChange, TimeSpan.Zero) + .Publish() + .RefCount(); + + if (initialTransfer) + { + var initialValue = await ReadData(plc, readData); + observer.OnNext(initialValue); + } + + notification + .Where(trigger => trigger) + .Select(_ => ReadDataAndAcknowlodge(plc, readData, ackTriggerAddress)) + .Subscribe(observer) + .AddDisposableTo(subscriptions); + + notification + .Where(trigger => !trigger) + .Select(_ => plc.SetValue(ackTriggerAddress, false)) + .Subscribe() + .AddDisposableTo(subscriptions); + + return subscriptions; + }); + } + + public static IObservable CreateDatatransferWithHandshake(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func> readData) + { + return CreateDatatransferWithHandshake(plc, triggerAddress, ackTriggerAddress, readData, false); + } + + private static async Task ReadData(IPlc plc, Func> receiveData) + { + return await receiveData(plc); + } + + private static async Task ReadDataAndAcknowlodge(IPlc plc, Func> readData, string ackTriggerAddress) + { + try + { + return await ReadData(plc, readData); + } + finally + { + await plc.SetValue(ackTriggerAddress, true); + } + } + } +} diff --git a/Sharp7.Rx/Interfaces/IPlc.cs b/Sharp7.Rx/Interfaces/IPlc.cs new file mode 100644 index 0000000..cff12eb --- /dev/null +++ b/Sharp7.Rx/Interfaces/IPlc.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; +using Sharp7.Rx.Enums; + +namespace Sharp7.Rx.Interfaces +{ + public interface IPlc : IDisposable + { + IObservable CreateNotification(string variableName, TransmissionMode transmissionMode, TimeSpan cycleSpan); + Task SetValue(string variableName, TValue value); + Task GetValue(string variableName); + } +} diff --git a/Sharp7.Rx/Interfaces/IS7Connector.cs b/Sharp7.Rx/Interfaces/IS7Connector.cs new file mode 100644 index 0000000..28468ae --- /dev/null +++ b/Sharp7.Rx/Interfaces/IS7Connector.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Sharp7.Rx.Enums; + +namespace Sharp7.Rx.Interfaces +{ + internal interface IS7Connector : IDisposable + { + IObservable ConnectionState { get; } + Task InitializeAsync(); + + Task Connect(); + Task Disconnect(); + + Task ReadBit(Operand operand, ushort byteAddress, byte bitAdress, ushort dbNr, CancellationToken token); + Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token); + + Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token); + Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token); + } +} \ No newline at end of file diff --git a/Sharp7.Rx/Resources/StringResources.Designer.cs b/Sharp7.Rx/Resources/StringResources.Designer.cs new file mode 100644 index 0000000..f1d725d --- /dev/null +++ b/Sharp7.Rx/Resources/StringResources.Designer.cs @@ -0,0 +1,117 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Sharp7.Rx.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class StringResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal StringResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Sharp7.Rx.Resources.StringResources", typeof(StringResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to S7 driver could not be initialized. + /// + internal static string StrErrorS7DriverCouldNotBeInitialized { + get { + return ResourceManager.GetString("StrErrorS7DriverCouldNotBeInitialized", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to S7 driver is not initialized.. + /// + internal static string StrErrorS7DriverNotInitialized { + get { + return ResourceManager.GetString("StrErrorS7DriverNotInitialized", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to TCP/IP connection established.. + /// + internal static string StrInfoConnectionEstablished { + get { + return ResourceManager.GetString("StrInfoConnectionEstablished", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Trying to connect to PLC ({2}) '{0}', CPU slot {1}.... + /// + internal static string StrInfoTryConnecting { + get { + return ResourceManager.GetString("StrInfoTryConnecting", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error while reading data from plc.. + /// + internal static string StrLogErrorReadingDataFromPlc { + get { + return ResourceManager.GetString("StrLogErrorReadingDataFromPlc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Communication error discovered. Reconnect is in progress.... + /// + internal static string StrLogWarningCommunictionErrorReconnecting { + get { + return ResourceManager.GetString("StrLogWarningCommunictionErrorReconnecting", resourceCulture); + } + } + } +} diff --git a/Sharp7.Rx/Resources/StringResources.resx b/Sharp7.Rx/Resources/StringResources.resx new file mode 100644 index 0000000..3eff273 --- /dev/null +++ b/Sharp7.Rx/Resources/StringResources.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Error while reading data from plc. + + + Communication error discovered. Reconnect is in progress... + + + S7 driver is not initialized. + + + Trying to connect to PLC ({2}) '{0}', CPU slot {1}... + + + TCP/IP connection established. + + + S7 driver could not be initialized + + \ No newline at end of file diff --git a/Sharp7.Rx/S7VariableAddress.cs b/Sharp7.Rx/S7VariableAddress.cs new file mode 100644 index 0000000..ef2fb5a --- /dev/null +++ b/Sharp7.Rx/S7VariableAddress.cs @@ -0,0 +1,14 @@ +using Sharp7.Rx.Enums; + +namespace Sharp7.Rx +{ + internal class S7VariableAddress + { + public Operand Operand { get; set; } + public ushort DbNr { get; set; } + public ushort Start { get; set; } + public ushort Length { get; set; } + public byte Bit { get; set; } + public DbType Type { get; set; } + } +} \ No newline at end of file diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs new file mode 100644 index 0000000..f641b0c --- /dev/null +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using Sharp7.Rx.Enums; + +namespace Sharp7.Rx +{ + internal class S7VaraibleNameParser + { + private readonly Regex regex = new Regex(@"^(?db{1})(?\d{1,4})\.?(?dbx|x|s|string|b|dbb|d|int|dbw|w|dint|dul|dulint|dulong|){1}(?\d+)(\.(?\d+))?$", RegexOptions.IgnoreCase); + + private readonly Dictionary types = new Dictionary + { + {"x", DbType.Bit}, + {"dbx", DbType.Bit}, + {"s", DbType.String}, + {"string", DbType.String}, + {"b", DbType.Byte}, + {"dbb", DbType.Byte}, + {"d", DbType.Double}, + {"int", DbType.Integer}, + {"dint", DbType.DInteger}, + {"w", DbType.Integer}, + {"dbw", DbType.Integer}, + {"dul", DbType.ULong }, + {"dulint", DbType.ULong }, + {"dulong", DbType.ULong } + }; + + + public S7VariableAddress Parse(string input) + { + var match = regex.Match(input); + if (match.Success) + { + var operand = (Operand)Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true); + var dbNr = ushort.Parse(match.Groups["dbNr"].Value, NumberStyles.Integer); + var start = ushort.Parse(match.Groups["start"].Value, NumberStyles.Integer); + var type = ParseType(match.Groups["type"].Value); + + var s7VariableAddress = new S7VariableAddress + { + Operand = operand, + DbNr = dbNr, + Start = start, + Type = type, + }; + + if (type == DbType.Bit) + { + s7VariableAddress.Length = 1; + s7VariableAddress.Bit = byte.Parse(match.Groups["bitOrLength"].Value); + } + else if (type == DbType.Byte) + { + s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort)1; + } + else if (type == DbType.String) + { + s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort)0; + } + else if (type == DbType.Integer) + { + s7VariableAddress.Length = 2; + } + else if (type == DbType.DInteger) + { + s7VariableAddress.Length = 4; + } + else if (type == DbType.ULong) + { + s7VariableAddress.Length = 8; + } + + return s7VariableAddress; + } + + return null; + } + + private DbType ParseType(string value) + { + return types + .Where(pair => pair.Key.Equals(value, StringComparison.InvariantCultureIgnoreCase)) + .Select(pair => pair.Value) + .FirstOrDefault(); + } + } +} \ No newline at end of file diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj new file mode 100644 index 0000000..c4da9c6 --- /dev/null +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0 + + + + + + + + + + True + True + StringResources.resx + + + + + + ResXFileCodeGenerator + StringResources.Designer.cs + + + + diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs new file mode 100644 index 0000000..eb70569 --- /dev/null +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -0,0 +1,266 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading; +using System.Threading.Tasks; +using Sharp7.Rx.Enums; +using Sharp7.Rx.Interfaces; +using Sharp7.Rx.Resources; + +namespace Sharp7.Rx +{ + internal class Sharp7Connector : IS7Connector + { + private readonly BehaviorSubject connectionStateSubject = new BehaviorSubject(Enums.ConnectionState.Initial); + private readonly CompositeDisposable disposables = new CompositeDisposable(); + private readonly TaskScheduler scheduler = TaskScheduler.Current; + private readonly string ipAddress; + private readonly int rackNr; + private readonly int cpuSlotNr; + + private S7Client sharp7; + private bool disposed; + + public Sharp7Connector(string ipAddress, int rackNr = 0, int cpuSlotNr = 2) + { + this.ipAddress = ipAddress; + this.cpuSlotNr = cpuSlotNr; + this.rackNr = rackNr; + + ReconnectDelay = TimeSpan.FromSeconds(5); + } + + public TimeSpan ReconnectDelay { get; set; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public async Task Connect() + { + if (sharp7 == null) + throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + + try + { + var errorCode = await Task.Factory.StartNew(() => sharp7.ConnectTo(ipAddress, rackNr, cpuSlotNr), CancellationToken.None, TaskCreationOptions.None, scheduler); + var success = await EvaluateErrorCode(errorCode); + if (success) + { + connectionStateSubject.OnNext(Enums.ConnectionState.Connected); + return true; + } + } + catch (Exception ex) + { + // TODO: + } + + return false; + } + + public IObservable ConnectionState => connectionStateSubject.DistinctUntilChanged().AsObservable(); + + + public async Task Disconnect() + { + connectionStateSubject.OnNext(Enums.ConnectionState.DisconnectedByUser); + await CloseConnection(); + } + + public Task InitializeAsync() + { + try + { + sharp7 = new S7Client(); + + var subscription = + ConnectionState + .Where(state => state == Enums.ConnectionState.ConnectionLost) + .Take(1) + .SelectMany(_ => Reconnect()) + // TODO: .RepeatAfterDelay(ReconnectDelay) + // TODO: .LogAndRetry(logger, "Error while reconnecting to S7.") + .Subscribe(); + + disposables.Add(subscription); + } + catch (Exception ex) + { + // TODO: + } + + return Task.FromResult(true); + } + + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + disposables.Dispose(); + + if (sharp7 != null) + { + sharp7.Disconnect(); + sharp7 = null; + } + + connectionStateSubject?.Dispose(); + } + + disposed = true; + } + } + + private async Task CloseConnection() + { + if (sharp7 == null) + throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + + await Task.Factory.StartNew(() => sharp7.Disconnect(), CancellationToken.None, TaskCreationOptions.None, scheduler); + } + + private async Task EvaluateErrorCode(int errorCode) + { + if (errorCode == 0) + return true; + + if (sharp7 == null) + throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + + var errorText = sharp7.ErrorText(errorCode); + + await SetConnectionLostState(); + + return false; + } + + private async Task Reconnect() + { + await CloseConnection(); + + return await Connect(); + } + + private async Task SetConnectionLostState() + { + var state = await connectionStateSubject.FirstAsync(); + if (state == Enums.ConnectionState.ConnectionLost) return; + + connectionStateSubject.OnNext(Enums.ConnectionState.ConnectionLost); + } + + ~Sharp7Connector() + { + Dispose(false); + } + + private bool IsConnected => connectionStateSubject.Value == Enums.ConnectionState.Connected; + + public async Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token) + { + EnsureConnectionValid(); + + var buffer = new byte[bytesToRead]; + + var area = FromOperand(operand); + + var result = + await Task.Factory.StartNew(() => sharp7.ReadArea(area, dBNr, startByteAddress, bytesToRead, S7Consts.S7WLByte, buffer), token, TaskCreationOptions.None, scheduler); + token.ThrowIfCancellationRequested(); + + if (result != 0) + { + await EvaluateErrorCode(result); + throw new InvalidOperationException($"Error reading {operand}{dBNr}:{startByteAddress}->{bytesToRead}"); + } + + var retBuffer = new byte[bytesToRead]; + Array.Copy(buffer, 0, retBuffer, 0, bytesToRead); + return (retBuffer); + } + + private int FromOperand(Operand operand) + { + switch (operand) + { + case Operand.Input: + return S7Consts.S7AreaPE; + case Operand.Output: + return S7Consts.S7AreaPA; + case Operand.Marker: + return S7Consts.S7AreaMK; + case Operand.Db: + return S7Consts.S7AreaDB; + default: + throw new ArgumentOutOfRangeException(nameof(operand), operand, null); + } + } + + private void EnsureConnectionValid() + { + if (disposed) + throw new ObjectDisposedException("S7Connector"); + + if (sharp7 == null) + throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + + if (!IsConnected) + throw new InvalidOperationException("Plc is not connected"); + } + + public async Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token) + { + EnsureConnectionValid(); + + var result = await Task.Factory.StartNew(() => sharp7.WriteArea(FromOperand(operand), dBNr, startByteAdress, data.Length, S7Consts.S7WLByte, data), token, TaskCreationOptions.None, scheduler); + token.ThrowIfCancellationRequested(); + + if (result != 0) + { + await EvaluateErrorCode(result); + return (0); + } + return (ushort)(data.Length); + } + + + public async Task ReadBit(Operand operand, ushort byteAddress, byte bitAdress, ushort dbNr, CancellationToken token) + { + EnsureConnectionValid(); + + var byteValue = await ReadBytes(operand, byteAddress, 1, dbNr, token); + token.ThrowIfCancellationRequested(); + + if (byteValue.Length != 1) + throw new InvalidOperationException("Read bytes does not have length 1"); + + return Convert.ToBoolean(byteValue[0] & (1 << bitAdress)); + } + + public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token) + { + EnsureConnectionValid(); + + var buffer = new byte[] { value ? (byte)0xff : (byte)0 }; + + var offsetStart = (startByteAddress * 8) + bitAdress; + + var result = await Task.Factory.StartNew(() => sharp7.WriteArea(FromOperand(operand), dbNr, offsetStart, 1, S7Consts.S7WLBit, buffer), token, TaskCreationOptions.None, scheduler); + token.ThrowIfCancellationRequested(); + + if (result != 0) + { + await EvaluateErrorCode(result); + return (false); + } + return (true); + } + } +} \ No newline at end of file diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs new file mode 100644 index 0000000..7a1d6bb --- /dev/null +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -0,0 +1,301 @@ +using System; +using System.Reactive; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Sharp7.Rx.Enums; +using Sharp7.Rx.Interfaces; +using Sharp7.Rx.Resources; + +namespace Sharp7.Rx +{ + public class Sharp7Plc : IPlc + { + private readonly string ipAddress; + private readonly int rackNumber; + private readonly int cpuMpiAddress; + private readonly S7VaraibleNameParser varaibleNameParser; + private bool disposed; + private ISubject disposingSubject = new Subject(); + private IS7Connector s7Connector; + + public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress) + { + this.ipAddress = ipAddress; + this.rackNumber = rackNumber; + this.cpuMpiAddress = cpuMpiAddress; + + varaibleNameParser = new S7VaraibleNameParser(); + } + + public IObservable ConnectionState { get; private set; } + + public async Task InitializeAsync() + { + s7Connector = new Sharp7Connector(ipAddress, rackNumber, cpuMpiAddress); + ConnectionState = s7Connector.ConnectionState; + + await s7Connector.InitializeAsync(); + +#pragma warning disable 4014 + Task.Run(async () => + { + try + { + await s7Connector.Connect(); + } + catch (Exception) + { + + } + }); +#pragma warning restore 4014 + + return true; + } + + public Task GetValue(string variableName) + { + return GetValue(variableName, CancellationToken.None); + } + + public async Task GetValue(string variableName, CancellationToken token) + { + var address = varaibleNameParser.Parse(variableName); + if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName)); + + if (typeof(TValue) == typeof(bool)) + { + var b = await s7Connector.ReadBit(address.Operand, address.Start, address.Bit, address.DbNr, token); + token.ThrowIfCancellationRequested(); + return (TValue)(object)b; + } + + if (typeof(TValue) == typeof(int)) + { + var b = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); + token.ThrowIfCancellationRequested(); + if (address.Length == 2) + return (TValue)(object)((b[0] << 8) + b[1]); + if (address.Length == 4) + { + Array.Reverse(b); + return (TValue)(object)Convert.ToInt32(b); + } + + + throw new InvalidOperationException($"length must be 2 or 4 but is {address.Length}"); + } + + if (typeof(TValue) == typeof(ulong)) + { + var b = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); + token.ThrowIfCancellationRequested(); + Array.Reverse(b); + return (TValue)(object)Convert.ToUInt64(b); + } + + if (typeof(TValue) == typeof(short)) + { + var b = await s7Connector.ReadBytes(address.Operand, address.Start, 2, address.DbNr, token); + token.ThrowIfCancellationRequested(); + return (TValue)(object)(short)((b[0] << 8) + b[1]); + } + + if (typeof(TValue) == typeof(byte) || typeof(TValue) == typeof(char)) + { + var b = await s7Connector.ReadBytes(address.Operand, address.Start, 1, address.DbNr, token); + token.ThrowIfCancellationRequested(); + + return (TValue)(object)b[0]; + } + + if (typeof(TValue) == typeof(byte[])) + { + var b = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); + token.ThrowIfCancellationRequested(); + return (TValue)(object)b; + } + + if (typeof(TValue) == typeof(double) || typeof(TValue) == typeof(float)) + { + var bytes = await s7Connector.ReadBytes(address.Operand, address.Start, 4, address.DbNr, token); + token.ThrowIfCancellationRequested(); + var d = Convert.ToSingle(bytes); + return (TValue)(object)d; + } + + if (typeof(TValue) == typeof(string)) + { + if (address.Type == DbType.String) + { + var bytes = await s7Connector.ReadBytes(address.Operand, address.Start, 2, address.DbNr, token); + token.ThrowIfCancellationRequested(); + var stringLength = bytes[1]; + + var stringStartAddress = (ushort)(address.Start + 2); + var stringInBytes = await s7Connector.ReadBytes(address.Operand, stringStartAddress, stringLength, address.DbNr, token); + token.ThrowIfCancellationRequested(); + return (TValue)(object)Encoding.ASCII.GetString(stringInBytes); + } + else + { + var stringInBytes = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); + token.ThrowIfCancellationRequested(); + return (TValue)(object)Encoding.ASCII.GetString(stringInBytes).Trim(); + } + } + + throw new InvalidOperationException(string.Format("type '{0}' not supported.", typeof(TValue))); + } + + + public Task SetValue(string variableName, TValue value) + { + return SetValue(variableName, value, CancellationToken.None); + } + + public async Task SetValue(string variableName, TValue value, CancellationToken token) + { + var address = varaibleNameParser.Parse(variableName); + if (address == null) throw new ArgumentException("Input variable name is not valid", "variableName"); + + if (typeof(TValue) == typeof(bool)) + { + await s7Connector.WriteBit(address.Operand, address.Start, address.Bit, (bool)(object)value, address.DbNr, token); + } + else if (typeof(TValue) == typeof(int) || typeof(TValue) == typeof(short)) + { + byte[] bytes; + if (address.Length == 4) + bytes = BitConverter.GetBytes((int)(object)value); + else + bytes = BitConverter.GetBytes((short)(object)value); + + Array.Reverse(bytes); + + await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); + } + else if (typeof(TValue) == typeof(byte) || typeof(TValue) == typeof(char)) + { + var bytes = new[] { Convert.ToByte(value) }; + await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); + } + else if (typeof(TValue) == typeof(byte[])) + { + await s7Connector.WriteBytes(address.Operand, address.Start, (byte[])(object)value, address.DbNr, token); + } + else if (typeof(TValue) == typeof(float)) + { + var buffer = new byte[sizeof(float)]; + S7.SetRealAt(buffer, 0, (float)(object)value); + await s7Connector.WriteBytes(address.Operand, address.Start, buffer, address.DbNr, token); + } + else if (typeof(TValue) == typeof(string)) + { + var stringValue = value as string; + if (stringValue == null) throw new ArgumentException("Value must be of type string", "value"); + + var bytes = Encoding.ASCII.GetBytes(stringValue); + Array.Resize(ref bytes, address.Length); + + if (address.Type == DbType.String) + { + var bytesWritten = await s7Connector.WriteBytes(address.Operand, address.Start, new[] { (byte)address.Length, (byte)bytes.Length }, address.DbNr, token); + token.ThrowIfCancellationRequested(); + if (bytesWritten == 2) + { + var stringStartAddress = (ushort)(address.Start + 2); + token.ThrowIfCancellationRequested(); + await s7Connector.WriteBytes(address.Operand, stringStartAddress, bytes, address.DbNr, token); + } + } + else + { + await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); + token.ThrowIfCancellationRequested(); + } + } + else + { + throw new InvalidOperationException($"type '{typeof(TValue)}' not supported."); + } + } + + public IObservable CreateNotification(string variableName, TransmissionMode transmissionMode, TimeSpan cycle) + { + var address = varaibleNameParser.Parse(variableName); + if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName)); + + if (cycle < TimeSpan.FromMilliseconds(100)) + cycle = TimeSpan.FromMilliseconds(100); + + var notification = ConnectionState.FirstAsync().Select(states => states == Enums.ConnectionState.Connected) + .SelectMany(async connected => + { + var value = default(TValue); + if (connected) + { + value = await GetValue(variableName, CancellationToken.None); + } + + return new + { + HasValue = connected, + Value = value + }; + }) + // TODO: .RepeatAfterDelay(cycle) + // TODO: .LogAndRetryAfterDelay(Logger, cycle, StringResources.StrLogErrorReadingDataFromPlc) + .TakeUntil(disposingSubject) + .Where(union => union.HasValue) + .Select(union => union.Value); + + if (transmissionMode == TransmissionMode.Cyclic) + return notification; + + if (transmissionMode == TransmissionMode.OnChange) + return notification.DistinctUntilChanged(); + + throw new ArgumentException("Transmission mode can either be Cyclic or OnChange", nameof(transmissionMode)); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + if (disposingSubject != null) + { + disposingSubject.OnNext(Unit.Default); + disposingSubject.OnCompleted(); + var disposable = (disposingSubject as IDisposable); + if (disposable != null) disposable.Dispose(); + disposingSubject = null; + } + if (s7Connector != null) + { + s7Connector.Disconnect().Wait(); + s7Connector.Dispose(); + s7Connector = null; + } + } + + disposed = true; + } + } + + ~Sharp7Plc() + { + Dispose(false); + } + } +}