mirror of
https://github.com/evopro-ag/Sharp7Reactive.git
synced 2026-02-04 07:42:53 +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 DeepEqual.Syntax;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using Sharp7.Rx.Enums;
|
using Sharp7.Rx.Enums;
|
||||||
|
using Shouldly;
|
||||||
|
|
||||||
namespace Sharp7.Rx.Tests;
|
namespace Sharp7.Rx.Tests;
|
||||||
|
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
internal class S7VariableNameParserTests
|
internal class S7VariableNameParserTests
|
||||||
{
|
{
|
||||||
[TestCaseSource(nameof(GetTestCases))]
|
[TestCaseSource(nameof(ValidTestCases))]
|
||||||
public void Run(TestCase tc)
|
public void Run(TestCase tc)
|
||||||
{
|
{
|
||||||
var parser = new S7VariableNameParser();
|
var parser = new S7VariableNameParser();
|
||||||
@@ -15,23 +16,72 @@ internal class S7VariableNameParserTests
|
|||||||
resp.ShouldDeepEqual(tc.Expected);
|
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("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("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("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, 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, 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, Type = DbType.Byte});
|
||||||
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, 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, Type = DbType.Single});
|
||||||
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, Type = DbType.DInt});
|
||||||
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, Type = DbType.Int});
|
||||||
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, Type = DbType.Int});
|
||||||
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, Type = DbType.ULInt});
|
||||||
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, Type = DbType.ULInt});
|
||||||
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, Type = DbType.ULInt});
|
||||||
yield return new TestCase("DB506.DULONG216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Bit = 0, Type = DbType.ULong});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record TestCase(string Input, S7VariableAddress Expected)
|
public record TestCase(string Input, S7VariableAddress Expected)
|
||||||
|
|||||||
@@ -1,12 +1,52 @@
|
|||||||
namespace Sharp7.Rx.Enums;
|
namespace Sharp7.Rx.Enums;
|
||||||
|
|
||||||
|
// see https://support.industry.siemens.com/cs/mdm/109747174?c=88343664523&lc=de-DE
|
||||||
internal enum DbType
|
internal enum DbType
|
||||||
{
|
{
|
||||||
Bit,
|
Bit,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ASCII string
|
||||||
|
/// </summary>
|
||||||
String,
|
String,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UTF16 string
|
||||||
|
/// </summary>
|
||||||
|
WString,
|
||||||
|
|
||||||
Byte,
|
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,
|
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<bool> Connect();
|
||||||
Task Disconnect();
|
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 WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNo, CancellationToken token);
|
||||||
Task<ushort> WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token);
|
Task WriteBytes(Operand operand, ushort startByteAddress, byte[] data, ushort dbNo, CancellationToken token);
|
||||||
|
|
||||||
Task<Dictionary<string, byte[]>> ExecuteMultiVarRequest(IReadOnlyList<string> variableNames);
|
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
|
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
|
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)
|
typeof(bool), (data, address, value) =>
|
||||||
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
|
|
||||||
{
|
{
|
||||||
UInt32 = BinaryPrimitives.ReadUInt32BigEndian(buffer)
|
var byteValue = (bool) value ? (byte) 1 : (byte) 0;
|
||||||
};
|
var shifted = (byte) (byteValue << address.Bit!);
|
||||||
return (TValue) (object) (double) d.Single;
|
data[0] = shifted;
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof(TValue) == typeof(float))
|
|
||||||
{
|
|
||||||
var d = new UInt32SingleMap
|
|
||||||
{
|
|
||||||
UInt32 = BinaryPrimitives.ReadUInt32BigEndian(buffer)
|
|
||||||
};
|
|
||||||
return (TValue) (object) d.Single;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof(TValue) == typeof(string))
|
|
||||||
if (address.Type == DbType.String)
|
|
||||||
{
|
|
||||||
// First byte is maximal length
|
|
||||||
// Second byte is actual length
|
|
||||||
// https://cache.industry.siemens.com/dl/files/480/22506480/att_105176/v1/s7_scl_string_parameterzuweisung_e.pdf
|
|
||||||
|
|
||||||
var length = Math.Min(address.Length, buffer[1]);
|
|
||||||
|
|
||||||
return (TValue) (object) Encoding.ASCII.GetString(buffer, 2, length);
|
|
||||||
}
|
}
|
||||||
else
|
},
|
||||||
return (TValue) (object) Encoding.ASCII.GetString(buffer).Trim();
|
|
||||||
|
|
||||||
throw new InvalidOperationException(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)]
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
private struct UInt32SingleMap
|
private struct UInt32SingleMap
|
||||||
{
|
{
|
||||||
[FieldOffset(0)] public uint UInt32;
|
[FieldOffset(0)] public uint UInt32;
|
||||||
[FieldOffset(0)] public float Single;
|
[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 DbNr { get; set; }
|
||||||
public ushort Start { get; set; }
|
public ushort Start { get; set; }
|
||||||
public ushort Length { get; set; }
|
public ushort Length { get; set; }
|
||||||
public byte Bit { get; set; }
|
public byte? Bit { get; set; }
|
||||||
public DbType Type { 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 System.Text.RegularExpressions;
|
||||||
using Sharp7.Rx.Enums;
|
using Sharp7.Rx.Enums;
|
||||||
using Sharp7.Rx.Interfaces;
|
using Sharp7.Rx.Interfaces;
|
||||||
@@ -7,75 +8,148 @@ namespace Sharp7.Rx;
|
|||||||
|
|
||||||
internal class S7VariableNameParser : IS7VariableNameParser
|
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)
|
private static readonly IReadOnlyDictionary<string, DbType> types = new Dictionary<string, DbType>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{"x", DbType.Bit},
|
{"bit", DbType.Bit},
|
||||||
{"dbx", DbType.Bit},
|
|
||||||
{"s", DbType.String},
|
|
||||||
{"string", DbType.String},
|
{"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},
|
{"dbb", DbType.Byte},
|
||||||
{"d", DbType.Double},
|
{"dbw", DbType.Int},
|
||||||
{"int", DbType.Integer},
|
{"dbx", DbType.Bit},
|
||||||
{"dint", DbType.DInteger},
|
{"dbd", DbType.DInt},
|
||||||
{"w", DbType.Integer},
|
|
||||||
{"dbw", DbType.Integer},
|
// used for legacy compatability
|
||||||
{"dul", DbType.ULong},
|
{"b", DbType.Byte},
|
||||||
{"dulint", DbType.ULong},
|
{"d", DbType.Single},
|
||||||
{"dulong", DbType.ULong}
|
{"dul", DbType.ULInt},
|
||||||
|
{"dulint", DbType.ULInt},
|
||||||
|
{"dulong", DbType.ULInt},
|
||||||
|
{"s", DbType.String},
|
||||||
|
{"w", DbType.Int},
|
||||||
|
{"x", DbType.Bit},
|
||||||
};
|
};
|
||||||
|
|
||||||
public S7VariableAddress Parse(string input)
|
public S7VariableAddress Parse(string input)
|
||||||
{
|
{
|
||||||
|
if (input == null)
|
||||||
|
throw new ArgumentNullException(nameof(input));
|
||||||
|
|
||||||
var match = regex.Match(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);
|
DbType.Bit => 1,
|
||||||
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.String => GetLength(),
|
||||||
|
DbType.WString => GetLength(),
|
||||||
|
|
||||||
var s7VariableAddress = new S7VariableAddress
|
DbType.Byte => GetLength(1),
|
||||||
{
|
|
||||||
Operand = operand,
|
|
||||||
DbNr = dbNr,
|
|
||||||
Start = start,
|
|
||||||
Type = type,
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (type)
|
DbType.Int => 2,
|
||||||
{
|
DbType.DInt => 4,
|
||||||
case DbType.Bit:
|
DbType.ULInt => 8,
|
||||||
s7VariableAddress.Length = 1;
|
DbType.UInt => 2,
|
||||||
s7VariableAddress.Bit = byte.Parse(match.Groups["bitOrLength"].Value);
|
DbType.UDInt => 4,
|
||||||
break;
|
DbType.LInt => 8,
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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" />
|
<PackageReference Include="System.Reactive" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</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>
|
</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.Enums;
|
||||||
using Sharp7.Rx.Extensions;
|
using Sharp7.Rx.Extensions;
|
||||||
using Sharp7.Rx.Interfaces;
|
using Sharp7.Rx.Interfaces;
|
||||||
using Sharp7.Rx.Resources;
|
|
||||||
using Sharp7.Rx.Settings;
|
using Sharp7.Rx.Settings;
|
||||||
|
|
||||||
namespace Sharp7.Rx;
|
namespace Sharp7.Rx;
|
||||||
|
|
||||||
internal class Sharp7Connector : IS7Connector
|
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 int cpuSlotNr;
|
||||||
|
|
||||||
private readonly CompositeDisposable disposables = new CompositeDisposable();
|
private readonly CompositeDisposable disposables = new();
|
||||||
private readonly string ipAddress;
|
private readonly string ipAddress;
|
||||||
private readonly int port;
|
private readonly int port;
|
||||||
private readonly int rackNr;
|
private readonly int rackNr;
|
||||||
private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism: 1);
|
private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new(maxDegreeOfParallelism: 1);
|
||||||
private readonly IS7VariableNameParser variableNameParser;
|
private readonly IS7VariableNameParser variableNameParser;
|
||||||
private bool disposed;
|
private bool disposed;
|
||||||
|
|
||||||
@@ -55,21 +54,26 @@ internal class Sharp7Connector : IS7Connector
|
|||||||
public async Task<bool> Connect()
|
public async Task<bool> Connect()
|
||||||
{
|
{
|
||||||
if (sharp7 == null)
|
if (sharp7 == null)
|
||||||
throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized);
|
throw new InvalidOperationException("S7 driver is not initialized.");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var errorCode = await Task.Factory.StartNew(() => sharp7.ConnectTo(ipAddress, rackNr, cpuSlotNr), CancellationToken.None, TaskCreationOptions.None, scheduler);
|
var errorCode = await Task.Factory.StartNew(() => sharp7.ConnectTo(ipAddress, rackNr, cpuSlotNr), CancellationToken.None, TaskCreationOptions.None, scheduler);
|
||||||
var success = EvaluateErrorCode(errorCode);
|
if (errorCode == 0)
|
||||||
if (success)
|
|
||||||
{
|
{
|
||||||
connectionStateSubject.OnNext(Enums.ConnectionState.Connected);
|
connectionStateSubject.OnNext(Enums.ConnectionState.Connected);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var errorText = EvaluateErrorCode(errorCode);
|
||||||
|
Logger.LogError("Failed to establish initial connection: {Error}", errorText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// TODO:
|
connectionStateSubject.OnNext(Enums.ConnectionState.ConnectionLost);
|
||||||
|
Logger.LogError(ex, "Failed to establish initial connection.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -102,8 +106,8 @@ internal class Sharp7Connector : IS7Connector
|
|||||||
var result = await Task.Factory.StartNew(() => s7MultiVar.Read(), CancellationToken.None, TaskCreationOptions.None, scheduler);
|
var result = await Task.Factory.StartNew(() => s7MultiVar.Read(), CancellationToken.None, TaskCreationOptions.None, scheduler);
|
||||||
if (result != 0)
|
if (result != 0)
|
||||||
{
|
{
|
||||||
EvaluateErrorCode(result);
|
var errorText = EvaluateErrorCode(result);
|
||||||
throw new InvalidOperationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)}");
|
throw new S7CommunicationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)} ({errorText})", result, errorText);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer);
|
return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer);
|
||||||
@@ -129,13 +133,13 @@ internal class Sharp7Connector : IS7Connector
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger?.LogError(ex, StringResources.StrErrorS7DriverCouldNotBeInitialized);
|
Logger?.LogError(ex, "S7 driver could not be initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(true);
|
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();
|
EnsureConnectionValid();
|
||||||
|
|
||||||
@@ -143,20 +147,19 @@ internal class Sharp7Connector : IS7Connector
|
|||||||
|
|
||||||
|
|
||||||
var result =
|
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();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (result != 0)
|
if (result != 0)
|
||||||
{
|
{
|
||||||
EvaluateErrorCode(result);
|
var errorText = EvaluateErrorCode(result);
|
||||||
var errorText = sharp7.ErrorText(result);
|
throw new S7CommunicationException($"Error reading {operand}{dbNo}:{startByteAddress}->{bytesToRead} ({errorText})", result, errorText);
|
||||||
throw new InvalidOperationException($"Error reading {operand}{dBNr}:{startByteAddress}->{bytesToRead} ({errorText})");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return buffer;
|
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();
|
EnsureConnectionValid();
|
||||||
|
|
||||||
@@ -164,32 +167,28 @@ internal class Sharp7Connector : IS7Connector
|
|||||||
|
|
||||||
var offsetStart = (startByteAddress * 8) + bitAdress;
|
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();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (result != 0)
|
if (result != 0)
|
||||||
{
|
{
|
||||||
EvaluateErrorCode(result);
|
var errorText = EvaluateErrorCode(result);
|
||||||
return (false);
|
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();
|
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();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (result != 0)
|
if (result != 0)
|
||||||
{
|
{
|
||||||
EvaluateErrorCode(result);
|
var errorText = EvaluateErrorCode(result);
|
||||||
return 0;
|
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()
|
private async Task CloseConnection()
|
||||||
{
|
{
|
||||||
if (sharp7 == null)
|
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);
|
await Task.Factory.StartNew(() => sharp7.Disconnect(), CancellationToken.None, TaskCreationOptions.None, scheduler);
|
||||||
}
|
}
|
||||||
@@ -229,19 +228,19 @@ internal class Sharp7Connector : IS7Connector
|
|||||||
throw new ObjectDisposedException("S7Connector");
|
throw new ObjectDisposedException("S7Connector");
|
||||||
|
|
||||||
if (sharp7 == null)
|
if (sharp7 == null)
|
||||||
throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized);
|
throw new InvalidOperationException("S7 driver is not initialized.");
|
||||||
|
|
||||||
if (!IsConnected)
|
if (!IsConnected)
|
||||||
throw new InvalidOperationException("Plc is not connected");
|
throw new InvalidOperationException("Plc is not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool EvaluateErrorCode(int errorCode)
|
private string EvaluateErrorCode(int errorCode)
|
||||||
{
|
{
|
||||||
if (errorCode == 0)
|
if (errorCode == 0)
|
||||||
return true;
|
return null;
|
||||||
|
|
||||||
if (sharp7 == null)
|
if (sharp7 == null)
|
||||||
throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized);
|
throw new InvalidOperationException("S7 driver is not initialized.");
|
||||||
|
|
||||||
var errorText = sharp7.ErrorText(errorCode);
|
var errorText = sharp7.ErrorText(errorCode);
|
||||||
Logger?.LogError($"Error Code {errorCode} {errorText}");
|
Logger?.LogError($"Error Code {errorCode} {errorText}");
|
||||||
@@ -249,7 +248,7 @@ internal class Sharp7Connector : IS7Connector
|
|||||||
if (S7ErrorCodes.AssumeConnectionLost(errorCode))
|
if (S7ErrorCodes.AssumeConnectionLost(errorCode))
|
||||||
SetConnectionLostState();
|
SetConnectionLostState();
|
||||||
|
|
||||||
return false;
|
return errorText;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> Reconnect()
|
private async Task<bool> Reconnect()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Text;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Sharp7.Rx.Basics;
|
using Sharp7.Rx.Basics;
|
||||||
using Sharp7.Rx.Enums;
|
using Sharp7.Rx.Enums;
|
||||||
@@ -14,13 +13,13 @@ namespace Sharp7.Rx;
|
|||||||
|
|
||||||
public class Sharp7Plc : IPlc
|
public class Sharp7Plc : IPlc
|
||||||
{
|
{
|
||||||
protected readonly CompositeDisposable Disposables = new CompositeDisposable();
|
private readonly CompositeDisposable disposables = new();
|
||||||
private readonly ConcurrentSubjectDictionary<string, byte[]> multiVariableSubscriptions = new ConcurrentSubjectDictionary<string, byte[]>(StringComparer.InvariantCultureIgnoreCase);
|
private readonly ConcurrentSubjectDictionary<string, byte[]> multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase);
|
||||||
private readonly List<long> performanceCoutner = new List<long>(1000);
|
private readonly List<long> performanceCoutner = new(1000);
|
||||||
private readonly PlcConnectionSettings plcConnectionSettings;
|
private readonly PlcConnectionSettings plcConnectionSettings;
|
||||||
private readonly IS7VariableNameParser varaibleNameParser = new CacheVariableNameParser(new S7VariableNameParser());
|
private readonly IS7VariableNameParser varaibleNameParser = new CacheVariableNameParser(new S7VariableNameParser());
|
||||||
private bool disposed;
|
private bool disposed;
|
||||||
private IS7Connector s7Connector;
|
private Sharp7Connector s7Connector;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -44,13 +43,26 @@ public class Sharp7Plc : IPlc
|
|||||||
public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress, int port = 102, TimeSpan? multiVarRequestCycleTime = null)
|
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};
|
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))
|
if (multiVarRequestCycleTime != null)
|
||||||
MultiVarRequestCycleTime = multiVarRequestCycleTime.Value;
|
{
|
||||||
|
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 TimeSpan MultiVarRequestCycleTime { get; } = TimeSpan.FromSeconds(0.1);
|
||||||
|
|
||||||
public int MultiVarRequestMaxItems { get; set; } = 16;
|
public int MultiVarRequestMaxItems { get; set; } = 16;
|
||||||
@@ -65,26 +77,40 @@ public class Sharp7Plc : IPlc
|
|||||||
{
|
{
|
||||||
return Observable.Create<TValue>(observer =>
|
return Observable.Create<TValue>(observer =>
|
||||||
{
|
{
|
||||||
var address = varaibleNameParser.Parse(variableName);
|
var address = ParseAndVerify(variableName, typeof(TValue));
|
||||||
if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName));
|
|
||||||
|
|
||||||
var disposables = new CompositeDisposable();
|
var disp = new CompositeDisposable();
|
||||||
var disposeableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName);
|
var disposeableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName);
|
||||||
disposeableContainer.AddDisposableTo(disposables);
|
disposeableContainer.AddDisposableTo(disp);
|
||||||
|
|
||||||
var observable = disposeableContainer.Observable
|
var observable =
|
||||||
.Select(bytes => S7ValueConverter.ConvertToType<TValue>(bytes, address));
|
// 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)
|
if (transmissionMode == TransmissionMode.OnChange)
|
||||||
observable = observable.DistinctUntilChanged();
|
observable = observable.DistinctUntilChanged();
|
||||||
|
|
||||||
observable.Subscribe(observer)
|
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)
|
public Task<TValue> GetValue<TValue>(string variableName)
|
||||||
{
|
{
|
||||||
return GetValue<TValue>(variableName, CancellationToken.None);
|
return GetValue<TValue>(variableName, CancellationToken.None);
|
||||||
@@ -99,18 +125,14 @@ public class Sharp7Plc : IPlc
|
|||||||
|
|
||||||
public async Task<TValue> GetValue<TValue>(string variableName, CancellationToken token)
|
public async Task<TValue> GetValue<TValue>(string variableName, CancellationToken token)
|
||||||
{
|
{
|
||||||
var address = varaibleNameParser.Parse(variableName);
|
var address = ParseAndVerify(variableName, typeof(TValue));
|
||||||
if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName));
|
|
||||||
|
|
||||||
var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token);
|
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()
|
public async Task<bool> InitializeAsync()
|
||||||
{
|
{
|
||||||
s7Connector = new Sharp7Connector(plcConnectionSettings, varaibleNameParser) {Logger = Logger};
|
|
||||||
ConnectionState = s7Connector.ConnectionState;
|
|
||||||
|
|
||||||
await s7Connector.InitializeAsync();
|
await s7Connector.InitializeAsync();
|
||||||
|
|
||||||
#pragma warning disable 4014
|
#pragma warning disable 4014
|
||||||
@@ -128,75 +150,29 @@ public class Sharp7Plc : IPlc
|
|||||||
#pragma warning restore 4014
|
#pragma warning restore 4014
|
||||||
|
|
||||||
RunNotifications(s7Connector, MultiVarRequestCycleTime)
|
RunNotifications(s7Connector, MultiVarRequestCycleTime)
|
||||||
.AddDisposableTo(Disposables);
|
.AddDisposableTo(disposables);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetValue<TValue>(string variableName, TValue value, CancellationToken token)
|
public async Task SetValue<TValue>(string variableName, TValue value, CancellationToken token)
|
||||||
{
|
{
|
||||||
var address = varaibleNameParser.Parse(variableName);
|
var address = ParseAndVerify(variableName, typeof(TValue));
|
||||||
if (address == null) throw new ArgumentException("Input variable name is not valid", "variableName");
|
|
||||||
|
|
||||||
if (typeof(TValue) == typeof(bool))
|
if (typeof(TValue) == typeof(bool))
|
||||||
{
|
{
|
||||||
await s7Connector.WriteBit(address.Operand, address.Start, address.Bit, (bool) (object) value, address.DbNr, token);
|
// Special handling for bools, which are written on a by-bit basis. Writing a complete byte would
|
||||||
}
|
// overwrite other bits within this byte.
|
||||||
else if (typeof(TValue) == typeof(int) || typeof(TValue) == typeof(short))
|
|
||||||
{
|
|
||||||
byte[] bytes;
|
|
||||||
if (address.Length == 4)
|
|
||||||
bytes = BitConverter.GetBytes((int) (object) value);
|
|
||||||
else
|
|
||||||
bytes = BitConverter.GetBytes((short) (object) value);
|
|
||||||
|
|
||||||
Array.Reverse(bytes);
|
await s7Connector.WriteBit(address.Operand, address.Start, address.Bit!.Value, (bool) (object) value, address.DbNr, token);
|
||||||
|
|
||||||
await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token);
|
|
||||||
}
|
|
||||||
else if (typeof(TValue) == typeof(byte) || typeof(TValue) == typeof(char))
|
|
||||||
{
|
|
||||||
var bytes = new[] {Convert.ToByte(value)};
|
|
||||||
await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token);
|
|
||||||
}
|
|
||||||
else if (typeof(TValue) == typeof(byte[]))
|
|
||||||
{
|
|
||||||
await s7Connector.WriteBytes(address.Operand, address.Start, (byte[]) (object) value, address.DbNr, token);
|
|
||||||
}
|
|
||||||
else if (typeof(TValue) == typeof(float))
|
|
||||||
{
|
|
||||||
var buffer = new byte[sizeof(float)];
|
|
||||||
buffer.SetRealAt(0, (float) (object) value);
|
|
||||||
await s7Connector.WriteBytes(address.Operand, address.Start, buffer, address.DbNr, token);
|
|
||||||
}
|
|
||||||
else if (typeof(TValue) == typeof(string))
|
|
||||||
{
|
|
||||||
var stringValue = value as string;
|
|
||||||
if (stringValue == null) throw new ArgumentException("Value must be of type string", "value");
|
|
||||||
|
|
||||||
var bytes = Encoding.ASCII.GetBytes(stringValue);
|
|
||||||
Array.Resize(ref bytes, address.Length);
|
|
||||||
|
|
||||||
if (address.Type == DbType.String)
|
|
||||||
{
|
|
||||||
var bytesWritten = await s7Connector.WriteBytes(address.Operand, address.Start, new[] {(byte) address.Length, (byte) bytes.Length}, address.DbNr, token);
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
if (bytesWritten == 2)
|
|
||||||
{
|
|
||||||
var stringStartAddress = (ushort) (address.Start + 2);
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
await s7Connector.WriteBytes(address.Operand, stringStartAddress, bytes, address.DbNr, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token);
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
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)
|
if (disposing)
|
||||||
{
|
{
|
||||||
Disposables.Dispose();
|
disposables.Dispose();
|
||||||
|
|
||||||
if (s7Connector != null)
|
if (s7Connector != null)
|
||||||
{
|
{
|
||||||
@@ -254,7 +230,8 @@ public class Sharp7Plc : IPlc
|
|||||||
var min = performanceCoutner.Min();
|
var min = performanceCoutner.Min();
|
||||||
var max = performanceCoutner.Max();
|
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(),
|
multiVariableSubscriptions.ExistingKeys.Count(),
|
||||||
MultiVarRequestMaxItems);
|
MultiVarRequestMaxItems);
|
||||||
performanceCoutner.Clear();
|
performanceCoutner.Clear();
|
||||||
|
|||||||
Reference in New Issue
Block a user