diff --git a/README.md b/README.md index a5f27b9..a80d530 100644 --- a/README.md +++ b/README.md @@ -50,21 +50,37 @@ using (var disposables = new CompositeDisposable()) the best way to test your PLC application is running your [SoftPLC](https://github.com/fbarresi/softplc) locally. -## S7 Addressing rules +## Addressing rules Sharp7Reactive uses a syntax for identifying addresses similar to official siemens syntax. -Every address has the form (case unsensible) `DB..`. -
i.e.: `DB42.DBX0.7` => (means) Datablock 42, bit (DBX), Start: 0, Position: 7 -
or
-`DB42.DBB4.25` => (means) Datablock 42, bytes (DBB), Start: 4, Length: 25. +Every address has the form (case unsensitive) `DB..`. -Following types are supported: -- `DBX` => Bit (bool) -- `DBB` => byte or byte[] -- `INT` -- `DINT` -- `DUL` => LINT -- `D` => REAL +| Example | Explaination | +| ------------------------------------ | ----------------------------------------------------------------- | +| `DB42.Int4` or
`DB42.DBD4` | Datablock 42, 16 bit integer from bytes 4 to 5 (zero based index) | +| `DB42.Bit0.7` or
`DB42.DBX0.7` | Datablock 42, bit from byte 0, position 7 | +| `DB42.Byte4.25` or
`DB42.DBB4.25` | Datablock 42, 25 bytes from byte 4 to 29 (zero based index) | + +Here is a table of supported data types: + +|.Net Type|Identifier |Description |Length or bit |Example |Example remark | +|---------|-----------------------------|----------------------------------------------|----------------------------------------|-------------------|--------------------------| +|bool |bit, dbx |Bit as boolean value |Bit index (0 .. 7) |`Db200.Bit2.2` |Reads bit 3 | +|byte |byte, dbb, b* |8 bit unsigned integer | |`Db200.Byte4` | | +|byte[] |byte, dbb, b* |Array of bytes |Array length in bytes |`Db200.Byte4.16` | | +|short |int, dbw, w* |16 bit signed integer | |`Db200.Int4` | | +|ushort |uint |16 bit unsigned integer | |`Db200.UInt4` | | +|int |dint, dbd |32 bit signed integer | |`Db200.DInt4` | | +|uint |udint |32 bit unsigned integer | |`Db200.UDInt4` | | +|long |lint |64 bit signed integer | |`Db200.LInt4` | | +|ulong |ulint, dul*, dulint*, dulong*|64 bit unsigned integer | |`Db200.ULInt4` | | +|float |real, d* |32 bit float | |`Db200.Real4` | | +|double |lreal |64 bit float | |`Db200.LReal4` | | +|string |string, s* |ASCII text string with string size |String length in bytes (1 .. 254) |`Db200.String4.16` |Uses 18 bytes = 16 + 2 | +|string |wstring |UTF-16 Big Endian text string with string size|String length in characters (1 .. 16382)|`Db200.WString4.16`|Uses 36 bytes = 16 * 2 + 4| +|string |byte[] |ASCII string as byte array |String length in bytes |`Db200.Byte4.16` |Uses 16 bytes | + +> Identifiers marked with * are kept for compatability reasons and might be removed in the future. ## Would you like to contribute? diff --git a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs deleted file mode 100644 index 50a460a..0000000 --- a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs +++ /dev/null @@ -1,91 +0,0 @@ -using DeepEqual.Syntax; -using NUnit.Framework; -using Sharp7.Rx.Enums; -using Shouldly; - -namespace Sharp7.Rx.Tests; - -[TestFixture] -internal class S7VariableNameParserTests -{ - [TestCaseSource(nameof(ValidTestCases))] - public void Run(TestCase tc) - { - var parser = new S7VariableNameParser(); - var resp = parser.Parse(tc.Input); - resp.ShouldDeepEqual(tc.Expected); - } - - [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")] - - [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.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) - { - public override string ToString() => Input; - } -} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs b/Sharp7.Rx.Tests/ValueConverterTests/ConvertBothWays.cs similarity index 92% rename from Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs rename to Sharp7.Rx.Tests/ValueConverterTests/ConvertBothWays.cs index b3956ac..39c9be3 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs +++ b/Sharp7.Rx.Tests/ValueConverterTests/ConvertBothWays.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using Shouldly; -namespace Sharp7.Rx.Tests.S7ValueConverterTests; +namespace Sharp7.Rx.Tests.ValueConverterTests; [TestFixture] internal class ConvertBothWays : ConverterTestBase diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs b/Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs similarity index 91% rename from Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs rename to Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs index f344256..d09c3ed 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs +++ b/Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs @@ -1,15 +1,15 @@ using System.Reflection; using Sharp7.Rx.Interfaces; -namespace Sharp7.Rx.Tests.S7ValueConverterTests; +namespace Sharp7.Rx.Tests.ValueConverterTests; internal abstract class ConverterTestBase { - protected static readonly IS7VariableNameParser Parser = new S7VariableNameParser(); + protected static readonly IVariableNameParser Parser = new VariableNameParser(); public static MethodInfo CreateReadMethod(ConverterTestCase tc) { - var convertMi = typeof(S7ValueConverter).GetMethod(nameof(S7ValueConverter.ReadFromBuffer)); + var convertMi = typeof(ValueConverter).GetMethod(nameof(ValueConverter.ReadFromBuffer)); var convert = convertMi!.MakeGenericMethod(tc.Value.GetType()); return convert; } @@ -69,14 +69,14 @@ internal abstract class ConverterTestBase /// 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) + public static void WriteToBuffer(byte[] buffer, TValue value, VariableAddress address) { - S7ValueConverter.WriteToBuffer(buffer, value, address); + ValueConverter.WriteToBuffer(buffer, value, address); } public record ConverterTestCase(object Value, string Address, byte[] Data) { - public S7VariableAddress VariableAddress => Parser.Parse(Address); + public VariableAddress VariableAddress => Parser.Parse(Address); public override string ToString() => $"{Value.GetType().Name}, {Address}: {Value}"; } diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs b/Sharp7.Rx.Tests/ValueConverterTests/ReadFromBuffer.cs similarity index 84% rename from Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs rename to Sharp7.Rx.Tests/ValueConverterTests/ReadFromBuffer.cs index cbb4542..66f08aa 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs +++ b/Sharp7.Rx.Tests/ValueConverterTests/ReadFromBuffer.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using Shouldly; -namespace Sharp7.Rx.Tests.S7ValueConverterTests; +namespace Sharp7.Rx.Tests.ValueConverterTests; [TestFixture] internal class ReadFromBuffer : ConverterTestBase @@ -34,7 +34,7 @@ internal class ReadFromBuffer : ConverterTestBase var variableAddress = Parser.Parse(address); //Act - Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); + Should.Throw(() => ValueConverter.ReadFromBuffer(data, variableAddress)); } [TestCase(123, "DB12.DINT3", new byte[] {0x01, 0x02, 0x03})] @@ -46,6 +46,6 @@ internal class ReadFromBuffer : ConverterTestBase var variableAddress = Parser.Parse(address); //Act - Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); + Should.Throw(() => ValueConverter.ReadFromBuffer(data, variableAddress)); } } diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs b/Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs similarity index 84% rename from Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs rename to Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs index 4e364fb..0d790f5 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs +++ b/Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using Shouldly; -namespace Sharp7.Rx.Tests.S7ValueConverterTests; +namespace Sharp7.Rx.Tests.ValueConverterTests; [TestFixture] internal class WriteToBuffer : ConverterTestBase @@ -37,7 +37,7 @@ internal class WriteToBuffer : ConverterTestBase var buffer = new byte[bufferSize]; //Act - Should.Throw(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); + Should.Throw(() => ValueConverter.WriteToBuffer(buffer, input, variableAddress)); } [TestCase((char) 18, "DB0.DBB0")] @@ -48,6 +48,6 @@ internal class WriteToBuffer : ConverterTestBase var buffer = new byte[variableAddress.BufferLength]; //Act - Should.Throw(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); + Should.Throw(() => ValueConverter.WriteToBuffer(buffer, input, variableAddress)); } } diff --git a/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs b/Sharp7.Rx.Tests/VariableAddressTests/MatchesType.cs similarity index 80% rename from Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs rename to Sharp7.Rx.Tests/VariableAddressTests/MatchesType.cs index 458c17d..0feffb3 100644 --- a/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs +++ b/Sharp7.Rx.Tests/VariableAddressTests/MatchesType.cs @@ -1,15 +1,15 @@ using NUnit.Framework; using Sharp7.Rx.Extensions; using Sharp7.Rx.Interfaces; -using Sharp7.Rx.Tests.S7ValueConverterTests; +using Sharp7.Rx.Tests.ValueConverterTests; using Shouldly; -namespace Sharp7.Rx.Tests.S7VariableAddressTests; +namespace Sharp7.Rx.Tests.VariableAddressTests; [TestFixture] public class MatchesType { - static readonly IS7VariableNameParser parser = new S7VariableNameParser(); + static readonly IVariableNameParser parser = new VariableNameParser(); private static readonly IReadOnlyList typeList = new[] { @@ -61,10 +61,10 @@ public class MatchesType // 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") - )) + (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") + )) ; } diff --git a/Sharp7.Rx.Tests/VariableNameParserTests.cs b/Sharp7.Rx.Tests/VariableNameParserTests.cs new file mode 100644 index 0000000..4ecf1c5 --- /dev/null +++ b/Sharp7.Rx.Tests/VariableNameParserTests.cs @@ -0,0 +1,89 @@ +using DeepEqual.Syntax; +using NUnit.Framework; +using Sharp7.Rx.Enums; +using Shouldly; + +namespace Sharp7.Rx.Tests; + +[TestFixture] +internal class VariableNameParserTests +{ + [TestCaseSource(nameof(ValidTestCases))] + public void Run(TestCase tc) + { + var parser = new VariableNameParser(); + var resp = parser.Parse(tc.Input); + resp.ShouldDeepEqual(tc.Expected); + } + + [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")] + [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 VariableNameParser(); + Should.Throw(() => parser.Parse(input)); + } + + public static IEnumerable ValidTestCases() + { + yield return new TestCase("DB506.Bit216.2", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Bit, Start: 216, Length: 1, Bit: 2)); + + yield return new TestCase("DB506.String216.10", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.String, Start: 216, Length: 10)); + yield return new TestCase("DB506.WString216.10", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.WString, Start: 216, Length: 10)); + + yield return new TestCase("DB506.Byte216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Byte, Start: 216, Length: 1)); + yield return new TestCase("DB506.Byte216.100", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Byte, Start: 216, Length: 100)); + yield return new TestCase("DB506.Int216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Int, Start: 216, Length: 2)); + yield return new TestCase("DB506.UInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.UInt, Start: 216, Length: 2)); + yield return new TestCase("DB506.DInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.DInt, Start: 216, Length: 4)); + yield return new TestCase("DB506.UDInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.UDInt, Start: 216, Length: 4)); + yield return new TestCase("DB506.LInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.LInt, Start: 216, Length: 8)); + yield return new TestCase("DB506.ULInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.ULInt, Start: 216, Length: 8)); + + yield return new TestCase("DB506.Real216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Single, Start: 216, Length: 4)); + yield return new TestCase("DB506.LReal216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Double, Start: 216, Length: 8)); + + + // Legacy + yield return new TestCase("DB13.DBX3.1", new VariableAddress(Operand: Operand.Db, DbNo: 13, Type: DbType.Bit, Start: 3, Length: 1, Bit: 1)); + yield return new TestCase("Db403.X5.2", new VariableAddress(Operand: Operand.Db, DbNo: 403, Type: DbType.Bit, Start: 5, Length: 1, Bit: 2)); + yield return new TestCase("DB55DBX23.6", new VariableAddress(Operand: Operand.Db, DbNo: 55, Type: DbType.Bit, Start: 23, Length: 1, Bit: 6)); + yield return new TestCase("DB1.S255.20", new VariableAddress(Operand: Operand.Db, DbNo: 1, Type: DbType.String, Start: 255, Length: 20)); + yield return new TestCase("DB5.String887.20", new VariableAddress(Operand: Operand.Db, DbNo: 5, Type: DbType.String, Start: 887, Length: 20)); + yield return new TestCase("DB506.B216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Byte, Start: 216, Length: 1)); + yield return new TestCase("DB506.DBB216.5", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Byte, Start: 216, Length: 5)); + yield return new TestCase("DB506.D216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Single, Start: 216, Length: 4)); + yield return new TestCase("DB506.DINT216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.DInt, Start: 216, Length: 4)); + yield return new TestCase("DB506.INT216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Int, Start: 216, Length: 2)); + yield return new TestCase("DB506.DBW216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Int, Start: 216, Length: 2)); + yield return new TestCase("DB506.DUL216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.ULInt, Start: 216, Length: 8)); + yield return new TestCase("DB506.DULINT216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.ULInt, Start: 216, Length: 8)); + yield return new TestCase("DB506.DULONG216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.ULInt, Start: 216, Length: 8)); + } + + public record TestCase(string Input, VariableAddress Expected) + { + public override string ToString() => Input; + } +} diff --git a/Sharp7.Rx/AssemblyInfo.cs b/Sharp7.Rx/AssemblyInfo.cs index e92998f..d9d730c 100644 --- a/Sharp7.Rx/AssemblyInfo.cs +++ b/Sharp7.Rx/AssemblyInfo.cs @@ -1,3 +1,3 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Sharp7.Rx.Tests")] \ No newline at end of file +[assembly: InternalsVisibleTo("Sharp7.Rx.Tests")] diff --git a/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs b/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs index 3ea9ef0..332e03c 100644 --- a/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs +++ b/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs @@ -53,11 +53,14 @@ internal class ConcurrentSubjectDictionary : IDisposable { lock (dictionaryLock) { - var subject = dictionary.AddOrUpdate(key, k => new SubjectWithRefCounter {Counter = 1, Subject = CreateSubject(k)}, (key1, counter) => - { - counter.Counter = counter.Counter + 1; - return counter; - }); + var subject = dictionary.AddOrUpdate( + key, + k => new SubjectWithRefCounter(CreateSubject(k)), + (_, subjectWithRefCounter) => + { + subjectWithRefCounter.IncreaseCount(); + return subjectWithRefCounter; + }); return new DisposableItem(subject.Subject.AsObservable(), () => RemoveIfNoLongerInUse(key)); } @@ -65,8 +68,7 @@ internal class ConcurrentSubjectDictionary : IDisposable public bool TryGetObserver(TKey key, out IObserver subject) { - SubjectWithRefCounter subjectWithRefCount; - if (dictionary.TryGetValue(key, out subjectWithRefCount)) + if (dictionary.TryGetValue(key, out var subjectWithRefCount)) { subject = subjectWithRefCount.Subject.AsObserver(); return true; @@ -101,15 +103,9 @@ internal class ConcurrentSubjectDictionary : IDisposable private void RemoveIfNoLongerInUse(TKey variableName) { lock (dictionaryLock) - { - SubjectWithRefCounter subjectWithRefCount; - if (dictionary.TryGetValue(variableName, out subjectWithRefCount)) - { - if (subjectWithRefCount.Counter == 1) - dictionary.TryRemove(variableName, out subjectWithRefCount); - else subjectWithRefCount.Counter--; - } - } + if (dictionary.TryGetValue(variableName, out var subjectWithRefCount)) + if (subjectWithRefCount.DecreaseCount() < 1) + dictionary.TryRemove(variableName, out _); } ~ConcurrentSubjectDictionary() @@ -119,7 +115,16 @@ internal class ConcurrentSubjectDictionary : IDisposable class SubjectWithRefCounter { - public int Counter { get; set; } - public ISubject Subject { get; set; } + private int counter = 1; + + public SubjectWithRefCounter(ISubject subject) + { + Subject = subject; + } + + public ISubject Subject { get; } + + public int DecreaseCount() => Interlocked.Decrement(ref counter); + public int IncreaseCount() => Interlocked.Increment(ref counter); } } diff --git a/Sharp7.Rx/CacheVariableNameParser.cs b/Sharp7.Rx/CacheVariableNameParser.cs index d24ef98..c4d42a9 100644 --- a/Sharp7.Rx/CacheVariableNameParser.cs +++ b/Sharp7.Rx/CacheVariableNameParser.cs @@ -3,16 +3,16 @@ using Sharp7.Rx.Interfaces; namespace Sharp7.Rx; -internal class CacheVariableNameParser : IS7VariableNameParser +internal class CacheVariableNameParser : IVariableNameParser { - private static readonly ConcurrentDictionary addressCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary addressCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - private readonly IS7VariableNameParser inner; + private readonly IVariableNameParser inner; - public CacheVariableNameParser(IS7VariableNameParser inner) + public CacheVariableNameParser(IVariableNameParser inner) { this.inner = inner; } - public S7VariableAddress Parse(string input) => addressCache.GetOrAdd(input, inner.Parse); + public VariableAddress Parse(string input) => addressCache.GetOrAdd(input, inner.Parse); } diff --git a/Sharp7.Rx/Enums/ConnectionState.cs b/Sharp7.Rx/Enums/ConnectionState.cs index 5cbedc5..ada7634 100644 --- a/Sharp7.Rx/Enums/ConnectionState.cs +++ b/Sharp7.Rx/Enums/ConnectionState.cs @@ -5,5 +5,6 @@ public enum ConnectionState Initial, Connected, DisconnectedByUser, - ConnectionLost + ConnectionLost, + Disposed } diff --git a/Sharp7.Rx/Exceptions/S7Exception.cs b/Sharp7.Rx/Exceptions/S7Exception.cs index 2d8fb44..7fd9915 100644 --- a/Sharp7.Rx/Exceptions/S7Exception.cs +++ b/Sharp7.Rx/Exceptions/S7Exception.cs @@ -31,13 +31,13 @@ public class S7CommunicationException : S7Exception public class DataTypeMissmatchException : S7Exception { - internal DataTypeMissmatchException(string message, Type type, S7VariableAddress address) : base(message) + internal DataTypeMissmatchException(string message, Type type, VariableAddress address) : base(message) { Type = type; Address = address.ToString(); } - internal DataTypeMissmatchException(string message, Exception innerException, Type type, S7VariableAddress address) : base(message, innerException) + internal DataTypeMissmatchException(string message, Exception innerException, Type type, VariableAddress address) : base(message, innerException) { Type = type; Address = address.ToString(); @@ -50,13 +50,13 @@ public class DataTypeMissmatchException : S7Exception public class UnsupportedS7TypeException : S7Exception { - internal UnsupportedS7TypeException(string message, Type type, S7VariableAddress address) : base(message) + internal UnsupportedS7TypeException(string message, Type type, VariableAddress address) : base(message) { Type = type; Address = address.ToString(); } - internal UnsupportedS7TypeException(string message, Exception innerException, Type type, S7VariableAddress address) : base(message, innerException) + internal UnsupportedS7TypeException(string message, Exception innerException, Type type, VariableAddress address) : base(message, innerException) { Type = type; Address = address.ToString(); diff --git a/Sharp7.Rx/Extensions/OperandExtensions.cs b/Sharp7.Rx/Extensions/OperandExtensions.cs index c78cfa5..b2c6744 100644 --- a/Sharp7.Rx/Extensions/OperandExtensions.cs +++ b/Sharp7.Rx/Extensions/OperandExtensions.cs @@ -13,5 +13,4 @@ internal static class OperandExtensions Operand.Db => S7Area.DB, _ => throw new ArgumentOutOfRangeException(nameof(operand), operand, null) }; - } diff --git a/Sharp7.Rx/Extensions/PlcExtensions.cs b/Sharp7.Rx/Extensions/PlcExtensions.cs index b1f1cd2..af2738d 100644 --- a/Sharp7.Rx/Extensions/PlcExtensions.cs +++ b/Sharp7.Rx/Extensions/PlcExtensions.cs @@ -9,7 +9,8 @@ namespace Sharp7.Rx.Extensions; public static class PlcExtensions { - public static IObservable CreateDatatransferWithHandshake(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func> readData, bool initialTransfer) + public static IObservable CreateDatatransferWithHandshake(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func> readData, + bool initialTransfer) { return Observable.Create(async observer => { diff --git a/Sharp7.Rx/Extensions/S7VariableExtensions.cs b/Sharp7.Rx/Extensions/S7VariableExtensions.cs index 40c8eed..fa1c87f 100644 --- a/Sharp7.Rx/Extensions/S7VariableExtensions.cs +++ b/Sharp7.Rx/Extensions/S7VariableExtensions.cs @@ -2,24 +2,24 @@ namespace Sharp7.Rx.Extensions; -internal static class S7VariableAddressExtensions +internal static class VariableAddressExtensions { - private static readonly Dictionary> supportedTypeMap = new() + private static readonly Dictionary> supportedTypeMap = new() { {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}, + {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) => + public static bool MatchesType(this VariableAddress address, Type type) => supportedTypeMap.TryGetValue(type, out var map) && map(address); } diff --git a/Sharp7.Rx/Interfaces/IS7Connector.cs b/Sharp7.Rx/Interfaces/IS7Connector.cs index fe365c1..66566ac 100644 --- a/Sharp7.Rx/Interfaces/IS7Connector.cs +++ b/Sharp7.Rx/Interfaces/IS7Connector.cs @@ -17,5 +17,5 @@ internal interface IS7Connector : IDisposable 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); + Task> ExecuteMultiVarRequest(IReadOnlyList variableNames); } diff --git a/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs b/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs deleted file mode 100644 index ae81d67..0000000 --- a/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs +++ /dev/null @@ -1,7 +0,0 @@ -#nullable enable -namespace Sharp7.Rx.Interfaces; - -internal interface IS7VariableNameParser -{ - S7VariableAddress Parse(string input); -} diff --git a/Sharp7.Rx/Interfaces/IVariableNameParser.cs b/Sharp7.Rx/Interfaces/IVariableNameParser.cs new file mode 100644 index 0000000..d6f8219 --- /dev/null +++ b/Sharp7.Rx/Interfaces/IVariableNameParser.cs @@ -0,0 +1,7 @@ +#nullable enable +namespace Sharp7.Rx.Interfaces; + +internal interface IVariableNameParser +{ + VariableAddress Parse(string input); +} diff --git a/Sharp7.Rx/S7ErrorCodes.cs b/Sharp7.Rx/S7ErrorCodes.cs index f076bf8..5e54922 100644 --- a/Sharp7.Rx/S7ErrorCodes.cs +++ b/Sharp7.Rx/S7ErrorCodes.cs @@ -1,4 +1,6 @@ -namespace Sharp7.Rx; +#nullable enable + +namespace Sharp7.Rx; public static class S7ErrorCodes { @@ -12,6 +14,18 @@ public static class S7ErrorCodes 0x900000, // CPU: Address out of range }; + private static readonly IReadOnlyDictionary additionalErrorTexts = new Dictionary + { + {0xC00000, "This happens when the DB does not exist."}, + {0x900000, "This happens when the DB is not long enough."}, + { + 0x40000, """ + This error occurs when the DB is "optimized" or "PUT/GET communication" is not enabled. + See https://snap7.sourceforge.net/snap7_client.html#target_compatibility. + """ + } + }; + /// /// Some error codes indicate connection lost, in which case, the driver tries to reestablish connection. /// Other error codes indicate a user error, like reading from an unavailable DB or exceeding @@ -21,4 +35,7 @@ public static class S7ErrorCodes { return !notDisconnectedErrorCodes.Contains(errorCode); } + + public static string? GetAdditionalErrorText(int errorCode) => + additionalErrorTexts.TryGetValue(errorCode, out var text) ? text : null; } diff --git a/Sharp7.Rx/S7VariableAddress.cs b/Sharp7.Rx/S7VariableAddress.cs deleted file mode 100644 index f04bd34..0000000 --- a/Sharp7.Rx/S7VariableAddress.cs +++ /dev/null @@ -1,32 +0,0 @@ -using JetBrains.Annotations; -using Sharp7.Rx.Enums; - -namespace Sharp7.Rx; - -[NoReorder] -internal class S7VariableAddress -{ - public Operand Operand { get; set; } - public ushort DbNr { get; set; } - public ushort Start { get; set; } - public ushort Length { get; set; } - public byte? Bit { get; set; } - public DbType Type { get; set; } - - public ushort BufferLength => Type switch - { - DbType.String => (ushort) (Length + 2), - 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}", - }; -} diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 951ff0c..f33c55b 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -20,13 +20,13 @@ internal class Sharp7Connector : IS7Connector private readonly int port; private readonly int rackNr; private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new(maxDegreeOfParallelism: 1); - private readonly IS7VariableNameParser variableNameParser; + private readonly IVariableNameParser variableNameParser; private bool disposed; private S7Client sharp7; - public Sharp7Connector(PlcConnectionSettings settings, IS7VariableNameParser variableNameParser) + public Sharp7Connector(PlcConnectionSettings settings, IVariableNameParser variableNameParser) { this.variableNameParser = variableNameParser; ipAddress = settings.IpAddress; @@ -86,7 +86,7 @@ internal class Sharp7Connector : IS7Connector await CloseConnection(); } - public async Task> ExecuteMultiVarRequest(IReadOnlyList variableNames) + public async Task> ExecuteMultiVarRequest(IReadOnlyList variableNames) { if (variableNames.IsEmpty()) return new Dictionary(); @@ -98,17 +98,14 @@ internal class Sharp7Connector : IS7Connector .Select(x => { var buffer = new byte[x.Address.Length]; - s7MultiVar.Add(S7Consts.S7AreaDB, S7Consts.S7WLByte, x.Address.DbNr, x.Address.Start, x.Address.Length, ref buffer); + s7MultiVar.Add(S7Consts.S7AreaDB, S7Consts.S7WLByte, x.Address.DbNo, x.Address.Start, x.Address.Length, ref buffer); return new {x.VariableName, Buffer = buffer}; }) .ToArray(); var result = await Task.Factory.StartNew(() => s7MultiVar.Read(), CancellationToken.None, TaskCreationOptions.None, scheduler); - if (result != 0) - { - var errorText = EvaluateErrorCode(result); - throw new S7CommunicationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)} ({errorText})", result, errorText); - } + + EnsureSuccessOrThrow(result, $"Error in MultiVar request for variables: {string.Join(",", variableNames)}"); return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer); } @@ -150,11 +147,7 @@ internal class Sharp7Connector : IS7Connector await Task.Factory.StartNew(() => sharp7.ReadArea(operand.ToArea(), dbNo, startByteAddress, bytesToRead, S7WordLength.Byte, buffer), token, TaskCreationOptions.None, scheduler); token.ThrowIfCancellationRequested(); - if (result != 0) - { - var errorText = EvaluateErrorCode(result); - throw new S7CommunicationException($"Error reading {operand}{dbNo}:{startByteAddress}->{bytesToRead} ({errorText})", result, errorText); - } + EnsureSuccessOrThrow(result, $"Error reading {operand}{dbNo}:{startByteAddress}->{bytesToRead}"); return buffer; } @@ -170,11 +163,7 @@ internal class Sharp7Connector : IS7Connector 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) - { - var errorText = EvaluateErrorCode(result); - throw new S7CommunicationException($"Error writing {operand}{dbNo}:{startByteAddress} bit {bitAdress} ({errorText})", result, errorText); - } + EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress} bit {bitAdress}"); } public async Task WriteBytes(Operand operand, ushort startByteAddress, byte[] data, ushort dbNo, CancellationToken token) @@ -184,11 +173,7 @@ internal class Sharp7Connector : IS7Connector 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) - { - var errorText = EvaluateErrorCode(result); - throw new S7CommunicationException($"Error writing {operand}{dbNo}:{startByteAddress}.{data.Length} ({errorText})", result, errorText); - } + EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress}.{data.Length}"); } @@ -206,6 +191,7 @@ internal class Sharp7Connector : IS7Connector sharp7 = null; } + connectionStateSubject?.OnNext(Enums.ConnectionState.Disposed); connectionStateSubject?.OnCompleted(); connectionStateSubject?.Dispose(); } @@ -234,6 +220,20 @@ internal class Sharp7Connector : IS7Connector throw new InvalidOperationException("Plc is not connected"); } + private void EnsureSuccessOrThrow(int result, string message) + { + if (result == 0) return; + + var errorText = EvaluateErrorCode(result); + var completeMessage = $"{message}: {errorText}"; + + var additionalErrorText = S7ErrorCodes.GetAdditionalErrorText(result); + if (additionalErrorText != null) + completeMessage += Environment.NewLine + additionalErrorText; + + throw new S7CommunicationException(completeMessage, result, errorText); + } + private string EvaluateErrorCode(int errorCode) { if (errorCode == 0) @@ -242,8 +242,8 @@ internal class Sharp7Connector : IS7Connector if (sharp7 == null) throw new InvalidOperationException("S7 driver is not initialized."); - var errorText = sharp7.ErrorText(errorCode); - Logger?.LogError($"Error Code {errorCode} {errorText}"); + var errorText = $"0x{errorCode:X}, {sharp7.ErrorText(errorCode)}"; + Logger?.LogError($"S7 Error {errorText}"); if (S7ErrorCodes.AssumeConnectionLost(errorCode)) SetConnectionLostState(); diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index bada2e2..a50d24d 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -17,7 +17,7 @@ public class Sharp7Plc : IPlc 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 readonly CacheVariableNameParser varaibleNameParser = new CacheVariableNameParser(new VariableNameParser()); private bool disposed; private Sharp7Connector s7Connector; @@ -89,7 +89,7 @@ public class Sharp7Plc : IPlc Observable.FromAsync(() => GetValue(variableName)) .Concat( disposeableContainer.Observable - .Select(bytes => S7ValueConverter.ReadFromBuffer(bytes, address)) + .Select(bytes => ValueConverter.ReadFromBuffer(bytes, address)) ); if (transmissionMode == TransmissionMode.OnChange) @@ -102,15 +102,6 @@ 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); @@ -127,8 +118,8 @@ public class Sharp7Plc : IPlc { 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); + var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.BufferLength, address.DbNo, token); + return ValueConverter.ReadFromBuffer(data, address); } public async Task InitializeAsync() @@ -164,15 +155,15 @@ 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. - 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.DbNo, token); } else { // TODO: Use ArrayPool.Rent() once we drop Framwework support var bytes = new byte[address.BufferLength]; - S7ValueConverter.WriteToBuffer(bytes, value, address); + ValueConverter.WriteToBuffer(bytes, value, address); - await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); + await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNo, token); } } @@ -222,6 +213,15 @@ public class Sharp7Plc : IPlc return Unit.Default; } + private VariableAddress 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; + } + private void PrintAndResetPerformanceStatistik() { if (performanceCoutner.Count == performanceCoutner.Capacity) diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/ValueConverter.cs similarity index 96% rename from Sharp7.Rx/S7ValueConverter.cs rename to Sharp7.Rx/ValueConverter.cs index 4bc23f8..83078c2 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/ValueConverter.cs @@ -5,7 +5,7 @@ using Sharp7.Rx.Enums; namespace Sharp7.Rx; -internal static class S7ValueConverter +internal static class ValueConverter { private static readonly Dictionary writeFunctions = new() { @@ -175,7 +175,7 @@ internal static class S7ValueConverter }, }; - public static TValue ReadFromBuffer(byte[] buffer, S7VariableAddress address) + public static TValue ReadFromBuffer(byte[] buffer, VariableAddress address) { // Todo: Change to Span when switched to newer .net @@ -191,7 +191,7 @@ internal static class S7ValueConverter return (TValue) result; } - public static void WriteToBuffer(Span buffer, TValue value, S7VariableAddress address) + public static void WriteToBuffer(Span buffer, TValue value, VariableAddress address) { if (buffer.Length < address.BufferLength) throw new ArgumentException($"Buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer)); @@ -204,7 +204,7 @@ internal static class S7ValueConverter writeFunc(buffer, address, value); } - delegate object ReadFunc(byte[] data, S7VariableAddress address); + delegate object ReadFunc(byte[] data, VariableAddress address); [StructLayout(LayoutKind.Explicit)] private struct UInt32SingleMap @@ -220,5 +220,5 @@ internal static class S7ValueConverter [FieldOffset(0)] public double Double; } - delegate void WriteFunc(Span data, S7VariableAddress address, object value); + delegate void WriteFunc(Span data, VariableAddress address, object value); } diff --git a/Sharp7.Rx/VariableAddress.cs b/Sharp7.Rx/VariableAddress.cs new file mode 100644 index 0000000..8405916 --- /dev/null +++ b/Sharp7.Rx/VariableAddress.cs @@ -0,0 +1,32 @@ +using JetBrains.Annotations; +using Sharp7.Rx.Enums; + +namespace Sharp7.Rx; + +[NoReorder] +internal record VariableAddress(Operand Operand, ushort DbNo, DbType Type, ushort Start, ushort Length, byte? Bit = null) +{ + public Operand Operand { get; } = Operand; + public ushort DbNo { get; } = DbNo; + public ushort Start { get; } = Start; + public ushort Length { get; } = Length; + public byte? Bit { get; } = Bit; + public DbType Type { get; } = Type; + + public ushort BufferLength => Type switch + { + DbType.String => (ushort) (Length + 2), + DbType.WString => (ushort) (Length * 2 + 4), + _ => Length + }; + + public override string ToString() => + Type switch + { + DbType.Bit => $"{Operand}{DbNo}.{Type}{Start}.{Bit}", + DbType.String => $"{Operand}{DbNo}.{Type}{Start}.{Length}", + DbType.WString => $"{Operand}{DbNo}.{Type}{Start}.{Length}", + DbType.Byte => Length == 1 ? $"{Operand}{DbNo}.{Type}{Start}" : $"{Operand}{DbNo}.{Type}{Start}.{Length}", + _ => $"{Operand}{DbNo}.{Type}{Start}", + }; +} diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/VariableNameParser.cs similarity index 93% rename from Sharp7.Rx/S7VariableNameParser.cs rename to Sharp7.Rx/VariableNameParser.cs index b53fcc9..734c33e 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/VariableNameParser.cs @@ -6,7 +6,7 @@ using Sharp7.Rx.Interfaces; namespace Sharp7.Rx; -internal class S7VariableNameParser : IS7VariableNameParser +internal class VariableNameParser : IVariableNameParser { private static readonly Regex regex = new(@"^(?db)(?\d+)\.?(?[a-z]+)(?\d+)(\.(?\d+))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); @@ -46,7 +46,7 @@ internal class S7VariableNameParser : IS7VariableNameParser {"x", DbType.Bit}, }; - public S7VariableAddress Parse(string input) + public VariableAddress Parse(string input) { if (input == null) throw new ArgumentNullException(nameof(input)); @@ -111,15 +111,7 @@ internal class S7VariableNameParser : IS7VariableNameParser byte? bit = type == DbType.Bit ? GetBit() : null; - var s7VariableAddress = new S7VariableAddress - { - Operand = operand, - DbNr = dbNr, - Start = start, - Type = type, - Length = length, - Bit = bit - }; + var s7VariableAddress = new VariableAddress(Operand: operand, DbNo: dbNr, Type: type, Start: start, Length: length, Bit: bit); return s7VariableAddress;