mirror of
https://github.com/evopro-ag/Sharp7Reactive.git
synced 2025-12-15 11:22:52 +00:00
Merge branch 'feature/performanceImprovements' into prerelease
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
25
Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs
Normal file
25
Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
83
Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs
Normal file
83
Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs
Normal 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<T> 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}";
|
||||
}
|
||||
}
|
||||
51
Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs
Normal file
51
Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
53
Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs
Normal file
53
Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
85
Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs
Normal file
85
Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
83
Sharp7.Rx/Exceptions/S7Exception.cs
Normal file
83
Sharp7.Rx/Exceptions/S7Exception.cs
Normal 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; }
|
||||
}
|
||||
25
Sharp7.Rx/Extensions/S7VariableExtensions.cs
Normal file
25
Sharp7.Rx/Extensions/S7VariableExtensions.cs
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
namespace Sharp7.Rx.Interfaces;
|
||||
#nullable enable
|
||||
namespace Sharp7.Rx.Interfaces;
|
||||
|
||||
internal interface IS7VariableNameParser
|
||||
{
|
||||
|
||||
117
Sharp7.Rx/Resources/StringResources.Designer.cs
generated
117
Sharp7.Rx/Resources/StringResources.Designer.cs
generated
@@ -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}) '{0}', 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
2
Sharp7.Rx/Sharp7.Rx.csproj.DotSettings
Normal file
2
Sharp7.Rx/Sharp7.Rx.csproj.DotSettings
Normal 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>
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user