Merge branch 'feature/performanceImprovements' into prerelease

This commit is contained in:
Peter Butzhammer
2024-02-09 13:17:19 +01:00
26 changed files with 296 additions and 267 deletions

View File

@@ -50,21 +50,37 @@ using (var disposables = new CompositeDisposable())
the best way to test your PLC application is running your [SoftPLC](https://github.com/fbarresi/softplc) locally. the best way to test your PLC application is running your [SoftPLC](https://github.com/fbarresi/softplc) locally.
## S7 Addressing rules ## Addressing rules
Sharp7Reactive uses a syntax for identifying addresses similar to official siemens syntax. Sharp7Reactive uses a syntax for identifying addresses similar to official siemens syntax.
Every address has the form (case unsensible) `DB<number>.<TYPE><Start>.<Length/Position>`. Every address has the form (case unsensitive) `DB<number>.<TYPE><Start>.<Length/Position>`.
<br/>i.e.: `DB42.DBX0.7` => (means) Datablock 42, bit (DBX), Start: 0, Position: 7
<br/>or<br/>
`DB42.DBB4.25` => (means) Datablock 42, bytes (DBB), Start: 4, Length: 25.
Following types are supported: | Example | Explaination |
- `DBX` => Bit (bool) | ------------------------------------ | ----------------------------------------------------------------- |
- `DBB` => byte or byte[] | `DB42.Int4` or<br> `DB42.DBD4` | Datablock 42, 16 bit integer from bytes 4 to 5 (zero based index) |
- `INT` | `DB42.Bit0.7` or<br>`DB42.DBX0.7` | Datablock 42, bit from byte 0, position 7 |
- `DINT` | `DB42.Byte4.25` or<br>`DB42.DBB4.25` | Datablock 42, 25 bytes from byte 4 to 29 (zero based index) |
- `DUL` => LINT
- `D` => REAL Here is a table of supported data types:
|.Net Type|Identifier |Description |Length or bit |Example |Example remark |
|---------|-----------------------------|----------------------------------------------|----------------------------------------|-------------------|--------------------------|
|bool |bit, dbx |Bit as boolean value |Bit index (0 .. 7) |`Db200.Bit2.2` |Reads bit 3 |
|byte |byte, dbb, b* |8 bit unsigned integer | |`Db200.Byte4` | |
|byte[] |byte, dbb, b* |Array of bytes |Array length in bytes |`Db200.Byte4.16` | |
|short |int, dbw, w* |16 bit signed integer | |`Db200.Int4` | |
|ushort |uint |16 bit unsigned integer | |`Db200.UInt4` | |
|int |dint, dbd |32 bit signed integer | |`Db200.DInt4` | |
|uint |udint |32 bit unsigned integer | |`Db200.UDInt4` | |
|long |lint |64 bit signed integer | |`Db200.LInt4` | |
|ulong |ulint, dul*, dulint*, dulong*|64 bit unsigned integer | |`Db200.ULInt4` | |
|float |real, d* |32 bit float | |`Db200.Real4` | |
|double |lreal |64 bit float | |`Db200.LReal4` | |
|string |string, s* |ASCII text string with string size |String length in bytes (1 .. 254) |`Db200.String4.16` |Uses 18 bytes = 16 + 2 |
|string |wstring |UTF-16 Big Endian text string with string size|String length in characters (1 .. 16382)|`Db200.WString4.16`|Uses 36 bytes = 16 * 2 + 4|
|string |byte[] |ASCII string as byte array |String length in bytes |`Db200.Byte4.16` |Uses 16 bytes |
> Identifiers marked with * are kept for compatability reasons and might be removed in the future.
## Would you like to contribute? ## Would you like to contribute?

View File

@@ -1,91 +0,0 @@
using DeepEqual.Syntax;
using NUnit.Framework;
using Sharp7.Rx.Enums;
using Shouldly;
namespace Sharp7.Rx.Tests;
[TestFixture]
internal class S7VariableNameParserTests
{
[TestCaseSource(nameof(ValidTestCases))]
public void Run(TestCase tc)
{
var parser = new S7VariableNameParser();
var resp = parser.Parse(tc.Input);
resp.ShouldDeepEqual(tc.Expected);
}
[TestCase("DB506.Bit216", TestName = "Bit without Bit")]
[TestCase("DB506.Bit216.8", TestName = "Bit to high")]
[TestCase("DB506.String216", TestName = "String without Length")]
[TestCase("DB506.WString216", TestName = "WString without Length")]
[TestCase("DB506.Int216.1", TestName = "Int with Length")]
[TestCase("DB506.UInt216.1", TestName = "UInt with Length")]
[TestCase("DB506.DInt216.1", TestName = "DInt with Length")]
[TestCase("DB506.UDInt216.1", TestName = "UDInt with Length")]
[TestCase("DB506.LInt216.1", TestName = "LInt with Length")]
[TestCase("DB506.ULInt216.1", TestName = "ULInt with Length")]
[TestCase("DB506.Real216.1", TestName = "LReal with Length")]
[TestCase("DB506.LReal216.1", TestName = "LReal with Length")]
[TestCase("DB506.xx216", TestName = "Invalid type")]
[TestCase("DB506.216", TestName = "No type")]
[TestCase("DB506.Int216.", TestName = "Trailing dot")]
[TestCase("x506.Int216", TestName = "Wrong type")]
[TestCase("506.Int216", TestName = "No type")]
[TestCase("", TestName = "empty")]
[TestCase(" ", TestName = "space")]
[TestCase(" DB506.Int216", TestName = "leading space")]
[TestCase("DB506.Int216 ", TestName = "trailing space")]
[TestCase("DB.Int216 ", TestName = "No db")]
[TestCase("DB5061234.Int216.1", TestName = "DB too large")]
public void Invalid(string? input)
{
var parser = new S7VariableNameParser();
Should.Throw<InvalidS7AddressException>(() => parser.Parse(input));
}
public static IEnumerable<TestCase> ValidTestCases()
{
yield return new TestCase("DB506.Bit216.2", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Bit = 2, Type = DbType.Bit});
yield return new TestCase("DB506.String216.10", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 10, Type = DbType.String});
yield return new TestCase("DB506.WString216.10", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 10, Type = DbType.WString});
yield return new TestCase("DB506.Byte216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Type = DbType.Byte});
yield return new TestCase("DB506.Byte216.100", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 100, Type = DbType.Byte});
yield return new TestCase("DB506.Int216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int});
yield return new TestCase("DB506.UInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.UInt});
yield return new TestCase("DB506.DInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.DInt});
yield return new TestCase("DB506.UDInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.UDInt});
yield return new TestCase("DB506.LInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.LInt});
yield return new TestCase("DB506.ULInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt});
yield return new TestCase("DB506.Real216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.Single});
yield return new TestCase("DB506.LReal216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.Double});
// Legacy
yield return new TestCase("DB13.DBX3.1", new S7VariableAddress {Operand = Operand.Db, DbNr = 13, Start = 3, Length = 1, Bit = 1, Type = DbType.Bit});
yield return new TestCase("Db403.X5.2", new S7VariableAddress {Operand = Operand.Db, DbNr = 403, Start = 5, Length = 1, Bit = 2, Type = DbType.Bit});
yield return new TestCase("DB55DBX23.6", new S7VariableAddress {Operand = Operand.Db, DbNr = 55, Start = 23, Length = 1, Bit = 6, Type = DbType.Bit});
yield return new TestCase("DB1.S255.20", new S7VariableAddress {Operand = Operand.Db, DbNr = 1, Start = 255, Length = 20, Type = DbType.String});
yield return new TestCase("DB5.String887.20", new S7VariableAddress {Operand = Operand.Db, DbNr = 5, Start = 887, Length = 20, Type = DbType.String});
yield return new TestCase("DB506.B216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Type = DbType.Byte});
yield return new TestCase("DB506.DBB216.5", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 5, Type = DbType.Byte});
yield return new TestCase("DB506.D216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.Single});
yield return new TestCase("DB506.DINT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.DInt});
yield return new TestCase("DB506.INT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int});
yield return new TestCase("DB506.DBW216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int});
yield return new TestCase("DB506.DUL216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt});
yield return new TestCase("DB506.DULINT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt});
yield return new TestCase("DB506.DULONG216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt});
}
public record TestCase(string Input, S7VariableAddress Expected)
{
public override string ToString() => Input;
}
}

View File

@@ -1,7 +1,7 @@
using NUnit.Framework; using NUnit.Framework;
using Shouldly; using Shouldly;
namespace Sharp7.Rx.Tests.S7ValueConverterTests; namespace Sharp7.Rx.Tests.ValueConverterTests;
[TestFixture] [TestFixture]
internal class ConvertBothWays : ConverterTestBase internal class ConvertBothWays : ConverterTestBase

View File

@@ -1,15 +1,15 @@
using System.Reflection; using System.Reflection;
using Sharp7.Rx.Interfaces; using Sharp7.Rx.Interfaces;
namespace Sharp7.Rx.Tests.S7ValueConverterTests; namespace Sharp7.Rx.Tests.ValueConverterTests;
internal abstract class ConverterTestBase internal abstract class ConverterTestBase
{ {
protected static readonly IS7VariableNameParser Parser = new S7VariableNameParser(); protected static readonly IVariableNameParser Parser = new VariableNameParser();
public static MethodInfo CreateReadMethod(ConverterTestCase tc) public static MethodInfo CreateReadMethod(ConverterTestCase tc)
{ {
var convertMi = typeof(S7ValueConverter).GetMethod(nameof(S7ValueConverter.ReadFromBuffer)); var convertMi = typeof(ValueConverter).GetMethod(nameof(ValueConverter.ReadFromBuffer));
var convert = convertMi!.MakeGenericMethod(tc.Value.GetType()); var convert = convertMi!.MakeGenericMethod(tc.Value.GetType());
return convert; return convert;
} }
@@ -69,14 +69,14 @@ internal abstract class ConverterTestBase
/// This helper method exists, since I could not manage to invoke a generic method /// This helper method exists, since I could not manage to invoke a generic method
/// accepring a Span&lt;T&gt; as parameter. /// accepring a Span&lt;T&gt; as parameter.
/// </summary> /// </summary>
public static void WriteToBuffer<TValue>(byte[] buffer, TValue value, S7VariableAddress address) public static void WriteToBuffer<TValue>(byte[] buffer, TValue value, VariableAddress address)
{ {
S7ValueConverter.WriteToBuffer(buffer, value, address); ValueConverter.WriteToBuffer(buffer, value, address);
} }
public record ConverterTestCase(object Value, string Address, byte[] Data) public record ConverterTestCase(object Value, string Address, byte[] Data)
{ {
public S7VariableAddress VariableAddress => Parser.Parse(Address); public VariableAddress VariableAddress => Parser.Parse(Address);
public override string ToString() => $"{Value.GetType().Name}, {Address}: {Value}"; public override string ToString() => $"{Value.GetType().Name}, {Address}: {Value}";
} }

View File

@@ -1,7 +1,7 @@
using NUnit.Framework; using NUnit.Framework;
using Shouldly; using Shouldly;
namespace Sharp7.Rx.Tests.S7ValueConverterTests; namespace Sharp7.Rx.Tests.ValueConverterTests;
[TestFixture] [TestFixture]
internal class ReadFromBuffer : ConverterTestBase internal class ReadFromBuffer : ConverterTestBase
@@ -34,7 +34,7 @@ internal class ReadFromBuffer : ConverterTestBase
var variableAddress = Parser.Parse(address); var variableAddress = Parser.Parse(address);
//Act //Act
Should.Throw<UnsupportedS7TypeException>(() => S7ValueConverter.ReadFromBuffer<T>(data, variableAddress)); Should.Throw<UnsupportedS7TypeException>(() => ValueConverter.ReadFromBuffer<T>(data, variableAddress));
} }
[TestCase(123, "DB12.DINT3", new byte[] {0x01, 0x02, 0x03})] [TestCase(123, "DB12.DINT3", new byte[] {0x01, 0x02, 0x03})]
@@ -46,6 +46,6 @@ internal class ReadFromBuffer : ConverterTestBase
var variableAddress = Parser.Parse(address); var variableAddress = Parser.Parse(address);
//Act //Act
Should.Throw<ArgumentException>(() => S7ValueConverter.ReadFromBuffer<T>(data, variableAddress)); Should.Throw<ArgumentException>(() => ValueConverter.ReadFromBuffer<T>(data, variableAddress));
} }
} }

View File

@@ -1,7 +1,7 @@
using NUnit.Framework; using NUnit.Framework;
using Shouldly; using Shouldly;
namespace Sharp7.Rx.Tests.S7ValueConverterTests; namespace Sharp7.Rx.Tests.ValueConverterTests;
[TestFixture] [TestFixture]
internal class WriteToBuffer : ConverterTestBase internal class WriteToBuffer : ConverterTestBase
@@ -37,7 +37,7 @@ internal class WriteToBuffer : ConverterTestBase
var buffer = new byte[bufferSize]; var buffer = new byte[bufferSize];
//Act //Act
Should.Throw<ArgumentException>(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); Should.Throw<ArgumentException>(() => ValueConverter.WriteToBuffer(buffer, input, variableAddress));
} }
[TestCase((char) 18, "DB0.DBB0")] [TestCase((char) 18, "DB0.DBB0")]
@@ -48,6 +48,6 @@ internal class WriteToBuffer : ConverterTestBase
var buffer = new byte[variableAddress.BufferLength]; var buffer = new byte[variableAddress.BufferLength];
//Act //Act
Should.Throw<UnsupportedS7TypeException>(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); Should.Throw<UnsupportedS7TypeException>(() => ValueConverter.WriteToBuffer(buffer, input, variableAddress));
} }
} }

View File

@@ -1,15 +1,15 @@
using NUnit.Framework; using NUnit.Framework;
using Sharp7.Rx.Extensions; using Sharp7.Rx.Extensions;
using Sharp7.Rx.Interfaces; using Sharp7.Rx.Interfaces;
using Sharp7.Rx.Tests.S7ValueConverterTests; using Sharp7.Rx.Tests.ValueConverterTests;
using Shouldly; using Shouldly;
namespace Sharp7.Rx.Tests.S7VariableAddressTests; namespace Sharp7.Rx.Tests.VariableAddressTests;
[TestFixture] [TestFixture]
public class MatchesType public class MatchesType
{ {
static readonly IS7VariableNameParser parser = new S7VariableNameParser(); static readonly IVariableNameParser parser = new VariableNameParser();
private static readonly IReadOnlyList<Type> typeList = new[] private static readonly IReadOnlyList<Type> typeList = new[]
{ {

View File

@@ -0,0 +1,89 @@
using DeepEqual.Syntax;
using NUnit.Framework;
using Sharp7.Rx.Enums;
using Shouldly;
namespace Sharp7.Rx.Tests;
[TestFixture]
internal class VariableNameParserTests
{
[TestCaseSource(nameof(ValidTestCases))]
public void Run(TestCase tc)
{
var parser = new VariableNameParser();
var resp = parser.Parse(tc.Input);
resp.ShouldDeepEqual(tc.Expected);
}
[TestCase("DB506.Bit216", TestName = "Bit without Bit")]
[TestCase("DB506.Bit216.8", TestName = "Bit to high")]
[TestCase("DB506.String216", TestName = "String without Length")]
[TestCase("DB506.WString216", TestName = "WString without Length")]
[TestCase("DB506.Int216.1", TestName = "Int with Length")]
[TestCase("DB506.UInt216.1", TestName = "UInt with Length")]
[TestCase("DB506.DInt216.1", TestName = "DInt with Length")]
[TestCase("DB506.UDInt216.1", TestName = "UDInt with Length")]
[TestCase("DB506.LInt216.1", TestName = "LInt with Length")]
[TestCase("DB506.ULInt216.1", TestName = "ULInt with Length")]
[TestCase("DB506.Real216.1", TestName = "LReal with Length")]
[TestCase("DB506.LReal216.1", TestName = "LReal with Length")]
[TestCase("DB506.xx216", TestName = "Invalid type")]
[TestCase("DB506.216", TestName = "No type")]
[TestCase("DB506.Int216.", TestName = "Trailing dot")]
[TestCase("x506.Int216", TestName = "Wrong type")]
[TestCase("506.Int216", TestName = "No type")]
[TestCase("", TestName = "empty")]
[TestCase(" ", TestName = "space")]
[TestCase(" DB506.Int216", TestName = "leading space")]
[TestCase("DB506.Int216 ", TestName = "trailing space")]
[TestCase("DB.Int216 ", TestName = "No db")]
[TestCase("DB5061234.Int216.1", TestName = "DB too large")]
public void Invalid(string? input)
{
var parser = new VariableNameParser();
Should.Throw<InvalidS7AddressException>(() => parser.Parse(input));
}
public static IEnumerable<TestCase> ValidTestCases()
{
yield return new TestCase("DB506.Bit216.2", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Bit, Start: 216, Length: 1, Bit: 2));
yield return new TestCase("DB506.String216.10", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.String, Start: 216, Length: 10));
yield return new TestCase("DB506.WString216.10", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.WString, Start: 216, Length: 10));
yield return new TestCase("DB506.Byte216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Byte, Start: 216, Length: 1));
yield return new TestCase("DB506.Byte216.100", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Byte, Start: 216, Length: 100));
yield return new TestCase("DB506.Int216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Int, Start: 216, Length: 2));
yield return new TestCase("DB506.UInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.UInt, Start: 216, Length: 2));
yield return new TestCase("DB506.DInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.DInt, Start: 216, Length: 4));
yield return new TestCase("DB506.UDInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.UDInt, Start: 216, Length: 4));
yield return new TestCase("DB506.LInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.LInt, Start: 216, Length: 8));
yield return new TestCase("DB506.ULInt216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.ULInt, Start: 216, Length: 8));
yield return new TestCase("DB506.Real216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Single, Start: 216, Length: 4));
yield return new TestCase("DB506.LReal216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Double, Start: 216, Length: 8));
// Legacy
yield return new TestCase("DB13.DBX3.1", new VariableAddress(Operand: Operand.Db, DbNo: 13, Type: DbType.Bit, Start: 3, Length: 1, Bit: 1));
yield return new TestCase("Db403.X5.2", new VariableAddress(Operand: Operand.Db, DbNo: 403, Type: DbType.Bit, Start: 5, Length: 1, Bit: 2));
yield return new TestCase("DB55DBX23.6", new VariableAddress(Operand: Operand.Db, DbNo: 55, Type: DbType.Bit, Start: 23, Length: 1, Bit: 6));
yield return new TestCase("DB1.S255.20", new VariableAddress(Operand: Operand.Db, DbNo: 1, Type: DbType.String, Start: 255, Length: 20));
yield return new TestCase("DB5.String887.20", new VariableAddress(Operand: Operand.Db, DbNo: 5, Type: DbType.String, Start: 887, Length: 20));
yield return new TestCase("DB506.B216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Byte, Start: 216, Length: 1));
yield return new TestCase("DB506.DBB216.5", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Byte, Start: 216, Length: 5));
yield return new TestCase("DB506.D216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Single, Start: 216, Length: 4));
yield return new TestCase("DB506.DINT216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.DInt, Start: 216, Length: 4));
yield return new TestCase("DB506.INT216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Int, Start: 216, Length: 2));
yield return new TestCase("DB506.DBW216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.Int, Start: 216, Length: 2));
yield return new TestCase("DB506.DUL216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.ULInt, Start: 216, Length: 8));
yield return new TestCase("DB506.DULINT216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.ULInt, Start: 216, Length: 8));
yield return new TestCase("DB506.DULONG216", new VariableAddress(Operand: Operand.Db, DbNo: 506, Type: DbType.ULInt, Start: 216, Length: 8));
}
public record TestCase(string Input, VariableAddress Expected)
{
public override string ToString() => Input;
}
}

View File

@@ -53,10 +53,13 @@ internal class ConcurrentSubjectDictionary<TKey, TValue> : IDisposable
{ {
lock (dictionaryLock) lock (dictionaryLock)
{ {
var subject = dictionary.AddOrUpdate(key, k => new SubjectWithRefCounter {Counter = 1, Subject = CreateSubject(k)}, (key1, counter) => var subject = dictionary.AddOrUpdate(
key,
k => new SubjectWithRefCounter(CreateSubject(k)),
(_, subjectWithRefCounter) =>
{ {
counter.Counter = counter.Counter + 1; subjectWithRefCounter.IncreaseCount();
return counter; return subjectWithRefCounter;
}); });
return new DisposableItem<TValue>(subject.Subject.AsObservable(), () => RemoveIfNoLongerInUse(key)); return new DisposableItem<TValue>(subject.Subject.AsObservable(), () => RemoveIfNoLongerInUse(key));
@@ -65,8 +68,7 @@ internal class ConcurrentSubjectDictionary<TKey, TValue> : IDisposable
public bool TryGetObserver(TKey key, out IObserver<TValue> subject) public bool TryGetObserver(TKey key, out IObserver<TValue> subject)
{ {
SubjectWithRefCounter subjectWithRefCount; if (dictionary.TryGetValue(key, out var subjectWithRefCount))
if (dictionary.TryGetValue(key, out subjectWithRefCount))
{ {
subject = subjectWithRefCount.Subject.AsObserver(); subject = subjectWithRefCount.Subject.AsObserver();
return true; return true;
@@ -101,15 +103,9 @@ internal class ConcurrentSubjectDictionary<TKey, TValue> : IDisposable
private void RemoveIfNoLongerInUse(TKey variableName) private void RemoveIfNoLongerInUse(TKey variableName)
{ {
lock (dictionaryLock) lock (dictionaryLock)
{ if (dictionary.TryGetValue(variableName, out var subjectWithRefCount))
SubjectWithRefCounter subjectWithRefCount; if (subjectWithRefCount.DecreaseCount() < 1)
if (dictionary.TryGetValue(variableName, out subjectWithRefCount)) dictionary.TryRemove(variableName, out _);
{
if (subjectWithRefCount.Counter == 1)
dictionary.TryRemove(variableName, out subjectWithRefCount);
else subjectWithRefCount.Counter--;
}
}
} }
~ConcurrentSubjectDictionary() ~ConcurrentSubjectDictionary()
@@ -119,7 +115,16 @@ internal class ConcurrentSubjectDictionary<TKey, TValue> : IDisposable
class SubjectWithRefCounter class SubjectWithRefCounter
{ {
public int Counter { get; set; } private int counter = 1;
public ISubject<TValue> Subject { get; set; }
public SubjectWithRefCounter(ISubject<TValue> subject)
{
Subject = subject;
}
public ISubject<TValue> Subject { get; }
public int DecreaseCount() => Interlocked.Decrement(ref counter);
public int IncreaseCount() => Interlocked.Increment(ref counter);
} }
} }

View File

@@ -3,16 +3,16 @@ using Sharp7.Rx.Interfaces;
namespace Sharp7.Rx; namespace Sharp7.Rx;
internal class CacheVariableNameParser : IS7VariableNameParser internal class CacheVariableNameParser : IVariableNameParser
{ {
private static readonly ConcurrentDictionary<string, S7VariableAddress> addressCache = new ConcurrentDictionary<string, S7VariableAddress>(StringComparer.OrdinalIgnoreCase); private static readonly ConcurrentDictionary<string, VariableAddress> addressCache = new ConcurrentDictionary<string, VariableAddress>(StringComparer.OrdinalIgnoreCase);
private readonly IS7VariableNameParser inner; private readonly IVariableNameParser inner;
public CacheVariableNameParser(IS7VariableNameParser inner) public CacheVariableNameParser(IVariableNameParser inner)
{ {
this.inner = inner; this.inner = inner;
} }
public S7VariableAddress Parse(string input) => addressCache.GetOrAdd(input, inner.Parse); public VariableAddress Parse(string input) => addressCache.GetOrAdd(input, inner.Parse);
} }

View File

@@ -5,5 +5,6 @@ public enum ConnectionState
Initial, Initial,
Connected, Connected,
DisconnectedByUser, DisconnectedByUser,
ConnectionLost ConnectionLost,
Disposed
} }

View File

@@ -31,13 +31,13 @@ public class S7CommunicationException : S7Exception
public class DataTypeMissmatchException : S7Exception public class DataTypeMissmatchException : S7Exception
{ {
internal DataTypeMissmatchException(string message, Type type, S7VariableAddress address) : base(message) internal DataTypeMissmatchException(string message, Type type, VariableAddress address) : base(message)
{ {
Type = type; Type = type;
Address = address.ToString(); Address = address.ToString();
} }
internal DataTypeMissmatchException(string message, Exception innerException, Type type, S7VariableAddress address) : base(message, innerException) internal DataTypeMissmatchException(string message, Exception innerException, Type type, VariableAddress address) : base(message, innerException)
{ {
Type = type; Type = type;
Address = address.ToString(); Address = address.ToString();
@@ -50,13 +50,13 @@ public class DataTypeMissmatchException : S7Exception
public class UnsupportedS7TypeException : S7Exception public class UnsupportedS7TypeException : S7Exception
{ {
internal UnsupportedS7TypeException(string message, Type type, S7VariableAddress address) : base(message) internal UnsupportedS7TypeException(string message, Type type, VariableAddress address) : base(message)
{ {
Type = type; Type = type;
Address = address.ToString(); Address = address.ToString();
} }
internal UnsupportedS7TypeException(string message, Exception innerException, Type type, S7VariableAddress address) : base(message, innerException) internal UnsupportedS7TypeException(string message, Exception innerException, Type type, VariableAddress address) : base(message, innerException)
{ {
Type = type; Type = type;
Address = address.ToString(); Address = address.ToString();

View File

@@ -13,5 +13,4 @@ internal static class OperandExtensions
Operand.Db => S7Area.DB, Operand.Db => S7Area.DB,
_ => throw new ArgumentOutOfRangeException(nameof(operand), operand, null) _ => throw new ArgumentOutOfRangeException(nameof(operand), operand, null)
}; };
} }

View File

@@ -9,7 +9,8 @@ namespace Sharp7.Rx.Extensions;
public static class PlcExtensions public static class PlcExtensions
{ {
public static IObservable<TReturn> CreateDatatransferWithHandshake<TReturn>(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func<IPlc, Task<TReturn>> readData, bool initialTransfer) public static IObservable<TReturn> CreateDatatransferWithHandshake<TReturn>(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func<IPlc, Task<TReturn>> readData,
bool initialTransfer)
{ {
return Observable.Create<TReturn>(async observer => return Observable.Create<TReturn>(async observer =>
{ {

View File

@@ -2,9 +2,9 @@
namespace Sharp7.Rx.Extensions; namespace Sharp7.Rx.Extensions;
internal static class S7VariableAddressExtensions internal static class VariableAddressExtensions
{ {
private static readonly Dictionary<Type, Func<S7VariableAddress, bool>> supportedTypeMap = new() private static readonly Dictionary<Type, Func<VariableAddress, bool>> supportedTypeMap = new()
{ {
{typeof(bool), a => a.Type == DbType.Bit}, {typeof(bool), a => a.Type == DbType.Bit},
{typeof(string), a => a.Type is DbType.String or DbType.WString or DbType.Byte}, {typeof(string), a => a.Type is DbType.String or DbType.WString or DbType.Byte},
@@ -20,6 +20,6 @@ internal static class S7VariableAddressExtensions
{typeof(byte[]), a => a.Type == DbType.Byte}, {typeof(byte[]), a => a.Type == DbType.Byte},
}; };
public static bool MatchesType(this S7VariableAddress address, Type type) => public static bool MatchesType(this VariableAddress address, Type type) =>
supportedTypeMap.TryGetValue(type, out var map) && map(address); supportedTypeMap.TryGetValue(type, out var map) && map(address);
} }

View File

@@ -17,5 +17,5 @@ internal interface IS7Connector : IDisposable
Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNo, CancellationToken token); Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNo, CancellationToken token);
Task WriteBytes(Operand operand, ushort startByteAddress, byte[] data, ushort dbNo, CancellationToken token); Task WriteBytes(Operand operand, ushort startByteAddress, byte[] data, ushort dbNo, CancellationToken token);
Task<Dictionary<string, byte[]>> ExecuteMultiVarRequest(IReadOnlyList<string> variableNames); Task<IReadOnlyDictionary<string, byte[]>> ExecuteMultiVarRequest(IReadOnlyList<string> variableNames);
} }

View File

@@ -1,7 +0,0 @@
#nullable enable
namespace Sharp7.Rx.Interfaces;
internal interface IS7VariableNameParser
{
S7VariableAddress Parse(string input);
}

View File

@@ -0,0 +1,7 @@
#nullable enable
namespace Sharp7.Rx.Interfaces;
internal interface IVariableNameParser
{
VariableAddress Parse(string input);
}

View File

@@ -1,4 +1,6 @@
namespace Sharp7.Rx; #nullable enable
namespace Sharp7.Rx;
public static class S7ErrorCodes public static class S7ErrorCodes
{ {
@@ -12,6 +14,18 @@ public static class S7ErrorCodes
0x900000, // CPU: Address out of range 0x900000, // CPU: Address out of range
}; };
private static readonly IReadOnlyDictionary<int, string> additionalErrorTexts = new Dictionary<int, string>
{
{0xC00000, "This happens when the DB does not exist."},
{0x900000, "This happens when the DB is not long enough."},
{
0x40000, """
This error occurs when the DB is "optimized" or "PUT/GET communication" is not enabled.
See https://snap7.sourceforge.net/snap7_client.html#target_compatibility.
"""
}
};
/// <summary> /// <summary>
/// Some error codes indicate connection lost, in which case, the driver tries to reestablish connection. /// Some error codes indicate connection lost, in which case, the driver tries to reestablish connection.
/// Other error codes indicate a user error, like reading from an unavailable DB or exceeding /// Other error codes indicate a user error, like reading from an unavailable DB or exceeding
@@ -21,4 +35,7 @@ public static class S7ErrorCodes
{ {
return !notDisconnectedErrorCodes.Contains(errorCode); return !notDisconnectedErrorCodes.Contains(errorCode);
} }
public static string? GetAdditionalErrorText(int errorCode) =>
additionalErrorTexts.TryGetValue(errorCode, out var text) ? text : null;
} }

View File

@@ -1,32 +0,0 @@
using JetBrains.Annotations;
using Sharp7.Rx.Enums;
namespace Sharp7.Rx;
[NoReorder]
internal class S7VariableAddress
{
public Operand Operand { get; set; }
public ushort DbNr { get; set; }
public ushort Start { get; set; }
public ushort Length { get; set; }
public byte? Bit { get; set; }
public DbType Type { get; set; }
public ushort BufferLength => Type switch
{
DbType.String => (ushort) (Length + 2),
DbType.WString => (ushort) (Length * 2 + 4),
_ => Length
};
public override string ToString() =>
Type switch
{
DbType.Bit => $"{Operand}{DbNr}.{Type}{Start}.{Bit}",
DbType.String => $"{Operand}{DbNr}.{Type}{Start}.{Length}",
DbType.WString => $"{Operand}{DbNr}.{Type}{Start}.{Length}",
DbType.Byte => Length == 1 ? $"{Operand}{DbNr}.{Type}{Start}" : $"{Operand}{DbNr}.{Type}{Start}.{Length}",
_ => $"{Operand}{DbNr}.{Type}{Start}",
};
}

View File

@@ -20,13 +20,13 @@ internal class Sharp7Connector : IS7Connector
private readonly int port; private readonly int port;
private readonly int rackNr; private readonly int rackNr;
private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new(maxDegreeOfParallelism: 1); private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new(maxDegreeOfParallelism: 1);
private readonly IS7VariableNameParser variableNameParser; private readonly IVariableNameParser variableNameParser;
private bool disposed; private bool disposed;
private S7Client sharp7; private S7Client sharp7;
public Sharp7Connector(PlcConnectionSettings settings, IS7VariableNameParser variableNameParser) public Sharp7Connector(PlcConnectionSettings settings, IVariableNameParser variableNameParser)
{ {
this.variableNameParser = variableNameParser; this.variableNameParser = variableNameParser;
ipAddress = settings.IpAddress; ipAddress = settings.IpAddress;
@@ -86,7 +86,7 @@ internal class Sharp7Connector : IS7Connector
await CloseConnection(); await CloseConnection();
} }
public async Task<Dictionary<string, byte[]>> ExecuteMultiVarRequest(IReadOnlyList<string> variableNames) public async Task<IReadOnlyDictionary<string, byte[]>> ExecuteMultiVarRequest(IReadOnlyList<string> variableNames)
{ {
if (variableNames.IsEmpty()) if (variableNames.IsEmpty())
return new Dictionary<string, byte[]>(); return new Dictionary<string, byte[]>();
@@ -98,17 +98,14 @@ internal class Sharp7Connector : IS7Connector
.Select(x => .Select(x =>
{ {
var buffer = new byte[x.Address.Length]; var buffer = new byte[x.Address.Length];
s7MultiVar.Add(S7Consts.S7AreaDB, S7Consts.S7WLByte, x.Address.DbNr, x.Address.Start, x.Address.Length, ref buffer); s7MultiVar.Add(S7Consts.S7AreaDB, S7Consts.S7WLByte, x.Address.DbNo, x.Address.Start, x.Address.Length, ref buffer);
return new {x.VariableName, Buffer = buffer}; return new {x.VariableName, Buffer = buffer};
}) })
.ToArray(); .ToArray();
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)
{ EnsureSuccessOrThrow(result, $"Error in MultiVar request for variables: {string.Join(",", variableNames)}");
var errorText = EvaluateErrorCode(result);
throw new S7CommunicationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)} ({errorText})", result, errorText);
}
return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer); return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer);
} }
@@ -150,11 +147,7 @@ internal class Sharp7Connector : IS7Connector
await Task.Factory.StartNew(() => sharp7.ReadArea(operand.ToArea(), dbNo, startByteAddress, bytesToRead, S7WordLength.Byte, buffer), token, TaskCreationOptions.None, scheduler); 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) EnsureSuccessOrThrow(result, $"Error reading {operand}{dbNo}:{startByteAddress}->{bytesToRead}");
{
var errorText = EvaluateErrorCode(result);
throw new S7CommunicationException($"Error reading {operand}{dbNo}:{startByteAddress}->{bytesToRead} ({errorText})", result, errorText);
}
return buffer; return buffer;
} }
@@ -170,11 +163,7 @@ internal class Sharp7Connector : IS7Connector
var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dbNo, offsetStart, 1, S7WordLength.Bit, buffer), token, TaskCreationOptions.None, scheduler); 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) EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress} bit {bitAdress}");
{
var errorText = EvaluateErrorCode(result);
throw new S7CommunicationException($"Error writing {operand}{dbNo}:{startByteAddress} bit {bitAdress} ({errorText})", result, errorText);
}
} }
public async Task WriteBytes(Operand operand, ushort startByteAddress, byte[] data, ushort dbNo, CancellationToken token) public async Task WriteBytes(Operand operand, ushort startByteAddress, byte[] data, ushort dbNo, CancellationToken token)
@@ -184,11 +173,7 @@ internal class Sharp7Connector : IS7Connector
var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dbNo, startByteAddress, data.Length, S7WordLength.Byte, data), token, TaskCreationOptions.None, scheduler); 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) EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress}.{data.Length}");
{
var errorText = EvaluateErrorCode(result);
throw new S7CommunicationException($"Error writing {operand}{dbNo}:{startByteAddress}.{data.Length} ({errorText})", result, errorText);
}
} }
@@ -206,6 +191,7 @@ internal class Sharp7Connector : IS7Connector
sharp7 = null; sharp7 = null;
} }
connectionStateSubject?.OnNext(Enums.ConnectionState.Disposed);
connectionStateSubject?.OnCompleted(); connectionStateSubject?.OnCompleted();
connectionStateSubject?.Dispose(); connectionStateSubject?.Dispose();
} }
@@ -234,6 +220,20 @@ internal class Sharp7Connector : IS7Connector
throw new InvalidOperationException("Plc is not connected"); throw new InvalidOperationException("Plc is not connected");
} }
private void EnsureSuccessOrThrow(int result, string message)
{
if (result == 0) return;
var errorText = EvaluateErrorCode(result);
var completeMessage = $"{message}: {errorText}";
var additionalErrorText = S7ErrorCodes.GetAdditionalErrorText(result);
if (additionalErrorText != null)
completeMessage += Environment.NewLine + additionalErrorText;
throw new S7CommunicationException(completeMessage, result, errorText);
}
private string EvaluateErrorCode(int errorCode) private string EvaluateErrorCode(int errorCode)
{ {
if (errorCode == 0) if (errorCode == 0)
@@ -242,8 +242,8 @@ internal class Sharp7Connector : IS7Connector
if (sharp7 == null) if (sharp7 == null)
throw new InvalidOperationException("S7 driver is not initialized."); throw new InvalidOperationException("S7 driver is not initialized.");
var errorText = sharp7.ErrorText(errorCode); var errorText = $"0x{errorCode:X}, {sharp7.ErrorText(errorCode)}";
Logger?.LogError($"Error Code {errorCode} {errorText}"); Logger?.LogError($"S7 Error {errorText}");
if (S7ErrorCodes.AssumeConnectionLost(errorCode)) if (S7ErrorCodes.AssumeConnectionLost(errorCode))
SetConnectionLostState(); SetConnectionLostState();

View File

@@ -17,7 +17,7 @@ public class Sharp7Plc : IPlc
private readonly ConcurrentSubjectDictionary<string, byte[]> multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase); private readonly ConcurrentSubjectDictionary<string, byte[]> multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase);
private readonly List<long> performanceCoutner = new(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 CacheVariableNameParser varaibleNameParser = new CacheVariableNameParser(new VariableNameParser());
private bool disposed; private bool disposed;
private Sharp7Connector s7Connector; private Sharp7Connector s7Connector;
@@ -89,7 +89,7 @@ public class Sharp7Plc : IPlc
Observable.FromAsync(() => GetValue<TValue>(variableName)) Observable.FromAsync(() => GetValue<TValue>(variableName))
.Concat( .Concat(
disposeableContainer.Observable disposeableContainer.Observable
.Select(bytes => S7ValueConverter.ReadFromBuffer<TValue>(bytes, address)) .Select(bytes => ValueConverter.ReadFromBuffer<TValue>(bytes, address))
); );
if (transmissionMode == TransmissionMode.OnChange) if (transmissionMode == TransmissionMode.OnChange)
@@ -102,15 +102,6 @@ public class Sharp7Plc : IPlc
}); });
} }
private S7VariableAddress ParseAndVerify(string variableName, Type type)
{
var address = varaibleNameParser.Parse(variableName);
if (!address.MatchesType(type))
throw new DataTypeMissmatchException($"Address \"{variableName}\" does not match type {type}.", type, address);
return address;
}
public Task<TValue> GetValue<TValue>(string variableName) public Task<TValue> GetValue<TValue>(string variableName)
{ {
return GetValue<TValue>(variableName, CancellationToken.None); return GetValue<TValue>(variableName, CancellationToken.None);
@@ -127,8 +118,8 @@ public class Sharp7Plc : IPlc
{ {
var address = ParseAndVerify(variableName, typeof(TValue)); var address = ParseAndVerify(variableName, typeof(TValue));
var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.BufferLength, address.DbNo, token);
return S7ValueConverter.ReadFromBuffer<TValue>(data, address); return ValueConverter.ReadFromBuffer<TValue>(data, address);
} }
public async Task<bool> InitializeAsync() public async Task<bool> InitializeAsync()
@@ -164,15 +155,15 @@ public class Sharp7Plc : IPlc
// Special handling for bools, which are written on a by-bit basis. Writing a complete byte would // Special handling for bools, which are written on a by-bit basis. Writing a complete byte would
// overwrite other bits within this byte. // overwrite other bits within this byte.
await s7Connector.WriteBit(address.Operand, address.Start, address.Bit!.Value, (bool) (object) value, address.DbNr, token); await s7Connector.WriteBit(address.Operand, address.Start, address.Bit!.Value, (bool) (object) value, address.DbNo, token);
} }
else else
{ {
// TODO: Use ArrayPool.Rent() once we drop Framwework support // TODO: Use ArrayPool.Rent() once we drop Framwework support
var bytes = new byte[address.BufferLength]; var bytes = new byte[address.BufferLength];
S7ValueConverter.WriteToBuffer(bytes, value, address); ValueConverter.WriteToBuffer(bytes, value, address);
await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNo, token);
} }
} }
@@ -222,6 +213,15 @@ public class Sharp7Plc : IPlc
return Unit.Default; return Unit.Default;
} }
private VariableAddress ParseAndVerify(string variableName, Type type)
{
var address = varaibleNameParser.Parse(variableName);
if (!address.MatchesType(type))
throw new DataTypeMissmatchException($"Address \"{variableName}\" does not match type {type}.", type, address);
return address;
}
private void PrintAndResetPerformanceStatistik() private void PrintAndResetPerformanceStatistik()
{ {
if (performanceCoutner.Count == performanceCoutner.Capacity) if (performanceCoutner.Count == performanceCoutner.Capacity)

View File

@@ -5,7 +5,7 @@ using Sharp7.Rx.Enums;
namespace Sharp7.Rx; namespace Sharp7.Rx;
internal static class S7ValueConverter internal static class ValueConverter
{ {
private static readonly Dictionary<Type, WriteFunc> writeFunctions = new() private static readonly Dictionary<Type, WriteFunc> writeFunctions = new()
{ {
@@ -175,7 +175,7 @@ internal static class S7ValueConverter
}, },
}; };
public static TValue ReadFromBuffer<TValue>(byte[] buffer, S7VariableAddress address) public static TValue ReadFromBuffer<TValue>(byte[] buffer, VariableAddress address)
{ {
// Todo: Change to Span<byte> when switched to newer .net // Todo: Change to Span<byte> when switched to newer .net
@@ -191,7 +191,7 @@ internal static class S7ValueConverter
return (TValue) result; return (TValue) result;
} }
public static void WriteToBuffer<TValue>(Span<byte> buffer, TValue value, S7VariableAddress address) public static void WriteToBuffer<TValue>(Span<byte> buffer, TValue value, VariableAddress address)
{ {
if (buffer.Length < address.BufferLength) if (buffer.Length < address.BufferLength)
throw new ArgumentException($"Buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer)); throw new ArgumentException($"Buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer));
@@ -204,7 +204,7 @@ internal static class S7ValueConverter
writeFunc(buffer, address, value); writeFunc(buffer, address, value);
} }
delegate object ReadFunc(byte[] data, S7VariableAddress address); delegate object ReadFunc(byte[] data, VariableAddress address);
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Explicit)]
private struct UInt32SingleMap private struct UInt32SingleMap
@@ -220,5 +220,5 @@ internal static class S7ValueConverter
[FieldOffset(0)] public double Double; [FieldOffset(0)] public double Double;
} }
delegate void WriteFunc(Span<byte> data, S7VariableAddress address, object value); delegate void WriteFunc(Span<byte> data, VariableAddress address, object value);
} }

