mirror of
https://github.com/evopro-ag/Sharp7Reactive.git
synced 2025-12-15 11:22:52 +00:00
Compare commits
15 Commits
bdfac4bd22
...
75a893f51f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75a893f51f | ||
|
|
61e04c7f63 | ||
|
|
af2df01617 | ||
|
|
a2b6d84862 | ||
|
|
79031d6f1c | ||
|
|
8836c14e2b | ||
|
|
e912d088eb | ||
|
|
2655d1ec41 | ||
|
|
f90ad5f9be | ||
|
|
636c56d252 | ||
|
|
f1734ebd73 | ||
|
|
0c1d5067c2 | ||
|
|
18659454f5 | ||
|
|
921360757f | ||
|
|
d9f1ad62b4 |
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
env:
|
env:
|
||||||
version: 2.0.${{ github.run_number }}${{ github.ref == 'refs/heads/master' && '' || '-prerelease' }}
|
version: 2.0.${{ github.run_number }}${{ github.ref != 'refs/heads/master' && '-prerelease' || '' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -26,8 +26,6 @@ jobs:
|
|||||||
run: nuget install NUnit.ConsoleRunner -Version 3.17.0 -DirectDownload -OutputDirectory .
|
run: nuget install NUnit.ConsoleRunner -Version 3.17.0 -DirectDownload -OutputDirectory .
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Install dependencies
|
|
||||||
run: dotnet restore
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build --configuration Release --no-restore /p:version=${{ env.version }}
|
run: dotnet build --configuration Release --no-restore /p:version=${{ env.version }}
|
||||||
- name: Tests
|
- name: Tests
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Sharp7Reactive
|
# Sharp7Reactive
|
||||||
|
|
||||||
[](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/dotnet-core.yml)
|
[](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/release.yml)
|
||||||

|

|
||||||
[](https://www.nuget.org/packages/Sharp7.Rx/)
|
[](https://www.nuget.org/packages/Sharp7.Rx/)
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
<PackageReference Include="DeepEqual" Version="5.1.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||||
<PackageReference Include="NUnit" Version="4.0.1" />
|
<PackageReference Include="NUnit" Version="4.1.0" />
|
||||||
<PackageReference Include="Shouldly" Version="4.2.1" />
|
<PackageReference Include="Shouldly" Version="4.2.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Sharp7.Rx.Enums;
|
|
||||||
|
|
||||||
internal enum CpuType
|
|
||||||
{
|
|
||||||
S7_300,
|
|
||||||
S7_400,
|
|
||||||
S7_1200,
|
|
||||||
S7_1500
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace Sharp7.Rx;
|
namespace Sharp7.Rx;
|
||||||
|
|
||||||
public static class S7ErrorCodes
|
public static class S7ErrorCodes
|
||||||
@@ -22,7 +20,7 @@ public static class S7ErrorCodes
|
|||||||
{0x900000, "This happens when the DB is not long enough."},
|
{0x900000, "This happens when the DB is not long enough."},
|
||||||
{
|
{
|
||||||
0x40000, """
|
0x40000, """
|
||||||
This error occurs when the DB is "optimized" or "PUT/GET communication" is not enabled.
|
This can happen when the cpu MPI address or rack is wrong, the DB is "optimized", or "PUT/GET communication" is not enabled.
|
||||||
See https://snap7.sourceforge.net/snap7_client.html#target_compatibility.
|
See https://snap7.sourceforge.net/snap7_client.html#target_compatibility.
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
<AnalysisLevel>latest-Recommended</AnalysisLevel>
|
<AnalysisLevel>latest-Recommended</AnalysisLevel>
|
||||||
|
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||||
|
|
||||||
<Authors>evopro system engineering AG</Authors>
|
<Authors>evopro system engineering AG</Authors>
|
||||||
<Company>evopro system engineering AG</Company>
|
<Company>evopro system engineering AG</Company>
|
||||||
<Description>Reactive framework for Sharp7, the Ethernet S7 PLC communication suite. Handling RFC1006 connections to Siemens S7 300, 1200 and 1500.</Description>
|
<Description>Reactive framework for Sharp7, the Ethernet S7 PLC communication suite. Handling RFC1006 connections to Siemens S7 300, 1200 and 1500.</Description>
|
||||||
@@ -23,16 +25,17 @@
|
|||||||
CA1848: For improved performance, use the LoggerMessage delegates
|
CA1848: For improved performance, use the LoggerMessage delegates
|
||||||
CA2254: The logging message template should not vary between calls
|
CA2254: The logging message template should not vary between calls
|
||||||
CA1859: Change type of field 'xxx' from interface to type for performance reasons
|
CA1859: Change type of field 'xxx' from interface to type for performance reasons
|
||||||
|
CS1591: Missing XML comment for publicly visible type or member
|
||||||
-->
|
-->
|
||||||
<NoWarn>$(NoWarn);CA1848;CA2254;CA1859</NoWarn>
|
<NoWarn>$(NoWarn);CA1848;CA2254;CA1859;CS1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" PrivateAssets="All" />
|
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||||
<PackageReference Include="Sharp7" Version="1.1.84" />
|
<PackageReference Include="Sharp7" Version="1.1.84" />
|
||||||
<PackageReference Include="System.Interactive" Version="6.0.1" />
|
<PackageReference Include="System.Interactive" Version="6.0.1" />
|
||||||
<PackageReference Include="System.Reactive" Version="6.0.0" />
|
<PackageReference Include="System.Reactive" Version="6.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ using Sharp7.Rx.Settings;
|
|||||||
|
|
||||||
namespace Sharp7.Rx;
|
namespace Sharp7.Rx;
|
||||||
|
|
||||||
internal class Sharp7Connector: IDisposable
|
internal class Sharp7Connector : IDisposable
|
||||||
{
|
{
|
||||||
private readonly BehaviorSubject<ConnectionState> connectionStateSubject = new(Enums.ConnectionState.Initial);
|
private readonly BehaviorSubject<ConnectionState> connectionStateSubject = new(Enums.ConnectionState.Initial);
|
||||||
private readonly int cpuSlotNr;
|
private readonly int cpuSlotNr;
|
||||||
@@ -35,16 +35,21 @@ internal class Sharp7Connector: IDisposable
|
|||||||
rackNr = settings.RackNumber;
|
rackNr = settings.RackNumber;
|
||||||
|
|
||||||
ReconnectDelay = TimeSpan.FromSeconds(5);
|
ReconnectDelay = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
ConnectionIdentifier = $"{ipAddress}:{port} Cpu {cpuSlotNr} Rack {rackNr}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public IObservable<ConnectionState> ConnectionState => connectionStateSubject.DistinctUntilChanged().AsObservable();
|
public IObservable<ConnectionState> ConnectionState => connectionStateSubject.DistinctUntilChanged().AsObservable();
|
||||||
|
public ConnectionState CurrentConnectionState => connectionStateSubject.Value;
|
||||||
|
|
||||||
public ILogger Logger { get; set; }
|
public ILogger Logger { get; set; }
|
||||||
|
|
||||||
public TimeSpan ReconnectDelay { get; set; }
|
private string ConnectionIdentifier { get; }
|
||||||
|
|
||||||
private bool IsConnected => connectionStateSubject.Value == Enums.ConnectionState.Connected;
|
private bool IsConnected => connectionStateSubject.Value == Enums.ConnectionState.Connected;
|
||||||
|
|
||||||
|
private TimeSpan ReconnectDelay { get; }
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Dispose(true);
|
Dispose(true);
|
||||||
@@ -67,13 +72,13 @@ internal class Sharp7Connector: IDisposable
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var errorText = EvaluateErrorCode(errorCode);
|
var errorText = EvaluateErrorCode(errorCode);
|
||||||
Logger.LogError("Failed to establish initial connection: {Error}", errorText);
|
Logger.LogError("Failed to establish initial connection to {Connection}: {Error}", ConnectionIdentifier, errorText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
connectionStateSubject.OnNext(Enums.ConnectionState.ConnectionLost);
|
connectionStateSubject.OnNext(Enums.ConnectionState.ConnectionLost);
|
||||||
Logger.LogError(ex, "Failed to establish initial connection.");
|
Logger.LogError(ex, "Failed to establish initial connection ro {Connection}.", ConnectionIdentifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -112,12 +117,11 @@ internal class Sharp7Connector: IDisposable
|
|||||||
return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer);
|
return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task InitializeAsync()
|
public void InitializeAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
sharp7 = new S7Client();
|
sharp7 = new S7Client {PLCPort = port};
|
||||||
sharp7.PLCPort = port;
|
|
||||||
|
|
||||||
var subscription =
|
var subscription =
|
||||||
ConnectionState
|
ConnectionState
|
||||||
@@ -125,17 +129,15 @@ internal class Sharp7Connector: IDisposable
|
|||||||
.Take(1)
|
.Take(1)
|
||||||
.SelectMany(_ => Reconnect())
|
.SelectMany(_ => Reconnect())
|
||||||
.RepeatAfterDelay(ReconnectDelay)
|
.RepeatAfterDelay(ReconnectDelay)
|
||||||
.LogAndRetry(Logger, "Error while reconnecting to S7.")
|
.LogAndRetry(Logger, $"Error while reconnecting to {ConnectionIdentifier}.")
|
||||||
.Subscribe();
|
.Subscribe();
|
||||||
|
|
||||||
disposables.Add(subscription);
|
disposables.Add(subscription);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger?.LogError(ex, "S7 driver could not be initialized");
|
Logger?.LogError(ex, "S7 driver for {Connection} could not be initialized", ConnectionIdentifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<byte[]> ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dbNo, CancellationToken token)
|
public async Task<byte[]> ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dbNo, CancellationToken token)
|
||||||
@@ -149,11 +151,13 @@ internal class Sharp7Connector: IDisposable
|
|||||||
await Task.Factory.StartNew(() => sharp7.ReadArea(operand.ToArea(), dbNo, startByteAddress, bytesToRead, S7WordLength.Byte, buffer), token, TaskCreationOptions.None, scheduler);
|
await Task.Factory.StartNew(() => sharp7.ReadArea(operand.ToArea(), dbNo, startByteAddress, bytesToRead, S7WordLength.Byte, buffer), token, TaskCreationOptions.None, scheduler);
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
EnsureSuccessOrThrow(result, $"Error reading {operand}{dbNo}:{startByteAddress}->{bytesToRead}");
|
EnsureSuccessOrThrow(result, $"Error reading {operand}{dbNo}:{startByteAddress} ({bytesToRead} bytes)");
|
||||||
|
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString() => ConnectionIdentifier;
|
||||||
|
|
||||||
public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNo, CancellationToken token)
|
public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNo, CancellationToken token)
|
||||||
{
|
{
|
||||||
EnsureConnectionValid();
|
EnsureConnectionValid();
|
||||||
@@ -175,7 +179,7 @@ internal class Sharp7Connector: IDisposable
|
|||||||
var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dbNo, startByteAddress, bytesToWrite, S7WordLength.Byte, data), token, TaskCreationOptions.None, scheduler);
|
var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dbNo, startByteAddress, bytesToWrite, S7WordLength.Byte, data), token, TaskCreationOptions.None, scheduler);
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress}.{data.Length}");
|
EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress} ({data.Length} bytes)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -222,18 +226,18 @@ internal class Sharp7Connector: IDisposable
|
|||||||
throw new InvalidOperationException("Plc is not connected");
|
throw new InvalidOperationException("Plc is not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureSuccessOrThrow(int result, string message)
|
private void EnsureSuccessOrThrow(int errorCode, string message)
|
||||||
{
|
{
|
||||||
if (result == 0) return;
|
if (errorCode == 0) return;
|
||||||
|
|
||||||
var errorText = EvaluateErrorCode(result);
|
var errorText = EvaluateErrorCode(errorCode);
|
||||||
var completeMessage = $"{message}: {errorText}";
|
var completeMessage = $"{message}: {errorText}";
|
||||||
|
|
||||||
var additionalErrorText = S7ErrorCodes.GetAdditionalErrorText(result);
|
var additionalErrorText = S7ErrorCodes.GetAdditionalErrorText(errorCode);
|
||||||
if (additionalErrorText != null)
|
if (additionalErrorText != null)
|
||||||
completeMessage += Environment.NewLine + additionalErrorText;
|
completeMessage += Environment.NewLine + additionalErrorText;
|
||||||
|
|
||||||
throw new S7CommunicationException(completeMessage, result, errorText);
|
throw new S7CommunicationException(completeMessage, errorCode, errorText);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string EvaluateErrorCode(int errorCode)
|
private string EvaluateErrorCode(int errorCode)
|
||||||
@@ -245,7 +249,6 @@ internal class Sharp7Connector: IDisposable
|
|||||||
throw new InvalidOperationException("S7 driver is not initialized.");
|
throw new InvalidOperationException("S7 driver is not initialized.");
|
||||||
|
|
||||||
var errorText = $"0x{errorCode:X}, {sharp7.ErrorText(errorCode)}";
|
var errorText = $"0x{errorCode:X}, {sharp7.ErrorText(errorCode)}";
|
||||||
Logger?.LogError($"S7 Error {errorText}");
|
|
||||||
|
|
||||||
if (S7ErrorCodes.AssumeConnectionLost(errorCode))
|
if (S7ErrorCodes.AssumeConnectionLost(errorCode))
|
||||||
SetConnectionLostState();
|
SetConnectionLostState();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class Sharp7Plc : IPlc
|
|||||||
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);
|
||||||
private readonly PlcConnectionSettings plcConnectionSettings;
|
private readonly PlcConnectionSettings plcConnectionSettings;
|
||||||
private readonly CacheVariableNameParser variableNameParser = new CacheVariableNameParser(new VariableNameParser());
|
private readonly CacheVariableNameParser variableNameParser = new(new VariableNameParser());
|
||||||
private bool disposed;
|
private bool disposed;
|
||||||
private int initialized;
|
private int initialized;
|
||||||
|
|
||||||
@@ -37,10 +37,10 @@ public class Sharp7Plc : IPlc
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ipAddress"></param>
|
/// <param name="ipAddress">IP address of S7.</param>
|
||||||
/// <param name="rackNumber"></param>
|
/// <param name="rackNumber">See <see href="https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot">Sharp7 wiki</see></param>
|
||||||
/// <param name="cpuMpiAddress"></param>
|
/// <param name="cpuMpiAddress">See <see href="https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot">Sharp7 wiki</see></param>
|
||||||
/// <param name="port"></param>
|
/// <param name="port">TCP port for communication</param>
|
||||||
/// <param name="multiVarRequestCycleTime">
|
/// <param name="multiVarRequestCycleTime">
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Polling interval for multi variable read from PLC.
|
/// Polling interval for multi variable read from PLC.
|
||||||
@@ -90,10 +90,6 @@ public class Sharp7Plc : IPlc
|
|||||||
/// Create an Observable for a given variable. Multiple notifications are automatically combined into a multi-variable subscription to
|
/// Create an Observable for a given variable. Multiple notifications are automatically combined into a multi-variable subscription to
|
||||||
/// reduce network trafic and PLC workload.
|
/// reduce network trafic and PLC workload.
|
||||||
/// </summary>
|
/// </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 =>
|
||||||
@@ -105,9 +101,13 @@ public class Sharp7Plc : IPlc
|
|||||||
disposableContainer.AddDisposableTo(disp);
|
disposableContainer.AddDisposableTo(disp);
|
||||||
|
|
||||||
var observable =
|
var observable =
|
||||||
// Read variable with GetValue first.
|
ConnectionState
|
||||||
// This will propagate any errors due to reading from invalid addresses.
|
// Wait for connection to be established
|
||||||
Observable.FromAsync(() => GetValue<TValue>(variableName))
|
.FirstAsync(c => c == Enums.ConnectionState.Connected)
|
||||||
|
// Read variable with GetValue first.
|
||||||
|
// This will propagate any errors due to reading from invalid addresses.
|
||||||
|
.SelectMany(_ => GetValue<TValue>(variableName))
|
||||||
|
// Output results from read loop
|
||||||
.Concat(
|
.Concat(
|
||||||
disposableContainer.Observable
|
disposableContainer.Observable
|
||||||
.Select(bytes => ValueConverter.ReadFromBuffer<TValue>(bytes, address))
|
.Select(bytes => ValueConverter.ReadFromBuffer<TValue>(bytes, address))
|
||||||
@@ -124,12 +124,28 @@ public class Sharp7Plc : IPlc
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read PLC variable as generic variable.
|
/// Creates an observable of object for a variable.
|
||||||
|
/// The return type is automatically infered from the variable name.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The return type is infered from the variable name.</returns>
|
||||||
|
public IObservable<object> CreateNotification(string variableName, TransmissionMode transmissionMode)
|
||||||
|
{
|
||||||
|
var address = variableNameParser.Parse(variableName);
|
||||||
|
var clrType = address.GetClrType();
|
||||||
|
|
||||||
|
var genericCreateNotification = createNotificationMethod!.MakeGenericMethod(clrType);
|
||||||
|
|
||||||
|
var genericNotification = genericCreateNotification.Invoke(this, [variableName, transmissionMode]);
|
||||||
|
|
||||||
|
return SignatureConverter.ConvertToObjectObservable(genericNotification, clrType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read PLC variable as generic variable.
|
||||||
|
/// <para>
|
||||||
|
/// The method will fail with a <see cref="InvalidOperationException" />, if <see cref="ConnectionState" /> is not <see cref="ConnectionState.Connected" />.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </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)
|
public async Task<TValue> GetValue<TValue>(string variableName, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var address = ParseAndVerify(variableName, typeof(TValue));
|
var address = ParseAndVerify(variableName, typeof(TValue));
|
||||||
@@ -141,9 +157,10 @@ public class Sharp7Plc : IPlc
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read PLC variable as object.
|
/// Read PLC variable as object.
|
||||||
/// The return type is automatically infered from the variable name.
|
/// The return type is automatically infered from the variable name.
|
||||||
|
/// <para>
|
||||||
|
/// The method will fail with a <see cref="InvalidOperationException" />, if <see cref="ConnectionState" /> is not <see cref="ConnectionState.Connected" />.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="variableName"></param>
|
|
||||||
/// <param name="token"></param>
|
|
||||||
/// <returns>The actual return type is infered from the variable name.</returns>
|
/// <returns>The actual return type is infered from the variable name.</returns>
|
||||||
public async Task<object> GetValue(string variableName, CancellationToken token = default)
|
public async Task<object> GetValue(string variableName, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
@@ -164,12 +181,10 @@ public class Sharp7Plc : IPlc
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Write value to the PLC.
|
/// Write value to the PLC.
|
||||||
|
/// <para>
|
||||||
|
/// The method will fail with a <see cref="InvalidOperationException" />, if <see cref="ConnectionState" /> is not <see cref="ConnectionState.Connected" />.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </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)
|
public async Task SetValue<TValue>(string variableName, TValue value, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var address = ParseAndVerify(variableName, typeof(TValue));
|
var address = ParseAndVerify(variableName, typeof(TValue));
|
||||||
@@ -192,30 +207,11 @@ public class Sharp7Plc : IPlc
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
ArrayPool<byte>.Shared.Return(buffer);
|
arrayPool.Return(buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates an observable of object for a variable.
|
|
||||||
/// The return type is automatically infered from the variable name.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="variableName"></param>
|
|
||||||
/// <param name="transmissionMode"></param>
|
|
||||||
/// <returns>The return type is infered from the variable name.</returns>
|
|
||||||
public IObservable<object> CreateNotification(string variableName, TransmissionMode transmissionMode)
|
|
||||||
{
|
|
||||||
var address = variableNameParser.Parse(variableName);
|
|
||||||
var clrType = address.GetClrType();
|
|
||||||
|
|
||||||
var genericCreateNotification = createNotificationMethod!.MakeGenericMethod(clrType);
|
|
||||||
|
|
||||||
var genericNotification = genericCreateNotification.Invoke(this, [variableName, transmissionMode]);
|
|
||||||
|
|
||||||
return SignatureConverter.ConvertToObjectObservable(genericNotification, clrType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trigger PLC connection and start notification loop.
|
/// Trigger PLC connection and start notification loop.
|
||||||
/// <para>
|
/// <para>
|
||||||
@@ -224,26 +220,37 @@ public class Sharp7Plc : IPlc
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Always true</returns>
|
/// <returns>Always true</returns>
|
||||||
[Obsolete($"Use {nameof(InitializeConnection)} or {nameof(TriggerConnection)}.")]
|
[Obsolete($"Use {nameof(InitializeConnection)} or {nameof(TriggerConnection)}.")]
|
||||||
public async Task<bool> InitializeAsync()
|
public Task<bool> InitializeAsync()
|
||||||
{
|
{
|
||||||
await TriggerConnection();
|
TriggerConnection();
|
||||||
return true;
|
return Task.FromResult(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize PLC connection and wait for connection to be established.
|
/// Initialize PLC connection and wait for connection to be established (<see cref="ConnectionState" /> is <see cref="ConnectionState.Connected" />).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="token"></param>
|
/// <param name="token"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task InitializeConnection(CancellationToken token = default) => await DoInitializeConnection(true, token);
|
public async Task InitializeConnection(CancellationToken token = default)
|
||||||
|
{
|
||||||
|
DoInitializeConnection();
|
||||||
|
await s7Connector.ConnectionState
|
||||||
|
.FirstAsync(c => c == Enums.ConnectionState.Connected)
|
||||||
|
.ToTask(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"S7 {s7Connector} ({s7Connector.CurrentConnectionState})";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize PLC and trigger connection. This method will not wait for the connection to be established.
|
/// Initialize PLC and trigger connection. This method will not wait for the connection to be established.
|
||||||
|
/// <para>
|
||||||
|
/// Without an established connection, it is safe to call <see cref="CreateNotification" />, but <see cref="GetValue{TValue}" />
|
||||||
|
/// and <see cref="SetValue{TValue}" /> will fail.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="token"></param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task TriggerConnection(CancellationToken token = default) => await DoInitializeConnection(false, token);
|
public void TriggerConnection() => DoInitializeConnection();
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
protected virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
@@ -266,11 +273,12 @@ public class Sharp7Plc : IPlc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DoInitializeConnection(bool waitForConnection, CancellationToken token)
|
private void DoInitializeConnection()
|
||||||
{
|
{
|
||||||
if (Interlocked.Exchange(ref initialized, 1) == 1) return;
|
if (Interlocked.Exchange(ref initialized, 1) == 1)
|
||||||
|
return;
|
||||||
|
|
||||||
await s7Connector.InitializeAsync();
|
s7Connector.InitializeAsync();
|
||||||
|
|
||||||
// Triger connection.
|
// Triger connection.
|
||||||
// The initial connection might fail. In this case a reconnect is initiated.
|
// The initial connection might fail. In this case a reconnect is initiated.
|
||||||
@@ -281,16 +289,11 @@ public class Sharp7Plc : IPlc
|
|||||||
{
|
{
|
||||||
await s7Connector.Connect();
|
await s7Connector.Connect();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
Logger?.LogError(e, "Intiial PLC connection failed.");
|
// Ignore. Exception is logged in the connector
|
||||||
}
|
}
|
||||||
}, token);
|
});
|
||||||
|
|
||||||
if (waitForConnection)
|
|
||||||
await s7Connector.ConnectionState
|
|
||||||
.FirstAsync(c => c == Enums.ConnectionState.Connected)
|
|
||||||
.ToTask(token);
|
|
||||||
|
|
||||||
StartNotificationLoop();
|
StartNotificationLoop();
|
||||||
}
|
}
|
||||||
@@ -329,9 +332,12 @@ public class Sharp7Plc : IPlc
|
|||||||
|
|
||||||
private void PrintAndResetPerformanceStatistik()
|
private void PrintAndResetPerformanceStatistik()
|
||||||
{
|
{
|
||||||
if (performanceCounter.Count == performanceCounter.Capacity)
|
if (performanceCounter.Count != performanceCounter.Capacity) return;
|
||||||
|
|
||||||
|
if (Logger.IsEnabled(LogLevel.Trace))
|
||||||
{
|
{
|
||||||
var average = performanceCounter.Average();
|
var average = performanceCounter.Average();
|
||||||
|
|
||||||
var min = performanceCounter.Min();
|
var min = performanceCounter.Min();
|
||||||
var max = performanceCounter.Max();
|
var max = performanceCounter.Max();
|
||||||
|
|
||||||
@@ -340,8 +346,9 @@ public class Sharp7Plc : IPlc
|
|||||||
performanceCounter.Capacity, min, max, average,
|
performanceCounter.Capacity, min, max, average,
|
||||||
multiVariableSubscriptions.ExistingKeys.Count(),
|
multiVariableSubscriptions.ExistingKeys.Count(),
|
||||||
MultiVarRequestMaxItems);
|
MultiVarRequestMaxItems);
|
||||||
performanceCounter.Clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
performanceCounter.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StartNotificationLoop()
|
private void StartNotificationLoop()
|
||||||
@@ -355,16 +362,11 @@ public class Sharp7Plc : IPlc
|
|||||||
.FirstAsync(states => states == Enums.ConnectionState.Connected)
|
.FirstAsync(states => states == Enums.ConnectionState.Connected)
|
||||||
.SelectMany(_ => GetAllValues(s7Connector))
|
.SelectMany(_ => GetAllValues(s7Connector))
|
||||||
.RepeatAfterDelay(MultiVarRequestCycleTime)
|
.RepeatAfterDelay(MultiVarRequestCycleTime)
|
||||||
.LogAndRetryAfterDelay(Logger, MultiVarRequestCycleTime, "Error while getting batch notifications from plc")
|
.LogAndRetryAfterDelay(Logger, MultiVarRequestCycleTime, $"Error while getting batch notifications from {s7Connector}")
|
||||||
.Subscribe();
|
.Subscribe();
|
||||||
|
|
||||||
if (Interlocked.CompareExchange(ref notificationSubscription, subscription, null) != null)
|
if (Interlocked.CompareExchange(ref notificationSubscription, subscription, null) != null)
|
||||||
// Subscription has already been created (race condition). Dispose new subscription.
|
// Subscription has already been created (race condition). Dispose new subscription.
|
||||||
subscription.Dispose();
|
subscription.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
~Sharp7Plc()
|
|
||||||
{
|
|
||||||
Dispose(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ internal class VariableNameParser : IVariableNameParser
|
|||||||
{
|
{
|
||||||
if (defaultValue.HasValue)
|
if (defaultValue.HasValue)
|
||||||
return defaultValue.Value;
|
return defaultValue.Value;
|
||||||
throw new InvalidS7AddressException($"Variable of type {type} must have a length set \"{input}\"", input);
|
throw new InvalidS7AddressException($"Variable of type {type} must have a length set. Example \"db12.byte10.3\", found \"{input}\"", input);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ushort.TryParse(match.Groups["bitOrLength"].Value, out var result))
|
if (!ushort.TryParse(match.Groups["bitOrLength"].Value, out var result))
|
||||||
@@ -132,7 +132,7 @@ internal class VariableNameParser : IVariableNameParser
|
|||||||
byte GetBit()
|
byte GetBit()
|
||||||
{
|
{
|
||||||
if (!match.Groups["bitOrLength"].Success)
|
if (!match.Groups["bitOrLength"].Success)
|
||||||
throw new InvalidS7AddressException($"Variable of type {type} must have a bit number set \"{input}\"", input);
|
throw new InvalidS7AddressException($"Variable of type {type} must have a bit number set. Example \"db12.bit10.3\", found \"{input}\"", input);
|
||||||
|
|
||||||
if (!byte.TryParse(match.Groups["bitOrLength"].Value, out var result))
|
if (!byte.TryParse(match.Groups["bitOrLength"].Value, out var result))
|
||||||
throw new InvalidS7AddressException($"\"{match.Groups["bitOrLength"].Value}\" is an invalid bit number in \"{input}\"", input);
|
throw new InvalidS7AddressException($"\"{match.Groups["bitOrLength"].Value}\" is an invalid bit number in \"{input}\"", input);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ await plc.InitializeConnection();
|
|||||||
// create an IObservable
|
// create an IObservable
|
||||||
var observable = plc.CreateNotification<short>($"DB{db}.Int6", Sharp7.Rx.Enums.TransmissionMode.OnChange);
|
var observable = plc.CreateNotification<short>($"DB{db}.Int6", Sharp7.Rx.Enums.TransmissionMode.OnChange);
|
||||||
|
|
||||||
observable.Dump();
|
_ = observable.Dump();
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress);
|
|||||||
// Initialize connection
|
// Initialize connection
|
||||||
await plc.InitializeConnection();
|
await plc.InitializeConnection();
|
||||||
|
|
||||||
// wait for connection to be established
|
// // Alternative: Trigger connection and wait for ConnectionState == Connected
|
||||||
await plc.ConnectionState
|
// plc.TriggerConnection();
|
||||||
.FirstAsync(c => c == Sharp7.Rx.Enums.ConnectionState.Connected)
|
// // wait for connection to be established
|
||||||
.ToTask();
|
//await plc.ConnectionState
|
||||||
|
// .FirstAsync(c => c == Sharp7.Rx.Enums.ConnectionState.Connected)
|
||||||
|
// .ToTask();
|
||||||
|
|
||||||
"Connection established".Dump();
|
"Connection established".Dump();
|
||||||
|
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ var cpuMpiAddress = 0;
|
|||||||
|
|
||||||
using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress);
|
using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress);
|
||||||
|
|
||||||
plc.ConnectionState.Dump();
|
_ = plc.ConnectionState.Dump();
|
||||||
|
|
||||||
await plc.InitializeConnection();
|
await plc.InitializeConnection();
|
||||||
|
|
||||||
// create an IObservable
|
// create an IObservable
|
||||||
plc.CreateNotification<short>($"DB{db}.Int6", Sharp7.Rx.Enums.TransmissionMode.OnChange).Dump("Int 6");
|
_ = plc.CreateNotification<short>($"DB{db}.Int6", Sharp7.Rx.Enums.TransmissionMode.OnChange).Dump("Int 6");
|
||||||
plc.CreateNotification<float>($"DB{db}.Real10", Sharp7.Rx.Enums.TransmissionMode.OnChange).Dump("Real 10");
|
_ = plc.CreateNotification<float>($"DB{db}.Real10", Sharp7.Rx.Enums.TransmissionMode.OnChange).Dump("Real 10");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user