Merge branch 'feature/performanceImprovements' into prerelease

This commit is contained in:
Peter Butzhammer
2024-02-08 18:32:35 +01:00
21 changed files with 957 additions and 587 deletions

View File

@@ -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>(T expected, string address, byte[] data)
{
//Arrange
var variableAddress = parser.Parse(address);
//Act
var result = S7ValueConverter.ConvertToType<T>(data, variableAddress);
//Assert
result.ShouldBe(expected);
}
[TestCase((ushort) 3532, "DB0.INT0", new byte[] {0xF2, 0x34})]
public void Invalid<T>(T expected, string address, byte[] data)
{
//Arrange
var variableAddress = parser.Parse(address);
//Act
Should.Throw<InvalidOperationException>(() => S7ValueConverter.ConvertToType<T>(data, variableAddress));
}
[TestCase(3532, "DB0.DINT0", new byte[] {0xF2, 0x34})]
public void Argument<T>(T expected, string address, byte[] data)
{
//Arrange
var variableAddress = parser.Parse(address);
//Act
Should.Throw<ArgumentException>(() => S7ValueConverter.ConvertToType<T>(data, variableAddress));
}
}

View File

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

View File

@@ -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<ConverterTestCase> 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]);
}
/// <summary>
/// This helper method exists, since I could not manage to invoke a generic method
/// accepring a Span&lt;T&gt; as parameter.
/// </summary>
public static void WriteToBuffer<TValue>(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}";
}
}

View File

@@ -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<ConverterTestCase> 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>(T template, string address, byte[] data)
{
//Arrange
var variableAddress = Parser.Parse(address);
//Act
Should.Throw<UnsupportedS7TypeException>(() => S7ValueConverter.ReadFromBuffer<T>(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>(T template, string address, byte[] data)
{
//Arrange
var variableAddress = Parser.Parse(address);
//Act
Should.Throw<ArgumentException>(() => S7ValueConverter.ReadFromBuffer<T>(data, variableAddress));
}
}

View File

@@ -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<ConverterTestCase> 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>(T input, string address, int bufferSize)
{
//Arrange
var variableAddress = Parser.Parse(address);
var buffer = new byte[bufferSize];
//Act
Should.Throw<ArgumentException>(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress));
}
[TestCase((char) 18, "DB0.DBB0")]
public void UnsupportedType<T>(T input, string address)
{
//Arrange
var variableAddress = Parser.Parse(address);
var buffer = new byte[variableAddress.BufferLength];
//Act
Should.Throw<UnsupportedS7TypeException>(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress));
}
}

View File

@@ -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<Type> 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<TestCase> GetValid()
{
return
ConverterTestBase.GetValidTestCases()
.Select(tc => new TestCase(tc.Value.GetType(), tc.Address));
}
public static IEnumerable<TestCase> 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}";
}
}

View File

@@ -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<TestCase> 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<InvalidS7AddressException>(() => parser.Parse(input));
}
public static IEnumerable<TestCase> 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)

View File

@@ -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,
/// <summary>
/// ASCII string
/// </summary>
String,
/// <summary>
/// UTF16 string
/// </summary>
WString,
Byte,
/// <summary>
/// Int16
/// </summary>
Int,
/// <summary>
/// UInt16
/// </summary>
UInt,
/// <summary>
/// Int32
/// </summary>
DInt,
/// <summary>
/// UInt32
/// </summary>
UDInt,
/// <summary>
/// Int64
/// </summary>
LInt,
/// <summary>
/// UInt64
/// </summary>
ULInt,
Single,
Double,
Integer,
DInteger,
ULong
}

View File

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

View File

@@ -0,0 +1,25 @@
using Sharp7.Rx.Enums;
namespace Sharp7.Rx.Extensions;
internal static class S7VariableAddressExtensions
{
private static readonly Dictionary<Type, Func<S7VariableAddress, bool>> 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);
}

View File

@@ -12,10 +12,10 @@ internal interface IS7Connector : IDisposable
Task<bool> Connect();
Task Disconnect();
Task<byte[]> ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token);
Task<byte[]> ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dbNo, CancellationToken token);
Task<bool> WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token);
Task<ushort> 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<Dictionary<string, byte[]>> ExecuteMultiVarRequest(IReadOnlyList<string> variableNames);
}

View File

