imported Sharp7.Rx

This commit is contained in:
Thomas Stangl
2018-11-05 16:23:37 +01:00
parent aedfa4075f
commit d8d07c1679
18 changed files with 1169 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
namespace Sharp7.Rx.Enums
{
public enum ConnectionState
{
Initial,
Connected,
DisconnectedByUser,
ConnectionLost
}
}

View File

@@ -0,0 +1,10 @@
namespace Sharp7.Rx.Enums
{
internal enum CpuType
{
S7_300,
S7_400,
S7_1200,
S7_1500
}
}

13
Sharp7.Rx/Enums/DbType.cs Normal file
View File

@@ -0,0 +1,13 @@
namespace Sharp7.Rx.Enums
{
internal enum DbType
{
Bit,
String,
Byte,
Double,
Integer,
DInteger,
ULong
}
}

View File

@@ -0,0 +1,10 @@
namespace Sharp7.Rx.Enums
{
internal enum Operand : byte
{
Input = 69,
Output = 65,
Marker = 77,
Db = 68,
}
}

View File

@@ -0,0 +1,8 @@
namespace Sharp7.Rx.Enums
{
public enum TransmissionMode
{
Cyclic = 3,
OnChange = 4,
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<Unit> Select<TSource>(this IObservable<TSource> source, Func<TSource, Task> selector)
{
return source
.Select(x => Observable.FromAsync(async () => await selector(x)))
.Concat();
}
public static IObservable<TResult> Select<TSource, TResult>(this IObservable<TSource> source, Func<TSource, Task<TResult>> selector)
{
return source
.Select(x => Observable.FromAsync(async () => await selector(x)))
.Concat();
}
}
}

View File

@@ -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<TReturn> CreateDatatransferWithHandshake<TReturn>(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func<IPlc, Task<TReturn>> readData, bool initialTransfer)
{
return Observable.Create<TReturn>(async observer =>
{
var subscriptions = new CompositeDisposable();
var notification = plc
.CreateNotification<bool>(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<TReturn> CreateDatatransferWithHandshake<TReturn>(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func<IPlc, Task<TReturn>> readData)
{
return CreateDatatransferWithHandshake(plc, triggerAddress, ackTriggerAddress, readData, false);
}
private static async Task<TReturn> ReadData<TReturn>(IPlc plc, Func<IPlc, Task<TReturn>> receiveData)
{
return await receiveData(plc);
}
private static async Task<TReturn> ReadDataAndAcknowlodge<TReturn>(IPlc plc, Func<IPlc, Task<TReturn>> readData, string ackTriggerAddress)
{
try
{
return await ReadData(plc, readData);
}
finally
{
await plc.SetValue(ackTriggerAddress, true);
}
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Threading.Tasks;
using Sharp7.Rx.Enums;
namespace Sharp7.Rx.Interfaces
{
public interface IPlc : IDisposable
{
IObservable<TValue> CreateNotification<TValue>(string variableName, TransmissionMode transmissionMode, TimeSpan cycleSpan);
Task SetValue<TValue>(string variableName, TValue value);
Task<TValue> GetValue<TValue>(string variableName);
}
}

View File

@@ -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> ConnectionState { get; }
Task InitializeAsync();
Task<bool> Connect();
Task Disconnect();
Task<bool> ReadBit(Operand operand, ushort byteAddress, byte bitAdress, ushort dbNr, CancellationToken token);
Task<byte[]> ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token);
Task<bool> WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token);
Task<ushort> WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token);
}
}

View File

@@ -0,0 +1,117 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Sharp7.Rx.Resources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to S7 driver could not be initialized.
/// </summary>
internal static string StrErrorS7DriverCouldNotBeInitialized {
get {
return ResourceManager.GetString("StrErrorS7DriverCouldNotBeInitialized", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to S7 driver is not initialized..
/// </summary>
internal static string StrErrorS7DriverNotInitialized {
get {
return ResourceManager.GetString("StrErrorS7DriverNotInitialized", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to TCP/IP connection established..
/// </summary>
internal static string StrInfoConnectionEstablished {
get {
return ResourceManager.GetString("StrInfoConnectionEstablished", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Trying to connect to PLC ({2}) &apos;{0}&apos;, CPU slot {1}....
/// </summary>
internal static string StrInfoTryConnecting {
get {
return ResourceManager.GetString("StrInfoTryConnecting", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Error while reading data from plc..
/// </summary>
internal static string StrLogErrorReadingDataFromPlc {
get {
return ResourceManager.GetString("StrLogErrorReadingDataFromPlc", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Communication error discovered. Reconnect is in progress....
/// </summary>
internal static string StrLogWarningCommunictionErrorReconnecting {
get {
return ResourceManager.GetString("StrLogWarningCommunictionErrorReconnecting", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="StrLogErrorReadingDataFromPlc" xml:space="preserve">
<value>Error while reading data from plc.</value>
</data>
<data name="StrLogWarningCommunictionErrorReconnecting" xml:space="preserve">
<value>Communication error discovered. Reconnect is in progress...</value>
</data>
<data name="StrErrorS7DriverNotInitialized" xml:space="preserve">
<value>S7 driver is not initialized.</value>
</data>
<data name="StrInfoTryConnecting" xml:space="preserve">
<value>Trying to connect to PLC ({2}) '{0}', CPU slot {1}...</value>
</data>
<data name="StrInfoConnectionEstablished" xml:space="preserve">
<value>TCP/IP connection established.</value>
</data>
<data name="StrErrorS7DriverCouldNotBeInitialized" xml:space="preserve">
<value>S7 driver could not be initialized</value>
</data>
</root>

View File

@@ -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; }
}
}

View File

@@ -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(@"^(?<operand>db{1})(?<dbNr>\d{1,4})\.?(?<type>dbx|x|s|string|b|dbb|d|int|dbw|w|dint|dul|dulint|dulong|){1}(?<start>\d+)(\.(?<bitOrLength>\d+))?$", RegexOptions.IgnoreCase);
private readonly Dictionary<string, DbType> types = new Dictionary<string, DbType>
{
{"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();
}
}
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Sharp7" Version="1.0.13" />
<PackageReference Include="System.Reactive" Version="4.1.0" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\StringResources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>StringResources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\StringResources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>StringResources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -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<ConnectionState> connectionStateSubject = new BehaviorSubject<ConnectionState>(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<bool> 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> 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<bool> 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<bool> 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<byte[]> 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<ushort> 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<bool> 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<bool> 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);
}
}
}

301
Sharp7.Rx/Sharp7Plc.cs Normal file
View File

@@ -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<Unit> disposingSubject = new Subject<Unit>();
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> ConnectionState { get; private set; }
public async Task<bool> 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<TValue> GetValue<TValue>(string variableName)
{
return GetValue<TValue>(variableName, CancellationToken.None);
}
public async Task<TValue> GetValue<TValue>(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<TValue>(string variableName, TValue value)
{
return SetValue(variableName, value, CancellationToken.None);
}
public async Task SetValue<TValue>(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<TValue> CreateNotification<TValue>(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<TValue>(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);
}
}
}