View File

@@ -0,0 +1,32 @@
using JetBrains.Annotations;
using Sharp7.Rx.Enums;
namespace Sharp7.Rx;
[NoReorder]
internal record VariableAddress(Operand Operand, ushort DbNo, DbType Type, ushort Start, ushort Length, byte? Bit = null)
{
public Operand Operand { get; } = Operand;
public ushort DbNo { get; } = DbNo;
public ushort Start { get; } = Start;
public ushort Length { get; } = Length;
public byte? Bit { get; } = Bit;
public DbType Type { get; } = Type;
public ushort BufferLength => Type switch
{
DbType.String => (ushort) (Length + 2),
DbType.WString => (ushort) (Length * 2 + 4),
_ => Length
};
public override string ToString() =>
Type switch
{
DbType.Bit => $"{Operand}{DbNo}.{Type}{Start}.{Bit}",
DbType.String => $"{Operand}{DbNo}.{Type}{Start}.{Length}",
DbType.WString => $"{Operand}{DbNo}.{Type}{Start}.{Length}",
DbType.Byte => Length == 1 ? $"{Operand}{DbNo}.{Type}{Start}" : $"{Operand}{DbNo}.{Type}{Start}.{Length}",
_ => $"{Operand}{DbNo}.{Type}{Start}",
};
}

