15 Commits

Author SHA1 Message Date
Peter Butzhammer
75a893f51f Add Sharp7Plc.ToString()
Some checks failed
Release / build (push) Has been cancelled
2024-07-30 12:24:01 +02:00
Peter Butzhammer
61e04c7f63 Update LinqPad samples 2024-07-30 12:19:52 +02:00
Peter Butzhammer
af2df01617 Make .TriggerConnection void and extend xml docs 2024-07-30 12:15:09 +02:00
Peter Butzhammer
a2b6d84862 Wait for connection before setting up notification 2024-07-30 12:13:50 +02:00
Peter Butzhammer
79031d6f1c Improve and unity logging 2024-07-30 10:09:53 +02:00
Peter Butzhammer
8836c14e2b Update nuget packages
Some checks failed
Release / build (push) Has been cancelled
2024-07-22 14:21:15 +02:00
Peter Butzhammer
e912d088eb Include xml doc in package 2024-07-22 14:20:32 +02:00
Peter Butzhammer
2655d1ec41 Remove duplicate build step 2024-07-22 14:18:21 +02:00
Peter Butzhammer
f90ad5f9be Improve error message 2024-07-22 14:18:03 +02:00
Peter Butzhammer
636c56d252 Use correct ArrayPool reference 2024-07-22 14:17:43 +02:00
Peter Butzhammer
f1734ebd73 Extend list of possible errors 2024-05-13 10:02:01 +02:00
Peter Butzhammer
0c1d5067c2 Remove unused enum 2024-05-13 10:01:44 +02:00
Peter Butzhammer
18659454f5 Fix badge 2024-05-07 09:19:24 +02:00
Peter Butzhammer
921360757f Try to fix build by swapping truthy value 2024-05-07 09:17:35 +02:00
Peter Butzhammer
d9f1ad62b4 Merge pull request #5 from evopro-ag/prerelease
2.0 Release
2024-05-07 08:48:46 +02:00
12 changed files with 118 additions and 121 deletions

View File

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

View File

@@ -1,6 +1,6 @@
# Sharp7Reactive # Sharp7Reactive
[![.NET Core Build](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/dotnet-core.yml/badge.svg)](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/dotnet-core.yml) [![Release](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/release.yml/badge.svg?branch=master)](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/release.yml)
![Licence](https://img.shields.io/github/license/evopro-ag/Sharp7Reactive.svg) ![Licence](https://img.shields.io/github/license/evopro-ag/Sharp7Reactive.svg)
[![Nuget Version](https://img.shields.io/nuget/v/Sharp7.Rx.svg)](https://www.nuget.org/packages/Sharp7.Rx/) [![Nuget Version](https://img.shields.io/nuget/v/Sharp7.Rx.svg)](https://www.nuget.org/packages/Sharp7.Rx/)

View File

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

View File

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

View File

@@ -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.
""" """
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -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++)
{ {

View File

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

View File

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