diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests.cs b/Sharp7.Rx.Tests/S7ValueConverterTests.cs deleted file mode 100644 index 032877d..0000000 --- a/Sharp7.Rx.Tests/S7ValueConverterTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using NUnit.Framework; -using Sharp7.Rx.Interfaces; -using Shouldly; - -namespace Sharp7.Rx.Tests; - -[TestFixture] -public class S7ValueConverterTests -{ - 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 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((ushort) 3532, "DB0.INT0", new byte[] {0xF2, 0x34})] - public void Invalid(T expected, 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 expected, string address, byte[] data) - { - //Arrange - var variableAddress = parser.Parse(address); - - //Act - Should.Throw(() => S7ValueConverter.ConvertToType(data, variableAddress)); - } -} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs new file mode 100644 index 0000000..b3956ac --- /dev/null +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs @@ -0,0 +1,25 @@ +using NUnit.Framework; +using Shouldly; + +namespace Sharp7.Rx.Tests.S7ValueConverterTests; + +[TestFixture] +internal class ConvertBothWays : ConverterTestBase +{ + [TestCaseSource(nameof(GetValidTestCases))] + public void Convert(ConverterTestCase tc) + { + //Arrange + var buffer = new byte[tc.VariableAddress.BufferLength]; + + var write = CreateWriteMethod(tc); + var read = CreateReadMethod(tc); + + //Act + write.Invoke(null, [buffer, tc.Value, tc.VariableAddress]); + var result = read.Invoke(null, [buffer, tc.VariableAddress]); + + //Assert + result.ShouldBe(tc.Value); + } +} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs new file mode 100644 index 0000000..f344256 --- /dev/null +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs @@ -0,0 +1,83 @@ +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) 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(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", [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]); + + 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(305419879, "DB99.DINT0", [0x12, 0x34, 0x56, 0x67]); + yield return new ConverterTestCase(-231451033, "DB99.DINT0", [0xF2, 0x34, 0x56, 0x67]); + 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() => $"{Value.GetType().Name}, {Address}: {Value}"; + } +} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs new file mode 100644 index 0000000..cbb4542 --- /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.6", [0x04, 0x04, 0x41, 0x42, 0x43, 0x44, 0x00, 0x00]); // Length in address exceeds PLC string length + } + + [TestCase((char) 18, "DB0.DBB0", new byte[] {0x12})] + public void UnsupportedType(T template, string address, byte[] data) + { + //Arrange + var variableAddress = Parser.Parse(address); + + //Act + Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); + } + + [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); + + //Act + Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); + } +} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs new file mode 100644 index 0000000..4e364fb --- /dev/null +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs @@ -0,0 +1,53 @@ +using NUnit.Framework; +using Shouldly; + +namespace Sharp7.Rx.Tests.S7ValueConverterTests; + +[TestFixture] +internal class WriteToBuffer : ConverterTestBase +{ + [TestCaseSource(nameof(GetValidTestCases))] + [TestCaseSource(nameof(GetAdditinalWriteTestCases))] + public void Write(ConverterTestCase tc) + { + //Arrange + var buffer = new byte[tc.VariableAddress.BufferLength]; + var write = CreateWriteMethod(tc); + + //Act + write.Invoke(null, [buffer, tc.Value, tc.VariableAddress]); + + //Assert + 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")] + 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)); + } +} diff --git a/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs b/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs new file mode 100644 index 0000000..458c17d --- /dev/null +++ b/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs @@ -0,0 +1,85 @@ +using NUnit.Framework; +using Sharp7.Rx.Extensions; +using Sharp7.Rx.Interfaces; +using Sharp7.Rx.Tests.S7ValueConverterTests; +using Shouldly; + +namespace Sharp7.Rx.Tests.S7VariableAddressTests; + +[TestFixture] +public class MatchesType +{ + static readonly IS7VariableNameParser parser = new S7VariableNameParser(); + + private static readonly IReadOnlyList typeList = new[] + { + 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 static IEnumerable GetInvalid() + { + 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") + )) + ; + } + + + 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) + { + public override string ToString() => $"{Type.Name} {Address}"; + } +} diff --git a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs index a5fa263..50a460a 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,72 @@ internal class S7VariableNameParserTests resp.ShouldDeepEqual(tc.Expected); } - public static IEnumerable GetTestCases() + [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", 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..2d8fb44 --- /dev/null +++ b/Sharp7.Rx/Exceptions/S7Exception.cs @@ -0,0 +1,83 @@ +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 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) + { + 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) + { + Input = input; + } + + public InvalidS7AddressException(string message, Exception innerException, string input) : base(message, innerException) + { + Input = input; + } + + public string Input { get; } +} diff --git a/Sharp7.Rx/Extensions/S7VariableExtensions.cs b/Sharp7.Rx/Extensions/S7VariableExtensions.cs new file mode 100644 index 0000000..40c8eed --- /dev/null +++ b/Sharp7.Rx/Extensions/S7VariableExtensions.cs @@ -0,0 +1,25 @@ +using Sharp7.Rx.Enums; + +namespace Sharp7.Rx.Extensions; + +internal static class S7VariableAddressExtensions +{ + 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}, + }; + + public static bool MatchesType(this S7VariableAddress 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 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/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/Resources/StringResources.Designer.cs b/Sharp7.Rx/Resources/StringResources.Designer.cs deleted file mode 100644 index f1d725d..0000000 --- a/Sharp7.Rx/Resources/StringResources.Designer.cs +++ /dev/null @@ -1,117 +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", "15.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class StringResources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal StringResources() { - } - - /// - /// 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 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.. - /// - 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 3eff273..0000000 --- a/Sharp7.Rx/Resources/StringResources.resx +++ /dev/null @@ -1,138 +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... - - - 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/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 26b86c5..4bc23f8 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -7,77 +7,218 @@ namespace Sharp7.Rx; internal static class S7ValueConverter { - public static TValue ConvertToType(byte[] buffer, S7VariableAddress address) + private static readonly Dictionary writeFunctions = new() { - if (typeof(TValue) == typeof(bool)) - return (TValue) (object) (((buffer[0] >> address.Bit) & 1) > 0); - - 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); - - throw new InvalidOperationException($"length must be 2 or 4 but is {address.Length}"); - } - - 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(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 + typeof(bool), (data, address, value) => { - UInt32 = BinaryPrimitives.ReadUInt32BigEndian(buffer) - }; - return (TValue) (object) (double) d.Single; - } - - 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); + var byteValue = (bool) value ? (byte) 1 : (byte) 0; + var shifted = (byte) (byteValue << address.Bit!); + data[0] = shifted; } - else - return (TValue) (object) Encoding.ASCII.GetString(buffer).Trim(); + }, - throw new InvalidOperationException(string.Format("type '{0}' not supported.", typeof(TValue))); + {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.ToArray()}, + + {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.ToArray()), + _ => 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) + { + // 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)) + throw new UnsupportedS7TypeException($"{type.Name} is not supported. {address}", type, address); + + var result = readFunc(buffer, address); + return (TValue) result; } + 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)); + + var type = typeof(TValue); + + if (!writeFunctions.TryGetValue(type, out var writeFunc)) + throw new UnsupportedS7TypeException($"{type.Name} is not supported. {address}", type, address); + + writeFunc(buffer, address, value); + } + + delegate object ReadFunc(byte[] data, S7VariableAddress address); + [StructLayout(LayoutKind.Explicit)] private struct UInt32SingleMap { [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; + } + + delegate void WriteFunc(Span data, S7VariableAddress address, object value); } diff --git a/Sharp7.Rx/S7VariableAddress.cs b/Sharp7.Rx/S7VariableAddress.cs index ae5e440..f04bd34 100644 --- a/Sharp7.Rx/S7VariableAddress.cs +++ b/Sharp7.Rx/S7VariableAddress.cs @@ -10,6 +10,23 @@ 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 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/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index 80e9913..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,75 +8,148 @@ 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(@"^(?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}, - {"b", DbType.Byte}, + {"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}, + + // S7 notation {"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}, + {"dbd", DbType.DInt}, + + // used for legacy compatability + {"b", DbType.Byte}, + {"d", DbType.Single}, + {"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}\". Expect format \"DB.(.)\".", 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); + + if (result > 7) + throw new InvalidS7AddressException($"Bit must be between 0 and 7 but is {result} in \"{input}\"", input); + + return result; + } } } diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 3b73209..faf60fe 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -27,19 +27,4 @@ - - - True - True - StringResources.resx - - - - - - ResXFileCodeGenerator - StringResources.Designer.cs - - - 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/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index e8a078a..951ff0c 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -6,21 +6,20 @@ 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; 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; @@ -55,21 +54,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 +106,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 +133,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 +147,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 +167,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 +217,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 +228,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 +248,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 d57f63c..bada2e2 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; @@ -14,13 +13,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 +43,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; @@ -65,26 +77,40 @@ 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 disposables = new CompositeDisposable(); + var disp = new CompositeDisposable(); var disposeableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName); - disposeableContainer.AddDisposableTo(disposables); + disposeableContainer.AddDisposableTo(disp); - 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.ReadFromBuffer(bytes, address)) + ); if (transmissionMode == TransmissionMode.OnChange) observable = observable.DistinctUntilChanged(); observable.Subscribe(observer) - .AddDisposableTo(disposables); + .AddDisposableTo(disp); - return disposables; + return disp; }); } + 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); @@ -99,18 +125,14 @@ 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.ConvertToType(data, address); + return S7ValueConverter.ReadFromBuffer(data, address); } public async Task InitializeAsync() { - s7Connector = new Sharp7Connector(plcConnectionSettings, varaibleNameParser) {Logger = Logger}; - ConnectionState = s7Connector.ConnectionState; - await s7Connector.InitializeAsync(); #pragma warning disable 4014 @@ -128,75 +150,29 @@ public class Sharp7Plc : IPlc #pragma warning restore 4014 RunNotifications(s7Connector, MultiVarRequestCycleTime) - .AddDisposableTo(Disposables); + .AddDisposableTo(disposables); return true; } 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)) { - 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); + // Special handling for bools, which are written on a by-bit basis. Writing a complete byte would + // overwrite other bits within this byte. - 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(); - } + await s7Connector.WriteBit(address.Operand, address.Start, address.Bit!.Value, (bool) (object) value, address.DbNr, token); } 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); } } @@ -207,7 +183,7 @@ public class Sharp7Plc : IPlc if (disposing) { - Disposables.Dispose(); + disposables.Dispose(); if (s7Connector != null) { @@ -254,7 +230,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();