View File

@@ -6,7 +6,7 @@ using Sharp7.Rx.Interfaces;
namespace Sharp7.Rx; namespace Sharp7.Rx;
internal class S7VariableNameParser : IS7VariableNameParser internal class VariableNameParser : IVariableNameParser
{ {
private static readonly Regex regex = new(@"^(?<operand>db)(?<dbNo>\d+)\.?(?<type>[a-z]+)(?<start>\d+)(\.(?<bitOrLength>\d+))?$", private static readonly Regex regex = new(@"^(?<operand>db)(?<dbNo>\d+)\.?(?<type>[a-z]+)(?<start>\d+)(\.(?<bitOrLength>\d+))?$",
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant);
@@ -46,7 +46,7 @@ internal class S7VariableNameParser : IS7VariableNameParser
{"x", DbType.Bit}, {"x", DbType.Bit},
}; };
public S7VariableAddress Parse(string input) public VariableAddress Parse(string input)
{ {
if (input == null) if (input == null)
throw new ArgumentNullException(nameof(input)); throw new ArgumentNullException(nameof(input));
@@ -111,15 +111,7 @@ internal class S7VariableNameParser : IS7VariableNameParser
byte? bit = type == DbType.Bit ? GetBit() : null; byte? bit = type == DbType.Bit ? GetBit() : null;
var s7VariableAddress = new S7VariableAddress var s7VariableAddress = new VariableAddress(Operand: operand, DbNo: dbNr, Type: type, Start: start, Length: length, Bit: bit);
{
Operand = operand,
DbNr = dbNr,
Start = start,
Type = type,
Length = length,
Bit = bit
};
return s7VariableAddress; return s7VariableAddress;