@@ -1,4 +1,5 @@
namespace Sharp7.Rx.Interfaces;
#nullable enable
namespace Sharp7.Rx.Interfaces;
internal interface IS7VariableNameParser
{

View File

@@ -1,117 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Sharp7.Rx.Resources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to S7 driver could not be initialized.
/// </summary>
internal static string StrErrorS7DriverCouldNotBeInitialized {
get {
return ResourceManager.GetString("StrErrorS7DriverCouldNotBeInitialized", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to S7 driver is not initialized..
/// </summary>
internal static string StrErrorS7DriverNotInitialized {
get {
return ResourceManager.GetString("StrErrorS7DriverNotInitialized", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to TCP/IP connection established..
/// </summary>
internal static string StrInfoConnectionEstablished {
get {
return ResourceManager.GetString("StrInfoConnectionEstablished", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Trying to connect to PLC ({2}) &apos;{0}&apos;, CPU slot {1}....
/// </summary>
internal static string StrInfoTryConnecting {
get {
return ResourceManager.GetString("StrInfoTryConnecting", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Error while reading data from plc..
/// </summary>
internal static string StrLogErrorReadingDataFromPlc {
get {
return ResourceManager.GetString("StrLogErrorReadingDataFromPlc", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Communication error discovered. Reconnect is in progress....
/// </summary>
internal static string StrLogWarningCommunictionErrorReconnecting {
get {
return ResourceManager.GetString("StrLogWarningCommunictionErrorReconnecting", resourceCulture);
}
}
}
}

View File

@@ -1,138 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="StrLogErrorReadingDataFromPlc" xml:space="preserve">
<value>Error while reading data from plc.</value>
</data>
<data name="StrLogWarningCommunictionErrorReconnecting" xml:space="preserve">
<value>Communication error discovered. Reconnect is in progress...</value>
</data>
<data name="StrErrorS7DriverNotInitialized" xml:space="preserve">
<value>S7 driver is not initialized.</value>
</data>
<data name="StrInfoTryConnecting" xml:space="preserve">
<value>Trying to connect to PLC ({2}) '{0}', CPU slot {1}...</value>
</data>
<data name="StrInfoConnectionEstablished" xml:space="preserve">
<value>TCP/IP connection established.</value>
</data>
<data name="StrErrorS7DriverCouldNotBeInitialized" xml:space="preserve">
<value>S7 driver could not be initialized</value>
</data>
</root>

View File

@@ -7,77 +7,218 @@ namespace Sharp7.Rx;
internal static class S7ValueConverter
{
public static TValue ConvertToType<TValue>(byte[] buffer, S7VariableAddress address)
private static readonly Dictionary<Type, WriteFunc> 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<Type, ReadFunc> 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<TValue>(byte[] buffer, S7VariableAddress address)
{
// Todo: Change to Span<byte> 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<TValue>(Span<byte> 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<byte> data, S7VariableAddress address, object value);
}

View File

@@ -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}",
};
}

View File

@@ -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(@"^(?<operand>db{1})(?<dbNr>\d{1,4})\.?(?<type>dbx|x|s|string|b|dbb|d|int|dbw|w|dint|dul|dulint|dulong|){1}(?<start>\d+)(\.(?<bitOrLength>\d+))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex regex = new(@"^(?<operand>db)(?<dbNo>\d+)\.?(?<type>[a-z]+)(?<start>\d+)(\.(?<bitOrLength>\d+))?$",
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly IReadOnlyDictionary<string, DbType> types = new Dictionary<string, DbType>(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<dbNo>.<type><startByte>(.<length>)\".", 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;
}
}
}

View File

@@ -27,19 +27,4 @@
<PackageReference Include="System.Reactive" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\StringResources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>StringResources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\StringResources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>StringResources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=exceptions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -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<ConnectionState> connectionStateSubject = new BehaviorSubject<ConnectionState>(Enums.ConnectionState.Initial);
private readonly BehaviorSubject<ConnectionState> 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<bool> 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<byte[]> ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token)
public async Task<byte[]> 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<bool> 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<ushort> 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<bool> Reconnect()

View File

@@ -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<string, byte[]> multiVariableSubscriptions = new ConcurrentSubjectDictionary<string, byte[]>(StringComparer.InvariantCultureIgnoreCase);
private readonly List<long> performanceCoutner = new List<long>(1000);
private readonly CompositeDisposable disposables = new();
private readonly ConcurrentSubjectDictionary<string, byte[]> multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase);
private readonly List<long> performanceCoutner = new(1000);
private readonly PlcConnectionSettings plcConnectionSettings;
private readonly IS7VariableNameParser varaibleNameParser = new CacheVariableNameParser(new S7VariableNameParser());
private bool disposed;
private IS7Connector s7Connector;
private Sharp7Connector s7Connector;
/// <summary>
@@ -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> ConnectionState { get; }
public ILogger Logger
{
get => s7Connector.Logger;
set => s7Connector.Logger = value;
}
public IObservable<ConnectionState> 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<TValue>(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<TValue>(bytes, address));
var observable =
// Directly read variable first.
// This will propagate any errors due to reading from invalid addresses.
Observable.FromAsync(() => GetValue<TValue>(variableName))
.Concat(
disposeableContainer.Observable
.Select(bytes => S7ValueConverter.ReadFromBuffer<TValue>(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<TValue> GetValue<TValue>(string variableName)
{
return GetValue<TValue>(variableName, CancellationToken.None);
@@ -99,18 +125,14 @@ public class Sharp7Plc : IPlc
public async Task<TValue> GetValue<TValue>(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<TValue>(data, address);
return S7ValueConverter.ReadFromBuffer<TValue>(data, address);
}
public async Task<bool> 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<TValue>(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();