From c667c113e3c348a882f2d6646df3a588c9c66a0c Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 11:29:48 +0100 Subject: [PATCH 01/16] Set s7Connector in constructor --- Sharp7.Rx/Sharp7Connector.cs | 6 +++--- Sharp7.Rx/Sharp7Plc.cs | 39 +++++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index e8a078a..efdd014 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -13,14 +13,14 @@ namespace Sharp7.Rx; internal class Sharp7Connector : IS7Connector { - private readonly BehaviorSubject connectionStateSubject = new BehaviorSubject(Enums.ConnectionState.Initial); + private readonly BehaviorSubject connectionStateSubject = new(Enums.ConnectionState.Initial); private readonly int cpuSlotNr; - private readonly CompositeDisposable disposables = new CompositeDisposable(); + private readonly CompositeDisposable disposables = new(); private readonly string ipAddress; private readonly int port; private readonly int rackNr; - private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism: 1); + private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new(maxDegreeOfParallelism: 1); private readonly IS7VariableNameParser variableNameParser; private bool disposed; diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index d57f63c..974619d 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -14,13 +14,13 @@ namespace Sharp7.Rx; public class Sharp7Plc : IPlc { - protected readonly CompositeDisposable Disposables = new CompositeDisposable(); - private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new ConcurrentSubjectDictionary(StringComparer.InvariantCultureIgnoreCase); - private readonly List performanceCoutner = new List(1000); + private readonly CompositeDisposable disposables = new(); + private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase); + private readonly List performanceCoutner = new(1000); private readonly PlcConnectionSettings plcConnectionSettings; private readonly IS7VariableNameParser varaibleNameParser = new CacheVariableNameParser(new S7VariableNameParser()); private bool disposed; - private IS7Connector s7Connector; + private Sharp7Connector s7Connector; /// @@ -44,13 +44,26 @@ public class Sharp7Plc : IPlc public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress, int port = 102, TimeSpan? multiVarRequestCycleTime = null) { plcConnectionSettings = new PlcConnectionSettings {IpAddress = ipAddress, RackNumber = rackNumber, CpuMpiAddress = cpuMpiAddress, Port = port}; + s7Connector = new Sharp7Connector(plcConnectionSettings, varaibleNameParser); + ConnectionState = s7Connector.ConnectionState; - if (multiVarRequestCycleTime != null && multiVarRequestCycleTime > TimeSpan.FromMilliseconds(5)) - MultiVarRequestCycleTime = multiVarRequestCycleTime.Value; + if (multiVarRequestCycleTime != null) + { + if (multiVarRequestCycleTime < TimeSpan.FromMilliseconds(5)) + MultiVarRequestCycleTime = TimeSpan.FromMilliseconds(5); + else + MultiVarRequestCycleTime = multiVarRequestCycleTime.Value; + } + } + + public IObservable ConnectionState { get; } + + public ILogger Logger + { + get => s7Connector.Logger; + set => s7Connector.Logger = value; } - public IObservable ConnectionState { get; private set; } - public ILogger Logger { get; set; } public TimeSpan MultiVarRequestCycleTime { get; } = TimeSpan.FromSeconds(0.1); public int MultiVarRequestMaxItems { get; set; } = 16; @@ -108,9 +121,6 @@ public class Sharp7Plc : IPlc public async Task InitializeAsync() { - s7Connector = new Sharp7Connector(plcConnectionSettings, varaibleNameParser) {Logger = Logger}; - ConnectionState = s7Connector.ConnectionState; - await s7Connector.InitializeAsync(); #pragma warning disable 4014 @@ -128,7 +138,7 @@ public class Sharp7Plc : IPlc #pragma warning restore 4014 RunNotifications(s7Connector, MultiVarRequestCycleTime) - .AddDisposableTo(Disposables); + .AddDisposableTo(disposables); return true; } @@ -207,7 +217,7 @@ public class Sharp7Plc : IPlc if (disposing) { - Disposables.Dispose(); + disposables.Dispose(); if (s7Connector != null) { @@ -254,7 +264,8 @@ public class Sharp7Plc : IPlc var min = performanceCoutner.Min(); var max = performanceCoutner.Max(); - Logger?.LogTrace("Performance statistic during {0} elements of plc notification. Min: {1}, Max: {2}, Average: {3}, Plc: '{4}', Number of variables: {5}, Batch size: {6}", performanceCoutner.Capacity, min, max, average, plcConnectionSettings.IpAddress, + Logger?.LogTrace("Performance statistic during {0} elements of plc notification. Min: {1}, Max: {2}, Average: {3}, Plc: '{4}', Number of variables: {5}, Batch size: {6}", + performanceCoutner.Capacity, min, max, average, plcConnectionSettings.IpAddress, multiVariableSubscriptions.ExistingKeys.Count(), MultiVarRequestMaxItems); performanceCoutner.Clear(); From 3c39996f5ef6a6de0ade93019878de9b8d4d6f4c Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 13:33:50 +0100 Subject: [PATCH 02/16] Make CreateNotification for nonexistent variable fail with Exception --- Sharp7.Rx/Sharp7Plc.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 974619d..0265a80 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -85,8 +85,14 @@ public class Sharp7Plc : IPlc var disposeableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName); disposeableContainer.AddDisposableTo(disposables); - var observable = disposeableContainer.Observable - .Select(bytes => S7ValueConverter.ConvertToType(bytes, address)); + var observable = + // Directly read variable first. + // This will propagate any errors due to reading from invalid addresses. + Observable.FromAsync(() => GetValue(variableName)) + .Concat( + disposeableContainer.Observable + .Select(bytes => S7ValueConverter.ConvertToType(bytes, address)) + ); if (transmissionMode == TransmissionMode.OnChange) observable = observable.DistinctUntilChanged(); From d4a8ef9cb32092f152bf4481d2b06e104bf939ee Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 13:34:12 +0100 Subject: [PATCH 03/16] Rename variable --- Sharp7.Rx/Sharp7Plc.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 0265a80..02a975c 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -81,9 +81,9 @@ public class Sharp7Plc : IPlc var address = varaibleNameParser.Parse(variableName); if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName)); - var disposables = new CompositeDisposable(); + var disp = new CompositeDisposable(); var disposeableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName); - disposeableContainer.AddDisposableTo(disposables); + disposeableContainer.AddDisposableTo(disp); var observable = // Directly read variable first. @@ -98,9 +98,9 @@ public class Sharp7Plc : IPlc observable = observable.DistinctUntilChanged(); observable.Subscribe(observer) - .AddDisposableTo(disposables); + .AddDisposableTo(disp); - return disposables; + return disp; }); } From 4389e813408f1ebffa9b3c1d594ea0a1745e4b27 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 13:47:13 +0100 Subject: [PATCH 04/16] Add Tests for WriteToBuffer --- .../S7ValueConverterTests.cs | 4 +- .../S7ValueConverterTests/WriteToBuffer.cs | 47 +++++++++++++++++++ Sharp7.Rx/S7ValueConverter.cs | 4 ++ 3 files changed, 53 insertions(+), 2 deletions(-) rename Sharp7.Rx.Tests/{ => S7ValueConverterTests}/S7ValueConverterTests.cs (97%) create mode 100644 Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/S7ValueConverterTests.cs similarity index 97% rename from Sharp7.Rx.Tests/S7ValueConverterTests.cs rename to Sharp7.Rx.Tests/S7ValueConverterTests/S7ValueConverterTests.cs index 032877d..23052d2 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/S7ValueConverterTests.cs @@ -2,10 +2,10 @@ using Sharp7.Rx.Interfaces; using Shouldly; -namespace Sharp7.Rx.Tests; +namespace Sharp7.Rx.Tests.S7ValueConverterTests; [TestFixture] -public class S7ValueConverterTests +public class ConvertToType { static readonly IS7VariableNameParser parser = new S7VariableNameParser(); diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs new file mode 100644 index 0000000..137bd07 --- /dev/null +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs @@ -0,0 +1,47 @@ +using NUnit.Framework; +using Sharp7.Rx.Interfaces; +using Shouldly; + +namespace Sharp7.Rx.Tests.S7ValueConverterTests; + +[TestFixture] +public class WriteToBuffer +{ + static readonly IS7VariableNameParser parser = new S7VariableNameParser(); + + [TestCase(true, "DB0.DBx0.0", new byte[] {0x01})] + [TestCase(false, "DB0.DBx0.0", new byte[] {0x00})] + [TestCase(true, "DB0.DBx0.4", new byte[] {0x10})] + [TestCase(false, "DB0.DBx0.4", new byte[] {0})] + [TestCase(true, "DB0.DBx0.4", new byte[] {0x1F})] + [TestCase(false, "DB0.DBx0.4", new byte[] {0xEF})] + [TestCase((byte) 18, "DB0.DBB0", new byte[] {0x12})] + [TestCase((char) 18, "DB0.DBB0", new byte[] {0x12})] + [TestCase((short) 4660, "DB0.INT0", new byte[] {0x12, 0x34})] + [TestCase((short) -3532, "DB0.INT0", new byte[] {0xF2, 0x34})] + [TestCase(-3532, "DB0.INT0", new byte[] {0xF2, 0x34})] + [TestCase(305419879, "DB0.DINT0", new byte[] {0x12, 0x34, 0x56, 0x67})] + [TestCase(-231451033, "DB0.DINT0", new byte[] {0xF2, 0x34, 0x56, 0x67})] + [TestCase(1311768394163015151L, "DB0.dul0", new byte[] {0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] + [TestCase(-994074615050678801L, "DB0.dul0", new byte[] {0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] + [TestCase(1311768394163015151uL, "DB0.dul0", new byte[] {0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] + [TestCase(17452669458658872815uL, "DB0.dul0", new byte[] {0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] + [TestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB0.DBB0.4", new byte[] {0x12, 0x34, 0x56, 0x67})] + [TestCase(0.25f, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] + [TestCase(0.25, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] + [TestCase("ABCD", "DB0.string0.4", new byte[] {0x00, 0x04, 0x41, 0x42, 0x43, 0x44})] + [TestCase("ABCD", "DB0.string0.4", new byte[] {0x00, 0xF0, 0x41, 0x42, 0x43, 0x44})] // Clip to length in Address + [TestCase("ABCD", "DB0.DBB0.4", new byte[] {0x41, 0x42, 0x43, 0x44})] + public void Write(T expected, string address, byte[] data) + { + //Arrange + var variableAddress = parser.Parse(address); + var buffer = new byte[variableAddress.Length]; + + //Act + S7ValueConverter.WriteToBuffer(buffer, expected, variableAddress); + + //Assert + buffer.ShouldBe(data); + } +} diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 26b86c5..612e159 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -7,6 +7,10 @@ namespace Sharp7.Rx; internal static class S7ValueConverter { + public static void WriteToBuffer(Span buffer, TValue value, S7VariableAddress address) + { + } + public static TValue ConvertToType(byte[] buffer, S7VariableAddress address) { if (typeof(TValue) == typeof(bool)) From 981a30647881dc4115171c6f03ff87b4c21a54f2 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 15:16:23 +0100 Subject: [PATCH 05/16] Add WriteToBuffer and unify supported types --- .../S7ValueConverterTests/ConvertBothWays.cs | 45 +++++++++ ...alueConverterTests.cs => ConvertToType.cs} | 8 +- .../S7ValueConverterTests/WriteToBuffer.cs | 29 ++++-- Sharp7.Rx/S7ValueConverter.cs | 95 +++++++++++++++---- Sharp7.Rx/S7VariableAddress.cs | 2 + 5 files changed, 148 insertions(+), 31 deletions(-) create mode 100644 Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs rename Sharp7.Rx.Tests/S7ValueConverterTests/{S7ValueConverterTests.cs => ConvertToType.cs} (95%) diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs new file mode 100644 index 0000000..5d264a1 --- /dev/null +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs @@ -0,0 +1,45 @@ +using NUnit.Framework; +using Sharp7.Rx.Interfaces; +using Shouldly; + +namespace Sharp7.Rx.Tests.S7ValueConverterTests; + +[TestFixture] +public class ConvertBothWays +{ + static readonly IS7VariableNameParser parser = new S7VariableNameParser(); + + [TestCase(true, "DB0.DBx0.0")] + [TestCase(false, "DB0.DBx0.0")] + [TestCase(true, "DB0.DBx0.4")] + [TestCase(false, "DB0.DBx0.4")] + [TestCase((byte) 18, "DB0.DBB0")] + [TestCase((short) 4660, "DB0.INT0")] + [TestCase((short)-3532, "DB0.INT0")] + [TestCase(-3532, "DB0.INT0")] + [TestCase(305419879, "DB0.DINT0")] + [TestCase(-231451033, "DB0.DINT0")] + [TestCase(1311768394163015151L, "DB0.dul0")] + [TestCase(-994074615050678801L, "DB0.dul0")] + [TestCase(1311768394163015151uL, "DB0.dul0")] + [TestCase(17452669458658872815uL, "DB0.dul0")] + [TestCase(new byte[] { 0x12, 0x34, 0x56, 0x67 }, "DB0.DBB0.4")] + [TestCase(0.25f, "DB0.D0")] + [TestCase("ABCD", "DB0.string0.4")] + [TestCase("ABCD", "DB0.string0.4")] // Clip to length in Address + [TestCase("ABCD", "DB0.DBB0.4")] + public void Write(T input, string address) + { + //Arrange + var variableAddress = parser.Parse(address); + var buffer = new byte[variableAddress.BufferLength]; + + //Act + S7ValueConverter.WriteToBuffer(buffer, input, variableAddress); + var result = S7ValueConverter.ConvertToType(buffer, variableAddress); + + //Assert + result.ShouldBe(input); + } + +} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/S7ValueConverterTests.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs similarity index 95% rename from Sharp7.Rx.Tests/S7ValueConverterTests/S7ValueConverterTests.cs rename to Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs index 23052d2..caa37a9 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/S7ValueConverterTests.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs @@ -16,7 +16,6 @@ public class ConvertToType [TestCase(true, "DB0.DBx0.4", new byte[] {0x1F})] [TestCase(false, "DB0.DBx0.4", new byte[] {0xEF})] [TestCase((byte) 18, "DB0.DBB0", new byte[] {0x12})] - [TestCase((char) 18, "DB0.DBB0", new byte[] {0x12})] [TestCase((short) 4660, "DB0.INT0", new byte[] {0x12, 0x34})] [TestCase((short) -3532, "DB0.INT0", new byte[] {0xF2, 0x34})] [TestCase(-3532, "DB0.INT0", new byte[] {0xF2, 0x34})] @@ -28,7 +27,6 @@ public class ConvertToType [TestCase(17452669458658872815uL, "DB0.dul0", new byte[] {0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] [TestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB0.DBB0.4", new byte[] {0x12, 0x34, 0x56, 0x67})] [TestCase(0.25f, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] - [TestCase(0.25, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] [TestCase("ABCD", "DB0.string0.4", new byte[] {0x00, 0x04, 0x41, 0x42, 0x43, 0x44})] [TestCase("ABCD", "DB0.string0.4", new byte[] {0x00, 0xF0, 0x41, 0x42, 0x43, 0x44})] // Clip to length in Address [TestCase("ABCD", "DB0.DBB0.4", new byte[] {0x41, 0x42, 0x43, 0x44})] @@ -44,8 +42,10 @@ public class ConvertToType result.ShouldBe(expected); } + [TestCase((char) 18, "DB0.DBB0", new byte[] {0x12})] [TestCase((ushort) 3532, "DB0.INT0", new byte[] {0xF2, 0x34})] - public void Invalid(T expected, string address, byte[] data) + [TestCase(0.25, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] + public void Invalid(T template, string address, byte[] data) { //Arrange var variableAddress = parser.Parse(address); @@ -55,7 +55,7 @@ public class ConvertToType } [TestCase(3532, "DB0.DINT0", new byte[] {0xF2, 0x34})] - public void Argument(T expected, string address, byte[] data) + public void Argument(T template, string address, byte[] data) { //Arrange var variableAddress = parser.Parse(address); diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs index 137bd07..c33209f 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs @@ -13,10 +13,7 @@ public class WriteToBuffer [TestCase(false, "DB0.DBx0.0", new byte[] {0x00})] [TestCase(true, "DB0.DBx0.4", new byte[] {0x10})] [TestCase(false, "DB0.DBx0.4", new byte[] {0})] - [TestCase(true, "DB0.DBx0.4", new byte[] {0x1F})] - [TestCase(false, "DB0.DBx0.4", new byte[] {0xEF})] [TestCase((byte) 18, "DB0.DBB0", new byte[] {0x12})] - [TestCase((char) 18, "DB0.DBB0", new byte[] {0x12})] [TestCase((short) 4660, "DB0.INT0", new byte[] {0x12, 0x34})] [TestCase((short) -3532, "DB0.INT0", new byte[] {0xF2, 0x34})] [TestCase(-3532, "DB0.INT0", new byte[] {0xF2, 0x34})] @@ -28,20 +25,32 @@ public class WriteToBuffer [TestCase(17452669458658872815uL, "DB0.dul0", new byte[] {0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] [TestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB0.DBB0.4", new byte[] {0x12, 0x34, 0x56, 0x67})] [TestCase(0.25f, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] - [TestCase(0.25, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] - [TestCase("ABCD", "DB0.string0.4", new byte[] {0x00, 0x04, 0x41, 0x42, 0x43, 0x44})] - [TestCase("ABCD", "DB0.string0.4", new byte[] {0x00, 0xF0, 0x41, 0x42, 0x43, 0x44})] // Clip to length in Address + [TestCase("ABCD", "DB0.string0.4", new byte[] {0x04, 0x04, 0x41, 0x42, 0x43, 0x44})] + [TestCase("ABCD", "DB0.string0.8", new byte[] {0x08, 0x04, 0x41, 0x42, 0x43, 0x44, 0x00, 0x00, 0x00, 0x00})] + [TestCase("ABCD", "DB0.string0.2", new byte[] {0x02, 0x02, 0x41, 0x42})] [TestCase("ABCD", "DB0.DBB0.4", new byte[] {0x41, 0x42, 0x43, 0x44})] - public void Write(T expected, string address, byte[] data) + public void Write(T input, string address, byte[] expected) { //Arrange var variableAddress = parser.Parse(address); - var buffer = new byte[variableAddress.Length]; + var buffer = new byte[variableAddress.BufferLength]; //Act - S7ValueConverter.WriteToBuffer(buffer, expected, variableAddress); + S7ValueConverter.WriteToBuffer(buffer, input, variableAddress); //Assert - buffer.ShouldBe(data); + buffer.ShouldBe(expected); + } + + [TestCase((char) 18, "DB0.DBB0")] + [TestCase(0.25, "DB0.D0")] + public void Invalid(T input, string address) + { + //Arrange + var variableAddress = parser.Parse(address); + var buffer = new byte[variableAddress.BufferLength]; + + //Act + Should.Throw(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); } } diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 612e159..247070a 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -7,10 +7,6 @@ namespace Sharp7.Rx; internal static class S7ValueConverter { - public static void WriteToBuffer(Span buffer, TValue value, S7VariableAddress address) - { - } - public static TValue ConvertToType(byte[] buffer, S7VariableAddress address) { if (typeof(TValue) == typeof(bool)) @@ -37,21 +33,10 @@ internal static class S7ValueConverter if (typeof(TValue) == typeof(byte)) return (TValue) (object) buffer[0]; - if (typeof(TValue) == typeof(char)) - return (TValue) (object) (char) buffer[0]; if (typeof(TValue) == typeof(byte[])) return (TValue) (object) buffer; - - if (typeof(TValue) == typeof(double)) - { - var d = new UInt32SingleMap - { - UInt32 = BinaryPrimitives.ReadUInt32BigEndian(buffer) - }; - return (TValue) (object) (double) d.Single; - } - + if (typeof(TValue) == typeof(float)) { var d = new UInt32SingleMap @@ -75,7 +60,83 @@ internal static class S7ValueConverter else return (TValue) (object) Encoding.ASCII.GetString(buffer).Trim(); - throw new InvalidOperationException(string.Format("type '{0}' not supported.", typeof(TValue))); + throw new InvalidOperationException($"type '{typeof(TValue)}' not supported."); + } + + public static void WriteToBuffer(Span buffer, TValue value, S7VariableAddress address) + { + if (buffer.Length < address.BufferLength) + throw new ArgumentException($"buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer)); + + if (typeof(TValue) == typeof(bool)) + { + var byteValue = (bool) (object) value ? (byte) 1 : (byte) 0; + var shifted = (byte) (byteValue << address.Bit); + buffer[0] = shifted; + } + + else if (typeof(TValue) == typeof(int)) + { + if (address.Length == 2) + BinaryPrimitives.WriteInt16BigEndian(buffer, (short) (int) (object) value); + else + BinaryPrimitives.WriteInt32BigEndian(buffer, (int) (object) value); + } + else if (typeof(TValue) == typeof(short)) + { + if (address.Length == 2) + BinaryPrimitives.WriteInt16BigEndian(buffer, (short) (object) value); + else + BinaryPrimitives.WriteInt32BigEndian(buffer, (short) (object) value); + } + else if (typeof(TValue) == typeof(long)) + BinaryPrimitives.WriteInt64BigEndian(buffer, (long) (object) value); + else if (typeof(TValue) == typeof(ulong)) + BinaryPrimitives.WriteUInt64BigEndian(buffer, (ulong) (object) value); + else if (typeof(TValue) == typeof(byte)) + buffer[0] = (byte) (object) value; + else if (typeof(TValue) == typeof(byte[])) + { + var source = (byte[]) (object) value; + + var length = Math.Min(Math.Min(source.Length, buffer.Length), address.Length); + + source.AsSpan(0, length).CopyTo(buffer); + } + else if (typeof(TValue) == typeof(float)) + { + var map = new UInt32SingleMap + { + Single = (float) (object) value + }; + + BinaryPrimitives.WriteUInt32BigEndian(buffer, map.UInt32); + } + else if (typeof(TValue) == typeof(string)) + { + if (value is not string stringValue) throw new ArgumentException("Value must be of type string", nameof(value)); + + // Todo: Serialize directly to Span, when upgrading to .net + var stringBytes = Encoding.ASCII.GetBytes(stringValue); + + var length = Math.Min(address.Length, stringValue.Length); + + int stringOffset; + if (address.Type == DbType.String) + { + stringOffset = 2; + buffer[0] = (byte) address.Length; + buffer[1] = (byte) length; + } + else + stringOffset = 0; + + stringBytes.AsSpan(0, length).CopyTo(buffer.Slice(stringOffset)); + } + else + { + throw new InvalidOperationException($"type '{typeof(TValue)}' not supported."); + } } [StructLayout(LayoutKind.Explicit)] diff --git a/Sharp7.Rx/S7VariableAddress.cs b/Sharp7.Rx/S7VariableAddress.cs index ae5e440..b12d55f 100644 --- a/Sharp7.Rx/S7VariableAddress.cs +++ b/Sharp7.Rx/S7VariableAddress.cs @@ -12,4 +12,6 @@ internal class S7VariableAddress public ushort Length { get; set; } public byte Bit { get; set; } public DbType Type { get; set; } + + public ushort BufferLength => Type == DbType.String ? (ushort)(Length + 2) : Length; } From 2a694bf9808e726dac151dca1241685df61be81c Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 16:43:18 +0100 Subject: [PATCH 06/16] Use WriteToBuffer in Sharp7Plc --- Sharp7.Rx/Sharp7Plc.cs | 63 +++++------------------------------------- 1 file changed, 7 insertions(+), 56 deletions(-) diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 02a975c..d7eb28d 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -2,7 +2,6 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Text; using Microsoft.Extensions.Logging; using Sharp7.Rx.Basics; using Sharp7.Rx.Enums; @@ -155,64 +154,16 @@ public class Sharp7Plc : IPlc if (address == null) throw new ArgumentException("Input variable name is not valid", "variableName"); if (typeof(TValue) == typeof(bool)) - { + // Special handling for bools, which are written on a by-bit basis. Writing a complete byte would + // overwrite other bits within this byte. 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)]; - buffer.SetRealAt(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."); + // TODO: Use ArrayPool.Rent() once we drop Framwework support + var bytes = new byte[address.BufferLength]; + S7ValueConverter.WriteToBuffer(bytes, value, address); + + await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); } } From 6492d039daad2d0dc164421cdfab74f5fc09b476 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 17:40:51 +0100 Subject: [PATCH 07/16] Extend supported variables and improve parser errors --- .../S7VariableAddressTests/MatchesType.cs | 40 ++++ Sharp7.Rx.Tests/S7VariableNameParserTests.cs | 77 ++++++-- Sharp7.Rx/Enums/DbType.cs | 46 ++++- Sharp7.Rx/Exceptions/S7Exception.cs | 27 +++ Sharp7.Rx/Extensions/S7VariableExtensions.cs | 9 + Sharp7.Rx/Interfaces/IS7VariableNameParser.cs | 3 +- Sharp7.Rx/S7VariableAddress.cs | 9 +- Sharp7.Rx/S7VariableNameParser.cs | 171 ++++++++++++------ Sharp7.Rx/Sharp7.Rx.csproj | 4 + Sharp7.Rx/Sharp7.Rx.csproj.DotSettings | 2 + Sharp7.Rx/Sharp7Plc.cs | 8 +- 11 files changed, 323 insertions(+), 73 deletions(-) create mode 100644 Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs create mode 100644 Sharp7.Rx/Exceptions/S7Exception.cs create mode 100644 Sharp7.Rx/Extensions/S7VariableExtensions.cs create mode 100644 Sharp7.Rx/Sharp7.Rx.csproj.DotSettings diff --git a/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs b/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs new file mode 100644 index 0000000..d3d428a --- /dev/null +++ b/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using NUnit.Framework; +using Sharp7.Rx.Extensions; +using Sharp7.Rx.Interfaces; +using Shouldly; + +namespace Sharp7.Rx.Tests.S7VariableAddressTests; + +[TestFixture] +public class MatchesType +{ + static readonly IS7VariableNameParser parser = new S7VariableNameParser(); + + + public void Supported(Type type, string address) + { + Check(type, address, true); + } + + public IEnumerable GetValid() + { + yield return new TestCase(typeof(bool), "DB0.DBx0.0"); + yield return new TestCase(typeof(short), "DB0.INT0"); + yield return new TestCase(typeof(int), "DB0.DINT0"); + yield return new TestCase(typeof(long), "DB0.DUL0"); + yield return new TestCase(typeof(ulong), "DB0.DUL0"); + } + + + private static void Check(Type type, string address, bool expected) + { + //Arrange + var variableAddress = parser.Parse(address); + + //Act + variableAddress.MatchesType(type).ShouldBe(expected); + } + + public record TestCase(Type Type, string Address); +} diff --git a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs index a5fa263..7f4869f 100644 --- a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs +++ b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs @@ -1,13 +1,14 @@ using DeepEqual.Syntax; using NUnit.Framework; using Sharp7.Rx.Enums; +using Shouldly; namespace Sharp7.Rx.Tests; [TestFixture] internal class S7VariableNameParserTests { - [TestCaseSource(nameof(GetTestCases))] + [TestCaseSource(nameof(ValidTestCases))] public void Run(TestCase tc) { var parser = new S7VariableNameParser(); @@ -15,23 +16,71 @@ internal class S7VariableNameParserTests resp.ShouldDeepEqual(tc.Expected); } - public static IEnumerable GetTestCases() + [TestCase("DB506.Bit216", TestName = "Bit without Bit")] + [TestCase("DB506.String216", TestName = "String without Length")] + [TestCase("DB506.WString216", TestName = "WString without Length")] + + [TestCase("DB506.Int216.1", TestName = "Int with Length")] + [TestCase("DB506.UInt216.1", TestName = "UInt with Length")] + [TestCase("DB506.DInt216.1", TestName = "DInt with Length")] + [TestCase("DB506.UDInt216.1", TestName = "UDInt with Length")] + [TestCase("DB506.LInt216.1", TestName = "LInt with Length")] + [TestCase("DB506.ULInt216.1", TestName = "ULInt with Length")] + [TestCase("DB506.Real216.1", TestName = "LReal with Length")] + [TestCase("DB506.LReal216.1", TestName = "LReal with Length")] + + [TestCase("DB506.xx216", TestName = "Invalid type")] + [TestCase("DB506.216", TestName = "No type")] + [TestCase("DB506.Int216.", TestName = "Trailing dot")] + [TestCase("x506.Int216", TestName = "Wrong type")] + [TestCase("506.Int216", TestName = "No type")] + [TestCase("", TestName = "empty")] + [TestCase(" ", TestName = "space")] + [TestCase(" DB506.Int216", TestName = "leading space")] + [TestCase("DB506.Int216 ", TestName = "trailing space")] + [TestCase("DB.Int216 ", TestName = "No db")] + [TestCase("DB5061234.Int216.1", TestName = "DB too large")] + public void Invalid(string? input) { + var parser = new S7VariableNameParser(); + Should.Throw(() => parser.Parse(input)); + } + + public static IEnumerable ValidTestCases() + { + yield return new TestCase("DB506.Bit216.2", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Bit = 2, Type = DbType.Bit}); + + yield return new TestCase("DB506.String216.10", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 10, Type = DbType.String}); + yield return new TestCase("DB506.WString216.10", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 10, Type = DbType.WString}); + + yield return new TestCase("DB506.Byte216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Type = DbType.Byte}); + yield return new TestCase("DB506.Byte216.100", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 100, Type = DbType.Byte}); + yield return new TestCase("DB506.Int216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int}); + yield return new TestCase("DB506.UInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.UInt}); + yield return new TestCase("DB506.DInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.DInt}); + yield return new TestCase("DB506.UDInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.UDInt}); + yield return new TestCase("DB506.LInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.LInt}); + yield return new TestCase("DB506.ULInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); + + yield return new TestCase("DB506.Real216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.Single}); + yield return new TestCase("DB506.LReal216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.Double}); + + + // Legacy yield return new TestCase("DB13.DBX3.1", new S7VariableAddress {Operand = Operand.Db, DbNr = 13, Start = 3, Length = 1, Bit = 1, Type = DbType.Bit}); yield return new TestCase("Db403.X5.2", new S7VariableAddress {Operand = Operand.Db, DbNr = 403, Start = 5, Length = 1, Bit = 2, Type = DbType.Bit}); yield return new TestCase("DB55DBX23.6", new S7VariableAddress {Operand = Operand.Db, DbNr = 55, Start = 23, Length = 1, Bit = 6, Type = DbType.Bit}); - yield return new TestCase("DB1.S255", new S7VariableAddress {Operand = Operand.Db, DbNr = 1, Start = 255, Length = 0, Bit = 0, Type = DbType.String}); - yield return new TestCase("DB1.S255.20", new S7VariableAddress {Operand = Operand.Db, DbNr = 1, Start = 255, Length = 20, Bit = 0, Type = DbType.String}); - yield return new TestCase("DB5.String887.20", new S7VariableAddress {Operand = Operand.Db, DbNr = 5, Start = 887, Length = 20, Bit = 0, Type = DbType.String}); - yield return new TestCase("DB506.B216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Bit = 0, Type = DbType.Byte}); - yield return new TestCase("DB506.DBB216.5", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 5, Bit = 0, Type = DbType.Byte}); - yield return new TestCase("DB506.D216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Bit = 0, Type = DbType.Double}); - yield return new TestCase("DB506.DINT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Bit = 0, Type = DbType.DInteger}); - yield return new TestCase("DB506.INT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Bit = 0, Type = DbType.Integer}); - yield return new TestCase("DB506.DBW216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Bit = 0, Type = DbType.Integer}); - yield return new TestCase("DB506.DUL216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Bit = 0, Type = DbType.ULong}); - yield return new TestCase("DB506.DULINT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Bit = 0, Type = DbType.ULong}); - yield return new TestCase("DB506.DULONG216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Bit = 0, Type = DbType.ULong}); + yield return new TestCase("DB1.S255.20", new S7VariableAddress {Operand = Operand.Db, DbNr = 1, Start = 255, Length = 20, Type = DbType.String}); + yield return new TestCase("DB5.String887.20", new S7VariableAddress {Operand = Operand.Db, DbNr = 5, Start = 887, Length = 20, Type = DbType.String}); + yield return new TestCase("DB506.B216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Type = DbType.Byte}); + yield return new TestCase("DB506.DBB216.5", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 5, Type = DbType.Byte}); + yield return new TestCase("DB506.D216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.Single}); + yield return new TestCase("DB506.DINT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.DInt}); + yield return new TestCase("DB506.INT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int}); + yield return new TestCase("DB506.DBW216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int}); + yield return new TestCase("DB506.DUL216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); + yield return new TestCase("DB506.DULINT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); + yield return new TestCase("DB506.DULONG216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); } public record TestCase(string Input, S7VariableAddress Expected) diff --git a/Sharp7.Rx/Enums/DbType.cs b/Sharp7.Rx/Enums/DbType.cs index 07066f8..30691d6 100644 --- a/Sharp7.Rx/Enums/DbType.cs +++ b/Sharp7.Rx/Enums/DbType.cs @@ -1,12 +1,52 @@ namespace Sharp7.Rx.Enums; +// see https://support.industry.siemens.com/cs/mdm/109747174?c=88343664523&lc=de-DE internal enum DbType { Bit, + + /// + /// ASCII string + /// String, + + /// + /// UTF16 string + /// + WString, + Byte, + + /// + /// Int16 + /// + Int, + + /// + /// UInt16 + /// + UInt, + + /// + /// Int32 + /// + DInt, + + /// + /// UInt32 + /// + UDInt, + + /// + /// Int64 + /// + LInt, + + /// + /// UInt64 + /// + ULInt, + + Single, Double, - Integer, - DInteger, - ULong } diff --git a/Sharp7.Rx/Exceptions/S7Exception.cs b/Sharp7.Rx/Exceptions/S7Exception.cs new file mode 100644 index 0000000..e9cc1d3 --- /dev/null +++ b/Sharp7.Rx/Exceptions/S7Exception.cs @@ -0,0 +1,27 @@ +namespace Sharp7.Rx; + +public abstract class S7Exception : Exception +{ + protected S7Exception(string message) : base(message) + { + } + + protected S7Exception(string message, Exception innerException) : base(message, innerException) + { + } +} + +public class InvalidS7AddressException : S7Exception +{ + public InvalidS7AddressException(string message, string input) : base(message) + { + Input = input; + } + + public InvalidS7AddressException(string message, Exception innerException, string input) : base(message, innerException) + { + Input = input; + } + + public string Input { get; private set; } +} diff --git a/Sharp7.Rx/Extensions/S7VariableExtensions.cs b/Sharp7.Rx/Extensions/S7VariableExtensions.cs new file mode 100644 index 0000000..a2b0a39 --- /dev/null +++ b/Sharp7.Rx/Extensions/S7VariableExtensions.cs @@ -0,0 +1,9 @@ +namespace Sharp7.Rx.Extensions; + +internal static class S7VariableAddressExtensions +{ + public static bool MatchesType(this S7VariableAddress address, Type type) + { + return false; + } +} diff --git a/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs b/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs index 07f4b9e..ae81d67 100644 --- a/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs +++ b/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs @@ -1,4 +1,5 @@ -namespace Sharp7.Rx.Interfaces; +#nullable enable +namespace Sharp7.Rx.Interfaces; internal interface IS7VariableNameParser { diff --git a/Sharp7.Rx/S7VariableAddress.cs b/Sharp7.Rx/S7VariableAddress.cs index b12d55f..0172c06 100644 --- a/Sharp7.Rx/S7VariableAddress.cs +++ b/Sharp7.Rx/S7VariableAddress.cs @@ -10,8 +10,13 @@ internal class S7VariableAddress public ushort DbNr { get; set; } public ushort Start { get; set; } public ushort Length { get; set; } - public byte Bit { get; set; } + public byte? Bit { get; set; } public DbType Type { get; set; } - public ushort BufferLength => Type == DbType.String ? (ushort)(Length + 2) : Length; + public ushort BufferLength => Type switch + { + DbType.String => (ushort) (Length + 2), + DbType.WString => (ushort) (Length * 2 + 4), + _ => Length + }; } diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index 80e9913..683ec18 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -7,75 +7,142 @@ namespace Sharp7.Rx; internal class S7VariableNameParser : IS7VariableNameParser { - private static readonly Regex regex = new Regex(@"^(?db{1})(?\d{1,4})\.?(?dbx|x|s|string|b|dbb|d|int|dbw|w|dint|dul|dulint|dulong|){1}(?\d+)(\.(?\d+))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex regex = new Regex(@"^(?db)(?\d+)\.?(?[a-z]+)(?\d+)(\.(?\d+))?$", + RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly IReadOnlyDictionary types = new Dictionary(StringComparer.OrdinalIgnoreCase) { - {"x", DbType.Bit}, - {"dbx", DbType.Bit}, - {"s", DbType.String}, + {"bit", DbType.Bit}, + {"string", DbType.String}, + {"wstring", DbType.WString}, + + {"byte", DbType.Byte}, + {"int", DbType.Int}, + {"uint", DbType.UInt}, + {"dint", DbType.DInt}, + {"udint", DbType.UDInt}, + {"lint", DbType.LInt}, + {"ulint", DbType.ULInt}, + + {"real", DbType.Single}, + {"lreal", DbType.Double}, + + // used for legacy compatability {"b", DbType.Byte}, + {"d", DbType.Single}, {"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} + {"dbw", DbType.Int}, + {"dbx", DbType.Bit}, + {"dul", DbType.ULInt}, + {"dulint", DbType.ULInt}, + {"dulong", DbType.ULInt}, + {"s", DbType.String}, + {"w", DbType.Int}, + {"x", DbType.Bit}, }; public S7VariableAddress Parse(string input) { + if (input == null) + throw new ArgumentNullException(nameof(input)); + var match = regex.Match(input); - if (match.Success) + if (!match.Success) + throw new InvalidS7AddressException($"Invalid S7 address: \"{input}\"", input); + + var operand = (Operand) Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true); + + if (!ushort.TryParse(match.Groups["dbNo"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var dbNr)) + throw new InvalidS7AddressException($"\"{match.Groups["dbNo"].Value}\" is an invalid DB number in \"{input}\"", input); + + if (!ushort.TryParse(match.Groups["start"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var start)) + throw new InvalidS7AddressException($"\"{match.Groups["start"].Value}\" is an invalid start bit in \"{input}\"", input); + + if (!types.TryGetValue(match.Groups["type"].Value, out var type)) + throw new InvalidS7AddressException($"\"{match.Groups["type"].Value}\" is an invalid type in \"{input}\"", input); + + ushort length = type switch { - var operand = (Operand) Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true); - var dbNr = ushort.Parse(match.Groups["dbNr"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture); - var start = ushort.Parse(match.Groups["start"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture); - if (!types.TryGetValue(match.Groups["type"].Value, out var type)) - return null; + DbType.Bit => 1, + DbType.String => GetLength(), + DbType.WString => GetLength(), - var s7VariableAddress = new S7VariableAddress - { - Operand = operand, - DbNr = dbNr, - Start = start, - Type = type, - }; + DbType.Byte => GetLength(1), - switch (type) - { - case DbType.Bit: - s7VariableAddress.Length = 1; - s7VariableAddress.Bit = byte.Parse(match.Groups["bitOrLength"].Value); - break; - case DbType.Byte: - s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort) 1; - break; - case DbType.String: - s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort) 0; - break; - case DbType.Integer: - s7VariableAddress.Length = 2; - break; - case DbType.DInteger: - s7VariableAddress.Length = 4; - break; - case DbType.ULong: - s7VariableAddress.Length = 8; - break; - case DbType.Double: - s7VariableAddress.Length = 4; - break; - } + DbType.Int => 2, + DbType.DInt => 4, + DbType.ULInt => 8, + DbType.UInt => 2, + DbType.UDInt => 4, + DbType.LInt => 8, - return s7VariableAddress; + DbType.Single => 4, + DbType.Double => 8, + _ => throw new ArgumentOutOfRangeException($"DbType {type} is not supported") + }; + + switch (type) + { + case DbType.Bit: + case DbType.String: + case DbType.WString: + case DbType.Byte: + break; + case DbType.Int: + case DbType.UInt: + case DbType.DInt: + case DbType.UDInt: + case DbType.LInt: + case DbType.ULInt: + case DbType.Single: + case DbType.Double: + default: + if (match.Groups["bitOrLength"].Success) + throw new InvalidS7AddressException($"{type} address must not have a length: \"{input}\"", input); + break; } - return null; + byte? bit = type == DbType.Bit ? GetBit() : null; + + + var s7VariableAddress = new S7VariableAddress + { + Operand = operand, + DbNr = dbNr, + Start = start, + Type = type, + Length = length, + Bit = bit + }; + + return s7VariableAddress; + + ushort GetLength(ushort? defaultValue = null) + { + if (!match.Groups["bitOrLength"].Success) + { + if (defaultValue.HasValue) + return defaultValue.Value; + throw new InvalidS7AddressException($"Variable of type {type} must have a length set \"{input}\"", input); + } + + if (!ushort.TryParse(match.Groups["bitOrLength"].Value, out var result)) + throw new InvalidS7AddressException($"\"{match.Groups["bitOrLength"].Value}\" is an invalid length in \"{input}\"", input); + + return result; + } + + byte GetBit() + { + if (!match.Groups["bitOrLength"].Success) + throw new InvalidS7AddressException($"Variable of type {type} must have a bit number set \"{input}\"", input); + + 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); + + return result; + } } } diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 3b73209..2e5a601 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -42,4 +42,8 @@ + + + + diff --git a/Sharp7.Rx/Sharp7.Rx.csproj.DotSettings b/Sharp7.Rx/Sharp7.Rx.csproj.DotSettings new file mode 100644 index 0000000..374f4af --- /dev/null +++ b/Sharp7.Rx/Sharp7.Rx.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index d7eb28d..c8401f1 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -154,9 +154,15 @@ public class Sharp7Plc : IPlc if (address == null) throw new ArgumentException("Input variable name is not valid", "variableName"); if (typeof(TValue) == typeof(bool)) + { // Special handling for bools, which are written on a by-bit basis. Writing a complete byte would // overwrite other bits within this byte. - await s7Connector.WriteBit(address.Operand, address.Start, address.Bit, (bool) (object) value, address.DbNr, token); + + if (address.Bit == null) + throw new InvalidOperationException("Address must have a Bit to write a bool."); + + await s7Connector.WriteBit(address.Operand, address.Start, address.Bit.Value, (bool) (object) value, address.DbNr, token); + } else { // TODO: Use ArrayPool.Rent() once we drop Framwework support From d1ec075aa7927722d9db26e492a07daf0c1fb446 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 10:12:13 +0100 Subject: [PATCH 08/16] Restructure and extens tests for converter --- .../S7ValueConverterTests/ConvertBothWays.cs | 40 +++------ .../S7ValueConverterTests/ConvertToType.cs | 66 -------------- .../ConverterTestBase.cs | 86 +++++++++++++++++++ .../S7ValueConverterTests/ReadFromBuffer.cs | 51 +++++++++++ .../S7ValueConverterTests/WriteToBuffer.cs | 37 ++------ Sharp7.Rx/S7ValueConverter.cs | 2 +- Sharp7.Rx/S7VariableNameParser.cs | 9 +- Sharp7.Rx/Sharp7Plc.cs | 4 +- 8 files changed, 164 insertions(+), 131 deletions(-) delete mode 100644 Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs create mode 100644 Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs create mode 100644 Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs index 5d264a1..b3956ac 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs @@ -1,45 +1,25 @@ using NUnit.Framework; -using Sharp7.Rx.Interfaces; using Shouldly; namespace Sharp7.Rx.Tests.S7ValueConverterTests; [TestFixture] -public class ConvertBothWays +internal class ConvertBothWays : ConverterTestBase { - static readonly IS7VariableNameParser parser = new S7VariableNameParser(); - - [TestCase(true, "DB0.DBx0.0")] - [TestCase(false, "DB0.DBx0.0")] - [TestCase(true, "DB0.DBx0.4")] - [TestCase(false, "DB0.DBx0.4")] - [TestCase((byte) 18, "DB0.DBB0")] - [TestCase((short) 4660, "DB0.INT0")] - [TestCase((short)-3532, "DB0.INT0")] - [TestCase(-3532, "DB0.INT0")] - [TestCase(305419879, "DB0.DINT0")] - [TestCase(-231451033, "DB0.DINT0")] - [TestCase(1311768394163015151L, "DB0.dul0")] - [TestCase(-994074615050678801L, "DB0.dul0")] - [TestCase(1311768394163015151uL, "DB0.dul0")] - [TestCase(17452669458658872815uL, "DB0.dul0")] - [TestCase(new byte[] { 0x12, 0x34, 0x56, 0x67 }, "DB0.DBB0.4")] - [TestCase(0.25f, "DB0.D0")] - [TestCase("ABCD", "DB0.string0.4")] - [TestCase("ABCD", "DB0.string0.4")] // Clip to length in Address - [TestCase("ABCD", "DB0.DBB0.4")] - public void Write(T input, string address) + [TestCaseSource(nameof(GetValidTestCases))] + public void Convert(ConverterTestCase tc) { //Arrange - var variableAddress = parser.Parse(address); - var buffer = new byte[variableAddress.BufferLength]; + var buffer = new byte[tc.VariableAddress.BufferLength]; + + var write = CreateWriteMethod(tc); + var read = CreateReadMethod(tc); //Act - S7ValueConverter.WriteToBuffer(buffer, input, variableAddress); - var result = S7ValueConverter.ConvertToType(buffer, variableAddress); + write.Invoke(null, [buffer, tc.Value, tc.VariableAddress]); + var result = read.Invoke(null, [buffer, tc.VariableAddress]); //Assert - result.ShouldBe(input); + result.ShouldBe(tc.Value); } - } diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs deleted file mode 100644 index caa37a9..0000000 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs +++ /dev/null @@ -1,66 +0,0 @@ -using NUnit.Framework; -using Sharp7.Rx.Interfaces; -using Shouldly; - -namespace Sharp7.Rx.Tests.S7ValueConverterTests; - -[TestFixture] -public class ConvertToType -{ - static readonly IS7VariableNameParser parser = new S7VariableNameParser(); - - [TestCase(true, "DB0.DBx0.0", new byte[] {0x01})] - [TestCase(false, "DB0.DBx0.0", new byte[] {0x00})] - [TestCase(true, "DB0.DBx0.4", new byte[] {0x10})] - [TestCase(false, "DB0.DBx0.4", new byte[] {0})] - [TestCase(true, "DB0.DBx0.4", new byte[] {0x1F})] - [TestCase(false, "DB0.DBx0.4", new byte[] {0xEF})] - [TestCase((byte) 18, "DB0.DBB0", new byte[] {0x12})] - [TestCase((short) 4660, "DB0.INT0", new byte[] {0x12, 0x34})] - [TestCase((short) -3532, "DB0.INT0", new byte[] {0xF2, 0x34})] - [TestCase(-3532, "DB0.INT0", new byte[] {0xF2, 0x34})] - [TestCase(305419879, "DB0.DINT0", new byte[] {0x12, 0x34, 0x56, 0x67})] - [TestCase(-231451033, "DB0.DINT0", new byte[] {0xF2, 0x34, 0x56, 0x67})] - [TestCase(1311768394163015151L, "DB0.dul0", new byte[] {0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] - [TestCase(-994074615050678801L, "DB0.dul0", new byte[] {0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] - [TestCase(1311768394163015151uL, "DB0.dul0", new byte[] {0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] - [TestCase(17452669458658872815uL, "DB0.dul0", new byte[] {0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] - [TestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB0.DBB0.4", new byte[] {0x12, 0x34, 0x56, 0x67})] - [TestCase(0.25f, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] - [TestCase("ABCD", "DB0.string0.4", new byte[] {0x00, 0x04, 0x41, 0x42, 0x43, 0x44})] - [TestCase("ABCD", "DB0.string0.4", new byte[] {0x00, 0xF0, 0x41, 0x42, 0x43, 0x44})] // Clip to length in Address - [TestCase("ABCD", "DB0.DBB0.4", new byte[] {0x41, 0x42, 0x43, 0x44})] - public void Parse(T expected, string address, byte[] data) - { - //Arrange - var variableAddress = parser.Parse(address); - - //Act - var result = S7ValueConverter.ConvertToType(data, variableAddress); - - //Assert - result.ShouldBe(expected); - } - - [TestCase((char) 18, "DB0.DBB0", new byte[] {0x12})] - [TestCase((ushort) 3532, "DB0.INT0", new byte[] {0xF2, 0x34})] - [TestCase(0.25, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] - public void Invalid(T template, string address, byte[] data) - { - //Arrange - var variableAddress = parser.Parse(address); - - //Act - Should.Throw(() => S7ValueConverter.ConvertToType(data, variableAddress)); - } - - [TestCase(3532, "DB0.DINT0", new byte[] {0xF2, 0x34})] - public void Argument(T template, string address, byte[] data) - { - //Arrange - var variableAddress = parser.Parse(address); - - //Act - Should.Throw(() => S7ValueConverter.ConvertToType(data, variableAddress)); - } -} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs new file mode 100644 index 0000000..273029c --- /dev/null +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs @@ -0,0 +1,86 @@ +using System.Reflection; +using Sharp7.Rx.Interfaces; + +namespace Sharp7.Rx.Tests.S7ValueConverterTests; + +internal abstract class ConverterTestBase +{ + protected static readonly IS7VariableNameParser Parser = new S7VariableNameParser(); + + public static MethodInfo CreateReadMethod(ConverterTestCase tc) + { + var convertMi = typeof(S7ValueConverter).GetMethod(nameof(S7ValueConverter.ReadFromBuffer)); + var convert = convertMi!.MakeGenericMethod(tc.Value.GetType()); + return convert; + } + + public static MethodInfo CreateWriteMethod(ConverterTestCase tc) + { + var writeMi = typeof(ConverterTestBase).GetMethod(nameof(WriteToBuffer)); + var write = writeMi!.MakeGenericMethod(tc.Value.GetType()); + return write; + } + + public static IEnumerable GetValidTestCases() + { + yield return new ConverterTestCase(true, "DB99.bit5.4", [0x10]); + yield return new ConverterTestCase(false, "DB99.bit5.4", [0x00]); + + yield return new ConverterTestCase((byte) 18, "DB99.Byte5", [0x12]); + yield return new ConverterTestCase((short) 4660, "DB99.Int5", [0x12, 0x34]); + yield return new ConverterTestCase((short) -3532, "DB99.Int5", [0xF2, 0x34]); + yield return new ConverterTestCase((ushort) 4660, "DB99.UInt5", [0x12, 0x34]); + yield return new ConverterTestCase((ushort) 3532, "DB99.UInt5", [0xF2, 0x34]); + yield return new ConverterTestCase(305419879, "DB99.DInt5", [0x12, 0x34, 0x56, 0x67]); + yield return new ConverterTestCase(-231451033, "DB99.DInt5", [0xF2, 0x34, 0x56, 0x67]); + yield return new ConverterTestCase(305419879u, "DB99.UDInt5", [0x12, 0x34, 0x56, 0x67]); + yield return new ConverterTestCase(231451033u, "DB99.UDInt5", [0xF2, 0x34, 0x56, 0x67]); + yield return new ConverterTestCase(1311768394163015151L, "DB99.LInt5", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + yield return new ConverterTestCase(-994074615050678801L, "DB99.LInt5", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + yield return new ConverterTestCase(1311768394163015151uL, "DB99.ULInt5", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + yield return new ConverterTestCase(17452669458658872815uL, "DB99.ULInt5", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + yield return new ConverterTestCase(0.25f, "DB99.Real5", [0x3E, 0x80, 0x00, 0x00]); + yield return new ConverterTestCase(0.25, "DB99.LReal5", [0x3E, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + + yield return new ConverterTestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB99.Byte5.4", [0x12, 0x34, 0x56, 0x67]); + + yield return new ConverterTestCase("ABCD", "DB99.String10.4", [0x04, 0x04, 0x41, 0x42, 0x43, 0x44]); + yield return new ConverterTestCase("ABCD", "DB99.String10.6", [0x06, 0x04, 0x41, 0x42, 0x43, 0x44, 0x00, 0x00]); + yield return new ConverterTestCase("ABCD", "DB99.WString10.4", [0x00, 0x04, 0x00, 0x04, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44]); + yield return new ConverterTestCase("ABCD", "DB99.WString10.6", [0x00, 0x06, 0x00, 0x04, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00]); + yield return new ConverterTestCase("ABCD", "DB99.Byte5.4", [0x41, 0x42, 0x43, 0x44]); + + yield return new ConverterTestCase(true, "DB99.DBx0.0", [0x01]); + yield return new ConverterTestCase(false, "DB99.DBx0.0", [0x00]); + yield return new ConverterTestCase(true, "DB99.DBx0.4", [0x10]); + yield return new ConverterTestCase(false, "DB99.DBx0.4", [0]); + yield return new ConverterTestCase((byte) 18, "DB99.DBB0", [0x12]); + yield return new ConverterTestCase((short) 4660, "DB99.INT0", [0x12, 0x34]); + yield return new ConverterTestCase((short) -3532, "DB99.INT0", [0xF2, 0x34]); + yield return new ConverterTestCase(-3532, "DB99.INT0", [0xF2, 0x34]); + yield return new ConverterTestCase(305419879, "DB99.DINT0", [0x12, 0x34, 0x56, 0x67]); + yield return new ConverterTestCase(-231451033, "DB99.DINT0", [0xF2, 0x34, 0x56, 0x67]); + yield return new ConverterTestCase(1311768394163015151L, "DB99.dul0", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + yield return new ConverterTestCase(-994074615050678801L, "DB99.dul0", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + yield return new ConverterTestCase(1311768394163015151uL, "DB99.dul0", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + yield return new ConverterTestCase(17452669458658872815uL, "DB99.dul0", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + yield return new ConverterTestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB99.DBB0.4", [0x12, 0x34, 0x56, 0x67]); + yield return new ConverterTestCase(0.25f, "DB99.D0", [0x3E, 0x80, 0x00, 0x00]); + } + + /// + /// This helper method exists, since I could not manage to invoke a generic method + /// accepring a Span<T> as parameter. + /// + public static void WriteToBuffer(byte[] buffer, TValue value, S7VariableAddress address) + { + S7ValueConverter.WriteToBuffer(buffer, value, address); + } + + public record ConverterTestCase(object Value, string Address, byte[] Data) + { + public S7VariableAddress VariableAddress => Parser.Parse(Address); + + public override string ToString() => $"{Address} {Value} ({Value.GetType().Name})"; + } +} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs new file mode 100644 index 0000000..cf9cc4d --- /dev/null +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs @@ -0,0 +1,51 @@ +using NUnit.Framework; +using Shouldly; + +namespace Sharp7.Rx.Tests.S7ValueConverterTests; + +[TestFixture] +internal class ReadFromBuffer : ConverterTestBase +{ + [TestCaseSource(nameof(GetValidTestCases))] + [TestCaseSource(nameof(GetAdditinalReadTestCases))] + public void Read(ConverterTestCase tc) + { + //Arrange + var convert = CreateReadMethod(tc); + + //Act + var result = convert.Invoke(null, [tc.Data, tc.VariableAddress]); + + //Assert + result.ShouldBe(tc.Value); + } + + public static IEnumerable GetAdditinalReadTestCases() + { + yield return new ConverterTestCase(true, "DB0.DBx0.4", [0x1F]); + yield return new ConverterTestCase(false, "DB0.DBx0.4", [0xEF]); + yield return new ConverterTestCase("ABCD", "DB0.string0.10", [0x04, 0x04, 0x41, 0x42, 0x43, 0x44]); // Length in address exceeds PLC string length + } + + [TestCase((char) 18, "DB0.DBB0", new byte[] {0x12})] + [TestCase((ushort) 3532, "DB0.INT0", new byte[] {0xF2, 0x34})] + [TestCase(0.25, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] + public void Invalid(T template, string address, byte[] data) + { + //Arrange + var variableAddress = Parser.Parse(address); + + //Act + Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); + } + + [TestCase(3532, "DB0.DINT0", new byte[] {0xF2, 0x34})] + public void Argument(T template, string address, byte[] data) + { + //Arrange + var variableAddress = Parser.Parse(address); + + //Act + Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); + } +} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs index c33209f..4a93719 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs @@ -5,41 +5,20 @@ using Shouldly; namespace Sharp7.Rx.Tests.S7ValueConverterTests; [TestFixture] -public class WriteToBuffer +internal class WriteToBuffer:ConverterTestBase { - static readonly IS7VariableNameParser parser = new S7VariableNameParser(); - - [TestCase(true, "DB0.DBx0.0", new byte[] {0x01})] - [TestCase(false, "DB0.DBx0.0", new byte[] {0x00})] - [TestCase(true, "DB0.DBx0.4", new byte[] {0x10})] - [TestCase(false, "DB0.DBx0.4", new byte[] {0})] - [TestCase((byte) 18, "DB0.DBB0", new byte[] {0x12})] - [TestCase((short) 4660, "DB0.INT0", new byte[] {0x12, 0x34})] - [TestCase((short) -3532, "DB0.INT0", new byte[] {0xF2, 0x34})] - [TestCase(-3532, "DB0.INT0", new byte[] {0xF2, 0x34})] - [TestCase(305419879, "DB0.DINT0", new byte[] {0x12, 0x34, 0x56, 0x67})] - [TestCase(-231451033, "DB0.DINT0", new byte[] {0xF2, 0x34, 0x56, 0x67})] - [TestCase(1311768394163015151L, "DB0.dul0", new byte[] {0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] - [TestCase(-994074615050678801L, "DB0.dul0", new byte[] {0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] - [TestCase(1311768394163015151uL, "DB0.dul0", new byte[] {0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] - [TestCase(17452669458658872815uL, "DB0.dul0", new byte[] {0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF})] - [TestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB0.DBB0.4", new byte[] {0x12, 0x34, 0x56, 0x67})] - [TestCase(0.25f, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] - [TestCase("ABCD", "DB0.string0.4", new byte[] {0x04, 0x04, 0x41, 0x42, 0x43, 0x44})] - [TestCase("ABCD", "DB0.string0.8", new byte[] {0x08, 0x04, 0x41, 0x42, 0x43, 0x44, 0x00, 0x00, 0x00, 0x00})] - [TestCase("ABCD", "DB0.string0.2", new byte[] {0x02, 0x02, 0x41, 0x42})] - [TestCase("ABCD", "DB0.DBB0.4", new byte[] {0x41, 0x42, 0x43, 0x44})] - public void Write(T input, string address, byte[] expected) + [TestCaseSource(nameof(GetValidTestCases))] + public void Write(ConverterTestCase tc) { //Arrange - var variableAddress = parser.Parse(address); - var buffer = new byte[variableAddress.BufferLength]; + var buffer = new byte[tc.VariableAddress.BufferLength]; + var write = CreateWriteMethod(tc); //Act - S7ValueConverter.WriteToBuffer(buffer, input, variableAddress); + write.Invoke(null, [buffer, tc.Value, tc.VariableAddress]); //Assert - buffer.ShouldBe(expected); + buffer.ShouldBe(tc.Data); } [TestCase((char) 18, "DB0.DBB0")] @@ -47,7 +26,7 @@ public class WriteToBuffer public void Invalid(T input, string address) { //Arrange - var variableAddress = parser.Parse(address); + var variableAddress = Parser.Parse(address); var buffer = new byte[variableAddress.BufferLength]; //Act diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 247070a..9618a70 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -7,7 +7,7 @@ namespace Sharp7.Rx; internal static class S7ValueConverter { - public static TValue ConvertToType(byte[] buffer, S7VariableAddress address) + public static TValue ReadFromBuffer(byte[] buffer, S7VariableAddress address) { if (typeof(TValue) == typeof(bool)) return (TValue) (object) (((buffer[0] >> address.Bit) & 1) > 0); diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index 683ec18..bf5d7ca 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -28,12 +28,15 @@ internal class S7VariableNameParser : IS7VariableNameParser {"real", DbType.Single}, {"lreal", DbType.Double}, - // used for legacy compatability - {"b", DbType.Byte}, - {"d", DbType.Single}, + // S7 notation {"dbb", DbType.Byte}, {"dbw", DbType.Int}, {"dbx", DbType.Bit}, + {"dbd", DbType.DInt}, + + // used for legacy compatability + {"b", DbType.Byte}, + {"d", DbType.Single}, {"dul", DbType.ULInt}, {"dulint", DbType.ULInt}, {"dulong", DbType.ULInt}, diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index c8401f1..46936ef 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -90,7 +90,7 @@ public class Sharp7Plc : IPlc Observable.FromAsync(() => GetValue(variableName)) .Concat( disposeableContainer.Observable - .Select(bytes => S7ValueConverter.ConvertToType(bytes, address)) + .Select(bytes => S7ValueConverter.ReadFromBuffer(bytes, address)) ); if (transmissionMode == TransmissionMode.OnChange) @@ -121,7 +121,7 @@ public class Sharp7Plc : IPlc if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName)); var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); - return S7ValueConverter.ConvertToType(data, address); + return S7ValueConverter.ReadFromBuffer(data, address); } public async Task InitializeAsync() From 314542643235e0f3440001851aaaa1443f32da01 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 11:02:16 +0100 Subject: [PATCH 09/16] Improve error message --- Sharp7.Rx/S7VariableNameParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index bf5d7ca..2355b24 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -52,7 +52,7 @@ internal class S7VariableNameParser : IS7VariableNameParser var match = regex.Match(input); if (!match.Success) - throw new InvalidS7AddressException($"Invalid S7 address: \"{input}\"", input); + throw new InvalidS7AddressException($"Invalid S7 address \"{input}\". Expect format \"DB.(.)\".", input); var operand = (Operand) Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true); From 3c592c6d466135879b7dbc54c54c77cf0780b4da Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 12:57:36 +0100 Subject: [PATCH 10/16] Add new types to S7ValueConverter.ReadFromBuffer --- .../ConverterTestBase.cs | 9 +- Sharp7.Rx/Exceptions/S7Exception.cs | 40 ++++- Sharp7.Rx/S7ValueConverter.cs | 137 +++++++++++------- Sharp7.Rx/S7VariableAddress.cs | 10 ++ 4 files changed, 141 insertions(+), 55 deletions(-) diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs index 273029c..a4fb1b4 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs @@ -30,17 +30,17 @@ internal abstract class ConverterTestBase yield return new ConverterTestCase((short) 4660, "DB99.Int5", [0x12, 0x34]); yield return new ConverterTestCase((short) -3532, "DB99.Int5", [0xF2, 0x34]); yield return new ConverterTestCase((ushort) 4660, "DB99.UInt5", [0x12, 0x34]); - yield return new ConverterTestCase((ushort) 3532, "DB99.UInt5", [0xF2, 0x34]); + yield return new ConverterTestCase((ushort) 62004, "DB99.UInt5", [0xF2, 0x34]); yield return new ConverterTestCase(305419879, "DB99.DInt5", [0x12, 0x34, 0x56, 0x67]); yield return new ConverterTestCase(-231451033, "DB99.DInt5", [0xF2, 0x34, 0x56, 0x67]); yield return new ConverterTestCase(305419879u, "DB99.UDInt5", [0x12, 0x34, 0x56, 0x67]); - yield return new ConverterTestCase(231451033u, "DB99.UDInt5", [0xF2, 0x34, 0x56, 0x67]); + yield return new ConverterTestCase(4063516263u, "DB99.UDInt5", [0xF2, 0x34, 0x56, 0x67]); yield return new ConverterTestCase(1311768394163015151L, "DB99.LInt5", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); yield return new ConverterTestCase(-994074615050678801L, "DB99.LInt5", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); yield return new ConverterTestCase(1311768394163015151uL, "DB99.ULInt5", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); yield return new ConverterTestCase(17452669458658872815uL, "DB99.ULInt5", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); yield return new ConverterTestCase(0.25f, "DB99.Real5", [0x3E, 0x80, 0x00, 0x00]); - yield return new ConverterTestCase(0.25, "DB99.LReal5", [0x3E, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + yield return new ConverterTestCase(0.25, "DB99.LReal5", [0x3F, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); yield return new ConverterTestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB99.Byte5.4", [0x12, 0x34, 0x56, 0x67]); @@ -57,7 +57,6 @@ internal abstract class ConverterTestBase yield return new ConverterTestCase((byte) 18, "DB99.DBB0", [0x12]); yield return new ConverterTestCase((short) 4660, "DB99.INT0", [0x12, 0x34]); yield return new ConverterTestCase((short) -3532, "DB99.INT0", [0xF2, 0x34]); - yield return new ConverterTestCase(-3532, "DB99.INT0", [0xF2, 0x34]); yield return new ConverterTestCase(305419879, "DB99.DINT0", [0x12, 0x34, 0x56, 0x67]); yield return new ConverterTestCase(-231451033, "DB99.DINT0", [0xF2, 0x34, 0x56, 0x67]); yield return new ConverterTestCase(1311768394163015151L, "DB99.dul0", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); @@ -81,6 +80,6 @@ internal abstract class ConverterTestBase { public S7VariableAddress VariableAddress => Parser.Parse(Address); - public override string ToString() => $"{Address} {Value} ({Value.GetType().Name})"; + public override string ToString() => $"{Value.GetType().Name}, {Address}: {Value}"; } } diff --git a/Sharp7.Rx/Exceptions/S7Exception.cs b/Sharp7.Rx/Exceptions/S7Exception.cs index e9cc1d3..fb836f9 100644 --- a/Sharp7.Rx/Exceptions/S7Exception.cs +++ b/Sharp7.Rx/Exceptions/S7Exception.cs @@ -11,6 +11,44 @@ public abstract class S7Exception : Exception } } +public class DataTypeMissmatchException : S7Exception +{ + internal DataTypeMissmatchException(string message, Type type, S7VariableAddress address) : base(message) + { + Type = type; + Address = address.ToString(); + } + + internal DataTypeMissmatchException(string message, Exception innerException, Type type, S7VariableAddress address) : base(message, innerException) + { + Type = type; + Address = address.ToString(); + } + + public string Address { get; } + + public Type Type { get; } +} + +public class UnsupportedS7TypeException : S7Exception +{ + internal UnsupportedS7TypeException(string message, Type type, S7VariableAddress address) : base(message) + { + Type = type; + Address = address.ToString(); + } + + internal UnsupportedS7TypeException(string message, Exception innerException, Type type, S7VariableAddress address) : base(message, innerException) + { + Type = type; + Address = address.ToString(); + } + + public string Address { get; } + + public Type Type { get; } +} + public class InvalidS7AddressException : S7Exception { public InvalidS7AddressException(string message, string input) : base(message) @@ -23,5 +61,5 @@ public class InvalidS7AddressException : S7Exception Input = input; } - public string Input { get; private set; } + public string Input { get; } } diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 9618a70..90b21d8 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -7,60 +7,92 @@ namespace Sharp7.Rx; internal static class S7ValueConverter { + private static readonly Dictionary> readFunctions = new() + { + {typeof(bool), (buffer, address) => (buffer[0] >> address.Bit & 1) > 0}, + + {typeof(byte), (buffer, address) => buffer[0]}, + {typeof(byte[]), (buffer, address) => buffer}, + + {typeof(short), (buffer, address) => BinaryPrimitives.ReadInt16BigEndian(buffer)}, + {typeof(ushort), (buffer, address) => BinaryPrimitives.ReadUInt16BigEndian(buffer)}, + {typeof(int), (buffer, address) => BinaryPrimitives.ReadInt32BigEndian(buffer)}, + {typeof(uint), (buffer, address) => BinaryPrimitives.ReadUInt32BigEndian(buffer)}, + {typeof(long), (buffer, address) => BinaryPrimitives.ReadInt64BigEndian(buffer)}, + {typeof(ulong), (buffer, address) => BinaryPrimitives.ReadUInt64BigEndian(buffer)}, + + { + typeof(float), (buffer, address) => + { + // Todo: Use BinaryPrimitives when switched to newer .net + var d = new UInt32SingleMap + { + UInt32 = BinaryPrimitives.ReadUInt32BigEndian(buffer) + }; + return d.Single; + } + }, + + { + typeof(double), (buffer, address) => + { + // Todo: Use BinaryPrimitives when switched to newer .net + var d = new UInt64DoubleMap + { + UInt64 = BinaryPrimitives.ReadUInt64BigEndian(buffer) + }; + return d.Double; + } + }, + + { + typeof(string), (buffer, address) => + { + return address.Type switch + { + DbType.String => ParseString(), + DbType.WString => ParseWString(), + DbType.Byte => Encoding.ASCII.GetString(buffer), + _ => throw new DataTypeMissmatchException($"Cannot read string from {address.Type}", typeof(string), address) + }; + + string ParseString() + { + // First byte is maximal length + // Second byte is actual length + // https://support.industry.siemens.com/cs/mdm/109747174?c=94063831435&lc=de-DE + + var length = Math.Min(address.Length, buffer[1]); + + return Encoding.ASCII.GetString(buffer, 2, length); + } + + string ParseWString() + { + // First 2 bytes are maximal length + // Second 2 bytes are actual length + // https://support.industry.siemens.com/cs/mdm/109747174?c=94063855243&lc=de-DE + + // the length of the string is two bytes per + var length = Math.Min(address.Length, BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(2,2))) * 2; + + return Encoding.BigEndianUnicode.GetString(buffer, 4, length); + } + } + }, + }; + public static TValue ReadFromBuffer(byte[] buffer, S7VariableAddress address) { - if (typeof(TValue) == typeof(bool)) - return (TValue) (object) (((buffer[0] >> address.Bit) & 1) > 0); + // Todo: Change to Span when switched to newer .net - if (typeof(TValue) == typeof(int)) - { - if (address.Length == 2) - return (TValue) (object) (int) BinaryPrimitives.ReadInt16BigEndian(buffer); - if (address.Length == 4) - return (TValue) (object) BinaryPrimitives.ReadInt32BigEndian(buffer); + var type = typeof(TValue); - throw new InvalidOperationException($"length must be 2 or 4 but is {address.Length}"); - } + if (!readFunctions.TryGetValue(type, out var readFunc)) + throw new UnsupportedS7TypeException($"{type.Name} is not supported. {address}", type, address); - if (typeof(TValue) == typeof(long)) - return (TValue) (object) BinaryPrimitives.ReadInt64BigEndian(buffer); - - if (typeof(TValue) == typeof(ulong)) - return (TValue) (object) BinaryPrimitives.ReadUInt64BigEndian(buffer); - - if (typeof(TValue) == typeof(short)) - return (TValue) (object) BinaryPrimitives.ReadInt16BigEndian(buffer); - - if (typeof(TValue) == typeof(byte)) - return (TValue) (object) buffer[0]; - - if (typeof(TValue) == typeof(byte[])) - return (TValue) (object) buffer; - - if (typeof(TValue) == typeof(float)) - { - var d = new UInt32SingleMap - { - UInt32 = BinaryPrimitives.ReadUInt32BigEndian(buffer) - }; - return (TValue) (object) d.Single; - } - - if (typeof(TValue) == typeof(string)) - if (address.Type == DbType.String) - { - // First byte is maximal length - // Second byte is actual length - // https://cache.industry.siemens.com/dl/files/480/22506480/att_105176/v1/s7_scl_string_parameterzuweisung_e.pdf - - var length = Math.Min(address.Length, buffer[1]); - - return (TValue) (object) Encoding.ASCII.GetString(buffer, 2, length); - } - else - return (TValue) (object) Encoding.ASCII.GetString(buffer).Trim(); - - throw new InvalidOperationException($"type '{typeof(TValue)}' not supported."); + var result = readFunc(buffer, address); + return (TValue) result; } public static void WriteToBuffer(Span buffer, TValue value, S7VariableAddress address) @@ -145,4 +177,11 @@ internal static class S7ValueConverter [FieldOffset(0)] public uint UInt32; [FieldOffset(0)] public float Single; } + + [StructLayout(LayoutKind.Explicit)] + private struct UInt64DoubleMap + { + [FieldOffset(0)] public ulong UInt64; + [FieldOffset(0)] public double Double; + } } diff --git a/Sharp7.Rx/S7VariableAddress.cs b/Sharp7.Rx/S7VariableAddress.cs index 0172c06..f04bd34 100644 --- a/Sharp7.Rx/S7VariableAddress.cs +++ b/Sharp7.Rx/S7VariableAddress.cs @@ -19,4 +19,14 @@ internal class S7VariableAddress DbType.WString => (ushort) (Length * 2 + 4), _ => Length }; + + public override string ToString() => + Type switch + { + DbType.Bit => $"{Operand}{DbNr}.{Type}{Start}.{Bit}", + DbType.String => $"{Operand}{DbNr}.{Type}{Start}.{Length}", + DbType.WString => $"{Operand}{DbNr}.{Type}{Start}.{Length}", + DbType.Byte => Length == 1 ? $"{Operand}{DbNr}.{Type}{Start}" : $"{Operand}{DbNr}.{Type}{Start}.{Length}", + _ => $"{Operand}{DbNr}.{Type}{Start}", + }; } From 1001303b8c26d44dfcc938a30d0b84e917493cd9 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 16:45:48 +0100 Subject: [PATCH 11/16] Improve WriteToBuffer implementation and tests --- .../S7ValueConverterTests/ReadFromBuffer.cs | 14 +- .../S7ValueConverterTests/WriteToBuffer.cs | 28 ++- Sharp7.Rx.Tests/S7VariableNameParserTests.cs | 1 + Sharp7.Rx/S7ValueConverter.cs | 181 +++++++++++------- 4 files changed, 140 insertions(+), 84 deletions(-) diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs index cf9cc4d..cbb4542 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs @@ -24,23 +24,23 @@ internal class ReadFromBuffer : ConverterTestBase { yield return new ConverterTestCase(true, "DB0.DBx0.4", [0x1F]); yield return new ConverterTestCase(false, "DB0.DBx0.4", [0xEF]); - yield return new ConverterTestCase("ABCD", "DB0.string0.10", [0x04, 0x04, 0x41, 0x42, 0x43, 0x44]); // Length in address exceeds PLC string length + yield return new ConverterTestCase("ABCD", "DB0.string0.6", [0x04, 0x04, 0x41, 0x42, 0x43, 0x44, 0x00, 0x00]); // Length in address exceeds PLC string length } [TestCase((char) 18, "DB0.DBB0", new byte[] {0x12})] - [TestCase((ushort) 3532, "DB0.INT0", new byte[] {0xF2, 0x34})] - [TestCase(0.25, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] - public void Invalid(T template, string address, byte[] data) + public void UnsupportedType(T template, string address, byte[] data) { //Arrange var variableAddress = Parser.Parse(address); //Act - Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); + Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); } - [TestCase(3532, "DB0.DINT0", new byte[] {0xF2, 0x34})] - public void Argument(T template, string address, byte[] data) + [TestCase(123, "DB12.DINT3", new byte[] {0x01, 0x02, 0x03})] + [TestCase((short) 123, "DB12.INT3", new byte[] {0xF2})] + [TestCase("ABC", "DB0.string0.6", new byte[] {0x01, 0x02, 0x03})] + public void BufferTooSmall(T template, string address, byte[] data) { //Arrange var variableAddress = Parser.Parse(address); diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs index 4a93719..4e364fb 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs @@ -1,13 +1,13 @@ using NUnit.Framework; -using Sharp7.Rx.Interfaces; using Shouldly; namespace Sharp7.Rx.Tests.S7ValueConverterTests; [TestFixture] -internal class WriteToBuffer:ConverterTestBase +internal class WriteToBuffer : ConverterTestBase { [TestCaseSource(nameof(GetValidTestCases))] + [TestCaseSource(nameof(GetAdditinalWriteTestCases))] public void Write(ConverterTestCase tc) { //Arrange @@ -21,15 +21,33 @@ internal class WriteToBuffer:ConverterTestBase buffer.ShouldBe(tc.Data); } + public static IEnumerable GetAdditinalWriteTestCases() + { + yield return new ConverterTestCase("aaaaBCDE", "DB0.string0.4", [0x04, 0x04, 0x61, 0x61, 0x61, 0x61]); // Length in address exceeds PLC string length + yield return new ConverterTestCase("aaaaBCDE", "DB0.WString0.4", [0x00, 0x04, 0x00, 0x04, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61]); // Length in address exceeds PLC string length + } + + [TestCase(18, "DB0.DInt12", 3)] + [TestCase(0.25f, "DB0.Real1", 3)] + [TestCase("test", "DB0.String1.10", 9)] + public void BufferToSmall(T input, string address, int bufferSize) + { + //Arrange + var variableAddress = Parser.Parse(address); + var buffer = new byte[bufferSize]; + + //Act + Should.Throw(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); + } + [TestCase((char) 18, "DB0.DBB0")] - [TestCase(0.25, "DB0.D0")] - public void Invalid(T input, string address) + public void UnsupportedType(T input, string address) { //Arrange var variableAddress = Parser.Parse(address); var buffer = new byte[variableAddress.BufferLength]; //Act - Should.Throw(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); + Should.Throw(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); } } diff --git a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs index 7f4869f..50a460a 100644 --- a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs +++ b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs @@ -17,6 +17,7 @@ internal class S7VariableNameParserTests } [TestCase("DB506.Bit216", TestName = "Bit without Bit")] + [TestCase("DB506.Bit216.8", TestName = "Bit to high")] [TestCase("DB506.String216", TestName = "String without Length")] [TestCase("DB506.WString216", TestName = "WString without Length")] diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 90b21d8..4bc23f8 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -7,12 +7,105 @@ namespace Sharp7.Rx; internal static class S7ValueConverter { - private static readonly Dictionary> readFunctions = new() + private static readonly Dictionary writeFunctions = new() + { + { + typeof(bool), (data, address, value) => + { + var byteValue = (bool) value ? (byte) 1 : (byte) 0; + var shifted = (byte) (byteValue << address.Bit!); + data[0] = shifted; + } + }, + + {typeof(byte), (data, address, value) => data[0] = (byte) value}, + { + typeof(byte[]), (data, address, value) => + { + var source = (byte[]) value; + + var length = Math.Min(Math.Min(source.Length, data.Length), address.Length); + + source.AsSpan(0, length).CopyTo(data); + } + }, + + {typeof(short), (data, address, value) => BinaryPrimitives.WriteInt16BigEndian(data, (short) value)}, + {typeof(ushort), (data, address, value) => BinaryPrimitives.WriteUInt16BigEndian(data, (ushort) value)}, + {typeof(int), (data, address, value) => BinaryPrimitives.WriteInt32BigEndian(data, (int) value)}, + {typeof(uint), (data, address, value) => BinaryPrimitives.WriteUInt32BigEndian(data, (uint) value)}, + {typeof(long), (data, address, value) => BinaryPrimitives.WriteInt64BigEndian(data, (long) value)}, + {typeof(ulong), (data, address, value) => BinaryPrimitives.WriteUInt64BigEndian(data, (ulong) value)}, + + { + typeof(float), (data, address, value) => + { + var map = new UInt32SingleMap + { + Single = (float) value + }; + + BinaryPrimitives.WriteUInt32BigEndian(data, map.UInt32); + } + }, + { + typeof(double), (data, address, value) => + { + var map = new UInt64DoubleMap + { + Double = (double) value + }; + + BinaryPrimitives.WriteUInt64BigEndian(data, map.UInt64); + } + }, + + { + typeof(string), (data, address, value) => + { + if (value is not string stringValue) throw new ArgumentException("Value must be of type string", nameof(value)); + + var length = Math.Min(address.Length, stringValue.Length); + + switch (address.Type) + { + case DbType.String: + data[0] = (byte) address.Length; + data[1] = (byte) length; + + // Todo: Serialize directly to Span, when upgrading to .net + Encoding.ASCII.GetBytes(stringValue) + .AsSpan(0, length) + .CopyTo(data.Slice(2)); + return; + case DbType.WString: + BinaryPrimitives.WriteUInt16BigEndian(data, address.Length); + BinaryPrimitives.WriteUInt16BigEndian(data.Slice(2), (ushort) length); + + // Todo: Serialize directly to Span, when upgrading to .net + Encoding.BigEndianUnicode.GetBytes(stringValue) + .AsSpan(0, length * 2) + .CopyTo(data.Slice(4)); + return; + case DbType.Byte: + // Todo: Serialize directly to Span, when upgrading to .net + Encoding.ASCII.GetBytes(stringValue) + .AsSpan(0, length) + .CopyTo(data); + return; + default: + throw new DataTypeMissmatchException($"Cannot write string to {address.Type}", typeof(string), address); + } + } + } + }; + + private static readonly Dictionary readFunctions = new() { {typeof(bool), (buffer, address) => (buffer[0] >> address.Bit & 1) > 0}, {typeof(byte), (buffer, address) => buffer[0]}, - {typeof(byte[]), (buffer, address) => buffer}, + {typeof(byte[]), (buffer, address) => buffer.ToArray()}, {typeof(short), (buffer, address) => BinaryPrimitives.ReadInt16BigEndian(buffer)}, {typeof(ushort), (buffer, address) => BinaryPrimitives.ReadUInt16BigEndian(buffer)}, @@ -52,7 +145,7 @@ internal static class S7ValueConverter { DbType.String => ParseString(), DbType.WString => ParseWString(), - DbType.Byte => Encoding.ASCII.GetString(buffer), + DbType.Byte => Encoding.ASCII.GetString(buffer.ToArray()), _ => throw new DataTypeMissmatchException($"Cannot read string from {address.Type}", typeof(string), address) }; @@ -74,7 +167,7 @@ internal static class S7ValueConverter // https://support.industry.siemens.com/cs/mdm/109747174?c=94063855243&lc=de-DE // the length of the string is two bytes per - var length = Math.Min(address.Length, BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(2,2))) * 2; + var length = Math.Min(address.Length, BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(2, 2))) * 2; return Encoding.BigEndianUnicode.GetString(buffer, 4, length); } @@ -86,6 +179,9 @@ internal static class S7ValueConverter { // Todo: Change to Span when switched to newer .net + if (buffer.Length < address.BufferLength) + throw new ArgumentException($"Buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer)); + var type = typeof(TValue); if (!readFunctions.TryGetValue(type, out var readFunc)) @@ -98,79 +194,18 @@ internal static class S7ValueConverter public static void WriteToBuffer(Span buffer, TValue value, S7VariableAddress address) { if (buffer.Length < address.BufferLength) - throw new ArgumentException($"buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer)); + throw new ArgumentException($"Buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer)); - if (typeof(TValue) == typeof(bool)) - { - var byteValue = (bool) (object) value ? (byte) 1 : (byte) 0; - var shifted = (byte) (byteValue << address.Bit); - buffer[0] = shifted; - } + var type = typeof(TValue); - else if (typeof(TValue) == typeof(int)) - { - if (address.Length == 2) - BinaryPrimitives.WriteInt16BigEndian(buffer, (short) (int) (object) value); - else - BinaryPrimitives.WriteInt32BigEndian(buffer, (int) (object) value); - } - else if (typeof(TValue) == typeof(short)) - { - if (address.Length == 2) - BinaryPrimitives.WriteInt16BigEndian(buffer, (short) (object) value); - else - BinaryPrimitives.WriteInt32BigEndian(buffer, (short) (object) value); - } - else if (typeof(TValue) == typeof(long)) - BinaryPrimitives.WriteInt64BigEndian(buffer, (long) (object) value); - else if (typeof(TValue) == typeof(ulong)) - BinaryPrimitives.WriteUInt64BigEndian(buffer, (ulong) (object) value); - else if (typeof(TValue) == typeof(byte)) - buffer[0] = (byte) (object) value; - else if (typeof(TValue) == typeof(byte[])) - { - var source = (byte[]) (object) value; + if (!writeFunctions.TryGetValue(type, out var writeFunc)) + throw new UnsupportedS7TypeException($"{type.Name} is not supported. {address}", type, address); - var length = Math.Min(Math.Min(source.Length, buffer.Length), address.Length); - - source.AsSpan(0, length).CopyTo(buffer); - } - else if (typeof(TValue) == typeof(float)) - { - var map = new UInt32SingleMap - { - Single = (float) (object) value - }; - - BinaryPrimitives.WriteUInt32BigEndian(buffer, map.UInt32); - } - else if (typeof(TValue) == typeof(string)) - { - if (value is not string stringValue) throw new ArgumentException("Value must be of type string", nameof(value)); - - // Todo: Serialize directly to Span, when upgrading to .net - var stringBytes = Encoding.ASCII.GetBytes(stringValue); - - var length = Math.Min(address.Length, stringValue.Length); - - int stringOffset; - if (address.Type == DbType.String) - { - stringOffset = 2; - buffer[0] = (byte) address.Length; - buffer[1] = (byte) length; - } - else - stringOffset = 0; - - stringBytes.AsSpan(0, length).CopyTo(buffer.Slice(stringOffset)); - } - else - { - throw new InvalidOperationException($"type '{typeof(TValue)}' not supported."); - } + writeFunc(buffer, address, value); } + delegate object ReadFunc(byte[] data, S7VariableAddress address); + [StructLayout(LayoutKind.Explicit)] private struct UInt32SingleMap { @@ -184,4 +219,6 @@ internal static class S7ValueConverter [FieldOffset(0)] public ulong UInt64; [FieldOffset(0)] public double Double; } + + delegate void WriteFunc(Span data, S7VariableAddress address, object value); } From fdc25d281763043eba24915ee7965bacdc50fd3a Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 16:47:10 +0100 Subject: [PATCH 12/16] Ensure bit size --- Sharp7.Rx/S7VariableNameParser.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index 2355b24..a7029a1 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -145,6 +145,11 @@ internal class S7VariableNameParser : IS7VariableNameParser 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); + if (result > 7) + throw new InvalidS7AddressException($"Bit must be between 0 and 7 but is {result} in \"{input}\"", input); + + + return result; } } From 829dee14afa236803604eff098ea00ace0fa37ab Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 17:54:15 +0100 Subject: [PATCH 13/16] Implement MatchesType --- .../ConverterTestBase.cs | 2 - .../S7VariableAddressTests/MatchesType.cs | 69 +++++++++++++++---- Sharp7.Rx/Extensions/S7VariableExtensions.cs | 24 +++++-- Sharp7.Rx/S7VariableNameParser.cs | 2 - 4 files changed, 77 insertions(+), 20 deletions(-) diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs index a4fb1b4..f344256 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs @@ -59,8 +59,6 @@ internal abstract class ConverterTestBase yield return new ConverterTestCase((short) -3532, "DB99.INT0", [0xF2, 0x34]); yield return new ConverterTestCase(305419879, "DB99.DINT0", [0x12, 0x34, 0x56, 0x67]); yield return new ConverterTestCase(-231451033, "DB99.DINT0", [0xF2, 0x34, 0x56, 0x67]); - yield return new ConverterTestCase(1311768394163015151L, "DB99.dul0", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); - yield return new ConverterTestCase(-994074615050678801L, "DB99.dul0", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); yield return new ConverterTestCase(1311768394163015151uL, "DB99.dul0", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); yield return new ConverterTestCase(17452669458658872815uL, "DB99.dul0", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); yield return new ConverterTestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB99.DBB0.4", [0x12, 0x34, 0x56, 0x67]); diff --git a/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs b/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs index d3d428a..458c17d 100644 --- a/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs +++ b/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs @@ -1,7 +1,7 @@ -using System.Reflection; -using NUnit.Framework; +using NUnit.Framework; using Sharp7.Rx.Extensions; using Sharp7.Rx.Interfaces; +using Sharp7.Rx.Tests.S7ValueConverterTests; using Shouldly; namespace Sharp7.Rx.Tests.S7VariableAddressTests; @@ -11,19 +11,61 @@ public class MatchesType { static readonly IS7VariableNameParser parser = new S7VariableNameParser(); - - public void Supported(Type type, string address) + private static readonly IReadOnlyList typeList = new[] { - Check(type, address, true); + typeof(byte), + typeof(byte[]), + + typeof(bool), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + + typeof(float), + typeof(double), + + typeof(string), + + typeof(int[]), + typeof(float[]), + typeof(DateTime[]), + typeof(object), + }; + + [TestCaseSource(nameof(GetValid))] + public void Supported(TestCase tc) => Check(tc.Type, tc.Address, true); + + [TestCaseSource(nameof(GetInvalid))] + public void Unsupported(TestCase tc) => Check(tc.Type, tc.Address, false); + + + public static IEnumerable GetValid() + { + return + ConverterTestBase.GetValidTestCases() + .Select(tc => new TestCase(tc.Value.GetType(), tc.Address)); } - public IEnumerable GetValid() + public static IEnumerable GetInvalid() { - yield return new TestCase(typeof(bool), "DB0.DBx0.0"); - yield return new TestCase(typeof(short), "DB0.INT0"); - yield return new TestCase(typeof(int), "DB0.DINT0"); - yield return new TestCase(typeof(long), "DB0.DUL0"); - yield return new TestCase(typeof(ulong), "DB0.DUL0"); + return + ConverterTestBase.GetValidTestCases() + .DistinctBy(tc => tc.Value.GetType()) + .SelectMany(tc => + typeList.Where(type => type != tc.Value.GetType()) + .Select(type => new TestCase(type, tc.Address)) + ) + + // Explicitly remove some valid combinations + .Where(tc => !( + (tc.Type == typeof(string) && tc.Address == "DB99.Byte5") || + (tc.Type == typeof(string) && tc.Address == "DB99.Byte5.4") || + (tc.Type == typeof(byte[]) && tc.Address == "DB99.Byte5") + )) + ; } @@ -36,5 +78,8 @@ public class MatchesType variableAddress.MatchesType(type).ShouldBe(expected); } - public record TestCase(Type Type, string Address); + public record TestCase(Type Type, string Address) + { + public override string ToString() => $"{Type.Name} {Address}"; + } } diff --git a/Sharp7.Rx/Extensions/S7VariableExtensions.cs b/Sharp7.Rx/Extensions/S7VariableExtensions.cs index a2b0a39..40c8eed 100644 --- a/Sharp7.Rx/Extensions/S7VariableExtensions.cs +++ b/Sharp7.Rx/Extensions/S7VariableExtensions.cs @@ -1,9 +1,25 @@ -namespace Sharp7.Rx.Extensions; +using Sharp7.Rx.Enums; + +namespace Sharp7.Rx.Extensions; internal static class S7VariableAddressExtensions { - public static bool MatchesType(this S7VariableAddress address, Type type) + private static readonly Dictionary> supportedTypeMap = new() { - return false; - } + {typeof(bool), a => a.Type == DbType.Bit}, + {typeof(string), a => a.Type is DbType.String or DbType.WString or DbType.Byte }, + {typeof(byte), a => a.Type==DbType.Byte && a.Length == 1}, + {typeof(short), a => a.Type==DbType.Int}, + {typeof(ushort), a => a.Type==DbType.UInt}, + {typeof(int), a => a.Type==DbType.DInt}, + {typeof(uint), a => a.Type==DbType.UDInt}, + {typeof(long), a => a.Type==DbType.LInt}, + {typeof(ulong), a => a.Type==DbType.ULInt}, + {typeof(float), a => a.Type==DbType.Single}, + {typeof(double), a => a.Type==DbType.Double}, + {typeof(byte[]), a => a.Type==DbType.Byte}, + }; + + public static bool MatchesType(this S7VariableAddress address, Type type) => + supportedTypeMap.TryGetValue(type, out var map) && map(address); } diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index a7029a1..38cecc3 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -148,8 +148,6 @@ internal class S7VariableNameParser : IS7VariableNameParser if (result > 7) throw new InvalidS7AddressException($"Bit must be between 0 and 7 but is {result} in \"{input}\"", input); - - return result; } } From 5d8582316770fc6555f019a03183bb9a00f87175 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 18:06:55 +0100 Subject: [PATCH 14/16] Verify data types --- Sharp7.Rx/S7VariableNameParser.cs | 7 ++++--- Sharp7.Rx/Sharp7Plc.cs | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index 38cecc3..b53fcc9 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -1,4 +1,5 @@ -using System.Globalization; +#nullable enable +using System.Globalization; using System.Text.RegularExpressions; using Sharp7.Rx.Enums; using Sharp7.Rx.Interfaces; @@ -7,8 +8,8 @@ namespace Sharp7.Rx; internal class S7VariableNameParser : IS7VariableNameParser { - private static readonly Regex regex = new Regex(@"^(?db)(?\d+)\.?(?[a-z]+)(?\d+)(\.(?\d+))?$", - RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex regex = new(@"^(?db)(?\d+)\.?(?[a-z]+)(?\d+)(\.(?\d+))?$", + RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly IReadOnlyDictionary types = new Dictionary(StringComparer.OrdinalIgnoreCase) { diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 46936ef..ec6504a 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -77,8 +77,7 @@ public class Sharp7Plc : IPlc { return Observable.Create(observer => { - var address = varaibleNameParser.Parse(variableName); - if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName)); + var address = ParseAndVerify(variableName, typeof(TValue)); var disp = new CompositeDisposable(); var disposeableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName); @@ -103,6 +102,15 @@ public class Sharp7Plc : IPlc }); } + private S7VariableAddress ParseAndVerify(string variableName, Type type) + { + var address = varaibleNameParser.Parse(variableName); + if (!address.MatchesType(type)) + throw new DataTypeMissmatchException($"Address \"{variableName}\" does not match type {type}.", type, address); + + return address; + } + public Task GetValue(string variableName) { return GetValue(variableName, CancellationToken.None); @@ -117,8 +125,7 @@ public class Sharp7Plc : IPlc public async Task GetValue(string variableName, CancellationToken token) { - var address = varaibleNameParser.Parse(variableName); - if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName)); + var address = ParseAndVerify(variableName, typeof(TValue)); var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); return S7ValueConverter.ReadFromBuffer(data, address); @@ -150,8 +157,7 @@ public class Sharp7Plc : IPlc public async Task SetValue(string variableName, TValue value, CancellationToken token) { - var address = varaibleNameParser.Parse(variableName); - if (address == null) throw new ArgumentException("Input variable name is not valid", "variableName"); + var address = ParseAndVerify(variableName, typeof(TValue)); if (typeof(TValue) == typeof(bool)) { From b43a595e13ae8bf21cbb58126039e210f72567b3 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 18:30:49 +0100 Subject: [PATCH 15/16] Return S7CommunicationException --- Sharp7.Rx/Exceptions/S7Exception.cs | 18 ++++++ Sharp7.Rx/Interfaces/IS7Connector.cs | 6 +- .../Resources/StringResources.Designer.cs | 20 +------ Sharp7.Rx/Resources/StringResources.resx | 6 -- Sharp7.Rx/Sharp7Connector.cs | 60 +++++++++---------- Sharp7.Rx/Sharp7Plc.cs | 5 +- 6 files changed, 53 insertions(+), 62 deletions(-) diff --git a/Sharp7.Rx/Exceptions/S7Exception.cs b/Sharp7.Rx/Exceptions/S7Exception.cs index fb836f9..2d8fb44 100644 --- a/Sharp7.Rx/Exceptions/S7Exception.cs +++ b/Sharp7.Rx/Exceptions/S7Exception.cs @@ -11,6 +11,24 @@ public abstract class S7Exception : Exception } } +public class S7CommunicationException : S7Exception +{ + public S7CommunicationException(string message, int s7ErrorCode, string s7ErrorText) : base(message) + { + S7ErrorCode = s7ErrorCode; + S7ErrorText = s7ErrorText; + } + + public S7CommunicationException(string message, Exception innerException, int s7ErrorCode, string s7ErrorText) : base(message, innerException) + { + S7ErrorCode = s7ErrorCode; + S7ErrorText = s7ErrorText; + } + + public int S7ErrorCode { get; } + public string S7ErrorText { get; } +} + public class DataTypeMissmatchException : S7Exception { internal DataTypeMissmatchException(string message, Type type, S7VariableAddress address) : base(message) diff --git a/Sharp7.Rx/Interfaces/IS7Connector.cs b/Sharp7.Rx/Interfaces/IS7Connector.cs index 5f9da81..fe365c1 100644 --- a/Sharp7.Rx/Interfaces/IS7Connector.cs +++ b/Sharp7.Rx/Interfaces/IS7Connector.cs @@ -12,10 +12,10 @@ internal interface IS7Connector : IDisposable Task Connect(); Task Disconnect(); - Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token); + Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dbNo, CancellationToken token); - Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token); - Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token); + Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNo, CancellationToken token); + Task WriteBytes(Operand operand, ushort startByteAddress, byte[] data, ushort dbNo, CancellationToken token); Task> ExecuteMultiVarRequest(IReadOnlyList variableNames); } diff --git a/Sharp7.Rx/Resources/StringResources.Designer.cs b/Sharp7.Rx/Resources/StringResources.Designer.cs index f1d725d..dd88d85 100644 --- a/Sharp7.Rx/Resources/StringResources.Designer.cs +++ b/Sharp7.Rx/Resources/StringResources.Designer.cs @@ -19,7 +19,7 @@ namespace Sharp7.Rx.Resources { // 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.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class StringResources { @@ -60,24 +60,6 @@ namespace Sharp7.Rx.Resources { } } - /// - /// Looks up a localized string similar to S7 driver could not be initialized. - /// - internal static string StrErrorS7DriverCouldNotBeInitialized { - get { - return ResourceManager.GetString("StrErrorS7DriverCouldNotBeInitialized", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to S7 driver is not initialized.. - /// - internal static string StrErrorS7DriverNotInitialized { - get { - return ResourceManager.GetString("StrErrorS7DriverNotInitialized", resourceCulture); - } - } - /// /// Looks up a localized string similar to TCP/IP connection established.. /// diff --git a/Sharp7.Rx/Resources/StringResources.resx b/Sharp7.Rx/Resources/StringResources.resx index 3eff273..ee55570 100644 --- a/Sharp7.Rx/Resources/StringResources.resx +++ b/Sharp7.Rx/Resources/StringResources.resx @@ -123,16 +123,10 @@ Communication error discovered. Reconnect is in progress... - - S7 driver is not initialized. - Trying to connect to PLC ({2}) '{0}', CPU slot {1}... TCP/IP connection established. - - S7 driver could not be initialized - \ No newline at end of file diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index efdd014..98f9c74 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -55,21 +55,26 @@ internal class Sharp7Connector : IS7Connector public async Task Connect() { if (sharp7 == null) - throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + throw new InvalidOperationException("S7 driver is not initialized."); try { var errorCode = await Task.Factory.StartNew(() => sharp7.ConnectTo(ipAddress, rackNr, cpuSlotNr), CancellationToken.None, TaskCreationOptions.None, scheduler); - var success = EvaluateErrorCode(errorCode); - if (success) + if (errorCode == 0) { connectionStateSubject.OnNext(Enums.ConnectionState.Connected); return true; } + else + { + var errorText = EvaluateErrorCode(errorCode); + Logger.LogError("Failed to establish initial connection: {Error}", errorText); + } } catch (Exception ex) { - // TODO: + connectionStateSubject.OnNext(Enums.ConnectionState.ConnectionLost); + Logger.LogError(ex, "Failed to establish initial connection."); } return false; @@ -102,8 +107,8 @@ internal class Sharp7Connector : IS7Connector var result = await Task.Factory.StartNew(() => s7MultiVar.Read(), CancellationToken.None, TaskCreationOptions.None, scheduler); if (result != 0) { - EvaluateErrorCode(result); - throw new InvalidOperationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)}"); + var errorText = EvaluateErrorCode(result); + throw new S7CommunicationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)} ({errorText})", result, errorText); } return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer); @@ -129,13 +134,13 @@ internal class Sharp7Connector : IS7Connector } catch (Exception ex) { - Logger?.LogError(ex, StringResources.StrErrorS7DriverCouldNotBeInitialized); + Logger?.LogError(ex, "S7 driver could not be initialized"); } return Task.FromResult(true); } - public async Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token) + public async Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dbNo, CancellationToken token) { EnsureConnectionValid(); @@ -143,20 +148,19 @@ internal class Sharp7Connector : IS7Connector var result = - await Task.Factory.StartNew(() => sharp7.ReadArea(operand.ToArea(), dBNr, 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(); if (result != 0) { - EvaluateErrorCode(result); - var errorText = sharp7.ErrorText(result); - throw new InvalidOperationException($"Error reading {operand}{dBNr}:{startByteAddress}->{bytesToRead} ({errorText})"); + var errorText = EvaluateErrorCode(result); + throw new S7CommunicationException($"Error reading {operand}{dbNo}:{startByteAddress}->{bytesToRead} ({errorText})", result, errorText); } return buffer; } - public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token) + public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNo, CancellationToken token) { EnsureConnectionValid(); @@ -164,32 +168,28 @@ internal class Sharp7Connector : IS7Connector var offsetStart = (startByteAddress * 8) + bitAdress; - var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dbNr, offsetStart, 1, S7WordLength.Bit, buffer), token, TaskCreationOptions.None, scheduler); + var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dbNo, offsetStart, 1, S7WordLength.Bit, buffer), token, TaskCreationOptions.None, scheduler); token.ThrowIfCancellationRequested(); if (result != 0) { - EvaluateErrorCode(result); - return (false); + var errorText = EvaluateErrorCode(result); + throw new S7CommunicationException($"Error writing {operand}{dbNo}:{startByteAddress} bit {bitAdress} ({errorText})", result, errorText); } - - return (true); } - public async Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token) + public async Task WriteBytes(Operand operand, ushort startByteAddress, byte[] data, ushort dbNo, CancellationToken token) { EnsureConnectionValid(); - var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dBNr, startByteAdress, data.Length, S7WordLength.Byte, data), token, TaskCreationOptions.None, scheduler); + var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dbNo, startByteAddress, data.Length, S7WordLength.Byte, data), token, TaskCreationOptions.None, scheduler); token.ThrowIfCancellationRequested(); if (result != 0) { - EvaluateErrorCode(result); - return 0; + var errorText = EvaluateErrorCode(result); + throw new S7CommunicationException($"Error writing {operand}{dbNo}:{startByteAddress}.{data.Length} ({errorText})", result, errorText); } - - return (ushort) (data.Length); } @@ -218,7 +218,7 @@ internal class Sharp7Connector : IS7Connector private async Task CloseConnection() { if (sharp7 == null) - throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + throw new InvalidOperationException("S7 driver is not initialized."); await Task.Factory.StartNew(() => sharp7.Disconnect(), CancellationToken.None, TaskCreationOptions.None, scheduler); } @@ -229,19 +229,19 @@ internal class Sharp7Connector : IS7Connector throw new ObjectDisposedException("S7Connector"); if (sharp7 == null) - throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + throw new InvalidOperationException("S7 driver is not initialized."); if (!IsConnected) throw new InvalidOperationException("Plc is not connected"); } - private bool EvaluateErrorCode(int errorCode) + private string EvaluateErrorCode(int errorCode) { if (errorCode == 0) - return true; + return null; if (sharp7 == null) - throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + throw new InvalidOperationException("S7 driver is not initialized."); var errorText = sharp7.ErrorText(errorCode); Logger?.LogError($"Error Code {errorCode} {errorText}"); @@ -249,7 +249,7 @@ internal class Sharp7Connector : IS7Connector if (S7ErrorCodes.AssumeConnectionLost(errorCode)) SetConnectionLostState(); - return false; + return errorText; } private async Task Reconnect() diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index ec6504a..bada2e2 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -164,10 +164,7 @@ public class Sharp7Plc : IPlc // Special handling for bools, which are written on a by-bit basis. Writing a complete byte would // overwrite other bits within this byte. - if (address.Bit == null) - throw new InvalidOperationException("Address must have a Bit to write a bool."); - - await s7Connector.WriteBit(address.Operand, address.Start, address.Bit.Value, (bool) (object) value, address.DbNr, token); + await s7Connector.WriteBit(address.Operand, address.Start, address.Bit!.Value, (bool) (object) value, address.DbNr, token); } else { From 32a7d7cd24b91970c9e898b19e4aa86d540c19bc Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 18:31:23 +0100 Subject: [PATCH 16/16] Remove unused localization --- .../Resources/StringResources.Designer.cs | 99 ------------- Sharp7.Rx/Resources/StringResources.resx | 132 ------------------ Sharp7.Rx/Sharp7.Rx.csproj | 19 --- Sharp7.Rx/Sharp7Connector.cs | 1 - 4 files changed, 251 deletions(-) delete mode 100644 Sharp7.Rx/Resources/StringResources.Designer.cs delete mode 100644 Sharp7.Rx/Resources/StringResources.resx diff --git a/Sharp7.Rx/Resources/StringResources.Designer.cs b/Sharp7.Rx/Resources/StringResources.Designer.cs deleted file mode 100644 index dd88d85..0000000 --- a/Sharp7.Rx/Resources/StringResources.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Sharp7.Rx.Resources { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class StringResources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal StringResources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Sharp7.Rx.Resources.StringResources", typeof(StringResources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to TCP/IP connection established.. - /// - internal static string StrInfoConnectionEstablished { - get { - return ResourceManager.GetString("StrInfoConnectionEstablished", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Trying to connect to PLC ({2}) '{0}', CPU slot {1}.... - /// - internal static string StrInfoTryConnecting { - get { - return ResourceManager.GetString("StrInfoTryConnecting", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error while reading data from plc.. - /// - internal static string StrLogErrorReadingDataFromPlc { - get { - return ResourceManager.GetString("StrLogErrorReadingDataFromPlc", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Communication error discovered. Reconnect is in progress.... - /// - internal static string StrLogWarningCommunictionErrorReconnecting { - get { - return ResourceManager.GetString("StrLogWarningCommunictionErrorReconnecting", resourceCulture); - } - } - } -} diff --git a/Sharp7.Rx/Resources/StringResources.resx b/Sharp7.Rx/Resources/StringResources.resx deleted file mode 100644 index ee55570..0000000 --- a/Sharp7.Rx/Resources/StringResources.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Error while reading data from plc. - - - Communication error discovered. Reconnect is in progress... - - - Trying to connect to PLC ({2}) '{0}', CPU slot {1}... - - - TCP/IP connection established. - - \ No newline at end of file diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 2e5a601..faf60fe 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -27,23 +27,4 @@ - - - True - True - StringResources.resx - - - - - - ResXFileCodeGenerator - StringResources.Designer.cs - - - - - - - diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 98f9c74..951ff0c 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -6,7 +6,6 @@ using Sharp7.Rx.Basics; using Sharp7.Rx.Enums; using Sharp7.Rx.Extensions; using Sharp7.Rx.Interfaces; -using Sharp7.Rx.Resources; using Sharp7.Rx.Settings; namespace Sharp7.Rx;