From 8d8d5617d15aa125413264bdfa58f5da66d0dfef Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Mon, 5 Feb 2024 14:21:40 +0100 Subject: [PATCH 01/78] Move unit tests from specflow to NUnit --- Sharp7.Rx.Tests/ParsingS7VariableName.cs | 50 ---- Sharp7.Rx.Tests/ParsingS7VariableName.feature | 40 --- .../ParsingS7VariableName.feature.cs | 242 ------------------ Sharp7.Rx.Tests/S7VariableNameParserTests.cs | 42 +++ Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj | 40 +-- 5 files changed, 55 insertions(+), 359 deletions(-) delete mode 100644 Sharp7.Rx.Tests/ParsingS7VariableName.cs delete mode 100644 Sharp7.Rx.Tests/ParsingS7VariableName.feature delete mode 100644 Sharp7.Rx.Tests/ParsingS7VariableName.feature.cs create mode 100644 Sharp7.Rx.Tests/S7VariableNameParserTests.cs diff --git a/Sharp7.Rx.Tests/ParsingS7VariableName.cs b/Sharp7.Rx.Tests/ParsingS7VariableName.cs deleted file mode 100644 index 0dc256a..0000000 --- a/Sharp7.Rx.Tests/ParsingS7VariableName.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows.Forms; -using TechTalk.SpecFlow; -using TechTalk.SpecFlow.Assist; - -namespace Sharp7.Rx.Tests -{ - [Binding] - public class ParsingS7VariableName - { - private S7VariableNameParser parser; - - [Given(@"I have an Parser")] - public void GivenIHaveAnParser() - { - parser = new S7VariableNameParser(); - } - - [Given(@"I have the following variables")] - public void GivenIHaveTheFollowingVariables(Table table) - { - var names = table.CreateSet(); - - ScenarioContext.Current.Set(names); - } - - [When(@"I parse the var name")] - public void WhenIParseTheVarName() - { - var names = ScenarioContext.Current.Get>(); - var addresses = names.Select(v => parser.Parse(v.VarName)).ToArray(); - - ScenarioContext.Current.Set(addresses); - } - - [Then(@"the result should be")] - public void ThenTheResultShouldBe(Table table) - { - var addresses = ScenarioContext.Current.Get(); - table.CompareToSet(addresses); - } - } - - class Vars - { - public string VarName { get; set; } - } -} diff --git a/Sharp7.Rx.Tests/ParsingS7VariableName.feature b/Sharp7.Rx.Tests/ParsingS7VariableName.feature deleted file mode 100644 index 82d3be1..0000000 --- a/Sharp7.Rx.Tests/ParsingS7VariableName.feature +++ /dev/null @@ -1,40 +0,0 @@ -Feature: ParsingS7VariableName - -@mytag -Scenario: Parsing variable name for bool - Given I have an Parser - And I have the following variables - | VarName | - | DB13.DBX3.1 | - | Db403.X5.2 | - | DB55DBX23.6 | - | DB1.S255 | - | DB1.S255.20 | - | DB5.String887.20 | - | DB506.B216 | - | DB506.DBB216.5 | - | DB506.D216 | - | DB506.DINT216 | - | DB506.INT216 | - | DB506.DBW216 | - | DB506.DUL216 | - | DB506.DULINT216 | - | DB506.DULONG216 | - When I parse the var name - Then the result should be - | Operand | DbNr | Start | Length | Bit | Type | - | Db | 13 | 3 | 1 | 1 | Bit | - | Db | 403 | 5 | 1 | 2 | Bit | - | Db | 55 | 23 | 1 | 6 | Bit | - | Db | 1 | 255 | 0 | 0 | String | - | Db | 1 | 255 | 20 | 0 | String | - | Db | 5 | 887 | 20 | 0 | String | - | Db | 506 | 216 | 1 | 0 | Byte | - | Db | 506 | 216 | 5 | 0 | Byte | - | Db | 506 | 216 | 4 | 0 | Double | - | Db | 506 | 216 | 4 | 0 | DInteger | - | Db | 506 | 216 | 2 | 0 | Integer | - | Db | 506 | 216 | 2 | 0 | Integer | - | Db | 506 | 216 | 8 | 0 | ULong | - | Db | 506 | 216 | 8 | 0 | ULong | - | Db | 506 | 216 | 8 | 0 | ULong | diff --git a/Sharp7.Rx.Tests/ParsingS7VariableName.feature.cs b/Sharp7.Rx.Tests/ParsingS7VariableName.feature.cs deleted file mode 100644 index 18959ea..0000000 --- a/Sharp7.Rx.Tests/ParsingS7VariableName.feature.cs +++ /dev/null @@ -1,242 +0,0 @@ -// ------------------------------------------------------------------------------ -// -// This code was generated by SpecFlow (http://www.specflow.org/). -// SpecFlow Version:2.4.0.0 -// SpecFlow Generator Version:2.4.0.0 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// ------------------------------------------------------------------------------ -#region Designer generated code -#pragma warning disable -namespace Sharp7.Rx.Tests -{ - using TechTalk.SpecFlow; - - - [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "2.4.0.0")] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [NUnit.Framework.TestFixtureAttribute()] - [NUnit.Framework.DescriptionAttribute("ParsingS7VariableName")] - public partial class ParsingS7VariableNameFeature - { - - private TechTalk.SpecFlow.ITestRunner testRunner; - -#line 1 "ParsingS7VariableName.feature" -#line hidden - - [NUnit.Framework.OneTimeSetUpAttribute()] - public virtual void FeatureSetup() - { - testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); - TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "ParsingS7VariableName", null, ProgrammingLanguage.CSharp, ((string[])(null))); - testRunner.OnFeatureStart(featureInfo); - } - - [NUnit.Framework.OneTimeTearDownAttribute()] - public virtual void FeatureTearDown() - { - testRunner.OnFeatureEnd(); - testRunner = null; - } - - [NUnit.Framework.SetUpAttribute()] - public virtual void TestInitialize() - { - } - - [NUnit.Framework.TearDownAttribute()] - public virtual void ScenarioTearDown() - { - testRunner.OnScenarioEnd(); - } - - public virtual void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) - { - testRunner.OnScenarioInitialize(scenarioInfo); - testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(NUnit.Framework.TestContext.CurrentContext); - } - - public virtual void ScenarioStart() - { - testRunner.OnScenarioStart(); - } - - public virtual void ScenarioCleanup() - { - testRunner.CollectScenarioErrors(); - } - - [NUnit.Framework.TestAttribute()] - [NUnit.Framework.DescriptionAttribute("Parsing variable name for bool")] - [NUnit.Framework.CategoryAttribute("mytag")] - public virtual void ParsingVariableNameForBool() - { - TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Parsing variable name for bool", null, new string[] { - "mytag"}); -#line 4 -this.ScenarioInitialize(scenarioInfo); - this.ScenarioStart(); -#line 5 - testRunner.Given("I have an Parser", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); -#line hidden - TechTalk.SpecFlow.Table table1 = new TechTalk.SpecFlow.Table(new string[] { - "VarName"}); - table1.AddRow(new string[] { - "DB13.DBX3.1"}); - table1.AddRow(new string[] { - "Db403.X5.2"}); - table1.AddRow(new string[] { - "DB55DBX23.6"}); - table1.AddRow(new string[] { - "DB1.S255"}); - table1.AddRow(new string[] { - "DB1.S255.20"}); - table1.AddRow(new string[] { - "DB5.String887.20"}); - table1.AddRow(new string[] { - "DB506.B216"}); - table1.AddRow(new string[] { - "DB506.DBB216.5"}); - table1.AddRow(new string[] { - "DB506.D216"}); - table1.AddRow(new string[] { - "DB506.DINT216"}); - table1.AddRow(new string[] { - "DB506.INT216"}); - table1.AddRow(new string[] { - "DB506.DBW216"}); - table1.AddRow(new string[] { - "DB506.DUL216"}); - table1.AddRow(new string[] { - "DB506.DULINT216"}); - table1.AddRow(new string[] { - "DB506.DULONG216"}); -#line 6 - testRunner.And("I have the following variables", ((string)(null)), table1, "And "); -#line 23 - testRunner.When("I parse the var name", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); -#line hidden - TechTalk.SpecFlow.Table table2 = new TechTalk.SpecFlow.Table(new string[] { - "Operand", - "DbNr", - "Start", - "Length", - "Bit", - "Type"}); - table2.AddRow(new string[] { - "Db", - "13", - "3", - "1", - "1", - "Bit"}); - table2.AddRow(new string[] { - "Db", - "403", - "5", - "1", - "2", - "Bit"}); - table2.AddRow(new string[] { - "Db", - "55", - "23", - "1", - "6", - "Bit"}); - table2.AddRow(new string[] { - "Db", - "1", - "255", - "0", - "0", - "String"}); - table2.AddRow(new string[] { - "Db", - "1", - "255", - "20", - "0", - "String"}); - table2.AddRow(new string[] { - "Db", - "5", - "887", - "20", - "0", - "String"}); - table2.AddRow(new string[] { - "Db", - "506", - "216", - "1", - "0", - "Byte"}); - table2.AddRow(new string[] { - "Db", - "506", - "216", - "5", - "0", - "Byte"}); - table2.AddRow(new string[] { - "Db", - "506", - "216", - "4", - "0", - "Double"}); - table2.AddRow(new string[] { - "Db", - "506", - "216", - "4", - "0", - "DInteger"}); - table2.AddRow(new string[] { - "Db", - "506", - "216", - "2", - "0", - "Integer"}); - table2.AddRow(new string[] { - "Db", - "506", - "216", - "2", - "0", - "Integer"}); - table2.AddRow(new string[] { - "Db", - "506", - "216", - "8", - "0", - "ULong"}); - table2.AddRow(new string[] { - "Db", - "506", - "216", - "8", - "0", - "ULong"}); - table2.AddRow(new string[] { - "Db", - "506", - "216", - "8", - "0", - "ULong"}); -#line 24 - testRunner.Then("the result should be", ((string)(null)), table2, "Then "); -#line hidden - this.ScenarioCleanup(); - } - } -} -#pragma warning restore -#endregion diff --git a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs new file mode 100644 index 0000000..b97641f --- /dev/null +++ b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using DeepEqual.Syntax; +using NUnit.Framework; +using Sharp7.Rx.Enums; + +namespace Sharp7.Rx.Tests; + +[TestFixture] +internal class S7VariableNameParserTests +{ + [TestCaseSource(nameof(GetTestCases))] + public void Run(TestCase tc) + { + var parser = new S7VariableNameParser(); + var resp = parser.Parse(tc.Input); + resp.ShouldDeepEqual(tc.Expected); + } + + public static IEnumerable GetTestCases() + { + yield return new TestCase("DB13.DBX3.1", new S7VariableAddress {Operand = Operand.Db, DbNr = 13, Start = 3, Length = 1, Bit = 1, Type = DbType.Bit}); + yield return new TestCase("Db403.X5.2", new S7VariableAddress {Operand = Operand.Db, DbNr = 403, Start = 5, Length = 1, Bit = 2, Type = DbType.Bit}); + yield return new TestCase("DB55DBX23.6", new S7VariableAddress {Operand = Operand.Db, DbNr = 55, Start = 23, Length = 1, Bit = 6, Type = DbType.Bit}); + yield return new TestCase("DB1.S255", new S7VariableAddress {Operand = Operand.Db, DbNr = 1, Start = 255, Length = 0, Bit = 0, Type = DbType.String}); + yield return new TestCase("DB1.S255.20", new S7VariableAddress {Operand = Operand.Db, DbNr = 1, Start = 255, Length = 20, Bit = 0, Type = DbType.String}); + yield return new TestCase("DB5.String887.20", new S7VariableAddress {Operand = Operand.Db, DbNr = 5, Start = 887, Length = 20, Bit = 0, Type = DbType.String}); + yield return new TestCase("DB506.B216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Bit = 0, Type = DbType.Byte}); + yield return new TestCase("DB506.DBB216.5", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 5, Bit = 0, Type = DbType.Byte}); + yield return new TestCase("DB506.D216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Bit = 0, Type = DbType.Double}); + yield return new TestCase("DB506.DINT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Bit = 0, Type = DbType.DInteger}); + yield return new TestCase("DB506.INT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Bit = 0, Type = DbType.Integer}); + yield return new TestCase("DB506.DBW216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Bit = 0, Type = DbType.Integer}); + yield return new TestCase("DB506.DUL216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Bit = 0, Type = DbType.ULong}); + yield return new TestCase("DB506.DULINT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Bit = 0, Type = DbType.ULong}); + yield return new TestCase("DB506.DULONG216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Bit = 0, Type = DbType.ULong}); + } + + public record TestCase(string Input, S7VariableAddress Expected) + { + public override string ToString() => Input; + } +} \ No newline at end of file diff --git a/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj b/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj index 39dae7a..58c7e5b 100644 --- a/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj +++ b/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj @@ -1,32 +1,18 @@  - - net461 - + + net8.0 + - - - - - + + + + + + - - - + + + - - - True - True - ParsingS7VariableName.feature - - - - - - SpecFlowSingleFileGenerator - ParsingS7VariableName.feature.cs - - - - + \ No newline at end of file From 55050dccd675daea98979b571a212f61650d1581 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Mon, 5 Feb 2024 15:30:56 +0100 Subject: [PATCH 02/78] Make regex static and compiled --- Sharp7.Rx/S7VariableNameParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index a53e8ef..4820717 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -10,7 +10,7 @@ namespace Sharp7.Rx { internal class S7VariableNameParser : IS7VariableNameParser { - private readonly Regex regex = new Regex(@"^(?db{1})(?\d{1,4})\.?(?dbx|x|s|string|b|dbb|d|int|dbw|w|dint|dul|dulint|dulong|){1}(?\d+)(\.(?\d+))?$", RegexOptions.IgnoreCase); + private static readonly Regex regex = new Regex(@"^(?db{1})(?\d{1,4})\.?(?dbx|x|s|string|b|dbb|d|int|dbw|w|dint|dul|dulint|dulong|){1}(?\d+)(\.(?\d+))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); private readonly Dictionary types = new Dictionary { From 49fe1968d930f306c28f6e08291a0d8d96b6ed88 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 13:19:32 +0100 Subject: [PATCH 03/78] Optimize dictionary access --- Sharp7.Rx/S7VariableNameParser.cs | 69 +++++++++++++------------------ 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index 4820717..b65f4c7 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -12,7 +12,7 @@ namespace Sharp7.Rx { private static readonly Regex regex = new Regex(@"^(?db{1})(?\d{1,4})\.?(?dbx|x|s|string|b|dbb|d|int|dbw|w|dint|dul|dulint|dulong|){1}(?\d+)(\.(?\d+))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); - private readonly Dictionary types = new Dictionary + private static readonly IReadOnlyDictionary types = new Dictionary(StringComparer.InvariantCultureIgnoreCase) { {"x", DbType.Bit}, {"dbx", DbType.Bit}, @@ -30,16 +30,17 @@ namespace Sharp7.Rx {"dulong", DbType.ULong } }; - public S7VariableAddress Parse(string input) { var match = regex.Match(input); if (match.Success) { var operand = (Operand)Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true); - var dbNr = ushort.Parse(match.Groups["dbNr"].Value, NumberStyles.Integer); - var start = ushort.Parse(match.Groups["start"].Value, NumberStyles.Integer); - var type = ParseType(match.Groups["type"].Value); + 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; + var s7VariableAddress = new S7VariableAddress { @@ -49,34 +50,30 @@ namespace Sharp7.Rx Type = type, }; - if (type == DbType.Bit) + switch (type) { - s7VariableAddress.Length = 1; - s7VariableAddress.Bit = byte.Parse(match.Groups["bitOrLength"].Value); - } - else if (type == DbType.Byte) - { - s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort)1; - } - else if (type == DbType.String) - { - s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort)0; - } - else if (type == DbType.Integer) - { - s7VariableAddress.Length = 2; - } - else if (type == DbType.DInteger) - { - s7VariableAddress.Length = 4; - } - else if (type == DbType.ULong) - { - s7VariableAddress.Length = 8; - } - else if (type == DbType.Double) - { - s7VariableAddress.Length = 4; + case DbType.Bit: + s7VariableAddress.Length = 1; + s7VariableAddress.Bit = byte.Parse(match.Groups["bitOrLength"].Value); + break; + case DbType.Byte: + s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort)1; + break; + case DbType.String: + s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort)0; + break; + case DbType.Integer: + s7VariableAddress.Length = 2; + break; + case DbType.DInteger: + s7VariableAddress.Length = 4; + break; + case DbType.ULong: + s7VariableAddress.Length = 8; + break; + case DbType.Double: + s7VariableAddress.Length = 4; + break; } return s7VariableAddress; @@ -84,13 +81,5 @@ namespace Sharp7.Rx return null; } - - private DbType ParseType(string value) - { - return types - .Where(pair => pair.Key.Equals(value, StringComparison.InvariantCultureIgnoreCase)) - .Select(pair => pair.Value) - .FirstOrDefault(); - } } } \ No newline at end of file From 9b0749baae86bcd6b5ed619a0fe5fc136be16ce1 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 13:39:55 +0100 Subject: [PATCH 04/78] Cache S7 variable names --- Sharp7.Rx/CacheVariableNameParser.cs | 20 ++++++++++++++++++++ Sharp7.Rx/Interfaces/IS7Connector.cs | 2 +- Sharp7.Rx/S7VariableNameParser.cs | 2 +- Sharp7.Rx/Sharp7Connector.cs | 22 ++++++++++------------ Sharp7.Rx/Sharp7Plc.cs | 15 ++------------- 5 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 Sharp7.Rx/CacheVariableNameParser.cs diff --git a/Sharp7.Rx/CacheVariableNameParser.cs b/Sharp7.Rx/CacheVariableNameParser.cs new file mode 100644 index 0000000..ddd258b --- /dev/null +++ b/Sharp7.Rx/CacheVariableNameParser.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Concurrent; +using Sharp7.Rx.Interfaces; + +namespace Sharp7.Rx +{ + internal class CacheVariableNameParser : IS7VariableNameParser + { + private static readonly ConcurrentDictionary addressCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + private readonly IS7VariableNameParser inner; + + public CacheVariableNameParser(IS7VariableNameParser inner) + { + this.inner = inner; + } + + public S7VariableAddress Parse(string input) => addressCache.GetOrAdd(input, inner.Parse); + } +} \ No newline at end of file diff --git a/Sharp7.Rx/Interfaces/IS7Connector.cs b/Sharp7.Rx/Interfaces/IS7Connector.cs index 74767a0..1515901 100644 --- a/Sharp7.Rx/Interfaces/IS7Connector.cs +++ b/Sharp7.Rx/Interfaces/IS7Connector.cs @@ -21,6 +21,6 @@ namespace Sharp7.Rx.Interfaces Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token); Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token); ILogger Logger { get; } - Task> ExecuteMultiVarRequest(IEnumerable variableNames); + Task> ExecuteMultiVarRequest(IReadOnlyList variableNames); } } \ No newline at end of file diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index b65f4c7..c898537 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -12,7 +12,7 @@ namespace Sharp7.Rx { private static readonly Regex regex = new Regex(@"^(?db{1})(?\d{1,4})\.?(?dbx|x|s|string|b|dbb|d|int|dbw|w|dint|dul|dulint|dulong|){1}(?\d+)(\.(?\d+))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); - private static readonly IReadOnlyDictionary types = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + private static readonly IReadOnlyDictionary types = new Dictionary(StringComparer.OrdinalIgnoreCase) { {"x", DbType.Bit}, {"dbx", DbType.Bit}, diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 89e7921..b87c324 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -21,7 +21,6 @@ namespace Sharp7.Rx { private readonly IS7VariableNameParser variableNameParser; private readonly BehaviorSubject connectionStateSubject = new BehaviorSubject(Enums.ConnectionState.Initial); - private ConcurrentDictionary s7VariableAddresses = new ConcurrentDictionary(); private readonly CompositeDisposable disposables = new CompositeDisposable(); private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism:1); @@ -34,16 +33,15 @@ namespace Sharp7.Rx private bool disposed; public ILogger Logger { get; set; } - public async Task> ExecuteMultiVarRequest(IEnumerable variableNames) + public async Task> ExecuteMultiVarRequest(IReadOnlyList variableNames) { - var enumerable = variableNames as string[] ?? variableNames.ToArray(); - - if (enumerable.IsEmpty()) + if (variableNames.IsEmpty()) return new Dictionary(); var s7MultiVar = new S7MultiVar(sharp7); - var buffers = enumerable.Select(key => new {VariableName = key, Address = s7VariableAddresses.GetOrAdd(key, s => variableNameParser.Parse(s))}) + var buffers = variableNames + .Select(key => new {VariableName = key, Address = variableNameParser.Parse(key)}) .Select(x => { var buffer = new byte[x.Address.Length]; @@ -67,10 +65,10 @@ namespace Sharp7.Rx public Sharp7Connector(PlcConnectionSettings settings, IS7VariableNameParser variableNameParser) { this.variableNameParser = variableNameParser; - this.ipAddress = settings.IpAddress; - this.cpuSlotNr = settings.CpuMpiAddress; - this.port = settings.Port; - this.rackNr = settings.RackNumber; + ipAddress = settings.IpAddress; + cpuSlotNr = settings.CpuMpiAddress; + port = settings.Port; + rackNr = settings.RackNumber; ReconnectDelay = TimeSpan.FromSeconds(5); } @@ -120,7 +118,7 @@ namespace Sharp7.Rx try { sharp7 = new S7Client(); - sharp7.PLCPort = this.port; + sharp7.PLCPort = port; var subscription = ConnectionState @@ -223,7 +221,7 @@ namespace Sharp7.Rx if (result != 0) { await EvaluateErrorCode(result); - var errorText = this.sharp7.ErrorText(result); + var errorText = sharp7.ErrorText(result); throw new InvalidOperationException($"Error reading {operand}{dBNr}:{startByteAddress}->{bytesToRead} ({errorText})"); } diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 3273a40..3f733cb 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -20,11 +20,7 @@ namespace Sharp7.Rx { public class Sharp7Plc : IPlc { - private readonly string ipAddress; - private readonly int rackNumber; - private readonly int cpuMpiAddress; - private readonly int port; - private readonly IS7VariableNameParser varaibleNameParser; + private readonly IS7VariableNameParser varaibleNameParser = new CacheVariableNameParser(new S7VariableNameParser()); private bool disposed; private ISubject disposingSubject = new Subject(); private IS7Connector s7Connector; @@ -37,14 +33,7 @@ namespace Sharp7.Rx public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress, int port = 102) { - this.ipAddress = ipAddress; - this.rackNumber = rackNumber; - this.cpuMpiAddress = cpuMpiAddress; - this.port = port; - plcConnectionSettings = new PlcConnectionSettings(){IpAddress = ipAddress, RackNumber = rackNumber, CpuMpiAddress = cpuMpiAddress, Port = port}; - - varaibleNameParser = new S7VariableNameParser(); } public IObservable ConnectionState { get; private set; } @@ -403,7 +392,7 @@ namespace Sharp7.Rx var stopWatch = Stopwatch.StartNew(); foreach (var partsOfMultiVarRequest in multiVariableSubscriptions.ExistingKeys.Buffer(MultiVarRequestMaxItems)) { - var multiVarRequest = await connector.ExecuteMultiVarRequest(partsOfMultiVarRequest); + var multiVarRequest = await connector.ExecuteMultiVarRequest(partsOfMultiVarRequest as IReadOnlyList??partsOfMultiVarRequest.ToList()); foreach (var pair in multiVarRequest) { From 81eb5e42aa3aad29a19b97458a328542ad851cae Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 13:56:00 +0100 Subject: [PATCH 05/78] Remove unused logger from internal interface --- Sharp7.Rx/Interfaces/IS7Connector.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sharp7.Rx/Interfaces/IS7Connector.cs b/Sharp7.Rx/Interfaces/IS7Connector.cs index 1515901..612dd82 100644 --- a/Sharp7.Rx/Interfaces/IS7Connector.cs +++ b/Sharp7.Rx/Interfaces/IS7Connector.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Sharp7.Rx.Enums; namespace Sharp7.Rx.Interfaces @@ -20,7 +19,7 @@ namespace Sharp7.Rx.Interfaces Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token); Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token); - ILogger Logger { get; } + Task> ExecuteMultiVarRequest(IReadOnlyList variableNames); } } \ No newline at end of file From fe68b1d565c5bb32c4bffd68a2a598c712829633 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 14:01:15 +0100 Subject: [PATCH 06/78] Remove extra byte array --- Sharp7.Rx/Sharp7Connector.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index b87c324..484118f 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; @@ -54,7 +53,7 @@ namespace Sharp7.Rx if (result != 0) { await EvaluateErrorCode(result); - throw new InvalidOperationException($"Error in MultiVar request for variables: {string.Join(",", enumerable)}"); + throw new InvalidOperationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)}"); } return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer); @@ -225,9 +224,7 @@ namespace Sharp7.Rx throw new InvalidOperationException($"Error reading {operand}{dBNr}:{startByteAddress}->{bytesToRead} ({errorText})"); } - var retBuffer = new byte[bytesToRead]; - Array.Copy(buffer, 0, retBuffer, 0, bytesToRead); - return (retBuffer); + return buffer; } private int FromOperand(Operand operand) @@ -269,7 +266,7 @@ namespace Sharp7.Rx if (result != 0) { await EvaluateErrorCode(result); - return (0); + return 0; } return (ushort)(data.Length); } From ffa4ee6236b19d89f5ff48501aeaeb091411ef32 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 14:36:32 +0100 Subject: [PATCH 07/78] Remove ReadBit Using ReadBytes to allow for later unification of Byte conversion logic --- Sharp7.Rx/Interfaces/IS7Connector.cs | 1 - Sharp7.Rx/Sharp7Connector.cs | 15 --------------- Sharp7.Rx/Sharp7Plc.cs | 5 ++--- 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/Sharp7.Rx/Interfaces/IS7Connector.cs b/Sharp7.Rx/Interfaces/IS7Connector.cs index 612dd82..309f750 100644 --- a/Sharp7.Rx/Interfaces/IS7Connector.cs +++ b/Sharp7.Rx/Interfaces/IS7Connector.cs @@ -14,7 +14,6 @@ namespace Sharp7.Rx.Interfaces Task Connect(); Task Disconnect(); - Task ReadBit(Operand operand, ushort byteAddress, byte bitAdress, ushort dbNr, CancellationToken token); Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token); Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token); diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 484118f..99c6644 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -270,21 +270,6 @@ namespace Sharp7.Rx } return (ushort)(data.Length); } - - - public async Task ReadBit(Operand operand, ushort byteAddress, byte bitAdress, ushort dbNr, CancellationToken token) - { - EnsureConnectionValid(); - - var byteValue = await ReadBytes(operand, byteAddress, 1, dbNr, token); - token.ThrowIfCancellationRequested(); - - if (byteValue.Length != 1) - throw new InvalidOperationException("Read bytes does not have length 1"); - - return Convert.ToBoolean(byteValue[0] & (1 << bitAdress)); - } - public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token) { EnsureConnectionValid(); diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 3f733cb..95b690c 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -144,9 +144,8 @@ namespace Sharp7.Rx if (typeof(TValue) == typeof(bool)) { - var b = await s7Connector.ReadBit(address.Operand, address.Start, address.Bit, address.DbNr, token); - token.ThrowIfCancellationRequested(); - return (TValue)(object)b; + var b = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); + return ConvertToType(b, address); } if (typeof(TValue) == typeof(int)) From 5b86b3e98403d8df1c7e3928d47b2258f167938a Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 14:43:50 +0100 Subject: [PATCH 08/78] Use ConvertToType for both GetValue and CreateNotification --- Sharp7.Rx/Sharp7Plc.cs | 92 +----------------------------------------- 1 file changed, 2 insertions(+), 90 deletions(-) diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 95b690c..76705fd 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -142,96 +142,8 @@ namespace Sharp7.Rx var address = varaibleNameParser.Parse(variableName); if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName)); - if (typeof(TValue) == typeof(bool)) - { - var b = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); - return ConvertToType(b, address); - } - - if (typeof(TValue) == typeof(int)) - { - var b = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); - token.ThrowIfCancellationRequested(); - if (address.Length == 2) - return (TValue)(object)((b[0] << 8) + b[1]); - if (address.Length == 4) - { - Array.Reverse(b); - return (TValue)(object)BitConverter.ToInt32(b,0); - } - - - throw new InvalidOperationException($"length must be 2 or 4 but is {address.Length}"); - } - - if (typeof(TValue) == typeof(long)) - { - var b = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); - token.ThrowIfCancellationRequested(); - Array.Reverse(b); - return (TValue)(object)BitConverter.ToInt64(b,0); - } - - if (typeof(TValue) == typeof(ulong)) - { - var b = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); - token.ThrowIfCancellationRequested(); - Array.Reverse(b); - return (TValue)(object)BitConverter.ToUInt64(b, 0); - } - - if (typeof(TValue) == typeof(short)) - { - var b = await s7Connector.ReadBytes(address.Operand, address.Start, 2, address.DbNr, token); - token.ThrowIfCancellationRequested(); - return (TValue)(object)(short)((b[0] << 8) + b[1]); - } - - if (typeof(TValue) == typeof(byte) || typeof(TValue) == typeof(char)) - { - var b = await s7Connector.ReadBytes(address.Operand, address.Start, 1, address.DbNr, token); - token.ThrowIfCancellationRequested(); - - return (TValue)(object)b[0]; - } - - if (typeof(TValue) == typeof(byte[])) - { - var b = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); - token.ThrowIfCancellationRequested(); - return (TValue)(object)b; - } - - if (typeof(TValue) == typeof(double) || typeof(TValue) == typeof(float)) - { - var bytes = await s7Connector.ReadBytes(address.Operand, address.Start, 4, address.DbNr, token); - token.ThrowIfCancellationRequested(); - var d = BitConverter.ToSingle(bytes.Reverse().ToArray(),0); - return (TValue)(object)d; - } - - if (typeof(TValue) == typeof(string)) - { - if (address.Type == DbType.String) - { - var bytes = await s7Connector.ReadBytes(address.Operand, address.Start, 2, address.DbNr, token); - token.ThrowIfCancellationRequested(); - var stringLength = bytes[1]; - - var stringStartAddress = (ushort)(address.Start + 2); - var stringInBytes = await s7Connector.ReadBytes(address.Operand, stringStartAddress, stringLength, address.DbNr, token); - token.ThrowIfCancellationRequested(); - return (TValue)(object)Encoding.ASCII.GetString(stringInBytes); - } - else - { - var stringInBytes = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); - token.ThrowIfCancellationRequested(); - return (TValue)(object)Encoding.ASCII.GetString(stringInBytes).Trim(); - } - } - - throw new InvalidOperationException(string.Format("type '{0}' not supported.", typeof(TValue))); + var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); + return ConvertToType(data, address); } From 173b576ad97ee685443f3cf94ac2fe5cd29d183f Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 14:45:39 +0100 Subject: [PATCH 09/78] Extract S7ValueConverter --- Sharp7.Rx/S7ValueConverter.cs | 76 +++++++++++++++++++++++++++++++++++ Sharp7.Rx/Sharp7Plc.cs | 68 +------------------------------ 2 files changed, 78 insertions(+), 66 deletions(-) create mode 100644 Sharp7.Rx/S7ValueConverter.cs diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs new file mode 100644 index 0000000..9662fb3 --- /dev/null +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using System.Text; +using Sharp7.Rx.Enums; + +namespace Sharp7.Rx +{ + internal static class S7ValueConverter + { + public static TValue ConvertToType(byte[] buffer, S7VariableAddress address) + { + if (typeof(TValue) == typeof(bool)) + { + return (TValue) (object) Convert.ToBoolean(buffer[0] & (1 << address.Bit)); + } + + if (typeof(TValue) == typeof(int)) + { + if (address.Length == 2) + return (TValue)(object)((buffer[0] << 8) + buffer[1]); + if (address.Length == 4) + { + Array.Reverse(buffer); + return (TValue)(object)BitConverter.ToInt32(buffer,0); + } + + throw new InvalidOperationException($"length must be 2 or 4 but is {address.Length}"); + } + + if (typeof(TValue) == typeof(long)) + { + Array.Reverse(buffer); + return (TValue)(object)BitConverter.ToInt64(buffer,0); + } + + if (typeof(TValue) == typeof(ulong)) + { + Array.Reverse(buffer); + return (TValue)(object)BitConverter.ToUInt64(buffer, 0); + } + + if (typeof(TValue) == typeof(short)) + { + return (TValue)(object)(short)((buffer[0] << 8) + buffer[1]); + } + + if (typeof(TValue) == typeof(byte) || typeof(TValue) == typeof(char)) + { + return (TValue)(object)buffer[0]; + } + + if (typeof(TValue) == typeof(byte[])) + { + return (TValue)(object)buffer; + } + + if (typeof(TValue) == typeof(double) || typeof(TValue) == typeof(float)) + { + var d = BitConverter.ToSingle(buffer.Reverse().ToArray(),0); + return (TValue)(object)d; + } + + if (typeof(TValue) == typeof(string)) + if (address.Type == DbType.String) + { + return (TValue) (object) Encoding.ASCII.GetString(buffer); + } + else + { + return (TValue) (object) Encoding.ASCII.GetString(buffer).Trim(); + } + + throw new InvalidOperationException(string.Format("type '{0}' not supported.", typeof(TValue))); + } + } +} \ No newline at end of file diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 76705fd..ca46309 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -71,71 +71,7 @@ namespace Sharp7.Rx return GetValue(variableName, CancellationToken.None); } - private TValue ConvertToType(byte[] buffer, S7VariableAddress address) - { - if (typeof(TValue) == typeof(bool)) - { - return (TValue) (object) Convert.ToBoolean(buffer[0] & (1 << address.Bit)); - } - if (typeof(TValue) == typeof(int)) - { - if (address.Length == 2) - return (TValue)(object)((buffer[0] << 8) + buffer[1]); - if (address.Length == 4) - { - Array.Reverse(buffer); - return (TValue)(object)BitConverter.ToInt32(buffer,0); - } - - throw new InvalidOperationException($"length must be 2 or 4 but is {address.Length}"); - } - - if (typeof(TValue) == typeof(long)) - { - Array.Reverse(buffer); - return (TValue)(object)BitConverter.ToInt64(buffer,0); - } - - if (typeof(TValue) == typeof(ulong)) - { - Array.Reverse(buffer); - return (TValue)(object)BitConverter.ToUInt64(buffer, 0); - } - - if (typeof(TValue) == typeof(short)) - { - return (TValue)(object)(short)((buffer[0] << 8) + buffer[1]); - } - - if (typeof(TValue) == typeof(byte) || typeof(TValue) == typeof(char)) - { - return (TValue)(object)buffer[0]; - } - - if (typeof(TValue) == typeof(byte[])) - { - return (TValue)(object)buffer; - } - - if (typeof(TValue) == typeof(double) || typeof(TValue) == typeof(float)) - { - var d = BitConverter.ToSingle(buffer.Reverse().ToArray(),0); - return (TValue)(object)d; - } - - if (typeof(TValue) == typeof(string)) - if (address.Type == DbType.String) - { - return (TValue) (object) Encoding.ASCII.GetString(buffer); - } - else - { - return (TValue) (object) Encoding.ASCII.GetString(buffer).Trim(); - } - - throw new InvalidOperationException(string.Format("type '{0}' not supported.", typeof(TValue))); - } public async Task GetValue(string variableName, CancellationToken token) { @@ -143,7 +79,7 @@ namespace Sharp7.Rx 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); - return ConvertToType(data, address); + return S7ValueConverter.ConvertToType(data, address); } @@ -231,7 +167,7 @@ namespace Sharp7.Rx disposeableContainer.AddDisposableTo(disposables); var observable = disposeableContainer.Observable - .Select(bytes => ConvertToType(bytes, address)); + .Select(bytes => S7ValueConverter.ConvertToType(bytes, address)); if (transmissionMode == TransmissionMode.OnChange) observable = observable.DistinctUntilChanged(); From 5c2f0c0a09cb5cc947e53924dc78322d5bd0a4b6 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 15:33:39 +0100 Subject: [PATCH 10/78] Update nuget packages --- Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj | 4 ++-- Sharp7.Rx/Sharp7.Rx.csproj | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj b/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj index 58c7e5b..8eb3805 100644 --- a/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj +++ b/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 853ccf0..329b3d9 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -14,10 +14,10 @@ - - - - + + + + From f45e2c8b1feb394fb54915f89db2c103453619fe Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 16:10:42 +0100 Subject: [PATCH 11/78] Add unit tests for S7ValueConverter --- Sharp7.Rx.Tests/S7ValueConverterTests.cs | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 Sharp7.Rx.Tests/S7ValueConverterTests.cs diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests.cs b/Sharp7.Rx.Tests/S7ValueConverterTests.cs new file mode 100644 index 0000000..54bf85c --- /dev/null +++ b/Sharp7.Rx.Tests/S7ValueConverterTests.cs @@ -0,0 +1,66 @@ +using System; +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(62004, "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.DBB0.4", new byte[] {0x41, 0x42, 0x43, 0x44})] + public void Parse(T expected, string address, byte[] data) + { + //Arrange + var variableAddress = parser.Parse(address); + + //Act + var result = S7ValueConverter.ConvertToType(data, variableAddress); + + //Assert + result.ShouldBe(expected); + } + + [TestCase((ushort) 3532, "DB0.INT0", new byte[] {0xF2, 0x34})] + public void Invalid(T expected, string address, byte[] data) + { + //Arrange + var variableAddress = parser.Parse(address); + + //Act + Should.Throw(() => S7ValueConverter.ConvertToType(data, variableAddress)); + } + + [TestCase(3532, "DB0.DINT0", new byte[] {0xF2, 0x34})] + public void Argument(T expected, string address, byte[] data) + { + //Arrange + var variableAddress = parser.Parse(address); + + //Act + Should.Throw(() => S7ValueConverter.ConvertToType(data, variableAddress)); + } +} \ No newline at end of file From b87480c4f98d810360ffeb3672a541e5fd853fb6 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 17:19:23 +0100 Subject: [PATCH 12/78] Fix String conversion --- Sharp7.Rx.Tests/S7ValueConverterTests.cs | 1 + Sharp7.Rx/S7ValueConverter.cs | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests.cs b/Sharp7.Rx.Tests/S7ValueConverterTests.cs index 54bf85c..0cdf048 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests.cs @@ -31,6 +31,7 @@ public class S7ValueConverterTests [TestCase(0.25f, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] [TestCase(0.25, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] [TestCase("ABCD", "DB0.string0.4", new byte[] {0x00, 0x04, 0x41, 0x42, 0x43, 0x44})] + [TestCase("ABCD", "DB0.string0.4", new byte[] {0x00, 0xF0, 0x41, 0x42, 0x43, 0x44})] // Clip to length in Address [TestCase("ABCD", "DB0.DBB0.4", new byte[] {0x41, 0x42, 0x43, 0x44})] public void Parse(T expected, string address, byte[] data) { diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 9662fb3..9533809 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -63,7 +63,13 @@ namespace Sharp7.Rx if (typeof(TValue) == typeof(string)) if (address.Type == DbType.String) { - return (TValue) (object) Encoding.ASCII.GetString(buffer); + // 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 { From baef1afac1c88a85d17a2468e164f22bd5185ab3 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 17:47:00 +0100 Subject: [PATCH 13/78] Use BinaryPrimitives --- Sharp7.Rx/S7ValueConverter.cs | 40 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 9533809..04d8965 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -1,5 +1,6 @@ using System; -using System.Linq; +using System.Buffers.Binary; +using System.Runtime.InteropServices; using System.Text; using Sharp7.Rx.Enums; @@ -10,18 +11,15 @@ namespace Sharp7.Rx public static TValue ConvertToType(byte[] buffer, S7VariableAddress address) { if (typeof(TValue) == typeof(bool)) - { - return (TValue) (object) Convert.ToBoolean(buffer[0] & (1 << address.Bit)); - } + return (TValue) (object) (((buffer[0] >> address.Bit) & 1) > 0); if (typeof(TValue) == typeof(int)) { if (address.Length == 2) - return (TValue)(object)((buffer[0] << 8) + buffer[1]); + return (TValue) (object) BinaryPrimitives.ReadInt16BigEndian(buffer); if (address.Length == 4) { - Array.Reverse(buffer); - return (TValue)(object)BitConverter.ToInt32(buffer,0); + return (TValue) (object) BinaryPrimitives.ReadInt32BigEndian(buffer); } throw new InvalidOperationException($"length must be 2 or 4 but is {address.Length}"); @@ -29,35 +27,36 @@ namespace Sharp7.Rx if (typeof(TValue) == typeof(long)) { - Array.Reverse(buffer); - return (TValue)(object)BitConverter.ToInt64(buffer,0); + return (TValue) (object) BinaryPrimitives.ReadInt64BigEndian(buffer); } if (typeof(TValue) == typeof(ulong)) { - Array.Reverse(buffer); - return (TValue)(object)BitConverter.ToUInt64(buffer, 0); + return (TValue) (object) BinaryPrimitives.ReadUInt64BigEndian(buffer); } if (typeof(TValue) == typeof(short)) { - return (TValue)(object)(short)((buffer[0] << 8) + buffer[1]); + return (TValue) (object) BinaryPrimitives.ReadInt16BigEndian(buffer); } if (typeof(TValue) == typeof(byte) || typeof(TValue) == typeof(char)) { - return (TValue)(object)buffer[0]; + return (TValue) (object) buffer[0]; } if (typeof(TValue) == typeof(byte[])) { - return (TValue)(object)buffer; + return (TValue) (object) buffer; } if (typeof(TValue) == typeof(double) || typeof(TValue) == typeof(float)) { - var d = BitConverter.ToSingle(buffer.Reverse().ToArray(),0); - return (TValue)(object)d; + var d = new UInt32SingleMap + { + UInt32 = BinaryPrimitives.ReadUInt32BigEndian(buffer) + }; + return (TValue) (object) d.Single; } if (typeof(TValue) == typeof(string)) @@ -68,7 +67,7 @@ namespace Sharp7.Rx // 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 @@ -78,5 +77,12 @@ namespace Sharp7.Rx throw new InvalidOperationException(string.Format("type '{0}' not supported.", typeof(TValue))); } + + [StructLayout(LayoutKind.Explicit)] + private struct UInt32SingleMap + { + [FieldOffset(0)] public uint UInt32; + [FieldOffset(0)] public float Single; + } } } \ No newline at end of file From d422249955f145a5f79790876e31fc8da09b8803 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 17:51:53 +0100 Subject: [PATCH 14/78] Fix all conversions --- Sharp7.Rx.Tests/S7ValueConverterTests.cs | 2 +- Sharp7.Rx/S7ValueConverter.cs | 29 +++++++++++------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests.cs b/Sharp7.Rx.Tests/S7ValueConverterTests.cs index 0cdf048..a1eb657 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests.cs @@ -20,7 +20,7 @@ public class S7ValueConverterTests [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(62004, "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})] diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 04d8965..d97fa31 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -16,41 +16,40 @@ namespace Sharp7.Rx if (typeof(TValue) == typeof(int)) { if (address.Length == 2) - return (TValue) (object) BinaryPrimitives.ReadInt16BigEndian(buffer); + 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) || typeof(TValue) == typeof(char)) - { + 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) + }; + return (TValue) (object) (double)d.Single; } - if (typeof(TValue) == typeof(double) || typeof(TValue) == typeof(float)) + if (typeof(TValue) == typeof(float)) { var d = new UInt32SingleMap { @@ -71,9 +70,7 @@ namespace Sharp7.Rx 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))); } From e7176c26e7282af8e47f07e7ec623e98c66c8602 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 18:19:15 +0100 Subject: [PATCH 15/78] Do not consider connection lost on some error codes --- Sharp7.Rx/S7ErrorCodes.cs | 27 +++++++++++++++++++++++++++ Sharp7.Rx/Sharp7Connector.cs | 21 +++++++++++---------- 2 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 Sharp7.Rx/S7ErrorCodes.cs diff --git a/Sharp7.Rx/S7ErrorCodes.cs b/Sharp7.Rx/S7ErrorCodes.cs new file mode 100644 index 0000000..db1b5eb --- /dev/null +++ b/Sharp7.Rx/S7ErrorCodes.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Sharp7.Rx +{ + public static class S7ErrorCodes + { + /// + /// This list is not exhaustive and should be considered work in progress. + /// + private static readonly HashSet notDisconnectedErrorCodes = new HashSet + { + 0x000000, // OK + 0xC00000, // CPU: Item not available + 0x900000, // CPU: Address out of range + }; + + /// + /// 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 + /// the DBs range. In this case the driver should not consider the connection to be lost. + /// + public static bool AssumeConnectionLost(int errorCode) + { + return !notDisconnectedErrorCodes.Contains(errorCode); + } + } +} \ No newline at end of file diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 99c6644..3063471 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -52,7 +52,7 @@ namespace Sharp7.Rx var result = await Task.Factory.StartNew(() => s7MultiVar.Read(), CancellationToken.None, TaskCreationOptions.None, scheduler); if (result != 0) { - await EvaluateErrorCode(result); + EvaluateErrorCode(result); throw new InvalidOperationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)}"); } @@ -88,7 +88,7 @@ namespace Sharp7.Rx try { var errorCode = await Task.Factory.StartNew(() => sharp7.ConnectTo(ipAddress, rackNr, cpuSlotNr), CancellationToken.None, TaskCreationOptions.None, scheduler); - var success = await EvaluateErrorCode(errorCode); + var success = EvaluateErrorCode(errorCode); if (success) { connectionStateSubject.OnNext(Enums.ConnectionState.Connected); @@ -168,7 +168,7 @@ namespace Sharp7.Rx await Task.Factory.StartNew(() => sharp7.Disconnect(), CancellationToken.None, TaskCreationOptions.None, scheduler); } - private async Task EvaluateErrorCode(int errorCode) + private bool EvaluateErrorCode(int errorCode) { if (errorCode == 0) return true; @@ -178,7 +178,9 @@ namespace Sharp7.Rx var errorText = sharp7.ErrorText(errorCode); Logger?.LogError($"Error Code {errorCode} {errorText}"); - await SetConnectionLostState(); + + if (S7ErrorCodes.AssumeConnectionLost(errorCode)) + SetConnectionLostState(); return false; } @@ -190,10 +192,9 @@ namespace Sharp7.Rx return await Connect(); } - private async Task SetConnectionLostState() + private void SetConnectionLostState() { - var state = await connectionStateSubject.FirstAsync(); - if (state == Enums.ConnectionState.ConnectionLost) return; + if (connectionStateSubject.Value == Enums.ConnectionState.ConnectionLost) return; connectionStateSubject.OnNext(Enums.ConnectionState.ConnectionLost); } @@ -219,7 +220,7 @@ namespace Sharp7.Rx if (result != 0) { - await EvaluateErrorCode(result); + EvaluateErrorCode(result); var errorText = sharp7.ErrorText(result); throw new InvalidOperationException($"Error reading {operand}{dBNr}:{startByteAddress}->{bytesToRead} ({errorText})"); } @@ -265,7 +266,7 @@ namespace Sharp7.Rx if (result != 0) { - await EvaluateErrorCode(result); + EvaluateErrorCode(result); return 0; } return (ushort)(data.Length); @@ -283,7 +284,7 @@ namespace Sharp7.Rx if (result != 0) { - await EvaluateErrorCode(result); + EvaluateErrorCode(result); return (false); } return (true); From d678924b6ee4d9f74f32329140b30df08d051492 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 6 Feb 2024 18:21:31 +0100 Subject: [PATCH 16/78] Complete ConnectionState on dispose --- Sharp7.Rx/Sharp7Connector.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 3063471..9b89375 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -153,6 +153,7 @@ namespace Sharp7.Rx sharp7 = null; } + connectionStateSubject?.OnCompleted(); connectionStateSubject?.Dispose(); } From 956f39cc6658fb3e3b4264b6a2cf20c14c7406c1 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 08:51:40 +0100 Subject: [PATCH 17/78] Complete Notifications when Plc is disposed --- .../Basics/ConcurrentSubjectDictionary.cs | 3 +- Sharp7.Rx/Extensions/DisposableExtensions.cs | 8 ---- Sharp7.Rx/Sharp7Plc.cs | 44 +++++++------------ 3 files changed, 17 insertions(+), 38 deletions(-) diff --git a/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs b/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs index fa5184e..041991c 100644 --- a/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs +++ b/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs @@ -85,7 +85,8 @@ namespace Sharp7.Rx.Basics return; if (disposing && dictionary != null) { - dictionary.Values.DisposeItems(); + foreach (var subjectWithRefCounter in dictionary) + subjectWithRefCounter.Value.Subject.OnCompleted(); dictionary.Clear(); dictionary = null; } diff --git a/Sharp7.Rx/Extensions/DisposableExtensions.cs b/Sharp7.Rx/Extensions/DisposableExtensions.cs index 0ad5c06..89f3c75 100644 --- a/Sharp7.Rx/Extensions/DisposableExtensions.cs +++ b/Sharp7.Rx/Extensions/DisposableExtensions.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Reactive.Disposables; namespace Sharp7.Rx.Extensions @@ -11,11 +9,5 @@ namespace Sharp7.Rx.Extensions { compositeDisposable.Add(disposable); } - - public static void DisposeItems(this IEnumerable disposables) - { - foreach (IDisposable disposable in disposables.OfType()) - disposable?.Dispose(); - } } } diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index ca46309..a1a7664 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Reactive.Subjects; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -22,7 +21,6 @@ namespace Sharp7.Rx { private readonly IS7VariableNameParser varaibleNameParser = new CacheVariableNameParser(new S7VariableNameParser()); private bool disposed; - private ISubject disposingSubject = new Subject(); private IS7Connector s7Connector; private readonly PlcConnectionSettings plcConnectionSettings; private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new ConcurrentSubjectDictionary(StringComparer.InvariantCultureIgnoreCase); @@ -182,33 +180,26 @@ namespace Sharp7.Rx public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { - if (!disposed) + if (disposed) return; + disposed = true; + + if (disposing) { - if (disposing) - { - Disposables.Dispose(); + Disposables.Dispose(); - if (disposingSubject != null) - { - disposingSubject.OnNext(Unit.Default); - disposingSubject.OnCompleted(); - var disposable = (disposingSubject as IDisposable); - if (disposable != null) disposable.Dispose(); - disposingSubject = null; - } - if (s7Connector != null) - { - s7Connector.Disconnect().Wait(); - s7Connector.Dispose(); - s7Connector = null; - } + if (s7Connector != null) + { + s7Connector.Disconnect().Wait(); + s7Connector.Dispose(); + s7Connector = null; } - disposed = true; + multiVariableSubscriptions.Dispose(); } } @@ -224,7 +215,6 @@ namespace Sharp7.Rx .SelectMany(connected => GetAllValues(connected, connector)) .RepeatAfterDelay(cycle) .LogAndRetryAfterDelay(Logger, cycle, "Error while getting batch notifications from plc") - .TakeUntil(disposingSubject) .Subscribe(); } @@ -239,15 +229,11 @@ namespace Sharp7.Rx var stopWatch = Stopwatch.StartNew(); foreach (var partsOfMultiVarRequest in multiVariableSubscriptions.ExistingKeys.Buffer(MultiVarRequestMaxItems)) { - var multiVarRequest = await connector.ExecuteMultiVarRequest(partsOfMultiVarRequest as IReadOnlyList??partsOfMultiVarRequest.ToList()); - + var multiVarRequest = await connector.ExecuteMultiVarRequest(partsOfMultiVarRequest as IReadOnlyList); + foreach (var pair in multiVarRequest) - { - if (multiVariableSubscriptions.TryGetObserver(pair.Key, out var subject)) - { + if (multiVariableSubscriptions.TryGetObserver(pair.Key, out var subject)) subject.OnNext(pair.Value); - } - } } stopWatch.Stop(); From 4432f3c0d4fd0e0ec81e64a0ebf30ce272a72387 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 09:30:52 +0100 Subject: [PATCH 18/78] Remove unused cycleTime --- Sharp7.Rx/Extensions/PlcExtensions.cs | 2 +- Sharp7.Rx/Interfaces/IPlc.cs | 2 +- Sharp7.Rx/Sharp7Plc.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sharp7.Rx/Extensions/PlcExtensions.cs b/Sharp7.Rx/Extensions/PlcExtensions.cs index 619dd24..14e3e58 100644 --- a/Sharp7.Rx/Extensions/PlcExtensions.cs +++ b/Sharp7.Rx/Extensions/PlcExtensions.cs @@ -18,7 +18,7 @@ namespace Sharp7.Rx.Extensions var subscriptions = new CompositeDisposable(); var notification = plc - .CreateNotification(triggerAddress, TransmissionMode.OnChange, TimeSpan.Zero) + .CreateNotification(triggerAddress, TransmissionMode.OnChange) .Publish() .RefCount(); diff --git a/Sharp7.Rx/Interfaces/IPlc.cs b/Sharp7.Rx/Interfaces/IPlc.cs index e5c4c89..a438957 100644 --- a/Sharp7.Rx/Interfaces/IPlc.cs +++ b/Sharp7.Rx/Interfaces/IPlc.cs @@ -7,7 +7,7 @@ namespace Sharp7.Rx.Interfaces { public interface IPlc : IDisposable { - IObservable CreateNotification(string variableName, TransmissionMode transmissionMode, TimeSpan cycleSpan); + IObservable CreateNotification(string variableName, TransmissionMode transmissionMode); Task SetValue(string variableName, TValue value); Task GetValue(string variableName); IObservable ConnectionState { get; } diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index a1a7664..fb0a4f4 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -153,7 +153,7 @@ namespace Sharp7.Rx } } - public IObservable CreateNotification(string variableName, TransmissionMode transmissionMode, TimeSpan cycleTime) + public IObservable CreateNotification(string variableName, TransmissionMode transmissionMode) { return Observable.Create(observer => { From c79e07be33231b091933645150f271bbc8a68f70 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 09:31:11 +0100 Subject: [PATCH 19/78] Add multiVarRequestCycleTime parameter --- Sharp7.Rx/Sharp7Plc.cs | 57 +++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index fb0a4f4..887bb6f 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -24,14 +24,34 @@ namespace Sharp7.Rx private IS7Connector s7Connector; private readonly PlcConnectionSettings plcConnectionSettings; private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new ConcurrentSubjectDictionary(StringComparer.InvariantCultureIgnoreCase); - protected readonly CompositeDisposable Disposables = new CompositeDisposable(); + protected readonly CompositeDisposable Disposables = new CompositeDisposable(); private readonly List performanceCoutner = new List(1000); - - public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress, int port = 102) + /// + /// + /// + /// + /// + /// + /// + /// + /// Polling interval used to read multi variable requests from PLC. + /// + /// + /// This is the wait time between two successive reads from PLC and determines the + /// time resolution for all variable reads reated with CreateNotification. + /// + /// + /// Default is 100 ms. The minimum supported time is 5 ms. + /// + /// + 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 }; + + if (multiVarRequestCycleTime != null && multiVarRequestCycleTime > TimeSpan.FromMilliseconds(5)) + MultiVarRequestCycleTime = multiVarRequestCycleTime.Value; } public IObservable ConnectionState { get; private set; } @@ -39,11 +59,11 @@ namespace Sharp7.Rx public async Task InitializeAsync() { - s7Connector = new Sharp7Connector(plcConnectionSettings, varaibleNameParser){Logger = Logger}; + s7Connector = new Sharp7Connector(plcConnectionSettings, varaibleNameParser) { Logger = Logger }; ConnectionState = s7Connector.ConnectionState; await s7Connector.InitializeAsync(); - + #pragma warning disable 4014 Task.Run(async () => { @@ -58,9 +78,9 @@ namespace Sharp7.Rx }); #pragma warning restore 4014 - RunNotifications(s7Connector, TimeSpan.FromMilliseconds(100)) + RunNotifications(s7Connector, MultiVarRequestCycleTime) .AddDisposableTo(Disposables); - + return true; } @@ -70,7 +90,7 @@ namespace Sharp7.Rx } - + public async Task GetValue(string variableName, CancellationToken token) { var address = varaibleNameParser.Parse(variableName); @@ -176,7 +196,7 @@ namespace Sharp7.Rx return disposables; }); } - + public void Dispose() { Dispose(true); @@ -191,7 +211,7 @@ namespace Sharp7.Rx if (disposing) { Disposables.Dispose(); - + if (s7Connector != null) { s7Connector.Disconnect().Wait(); @@ -207,22 +227,22 @@ namespace Sharp7.Rx { Dispose(false); } - + private IDisposable RunNotifications(IS7Connector connector, TimeSpan cycle) { return ConnectionState.FirstAsync() .Select(states => states == Enums.ConnectionState.Connected) .SelectMany(connected => GetAllValues(connected, connector)) - .RepeatAfterDelay(cycle) + .RepeatAfterDelay(MultiVarRequestCycleTime) .LogAndRetryAfterDelay(Logger, cycle, "Error while getting batch notifications from plc") .Subscribe(); } - + private async Task GetAllValues(bool connected, IS7Connector connector) { if (!connected) return Unit.Default; - + if (multiVariableSubscriptions.ExistingKeys.IsEmpty()) return Unit.Default; @@ -230,9 +250,9 @@ namespace Sharp7.Rx foreach (var partsOfMultiVarRequest in multiVariableSubscriptions.ExistingKeys.Buffer(MultiVarRequestMaxItems)) { var multiVarRequest = await connector.ExecuteMultiVarRequest(partsOfMultiVarRequest as IReadOnlyList); - + foreach (var pair in multiVarRequest) - if (multiVariableSubscriptions.TryGetObserver(pair.Key, out var subject)) + if (multiVariableSubscriptions.TryGetObserver(pair.Key, out var subject)) subject.OnNext(pair.Value); } @@ -243,7 +263,7 @@ namespace Sharp7.Rx return Unit.Default; } - + private void PrintAndResetPerformanceStatistik() { if (performanceCoutner.Count == performanceCoutner.Capacity) @@ -259,5 +279,6 @@ namespace Sharp7.Rx } public int MultiVarRequestMaxItems { get; set; } = 16; + public TimeSpan MultiVarRequestCycleTime { get; private set; } = TimeSpan.FromSeconds(0.1); } } From babbb1a6bcee963d27ddfcd4d4f56d1b75960d23 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 09:35:21 +0100 Subject: [PATCH 20/78] Cleanup --- Sharp7.Rx/AssemblyInfo.cs | 3 +- .../Basics/ConcurrentSubjectDictionary.cs | 3 +- Sharp7.Rx/Basics/DisposableItem.cs | 1 - Sharp7.Rx/Enums/ConnectionState.cs | 2 +- Sharp7.Rx/Enums/CpuType.cs | 2 +- Sharp7.Rx/Enums/DbType.cs | 2 +- Sharp7.Rx/Enums/Operand.cs | 2 +- Sharp7.Rx/Enums/TransmissionMode.cs | 2 +- Sharp7.Rx/Extensions/DisposableExtensions.cs | 2 +- Sharp7.Rx/Extensions/ObservableExtensions.cs | 142 +++++---- Sharp7.Rx/Extensions/PlcExtensions.cs | 10 +- Sharp7.Rx/Interfaces/IPlc.cs | 2 + Sharp7.Rx/Interfaces/IS7Connector.cs | 2 + Sharp7.Rx/S7ValueConverter.cs | 4 +- Sharp7.Rx/S7VariableAddress.cs | 4 +- Sharp7.Rx/S7VariableNameParser.cs | 13 +- Sharp7.Rx/Settings/PlcConnectionSettings.cs | 4 +- Sharp7.Rx/Sharp7.Rx.csproj | 1 + Sharp7.Rx/Sharp7Connector.cs | 263 ++++++++-------- Sharp7.Rx/Sharp7Plc.cs | 282 +++++++++--------- 20 files changed, 372 insertions(+), 374 deletions(-) diff --git a/Sharp7.Rx/AssemblyInfo.cs b/Sharp7.Rx/AssemblyInfo.cs index 9d8031c..e92998f 100644 --- a/Sharp7.Rx/AssemblyInfo.cs +++ b/Sharp7.Rx/AssemblyInfo.cs @@ -1,4 +1,3 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Sharp7.Rx.Tests")] - +[assembly: InternalsVisibleTo("Sharp7.Rx.Tests")] \ No newline at end of file diff --git a/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs b/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs index 041991c..d831707 100644 --- a/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs +++ b/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; -using Sharp7.Rx.Extensions; namespace Sharp7.Rx.Basics { @@ -85,7 +84,7 @@ namespace Sharp7.Rx.Basics return; if (disposing && dictionary != null) { - foreach (var subjectWithRefCounter in dictionary) + foreach (var subjectWithRefCounter in dictionary) subjectWithRefCounter.Value.Subject.OnCompleted(); dictionary.Clear(); dictionary = null; diff --git a/Sharp7.Rx/Basics/DisposableItem.cs b/Sharp7.Rx/Basics/DisposableItem.cs index f6ab006..29108a4 100644 --- a/Sharp7.Rx/Basics/DisposableItem.cs +++ b/Sharp7.Rx/Basics/DisposableItem.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; namespace Sharp7.Rx.Basics { diff --git a/Sharp7.Rx/Enums/ConnectionState.cs b/Sharp7.Rx/Enums/ConnectionState.cs index acf93b6..31e54ac 100644 --- a/Sharp7.Rx/Enums/ConnectionState.cs +++ b/Sharp7.Rx/Enums/ConnectionState.cs @@ -7,4 +7,4 @@ DisconnectedByUser, ConnectionLost } -} +} \ No newline at end of file diff --git a/Sharp7.Rx/Enums/CpuType.cs b/Sharp7.Rx/Enums/CpuType.cs index 28cd172..ad0a679 100644 --- a/Sharp7.Rx/Enums/CpuType.cs +++ b/Sharp7.Rx/Enums/CpuType.cs @@ -7,4 +7,4 @@ S7_1200, S7_1500 } -} +} \ No newline at end of file diff --git a/Sharp7.Rx/Enums/DbType.cs b/Sharp7.Rx/Enums/DbType.cs index 66116c8..8a134ec 100644 --- a/Sharp7.Rx/Enums/DbType.cs +++ b/Sharp7.Rx/Enums/DbType.cs @@ -10,4 +10,4 @@ DInteger, ULong } -} +} \ No newline at end of file diff --git a/Sharp7.Rx/Enums/Operand.cs b/Sharp7.Rx/Enums/Operand.cs index 79ed488..4f6a687 100644 --- a/Sharp7.Rx/Enums/Operand.cs +++ b/Sharp7.Rx/Enums/Operand.cs @@ -7,4 +7,4 @@ Marker = 77, Db = 68, } -} +} \ No newline at end of file diff --git a/Sharp7.Rx/Enums/TransmissionMode.cs b/Sharp7.Rx/Enums/TransmissionMode.cs index 2e9304a..c33ef62 100644 --- a/Sharp7.Rx/Enums/TransmissionMode.cs +++ b/Sharp7.Rx/Enums/TransmissionMode.cs @@ -5,4 +5,4 @@ Cyclic = 3, OnChange = 4, } -} +} \ No newline at end of file diff --git a/Sharp7.Rx/Extensions/DisposableExtensions.cs b/Sharp7.Rx/Extensions/DisposableExtensions.cs index 89f3c75..6f68c0f 100644 --- a/Sharp7.Rx/Extensions/DisposableExtensions.cs +++ b/Sharp7.Rx/Extensions/DisposableExtensions.cs @@ -10,4 +10,4 @@ namespace Sharp7.Rx.Extensions compositeDisposable.Add(disposable); } } -} +} \ No newline at end of file diff --git a/Sharp7.Rx/Extensions/ObservableExtensions.cs b/Sharp7.Rx/Extensions/ObservableExtensions.cs index 052b6ee..8cdc26e 100644 --- a/Sharp7.Rx/Extensions/ObservableExtensions.cs +++ b/Sharp7.Rx/Extensions/ObservableExtensions.cs @@ -1,91 +1,83 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Linq.Expressions; -using System.Reactive; using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Reactive.Subjects; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Sharp7.Rx.Resources; namespace Sharp7.Rx.Extensions { - internal static class ObservableExtensions - { - public static IObservable LogAndRetry(this IObservable source, ILogger logger, string message) - { - return source - .Do( - _ => { }, - ex => logger?.LogError(ex, message)) - .Retry(); - } + internal static class ObservableExtensions + { + public static IObservable DisposeMany(this IObservable source) + { + return Observable.Create(obs => + { + var serialDisposable = new SerialDisposable(); + var subscription = + source.Subscribe( + item => + { + serialDisposable.Disposable = item as IDisposable; + obs.OnNext(item); + }, + obs.OnError, + obs.OnCompleted); + return new CompositeDisposable(serialDisposable, subscription); + }); + } - public static IObservable RetryAfterDelay( - this IObservable source, - TimeSpan retryDelay, - int retryCount = -1, - IScheduler scheduler = null) - { - return RedoAfterDelay(source, retryDelay, retryCount, scheduler, Observable.Retry, Observable.Retry); - } + public static IObservable LogAndRetry(this IObservable source, ILogger logger, string message) + { + return source + .Do( + _ => { }, + ex => logger?.LogError(ex, message)) + .Retry(); + } - public static IObservable RepeatAfterDelay( - this IObservable source, - TimeSpan retryDelay, - int repeatCount = -1, - IScheduler scheduler = null) - { - return RedoAfterDelay(source, retryDelay, repeatCount, scheduler, Observable.Repeat, Observable.Repeat); - } + public static IObservable LogAndRetryAfterDelay( + this IObservable source, + ILogger logger, + TimeSpan retryDelay, + string message, + int retryCount = -1, + IScheduler scheduler = null) + { + var sourceLogged = + source + .Do( + _ => { }, + ex => logger?.LogError(ex, message)); - public static IObservable LogAndRetryAfterDelay( - this IObservable source, - ILogger logger, - TimeSpan retryDelay, - string message, - int retryCount = -1, - IScheduler scheduler = null) - { - var sourceLogged = - source - .Do( - _ => { }, - ex => logger?.LogError(ex, message)); + return RetryAfterDelay(sourceLogged, retryDelay, retryCount, scheduler); + } - return RetryAfterDelay(sourceLogged, retryDelay, retryCount, scheduler); - } + public static IObservable RepeatAfterDelay( + this IObservable source, + TimeSpan retryDelay, + int repeatCount = -1, + IScheduler scheduler = null) + { + return RedoAfterDelay(source, retryDelay, repeatCount, scheduler, Observable.Repeat, Observable.Repeat); + } - private static IObservable RedoAfterDelay(IObservable source, TimeSpan retryDelay, int retryCount, IScheduler scheduler, Func, IObservable> reDo, - Func, int, IObservable> reDoCount) - { - scheduler = scheduler ?? TaskPoolScheduler.Default; - var attempt = 0; + public static IObservable RetryAfterDelay( + this IObservable source, + TimeSpan retryDelay, + int retryCount = -1, + IScheduler scheduler = null) + { + return RedoAfterDelay(source, retryDelay, retryCount, scheduler, Observable.Retry, Observable.Retry); + } - var deferedObs = Observable.Defer(() => ((++attempt == 1) ? source : source.DelaySubscription(retryDelay, scheduler))); - return retryCount > 0 ? reDoCount(deferedObs, retryCount) : reDo(deferedObs); - } + private static IObservable RedoAfterDelay(IObservable source, TimeSpan retryDelay, int retryCount, IScheduler scheduler, Func, IObservable> reDo, + Func, int, IObservable> reDoCount) + { + scheduler = scheduler ?? TaskPoolScheduler.Default; + var attempt = 0; - public static IObservable DisposeMany(this IObservable source) - { - return Observable.Create(obs => - { - var serialDisposable = new SerialDisposable(); - var subscription = - source.Subscribe( - item => - { - serialDisposable.Disposable = item as IDisposable; - obs.OnNext(item); - }, - obs.OnError, - obs.OnCompleted); - return new CompositeDisposable(serialDisposable, subscription); - }); - } - } + var deferedObs = Observable.Defer(() => ((++attempt == 1) ? source : source.DelaySubscription(retryDelay, scheduler))); + return retryCount > 0 ? reDoCount(deferedObs, retryCount) : reDo(deferedObs); + } + } } \ No newline at end of file diff --git a/Sharp7.Rx/Extensions/PlcExtensions.cs b/Sharp7.Rx/Extensions/PlcExtensions.cs index 14e3e58..2cfbc85 100644 --- a/Sharp7.Rx/Extensions/PlcExtensions.cs +++ b/Sharp7.Rx/Extensions/PlcExtensions.cs @@ -38,10 +38,10 @@ namespace Sharp7.Rx.Extensions notification .Where(trigger => !trigger) .SelectMany(async _ => - { - await plc.SetValue(ackTriggerAddress, false); - return Unit.Default; - }) + { + await plc.SetValue(ackTriggerAddress, false); + return Unit.Default; + }) .Subscribe() .AddDisposableTo(subscriptions); @@ -71,4 +71,4 @@ namespace Sharp7.Rx.Extensions } } } -} +} \ No newline at end of file diff --git a/Sharp7.Rx/Interfaces/IPlc.cs b/Sharp7.Rx/Interfaces/IPlc.cs index a438957..683aed9 100644 --- a/Sharp7.Rx/Interfaces/IPlc.cs +++ b/Sharp7.Rx/Interfaces/IPlc.cs @@ -1,10 +1,12 @@ using System; using System.Threading.Tasks; +using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Sharp7.Rx.Enums; namespace Sharp7.Rx.Interfaces { + [NoReorder] public interface IPlc : IDisposable { IObservable CreateNotification(string variableName, TransmissionMode transmissionMode); diff --git a/Sharp7.Rx/Interfaces/IS7Connector.cs b/Sharp7.Rx/Interfaces/IS7Connector.cs index 309f750..10aa850 100644 --- a/Sharp7.Rx/Interfaces/IS7Connector.cs +++ b/Sharp7.Rx/Interfaces/IS7Connector.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using Sharp7.Rx.Enums; namespace Sharp7.Rx.Interfaces { + [NoReorder] internal interface IS7Connector : IDisposable { IObservable ConnectionState { get; } diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index d97fa31..fb6f985 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -35,7 +35,7 @@ namespace Sharp7.Rx if (typeof(TValue) == typeof(byte)) return (TValue) (object) buffer[0]; if (typeof(TValue) == typeof(char)) - return (TValue) (object) (char)buffer[0]; + return (TValue) (object) (char) buffer[0]; if (typeof(TValue) == typeof(byte[])) return (TValue) (object) buffer; @@ -46,7 +46,7 @@ namespace Sharp7.Rx { UInt32 = BinaryPrimitives.ReadUInt32BigEndian(buffer) }; - return (TValue) (object) (double)d.Single; + return (TValue) (object) (double) d.Single; } if (typeof(TValue) == typeof(float)) diff --git a/Sharp7.Rx/S7VariableAddress.cs b/Sharp7.Rx/S7VariableAddress.cs index ef2fb5a..7c751f7 100644 --- a/Sharp7.Rx/S7VariableAddress.cs +++ b/Sharp7.Rx/S7VariableAddress.cs @@ -1,7 +1,9 @@ -using Sharp7.Rx.Enums; +using JetBrains.Annotations; +using Sharp7.Rx.Enums; namespace Sharp7.Rx { + [NoReorder] internal class S7VariableAddress { public Operand Operand { get; set; } diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index c898537..df34b3a 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text.RegularExpressions; using Sharp7.Rx.Enums; using Sharp7.Rx.Interfaces; @@ -25,9 +24,9 @@ namespace Sharp7.Rx {"dint", DbType.DInteger}, {"w", DbType.Integer}, {"dbw", DbType.Integer}, - {"dul", DbType.ULong }, - {"dulint", DbType.ULong }, - {"dulong", DbType.ULong } + {"dul", DbType.ULong}, + {"dulint", DbType.ULong}, + {"dulong", DbType.ULong} }; public S7VariableAddress Parse(string input) @@ -35,7 +34,7 @@ namespace Sharp7.Rx var match = regex.Match(input); if (match.Success) { - var operand = (Operand)Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true); + var operand = (Operand) Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true); var dbNr = ushort.Parse(match.Groups["dbNr"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture); var start = ushort.Parse(match.Groups["start"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture); if (!types.TryGetValue(match.Groups["type"].Value, out var type)) @@ -57,10 +56,10 @@ namespace Sharp7.Rx s7VariableAddress.Bit = byte.Parse(match.Groups["bitOrLength"].Value); break; case DbType.Byte: - s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort)1; + 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; + s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort) 0; break; case DbType.Integer: s7VariableAddress.Length = 2; diff --git a/Sharp7.Rx/Settings/PlcConnectionSettings.cs b/Sharp7.Rx/Settings/PlcConnectionSettings.cs index b4b5f85..8adffbf 100644 --- a/Sharp7.Rx/Settings/PlcConnectionSettings.cs +++ b/Sharp7.Rx/Settings/PlcConnectionSettings.cs @@ -2,9 +2,9 @@ { internal class PlcConnectionSettings { - public string IpAddress { get; set; } - public int RackNumber { get; set; } public int CpuMpiAddress { get; set; } + public string IpAddress { get; set; } public int Port { get; set; } + public int RackNumber { get; set; } } } \ No newline at end of file diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 329b3d9..4e36f88 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -14,6 +14,7 @@ + diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 9b89375..b983c46 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -18,62 +18,39 @@ namespace Sharp7.Rx { internal class Sharp7Connector : IS7Connector { - private readonly IS7VariableNameParser variableNameParser; private readonly BehaviorSubject connectionStateSubject = new BehaviorSubject(Enums.ConnectionState.Initial); + private readonly int cpuSlotNr; private readonly CompositeDisposable disposables = new CompositeDisposable(); - private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism:1); private readonly string ipAddress; + private readonly int port; private readonly int rackNr; - private readonly int cpuSlotNr; - private readonly int port; - - private S7Client sharp7; + private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism: 1); + private readonly IS7VariableNameParser variableNameParser; private bool disposed; - public ILogger Logger { get; set; } - public async Task> ExecuteMultiVarRequest(IReadOnlyList variableNames) - { - if (variableNames.IsEmpty()) - return new Dictionary(); + private S7Client sharp7; - var s7MultiVar = new S7MultiVar(sharp7); - var buffers = variableNames - .Select(key => new {VariableName = key, Address = variableNameParser.Parse(key)}) - .Select(x => - { - var buffer = new byte[x.Address.Length]; - s7MultiVar.Add(S7Consts.S7AreaDB, S7Consts.S7WLByte, x.Address.DbNr, x.Address.Start,x.Address.Length, ref buffer); - return new { x.VariableName, Buffer = buffer}; - }) - .ToArray(); - - var result = await Task.Factory.StartNew(() => s7MultiVar.Read(), CancellationToken.None, TaskCreationOptions.None, scheduler); - if (result != 0) - { - EvaluateErrorCode(result); - throw new InvalidOperationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)}"); - } - - return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer); - } - - - public Sharp7Connector(PlcConnectionSettings settings, IS7VariableNameParser variableNameParser) - { + { this.variableNameParser = variableNameParser; ipAddress = settings.IpAddress; cpuSlotNr = settings.CpuMpiAddress; - port = settings.Port; - rackNr = settings.RackNumber; + port = settings.Port; + rackNr = settings.RackNumber; - ReconnectDelay = TimeSpan.FromSeconds(5); + ReconnectDelay = TimeSpan.FromSeconds(5); } + public IObservable ConnectionState => connectionStateSubject.DistinctUntilChanged().AsObservable(); + + public ILogger Logger { get; set; } + public TimeSpan ReconnectDelay { get; set; } + private bool IsConnected => connectionStateSubject.Value == Enums.ConnectionState.Connected; + public void Dispose() { Dispose(true); @@ -103,8 +80,6 @@ namespace Sharp7.Rx return false; } - public IObservable ConnectionState => connectionStateSubject.DistinctUntilChanged().AsObservable(); - public async Task Disconnect() { @@ -112,14 +87,41 @@ namespace Sharp7.Rx await CloseConnection(); } + public async Task> ExecuteMultiVarRequest(IReadOnlyList variableNames) + { + if (variableNames.IsEmpty()) + return new Dictionary(); + + var s7MultiVar = new S7MultiVar(sharp7); + + var buffers = variableNames + .Select(key => new {VariableName = key, Address = variableNameParser.Parse(key)}) + .Select(x => + { + var buffer = new byte[x.Address.Length]; + s7MultiVar.Add(S7Consts.S7AreaDB, S7Consts.S7WLByte, x.Address.DbNr, x.Address.Start, x.Address.Length, ref buffer); + return new {x.VariableName, Buffer = buffer}; + }) + .ToArray(); + + var result = await Task.Factory.StartNew(() => s7MultiVar.Read(), CancellationToken.None, TaskCreationOptions.None, scheduler); + if (result != 0) + { + EvaluateErrorCode(result); + throw new InvalidOperationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)}"); + } + + return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer); + } + public Task InitializeAsync() { try { sharp7 = new S7Client(); - sharp7.PLCPort = port; + sharp7.PLCPort = port; - var subscription = + var subscription = ConnectionState .Where(state => state == Enums.ConnectionState.ConnectionLost) .Take(1) @@ -132,12 +134,70 @@ namespace Sharp7.Rx } catch (Exception ex) { - Logger?.LogError(ex, StringResources.StrErrorS7DriverCouldNotBeInitialized); - } + Logger?.LogError(ex, StringResources.StrErrorS7DriverCouldNotBeInitialized); + } return Task.FromResult(true); } + public async Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token) + { + EnsureConnectionValid(); + + var buffer = new byte[bytesToRead]; + + var area = FromOperand(operand); + + var result = + await Task.Factory.StartNew(() => sharp7.ReadArea(area, dBNr, startByteAddress, bytesToRead, S7Consts.S7WLByte, buffer), token, TaskCreationOptions.None, scheduler); + token.ThrowIfCancellationRequested(); + + if (result != 0) + { + EvaluateErrorCode(result); + var errorText = sharp7.ErrorText(result); + throw new InvalidOperationException($"Error reading {operand}{dBNr}:{startByteAddress}->{bytesToRead} ({errorText})"); + } + + return buffer; + } + + public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token) + { + EnsureConnectionValid(); + + var buffer = new[] {value ? (byte) 0xff : (byte) 0}; + + var offsetStart = (startByteAddress * 8) + bitAdress; + + var result = await Task.Factory.StartNew(() => sharp7.WriteArea(FromOperand(operand), dbNr, offsetStart, 1, S7Consts.S7WLBit, buffer), token, TaskCreationOptions.None, scheduler); + token.ThrowIfCancellationRequested(); + + if (result != 0) + { + EvaluateErrorCode(result); + return (false); + } + + return (true); + } + + public async Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token) + { + EnsureConnectionValid(); + + var result = await Task.Factory.StartNew(() => sharp7.WriteArea(FromOperand(operand), dBNr, startByteAdress, data.Length, S7Consts.S7WLByte, data), token, TaskCreationOptions.None, scheduler); + token.ThrowIfCancellationRequested(); + + if (result != 0) + { + EvaluateErrorCode(result); + return 0; + } + + return (ushort) (data.Length); + } + protected virtual void Dispose(bool disposing) { @@ -169,6 +229,18 @@ namespace Sharp7.Rx await Task.Factory.StartNew(() => sharp7.Disconnect(), CancellationToken.None, TaskCreationOptions.None, scheduler); } + private void EnsureConnectionValid() + { + if (disposed) + throw new ObjectDisposedException("S7Connector"); + + if (sharp7 == null) + throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + + if (!IsConnected) + throw new InvalidOperationException("Plc is not connected"); + } + private bool EvaluateErrorCode(int errorCode) { if (errorCode == 0) @@ -186,6 +258,23 @@ namespace Sharp7.Rx return false; } + private int FromOperand(Operand operand) + { + switch (operand) + { + case Operand.Input: + return S7Consts.S7AreaPE; + case Operand.Output: + return S7Consts.S7AreaPA; + case Operand.Marker: + return S7Consts.S7AreaMK; + case Operand.Db: + return S7Consts.S7AreaDB; + default: + throw new ArgumentOutOfRangeException(nameof(operand), operand, null); + } + } + private async Task Reconnect() { await CloseConnection(); @@ -204,91 +293,5 @@ namespace Sharp7.Rx { Dispose(false); } - - private bool IsConnected => connectionStateSubject.Value == Enums.ConnectionState.Connected; - - public async Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token) - { - EnsureConnectionValid(); - - var buffer = new byte[bytesToRead]; - - var area = FromOperand(operand); - - var result = - await Task.Factory.StartNew(() => sharp7.ReadArea(area, dBNr, startByteAddress, bytesToRead, S7Consts.S7WLByte, buffer), token, TaskCreationOptions.None, scheduler); - token.ThrowIfCancellationRequested(); - - if (result != 0) - { - EvaluateErrorCode(result); - var errorText = sharp7.ErrorText(result); - throw new InvalidOperationException($"Error reading {operand}{dBNr}:{startByteAddress}->{bytesToRead} ({errorText})"); - } - - return buffer; - } - - private int FromOperand(Operand operand) - { - switch (operand) - { - case Operand.Input: - return S7Consts.S7AreaPE; - case Operand.Output: - return S7Consts.S7AreaPA; - case Operand.Marker: - return S7Consts.S7AreaMK; - case Operand.Db: - return S7Consts.S7AreaDB; - default: - throw new ArgumentOutOfRangeException(nameof(operand), operand, null); - } - } - - private void EnsureConnectionValid() - { - if (disposed) - throw new ObjectDisposedException("S7Connector"); - - if (sharp7 == null) - throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); - - if (!IsConnected) - throw new InvalidOperationException("Plc is not connected"); - } - - public async Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token) - { - EnsureConnectionValid(); - - var result = await Task.Factory.StartNew(() => sharp7.WriteArea(FromOperand(operand), dBNr, startByteAdress, data.Length, S7Consts.S7WLByte, data), token, TaskCreationOptions.None, scheduler); - token.ThrowIfCancellationRequested(); - - if (result != 0) - { - EvaluateErrorCode(result); - return 0; - } - return (ushort)(data.Length); - } - public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token) - { - EnsureConnectionValid(); - - var buffer = new byte[] { value ? (byte)0xff : (byte)0 }; - - var offsetStart = (startByteAddress * 8) + bitAdress; - - var result = await Task.Factory.StartNew(() => sharp7.WriteArea(FromOperand(operand), dbNr, offsetStart, 1, S7Consts.S7WLBit, buffer), token, TaskCreationOptions.None, scheduler); - token.ThrowIfCancellationRequested(); - - if (result != 0) - { - EvaluateErrorCode(result); - return (false); - } - return (true); - } } } \ No newline at end of file diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 887bb6f..9902224 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -19,13 +19,13 @@ namespace Sharp7.Rx { public class Sharp7Plc : IPlc { + protected readonly CompositeDisposable Disposables = new CompositeDisposable(); + private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new ConcurrentSubjectDictionary(StringComparer.InvariantCultureIgnoreCase); + private readonly List performanceCoutner = new List(1000); + private readonly PlcConnectionSettings plcConnectionSettings; private readonly IS7VariableNameParser varaibleNameParser = new CacheVariableNameParser(new S7VariableNameParser()); private bool disposed; private IS7Connector s7Connector; - private readonly PlcConnectionSettings plcConnectionSettings; - private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new ConcurrentSubjectDictionary(StringComparer.InvariantCultureIgnoreCase); - protected readonly CompositeDisposable Disposables = new CompositeDisposable(); - private readonly List performanceCoutner = new List(1000); /// @@ -48,7 +48,7 @@ namespace Sharp7.Rx /// 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}; if (multiVarRequestCycleTime != null && multiVarRequestCycleTime > TimeSpan.FromMilliseconds(5)) MultiVarRequestCycleTime = multiVarRequestCycleTime.Value; @@ -56,121 +56,14 @@ namespace Sharp7.Rx public IObservable ConnectionState { get; private set; } public ILogger Logger { get; set; } + public TimeSpan MultiVarRequestCycleTime { get; } = TimeSpan.FromSeconds(0.1); - public async Task InitializeAsync() + public int MultiVarRequestMaxItems { get; set; } = 16; + + public void Dispose() { - s7Connector = new Sharp7Connector(plcConnectionSettings, varaibleNameParser) { Logger = Logger }; - ConnectionState = s7Connector.ConnectionState; - - await s7Connector.InitializeAsync(); - -#pragma warning disable 4014 - Task.Run(async () => - { - try - { - await s7Connector.Connect(); - } - catch (Exception e) - { - Logger?.LogError(e, "Error while connecting to PLC"); - } - }); -#pragma warning restore 4014 - - RunNotifications(s7Connector, MultiVarRequestCycleTime) - .AddDisposableTo(Disposables); - - return true; - } - - public Task GetValue(string variableName) - { - return GetValue(variableName, CancellationToken.None); - } - - - - public async Task GetValue(string variableName, CancellationToken token) - { - var address = varaibleNameParser.Parse(variableName); - if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName)); - - var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); - return S7ValueConverter.ConvertToType(data, address); - } - - - public Task SetValue(string variableName, TValue value) - { - return SetValue(variableName, value, CancellationToken.None); - } - - public async Task SetValue(string variableName, TValue value, CancellationToken token) - { - var address = varaibleNameParser.Parse(variableName); - if (address == null) throw new ArgumentException("Input variable name is not valid", "variableName"); - - if (typeof(TValue) == typeof(bool)) - { - await s7Connector.WriteBit(address.Operand, address.Start, address.Bit, (bool)(object)value, address.DbNr, token); - } - else if (typeof(TValue) == typeof(int) || typeof(TValue) == typeof(short)) - { - byte[] bytes; - if (address.Length == 4) - bytes = BitConverter.GetBytes((int)(object)value); - else - bytes = BitConverter.GetBytes((short)(object)value); - - Array.Reverse(bytes); - - await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); - } - else if (typeof(TValue) == typeof(byte) || typeof(TValue) == typeof(char)) - { - var bytes = new[] { Convert.ToByte(value) }; - await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); - } - else if (typeof(TValue) == typeof(byte[])) - { - await s7Connector.WriteBytes(address.Operand, address.Start, (byte[])(object)value, address.DbNr, token); - } - else if (typeof(TValue) == typeof(float)) - { - var buffer = new byte[sizeof(float)]; - S7.SetRealAt(buffer, 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 - { - throw new InvalidOperationException($"type '{typeof(TValue)}' not supported."); - } + Dispose(true); + GC.SuppressFinalize(this); } public IObservable CreateNotification(string variableName, TransmissionMode transmissionMode) @@ -197,10 +90,119 @@ namespace Sharp7.Rx }); } - public void Dispose() + public Task GetValue(string variableName) { - Dispose(true); - GC.SuppressFinalize(this); + return GetValue(variableName, CancellationToken.None); + } + + + public Task SetValue(string variableName, TValue value) + { + return SetValue(variableName, value, CancellationToken.None); + } + + + public async Task GetValue(string variableName, CancellationToken token) + { + var address = varaibleNameParser.Parse(variableName); + if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName)); + + var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); + return S7ValueConverter.ConvertToType(data, address); + } + + public async Task InitializeAsync() + { + s7Connector = new Sharp7Connector(plcConnectionSettings, varaibleNameParser) {Logger = Logger}; + ConnectionState = s7Connector.ConnectionState; + + await s7Connector.InitializeAsync(); + +#pragma warning disable 4014 + Task.Run(async () => + { + try + { + await s7Connector.Connect(); + } + catch (Exception e) + { + Logger?.LogError(e, "Error while connecting to PLC"); + } + }); +#pragma warning restore 4014 + + RunNotifications(s7Connector, MultiVarRequestCycleTime) + .AddDisposableTo(Disposables); + + return true; + } + + public async Task SetValue(string variableName, TValue value, CancellationToken token) + { + var address = varaibleNameParser.Parse(variableName); + if (address == null) throw new ArgumentException("Input variable name is not valid", "variableName"); + + if (typeof(TValue) == typeof(bool)) + { + await s7Connector.WriteBit(address.Operand, address.Start, address.Bit, (bool) (object) value, address.DbNr, token); + } + else if (typeof(TValue) == typeof(int) || typeof(TValue) == typeof(short)) + { + byte[] bytes; + if (address.Length == 4) + bytes = BitConverter.GetBytes((int) (object) value); + else + bytes = BitConverter.GetBytes((short) (object) value); + + Array.Reverse(bytes); + + await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); + } + else if (typeof(TValue) == typeof(byte) || typeof(TValue) == typeof(char)) + { + var bytes = new[] {Convert.ToByte(value)}; + await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); + } + else if (typeof(TValue) == typeof(byte[])) + { + await s7Connector.WriteBytes(address.Operand, address.Start, (byte[]) (object) value, address.DbNr, token); + } + else if (typeof(TValue) == typeof(float)) + { + var buffer = new byte[sizeof(float)]; + buffer.SetRealAt(0, (float) (object) value); + await s7Connector.WriteBytes(address.Operand, address.Start, buffer, address.DbNr, token); + } + else if (typeof(TValue) == typeof(string)) + { + var stringValue = value as string; + if (stringValue == null) throw new ArgumentException("Value must be of type string", "value"); + + var bytes = Encoding.ASCII.GetBytes(stringValue); + Array.Resize(ref bytes, address.Length); + + if (address.Type == DbType.String) + { + var bytesWritten = await s7Connector.WriteBytes(address.Operand, address.Start, new[] {(byte) address.Length, (byte) bytes.Length}, address.DbNr, token); + token.ThrowIfCancellationRequested(); + if (bytesWritten == 2) + { + var stringStartAddress = (ushort) (address.Start + 2); + token.ThrowIfCancellationRequested(); + await s7Connector.WriteBytes(address.Operand, stringStartAddress, bytes, address.DbNr, token); + } + } + else + { + await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); + token.ThrowIfCancellationRequested(); + } + } + else + { + throw new InvalidOperationException($"type '{typeof(TValue)}' not supported."); + } } protected virtual void Dispose(bool disposing) @@ -223,21 +225,6 @@ namespace Sharp7.Rx } } - ~Sharp7Plc() - { - Dispose(false); - } - - private IDisposable RunNotifications(IS7Connector connector, TimeSpan cycle) - { - return ConnectionState.FirstAsync() - .Select(states => states == Enums.ConnectionState.Connected) - .SelectMany(connected => GetAllValues(connected, connector)) - .RepeatAfterDelay(MultiVarRequestCycleTime) - .LogAndRetryAfterDelay(Logger, cycle, "Error while getting batch notifications from plc") - .Subscribe(); - } - private async Task GetAllValues(bool connected, IS7Connector connector) { if (!connected) @@ -272,13 +259,26 @@ namespace Sharp7.Rx var min = performanceCoutner.Min(); var max = performanceCoutner.Max(); - Logger?.LogTrace("Performance statistic during {0} elements of plc notification. Min: {1}, Max: {2}, Average: {3}, Plc: '{4}', Number of variables: {5}, Batch size: {6}", performanceCoutner.Capacity, min, max, average, plcConnectionSettings.IpAddress, multiVariableSubscriptions.ExistingKeys.Count(), - MultiVarRequestMaxItems); + Logger?.LogTrace("Performance statistic during {0} elements of plc notification. Min: {1}, Max: {2}, Average: {3}, Plc: '{4}', Number of variables: {5}, Batch size: {6}", performanceCoutner.Capacity, min, max, average, plcConnectionSettings.IpAddress, + multiVariableSubscriptions.ExistingKeys.Count(), + MultiVarRequestMaxItems); performanceCoutner.Clear(); } } - public int MultiVarRequestMaxItems { get; set; } = 16; - public TimeSpan MultiVarRequestCycleTime { get; private set; } = TimeSpan.FromSeconds(0.1); + private IDisposable RunNotifications(IS7Connector connector, TimeSpan cycle) + { + return ConnectionState.FirstAsync() + .Select(states => states == Enums.ConnectionState.Connected) + .SelectMany(connected => GetAllValues(connected, connector)) + .RepeatAfterDelay(MultiVarRequestCycleTime) + .LogAndRetryAfterDelay(Logger, cycle, "Error while getting batch notifications from plc") + .Subscribe(); + } + + ~Sharp7Plc() + { + Dispose(false); + } } -} +} \ No newline at end of file From 662ba7cb6b2ce9e6e636b87a8851168b7d651c42 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 09:36:20 +0100 Subject: [PATCH 21/78] Add .editorconfig --- .editorconfig | 20 ++++++++++++++++++++ Sharp7.Rx/Sharp7.Rx.csproj | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..23fd735 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Baseline +[*] +charset = utf-8-bom +indent_style = space +indent_size = 4 +tab_width = 4 +trim_trailing_whitespace = true +max_line_length = 200 +insert_final_newline = true + +[*.{json,xml,csproj,config}] +indent_size = 2 + +[Directory.*.props] +indent_size = 2 diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 4e36f88..11fce0d 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -14,7 +14,7 @@ - + From d6dafebfe7092b99554a7499afda69a9f91c119b Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 09:40:16 +0100 Subject: [PATCH 22/78] Set langversion to 12 and enable nullables --- Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj | 30 +++++++++++++++----------- Sharp7.Rx/Sharp7.Rx.csproj | 6 ++++++ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj b/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj index 8eb3805..d1de65b 100644 --- a/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj +++ b/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj @@ -1,18 +1,22 @@  - - net8.0 - + + net8.0 + 12.0 + enable + enable + latest-Recommended + - - - - - - + + + + + + - - - + + + - \ No newline at end of file + diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 11fce0d..3b73209 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -2,6 +2,12 @@ netstandard2.0 + + 12.0 + disable + enable + latest-Recommended + true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb evopro system engineering AG From bacfbf05e24c70dcca6f94d4de834cc5a89c8e0b Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 09:53:43 +0100 Subject: [PATCH 23/78] Move ToArea to extension method --- Sharp7.Rx/Extensions/OperandExtensions.cs | 17 +++++++++++++++ Sharp7.Rx/Sharp7Connector.cs | 26 ++++------------------- 2 files changed, 21 insertions(+), 22 deletions(-) create mode 100644 Sharp7.Rx/Extensions/OperandExtensions.cs diff --git a/Sharp7.Rx/Extensions/OperandExtensions.cs b/Sharp7.Rx/Extensions/OperandExtensions.cs new file mode 100644 index 0000000..c78cfa5 --- /dev/null +++ b/Sharp7.Rx/Extensions/OperandExtensions.cs @@ -0,0 +1,17 @@ +using Sharp7.Rx.Enums; + +namespace Sharp7.Rx.Extensions; + +internal static class OperandExtensions +{ + public static S7Area ToArea(this Operand operand) => + operand switch + { + Operand.Input => S7Area.PE, + Operand.Output => S7Area.PA, + Operand.Marker => S7Area.MK, + Operand.Db => S7Area.DB, + _ => throw new ArgumentOutOfRangeException(nameof(operand), operand, null) + }; + +} diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index b983c46..7adfa2e 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -146,10 +146,9 @@ namespace Sharp7.Rx var buffer = new byte[bytesToRead]; - var area = FromOperand(operand); var result = - await Task.Factory.StartNew(() => sharp7.ReadArea(area, dBNr, startByteAddress, bytesToRead, S7Consts.S7WLByte, buffer), token, TaskCreationOptions.None, scheduler); + await Task.Factory.StartNew(() => sharp7.ReadArea(operand.ToArea(), dBNr, startByteAddress, bytesToRead, S7WordLength.Byte, buffer), token, TaskCreationOptions.None, scheduler); token.ThrowIfCancellationRequested(); if (result != 0) @@ -170,7 +169,7 @@ namespace Sharp7.Rx var offsetStart = (startByteAddress * 8) + bitAdress; - var result = await Task.Factory.StartNew(() => sharp7.WriteArea(FromOperand(operand), dbNr, offsetStart, 1, S7Consts.S7WLBit, buffer), token, TaskCreationOptions.None, scheduler); + var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dbNr, offsetStart, 1, S7WordLength.Bit, buffer), token, TaskCreationOptions.None, scheduler); token.ThrowIfCancellationRequested(); if (result != 0) @@ -186,7 +185,7 @@ namespace Sharp7.Rx { EnsureConnectionValid(); - var result = await Task.Factory.StartNew(() => sharp7.WriteArea(FromOperand(operand), dBNr, startByteAdress, data.Length, S7Consts.S7WLByte, data), token, TaskCreationOptions.None, scheduler); + var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dBNr, startByteAdress, data.Length, S7WordLength.Byte, data), token, TaskCreationOptions.None, scheduler); token.ThrowIfCancellationRequested(); if (result != 0) @@ -258,23 +257,6 @@ namespace Sharp7.Rx return false; } - private int FromOperand(Operand operand) - { - switch (operand) - { - case Operand.Input: - return S7Consts.S7AreaPE; - case Operand.Output: - return S7Consts.S7AreaPA; - case Operand.Marker: - return S7Consts.S7AreaMK; - case Operand.Db: - return S7Consts.S7AreaDB; - default: - throw new ArgumentOutOfRangeException(nameof(operand), operand, null); - } - } - private async Task Reconnect() { await CloseConnection(); @@ -294,4 +276,4 @@ namespace Sharp7.Rx Dispose(false); } } -} \ No newline at end of file +} From e6cea1e04fdffbb10cd1ed00312131ccf078fc16 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 09:54:18 +0100 Subject: [PATCH 24/78] Use file scoped namespaces --- Sharp7.Rx.Tests/S7ValueConverterTests.cs | 5 +- Sharp7.Rx.Tests/S7VariableNameParserTests.cs | 5 +- .../Basics/ConcurrentSubjectDictionary.cs | 229 ++++---- Sharp7.Rx/Basics/DisposableItem.cs | 63 ++- .../LimitedConcurrencyLevelTaskScheduler.cs | 261 +++++----- Sharp7.Rx/CacheVariableNameParser.cs | 28 +- Sharp7.Rx/Enums/ConnectionState.cs | 17 +- Sharp7.Rx/Enums/CpuType.cs | 17 +- Sharp7.Rx/Enums/DbType.cs | 23 +- Sharp7.Rx/Enums/Operand.cs | 17 +- Sharp7.Rx/Enums/TransmissionMode.cs | 13 +- Sharp7.Rx/Extensions/DisposableExtensions.cs | 16 +- Sharp7.Rx/Extensions/ObservableExtensions.cs | 144 +++-- Sharp7.Rx/Extensions/PlcExtensions.cs | 101 ++-- Sharp7.Rx/Interfaces/IPlc.cs | 23 +- Sharp7.Rx/Interfaces/IS7Connector.cs | 33 +- Sharp7.Rx/Interfaces/IS7VariableNameParser.cs | 11 +- Sharp7.Rx/S7ErrorCodes.cs | 41 +- Sharp7.Rx/S7ValueConverter.cs | 142 +++-- Sharp7.Rx/S7VariableAddress.cs | 23 +- Sharp7.Rx/S7VariableNameParser.cs | 135 +++-- Sharp7.Rx/Settings/PlcConnectionSettings.cs | 17 +- Sharp7.Rx/Sharp7Connector.cs | 490 +++++++++--------- Sharp7.Rx/Sharp7Plc.cs | 468 +++++++++-------- 24 files changed, 1131 insertions(+), 1191 deletions(-) diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests.cs b/Sharp7.Rx.Tests/S7ValueConverterTests.cs index a1eb657..032877d 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests.cs @@ -1,5 +1,4 @@ -using System; -using NUnit.Framework; +using NUnit.Framework; using Sharp7.Rx.Interfaces; using Shouldly; @@ -64,4 +63,4 @@ public class S7ValueConverterTests //Act Should.Throw(() => S7ValueConverter.ConvertToType(data, variableAddress)); } -} \ No newline at end of file +} diff --git a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs index b97641f..a5fa263 100644 --- a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs +++ b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using DeepEqual.Syntax; +using DeepEqual.Syntax; using NUnit.Framework; using Sharp7.Rx.Enums; @@ -39,4 +38,4 @@ internal class S7VariableNameParserTests { public override string ToString() => Input; } -} \ No newline at end of file +} diff --git a/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs b/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs index d831707..3ea9ef0 100644 --- a/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs +++ b/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs @@ -1,128 +1,125 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; -namespace Sharp7.Rx.Basics +namespace Sharp7.Rx.Basics; + +internal class ConcurrentSubjectDictionary : IDisposable { - internal class ConcurrentSubjectDictionary : IDisposable + private readonly object dictionaryLock = new object(); + private readonly Func valueFactory; + private ConcurrentDictionary dictionary; + + public ConcurrentSubjectDictionary() { - private readonly object dictionaryLock = new object(); - private readonly Func valueFactory; - private ConcurrentDictionary dictionary; + dictionary = new ConcurrentDictionary(); + } - public ConcurrentSubjectDictionary() + public ConcurrentSubjectDictionary(IEqualityComparer comparer) + { + dictionary = new ConcurrentDictionary(comparer); + } + + public ConcurrentSubjectDictionary(TValue initialValue, IEqualityComparer comparer) + { + valueFactory = _ => initialValue; + dictionary = new ConcurrentDictionary(comparer); + } + + public ConcurrentSubjectDictionary(TValue initialValue) + { + valueFactory = _ => initialValue; + dictionary = new ConcurrentDictionary(); + } + + public ConcurrentSubjectDictionary(Func valueFactory = null) + { + this.valueFactory = valueFactory; + dictionary = new ConcurrentDictionary(); + } + + public IEnumerable ExistingKeys => dictionary.Keys; + + public bool IsDisposed { get; private set; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public DisposableItem GetOrCreateObservable(TKey key) + { + lock (dictionaryLock) { - dictionary = new ConcurrentDictionary(); - } - - public ConcurrentSubjectDictionary(IEqualityComparer comparer) - { - dictionary = new ConcurrentDictionary(comparer); - } - - public ConcurrentSubjectDictionary(TValue initialValue, IEqualityComparer comparer) - { - valueFactory = _ => initialValue; - dictionary = new ConcurrentDictionary(comparer); - } - - public ConcurrentSubjectDictionary(TValue initialValue) - { - valueFactory = _ => initialValue; - dictionary = new ConcurrentDictionary(); - } - - public ConcurrentSubjectDictionary(Func valueFactory = null) - { - this.valueFactory = valueFactory; - dictionary = new ConcurrentDictionary(); - } - - public IEnumerable ExistingKeys => dictionary.Keys; - - public bool IsDisposed { get; private set; } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public DisposableItem GetOrCreateObservable(TKey key) - { - 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 {Counter = 1, Subject = CreateSubject(k)}, (key1, counter) => - { - counter.Counter = counter.Counter + 1; - return counter; - }); + counter.Counter = counter.Counter + 1; + return counter; + }); - return new DisposableItem(subject.Subject.AsObservable(), () => RemoveIfNoLongerInUse(key)); - } - } - - public bool TryGetObserver(TKey key, out IObserver subject) - { - SubjectWithRefCounter subjectWithRefCount; - if (dictionary.TryGetValue(key, out subjectWithRefCount)) - { - subject = subjectWithRefCount.Subject.AsObserver(); - return true; - } - - subject = null; - return false; - } - - protected virtual void Dispose(bool disposing) - { - if (IsDisposed) - return; - if (disposing && dictionary != null) - { - foreach (var subjectWithRefCounter in dictionary) - subjectWithRefCounter.Value.Subject.OnCompleted(); - dictionary.Clear(); - dictionary = null; - } - - IsDisposed = true; - } - - private ISubject CreateSubject(TKey key) - { - if (valueFactory == null) - return new Subject(); - return new BehaviorSubject(valueFactory(key)); - } - - private void RemoveIfNoLongerInUse(TKey variableName) - { - lock (dictionaryLock) - { - SubjectWithRefCounter subjectWithRefCount; - if (dictionary.TryGetValue(variableName, out subjectWithRefCount)) - { - if (subjectWithRefCount.Counter == 1) - dictionary.TryRemove(variableName, out subjectWithRefCount); - else subjectWithRefCount.Counter--; - } - } - } - - ~ConcurrentSubjectDictionary() - { - Dispose(false); - } - - class SubjectWithRefCounter - { - public int Counter { get; set; } - public ISubject Subject { get; set; } + return new DisposableItem(subject.Subject.AsObservable(), () => RemoveIfNoLongerInUse(key)); } } -} \ No newline at end of file + + public bool TryGetObserver(TKey key, out IObserver subject) + { + SubjectWithRefCounter subjectWithRefCount; + if (dictionary.TryGetValue(key, out subjectWithRefCount)) + { + subject = subjectWithRefCount.Subject.AsObserver(); + return true; + } + + subject = null; + return false; + } + + protected virtual void Dispose(bool disposing) + { + if (IsDisposed) + return; + if (disposing && dictionary != null) + { + foreach (var subjectWithRefCounter in dictionary) + subjectWithRefCounter.Value.Subject.OnCompleted(); + dictionary.Clear(); + dictionary = null; + } + + IsDisposed = true; + } + + private ISubject CreateSubject(TKey key) + { + if (valueFactory == null) + return new Subject(); + return new BehaviorSubject(valueFactory(key)); + } + + private void RemoveIfNoLongerInUse(TKey variableName) + { + lock (dictionaryLock) + { + SubjectWithRefCounter subjectWithRefCount; + if (dictionary.TryGetValue(variableName, out subjectWithRefCount)) + { + if (subjectWithRefCount.Counter == 1) + dictionary.TryRemove(variableName, out subjectWithRefCount); + else subjectWithRefCount.Counter--; + } + } + } + + ~ConcurrentSubjectDictionary() + { + Dispose(false); + } + + class SubjectWithRefCounter + { + public int Counter { get; set; } + public ISubject Subject { get; set; } + } +} diff --git a/Sharp7.Rx/Basics/DisposableItem.cs b/Sharp7.Rx/Basics/DisposableItem.cs index 29108a4..dd52a88 100644 --- a/Sharp7.Rx/Basics/DisposableItem.cs +++ b/Sharp7.Rx/Basics/DisposableItem.cs @@ -1,37 +1,34 @@ -using System; +namespace Sharp7.Rx.Basics; -namespace Sharp7.Rx.Basics +internal class DisposableItem : IDisposable { - internal class DisposableItem : IDisposable + private readonly Action disposeAction; + + bool disposed; + + public DisposableItem(IObservable observable, Action disposeAction) { - private readonly Action disposeAction; - - bool disposed; - - public DisposableItem(IObservable observable, Action disposeAction) - { - this.disposeAction = disposeAction; - Observable = observable; - } - - public IObservable Observable { get; } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposed) return; - - if (disposing) - { - disposeAction(); - } - - disposed = true; - } + this.disposeAction = disposeAction; + Observable = observable; } -} \ No newline at end of file + + public IObservable Observable { get; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposed) return; + + if (disposing) + { + disposeAction(); + } + + disposed = true; + } +} diff --git a/Sharp7.Rx/Basics/LimitedConcurrencyLevelTaskScheduler.cs b/Sharp7.Rx/Basics/LimitedConcurrencyLevelTaskScheduler.cs index ac1754d..98c24c6 100644 --- a/Sharp7.Rx/Basics/LimitedConcurrencyLevelTaskScheduler.cs +++ b/Sharp7.Rx/Basics/LimitedConcurrencyLevelTaskScheduler.cs @@ -1,146 +1,139 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +namespace Sharp7.Rx.Basics; -namespace Sharp7.Rx.Basics +/// +/// Provides a task scheduler that ensures a maximum concurrency level while +/// running on top of the ThreadPool. +/// from http://msdn.microsoft.com/en-us/library/ee789351.aspx +/// +internal class LimitedConcurrencyLevelTaskScheduler : TaskScheduler { + /// Whether the current thread is processing work items. + [ThreadStatic] private static bool currentThreadIsProcessingItems; + + /// The maximum concurrency level allowed by this scheduler. + private readonly int maxDegreeOfParallelism; + + /// The list of tasks to be executed. + private readonly LinkedList tasks = new LinkedList(); // protected by lock(_tasks) + + /// Whether the scheduler is currently processing work items. + private int delegatesQueuedOrRunning; // protected by lock(_tasks) + /// - /// Provides a task scheduler that ensures a maximum concurrency level while - /// running on top of the ThreadPool. - /// from http://msdn.microsoft.com/en-us/library/ee789351.aspx + /// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the + /// specified degree of parallelism. /// - internal class LimitedConcurrencyLevelTaskScheduler : TaskScheduler + /// The maximum degree of parallelism provided by this scheduler. + public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism) { - /// Whether the current thread is processing work items. - [ThreadStatic] private static bool currentThreadIsProcessingItems; + if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException("maxDegreeOfParallelism"); + this.maxDegreeOfParallelism = maxDegreeOfParallelism; + } - /// The maximum concurrency level allowed by this scheduler. - private readonly int maxDegreeOfParallelism; + /// Gets the maximum concurrency level supported by this scheduler. + public sealed override int MaximumConcurrencyLevel => maxDegreeOfParallelism; - /// The list of tasks to be executed. - private readonly LinkedList tasks = new LinkedList(); // protected by lock(_tasks) - - /// Whether the scheduler is currently processing work items. - private int delegatesQueuedOrRunning; // protected by lock(_tasks) - - /// - /// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the - /// specified degree of parallelism. - /// - /// The maximum degree of parallelism provided by this scheduler. - public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism) + /// Gets an enumerable of the tasks currently scheduled on this scheduler. + /// An enumerable of the tasks currently scheduled. + protected sealed override IEnumerable GetScheduledTasks() + { + var lockTaken = false; + try { - if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException("maxDegreeOfParallelism"); - this.maxDegreeOfParallelism = maxDegreeOfParallelism; + Monitor.TryEnter(tasks, ref lockTaken); + if (lockTaken) return tasks.ToArray(); + else throw new NotSupportedException(); } - - /// Gets the maximum concurrency level supported by this scheduler. - public sealed override int MaximumConcurrencyLevel => maxDegreeOfParallelism; - - /// Gets an enumerable of the tasks currently scheduled on this scheduler. - /// An enumerable of the tasks currently scheduled. - protected sealed override IEnumerable GetScheduledTasks() + finally { - var lockTaken = false; - try - { - Monitor.TryEnter(tasks, ref lockTaken); - if (lockTaken) return tasks.ToArray(); - else throw new NotSupportedException(); - } - finally - { - if (lockTaken) Monitor.Exit(tasks); - } - } - - /// Queues a task to the scheduler. - /// The task to be queued. - protected sealed override void QueueTask(Task task) - { - // Add the task to the list of tasks to be processed. If there aren't enough - // delegates currently queued or running to process tasks, schedule another. - lock (tasks) - { - tasks.AddLast(task); - if (delegatesQueuedOrRunning < maxDegreeOfParallelism) - { - ++delegatesQueuedOrRunning; - NotifyThreadPoolOfPendingWork(); - } - } - } - - /// Attempts to remove a previously scheduled task from the scheduler. - /// The task to be removed. - /// Whether the task could be found and removed. - protected sealed override bool TryDequeue(Task task) - { - lock (tasks) - { - return tasks.Remove(task); - } - } - - /// Attempts to execute the specified task on the current thread. - /// The task to be executed. - /// - /// Whether the task could be executed on the current thread. - protected sealed override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) - { - // If this thread isn't already processing a task, we don't support inlining - if (!currentThreadIsProcessingItems) return false; - - // If the task was previously queued, remove it from the queue - if (taskWasPreviouslyQueued) TryDequeue(task); - - // Try to run the task. - return TryExecuteTask(task); - } - - /// - /// Informs the ThreadPool that there's work to be executed for this scheduler. - /// - private void NotifyThreadPoolOfPendingWork() - { - ThreadPool.UnsafeQueueUserWorkItem(_ => - { - // Note that the current thread is now processing work items. - // This is necessary to enable inlining of tasks into this thread. - currentThreadIsProcessingItems = true; - try - { - // Process all available items in the queue. - while (true) - { - Task item; - lock (tasks) - { - // When there are no more items to be processed, - // note that we're done processing, and get out. - if (tasks.Count == 0) - { - --delegatesQueuedOrRunning; - break; - } - - // Get the next item from the queue - item = tasks.First.Value; - tasks.RemoveFirst(); - } - - // Execute the task we pulled out of the queue - TryExecuteTask(item); - } - } - // We're done processing items on the current thread - finally - { - currentThreadIsProcessingItems = false; - } - }, null); + if (lockTaken) Monitor.Exit(tasks); } } -} \ No newline at end of file + + /// Queues a task to the scheduler. + /// The task to be queued. + protected sealed override void QueueTask(Task task) + { + // Add the task to the list of tasks to be processed. If there aren't enough + // delegates currently queued or running to process tasks, schedule another. + lock (tasks) + { + tasks.AddLast(task); + if (delegatesQueuedOrRunning < maxDegreeOfParallelism) + { + ++delegatesQueuedOrRunning; + NotifyThreadPoolOfPendingWork(); + } + } + } + + /// Attempts to remove a previously scheduled task from the scheduler. + /// The task to be removed. + /// Whether the task could be found and removed. + protected sealed override bool TryDequeue(Task task) + { + lock (tasks) + { + return tasks.Remove(task); + } + } + + /// Attempts to execute the specified task on the current thread. + /// The task to be executed. + /// + /// Whether the task could be executed on the current thread. + protected sealed override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + // If this thread isn't already processing a task, we don't support inlining + if (!currentThreadIsProcessingItems) return false; + + // If the task was previously queued, remove it from the queue + if (taskWasPreviouslyQueued) TryDequeue(task); + + // Try to run the task. + return TryExecuteTask(task); + } + + /// + /// Informs the ThreadPool that there's work to be executed for this scheduler. + /// + private void NotifyThreadPoolOfPendingWork() + { + ThreadPool.UnsafeQueueUserWorkItem(_ => + { + // Note that the current thread is now processing work items. + // This is necessary to enable inlining of tasks into this thread. + currentThreadIsProcessingItems = true; + try + { + // Process all available items in the queue. + while (true) + { + Task item; + lock (tasks) + { + // When there are no more items to be processed, + // note that we're done processing, and get out. + if (tasks.Count == 0) + { + --delegatesQueuedOrRunning; + break; + } + + // Get the next item from the queue + item = tasks.First.Value; + tasks.RemoveFirst(); + } + + // Execute the task we pulled out of the queue + TryExecuteTask(item); + } + } + // We're done processing items on the current thread + finally + { + currentThreadIsProcessingItems = false; + } + }, null); + } +} diff --git a/Sharp7.Rx/CacheVariableNameParser.cs b/Sharp7.Rx/CacheVariableNameParser.cs index ddd258b..d24ef98 100644 --- a/Sharp7.Rx/CacheVariableNameParser.cs +++ b/Sharp7.Rx/CacheVariableNameParser.cs @@ -1,20 +1,18 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using Sharp7.Rx.Interfaces; -namespace Sharp7.Rx +namespace Sharp7.Rx; + +internal class CacheVariableNameParser : IS7VariableNameParser { - internal class CacheVariableNameParser : IS7VariableNameParser + private static readonly ConcurrentDictionary addressCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + private readonly IS7VariableNameParser inner; + + public CacheVariableNameParser(IS7VariableNameParser inner) { - private static readonly ConcurrentDictionary addressCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - private readonly IS7VariableNameParser inner; - - public CacheVariableNameParser(IS7VariableNameParser inner) - { - this.inner = inner; - } - - public S7VariableAddress Parse(string input) => addressCache.GetOrAdd(input, inner.Parse); + this.inner = inner; } -} \ No newline at end of file + + public S7VariableAddress Parse(string input) => addressCache.GetOrAdd(input, inner.Parse); +} diff --git a/Sharp7.Rx/Enums/ConnectionState.cs b/Sharp7.Rx/Enums/ConnectionState.cs index 31e54ac..5cbedc5 100644 --- a/Sharp7.Rx/Enums/ConnectionState.cs +++ b/Sharp7.Rx/Enums/ConnectionState.cs @@ -1,10 +1,9 @@ -namespace Sharp7.Rx.Enums +namespace Sharp7.Rx.Enums; + +public enum ConnectionState { - public enum ConnectionState - { - Initial, - Connected, - DisconnectedByUser, - ConnectionLost - } -} \ No newline at end of file + Initial, + Connected, + DisconnectedByUser, + ConnectionLost +} diff --git a/Sharp7.Rx/Enums/CpuType.cs b/Sharp7.Rx/Enums/CpuType.cs index ad0a679..a923cbc 100644 --- a/Sharp7.Rx/Enums/CpuType.cs +++ b/Sharp7.Rx/Enums/CpuType.cs @@ -1,10 +1,9 @@ -namespace Sharp7.Rx.Enums +namespace Sharp7.Rx.Enums; + +internal enum CpuType { - internal enum CpuType - { - S7_300, - S7_400, - S7_1200, - S7_1500 - } -} \ No newline at end of file + S7_300, + S7_400, + S7_1200, + S7_1500 +} diff --git a/Sharp7.Rx/Enums/DbType.cs b/Sharp7.Rx/Enums/DbType.cs index 8a134ec..07066f8 100644 --- a/Sharp7.Rx/Enums/DbType.cs +++ b/Sharp7.Rx/Enums/DbType.cs @@ -1,13 +1,12 @@ -namespace Sharp7.Rx.Enums +namespace Sharp7.Rx.Enums; + +internal enum DbType { - internal enum DbType - { - Bit, - String, - Byte, - Double, - Integer, - DInteger, - ULong - } -} \ No newline at end of file + Bit, + String, + Byte, + Double, + Integer, + DInteger, + ULong +} diff --git a/Sharp7.Rx/Enums/Operand.cs b/Sharp7.Rx/Enums/Operand.cs index 4f6a687..6ad4970 100644 --- a/Sharp7.Rx/Enums/Operand.cs +++ b/Sharp7.Rx/Enums/Operand.cs @@ -1,10 +1,9 @@ -namespace Sharp7.Rx.Enums +namespace Sharp7.Rx.Enums; + +internal enum Operand : byte { - internal enum Operand : byte - { - Input = 69, - Output = 65, - Marker = 77, - Db = 68, - } -} \ No newline at end of file + Input = 69, + Output = 65, + Marker = 77, + Db = 68, +} diff --git a/Sharp7.Rx/Enums/TransmissionMode.cs b/Sharp7.Rx/Enums/TransmissionMode.cs index c33ef62..5ba9dc2 100644 --- a/Sharp7.Rx/Enums/TransmissionMode.cs +++ b/Sharp7.Rx/Enums/TransmissionMode.cs @@ -1,8 +1,7 @@ -namespace Sharp7.Rx.Enums +namespace Sharp7.Rx.Enums; + +public enum TransmissionMode { - public enum TransmissionMode - { - Cyclic = 3, - OnChange = 4, - } -} \ No newline at end of file + Cyclic = 3, + OnChange = 4, +} diff --git a/Sharp7.Rx/Extensions/DisposableExtensions.cs b/Sharp7.Rx/Extensions/DisposableExtensions.cs index 6f68c0f..5638168 100644 --- a/Sharp7.Rx/Extensions/DisposableExtensions.cs +++ b/Sharp7.Rx/Extensions/DisposableExtensions.cs @@ -1,13 +1,11 @@ -using System; -using System.Reactive.Disposables; +using System.Reactive.Disposables; -namespace Sharp7.Rx.Extensions +namespace Sharp7.Rx.Extensions; + +internal static class DisposableExtensions { - internal static class DisposableExtensions + public static void AddDisposableTo(this IDisposable disposable, CompositeDisposable compositeDisposable) { - public static void AddDisposableTo(this IDisposable disposable, CompositeDisposable compositeDisposable) - { - compositeDisposable.Add(disposable); - } + compositeDisposable.Add(disposable); } -} \ No newline at end of file +} diff --git a/Sharp7.Rx/Extensions/ObservableExtensions.cs b/Sharp7.Rx/Extensions/ObservableExtensions.cs index 8cdc26e..3c5735b 100644 --- a/Sharp7.Rx/Extensions/ObservableExtensions.cs +++ b/Sharp7.Rx/Extensions/ObservableExtensions.cs @@ -1,83 +1,81 @@ -using System; -using System.Reactive.Concurrency; +using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; using Microsoft.Extensions.Logging; -namespace Sharp7.Rx.Extensions -{ - internal static class ObservableExtensions - { - public static IObservable DisposeMany(this IObservable source) - { - return Observable.Create(obs => - { - var serialDisposable = new SerialDisposable(); - var subscription = - source.Subscribe( - item => - { - serialDisposable.Disposable = item as IDisposable; - obs.OnNext(item); - }, - obs.OnError, - obs.OnCompleted); - return new CompositeDisposable(serialDisposable, subscription); - }); - } +namespace Sharp7.Rx.Extensions; - public static IObservable LogAndRetry(this IObservable source, ILogger logger, string message) +internal static class ObservableExtensions +{ + public static IObservable DisposeMany(this IObservable source) + { + return Observable.Create(obs => { - return source + var serialDisposable = new SerialDisposable(); + var subscription = + source.Subscribe( + item => + { + serialDisposable.Disposable = item as IDisposable; + obs.OnNext(item); + }, + obs.OnError, + obs.OnCompleted); + return new CompositeDisposable(serialDisposable, subscription); + }); + } + + public static IObservable LogAndRetry(this IObservable source, ILogger logger, string message) + { + return source + .Do( + _ => { }, + ex => logger?.LogError(ex, message)) + .Retry(); + } + + public static IObservable LogAndRetryAfterDelay( + this IObservable source, + ILogger logger, + TimeSpan retryDelay, + string message, + int retryCount = -1, + IScheduler scheduler = null) + { + var sourceLogged = + source .Do( _ => { }, - ex => logger?.LogError(ex, message)) - .Retry(); - } + ex => logger?.LogError(ex, message)); - public static IObservable LogAndRetryAfterDelay( - this IObservable source, - ILogger logger, - TimeSpan retryDelay, - string message, - int retryCount = -1, - IScheduler scheduler = null) - { - var sourceLogged = - source - .Do( - _ => { }, - ex => logger?.LogError(ex, message)); - - return RetryAfterDelay(sourceLogged, retryDelay, retryCount, scheduler); - } - - public static IObservable RepeatAfterDelay( - this IObservable source, - TimeSpan retryDelay, - int repeatCount = -1, - IScheduler scheduler = null) - { - return RedoAfterDelay(source, retryDelay, repeatCount, scheduler, Observable.Repeat, Observable.Repeat); - } - - public static IObservable RetryAfterDelay( - this IObservable source, - TimeSpan retryDelay, - int retryCount = -1, - IScheduler scheduler = null) - { - return RedoAfterDelay(source, retryDelay, retryCount, scheduler, Observable.Retry, Observable.Retry); - } - - private static IObservable RedoAfterDelay(IObservable source, TimeSpan retryDelay, int retryCount, IScheduler scheduler, Func, IObservable> reDo, - Func, int, IObservable> reDoCount) - { - scheduler = scheduler ?? TaskPoolScheduler.Default; - var attempt = 0; - - var deferedObs = Observable.Defer(() => ((++attempt == 1) ? source : source.DelaySubscription(retryDelay, scheduler))); - return retryCount > 0 ? reDoCount(deferedObs, retryCount) : reDo(deferedObs); - } + return RetryAfterDelay(sourceLogged, retryDelay, retryCount, scheduler); } -} \ No newline at end of file + + public static IObservable RepeatAfterDelay( + this IObservable source, + TimeSpan retryDelay, + int repeatCount = -1, + IScheduler scheduler = null) + { + return RedoAfterDelay(source, retryDelay, repeatCount, scheduler, Observable.Repeat, Observable.Repeat); + } + + public static IObservable RetryAfterDelay( + this IObservable source, + TimeSpan retryDelay, + int retryCount = -1, + IScheduler scheduler = null) + { + return RedoAfterDelay(source, retryDelay, retryCount, scheduler, Observable.Retry, Observable.Retry); + } + + private static IObservable RedoAfterDelay(IObservable source, TimeSpan retryDelay, int retryCount, IScheduler scheduler, Func, IObservable> reDo, + Func, int, IObservable> reDoCount) + { + scheduler = scheduler ?? TaskPoolScheduler.Default; + var attempt = 0; + + var deferedObs = Observable.Defer(() => ((++attempt == 1) ? source : source.DelaySubscription(retryDelay, scheduler))); + return retryCount > 0 ? reDoCount(deferedObs, retryCount) : reDo(deferedObs); + } +} diff --git a/Sharp7.Rx/Extensions/PlcExtensions.cs b/Sharp7.Rx/Extensions/PlcExtensions.cs index 2cfbc85..b1f1cd2 100644 --- a/Sharp7.Rx/Extensions/PlcExtensions.cs +++ b/Sharp7.Rx/Extensions/PlcExtensions.cs @@ -1,74 +1,71 @@ -using System; -using System.Reactive; +using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; -using System.Threading.Tasks; using Sharp7.Rx.Enums; using Sharp7.Rx.Interfaces; -namespace Sharp7.Rx.Extensions +namespace Sharp7.Rx.Extensions; + +public static class PlcExtensions { - public static class PlcExtensions + public static IObservable CreateDatatransferWithHandshake(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func> readData, bool initialTransfer) { - public static IObservable CreateDatatransferWithHandshake(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func> readData, bool initialTransfer) + return Observable.Create(async observer => { - return Observable.Create(async observer => + var subscriptions = new CompositeDisposable(); + + var notification = plc + .CreateNotification(triggerAddress, TransmissionMode.OnChange) + .Publish() + .RefCount(); + + if (initialTransfer) { - var subscriptions = new CompositeDisposable(); + await plc.ConnectionState.FirstAsync(state => state == ConnectionState.Connected).ToTask(); + var initialValue = await ReadData(plc, readData); + observer.OnNext(initialValue); + } - var notification = plc - .CreateNotification(triggerAddress, TransmissionMode.OnChange) - .Publish() - .RefCount(); + notification + .Where(trigger => trigger) + .SelectMany(_ => ReadDataAndAcknowlodge(plc, readData, ackTriggerAddress)) + .Subscribe(observer) + .AddDisposableTo(subscriptions); - if (initialTransfer) + notification + .Where(trigger => !trigger) + .SelectMany(async _ => { - await plc.ConnectionState.FirstAsync(state => state == ConnectionState.Connected).ToTask(); - var initialValue = await ReadData(plc, readData); - observer.OnNext(initialValue); - } + await plc.SetValue(ackTriggerAddress, false); + return Unit.Default; + }) + .Subscribe() + .AddDisposableTo(subscriptions); - notification - .Where(trigger => trigger) - .SelectMany(_ => ReadDataAndAcknowlodge(plc, readData, ackTriggerAddress)) - .Subscribe(observer) - .AddDisposableTo(subscriptions); + return subscriptions; + }); + } - notification - .Where(trigger => !trigger) - .SelectMany(async _ => - { - await plc.SetValue(ackTriggerAddress, false); - return Unit.Default; - }) - .Subscribe() - .AddDisposableTo(subscriptions); + public static IObservable CreateDatatransferWithHandshake(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func> readData) + { + return CreateDatatransferWithHandshake(plc, triggerAddress, ackTriggerAddress, readData, false); + } - return subscriptions; - }); - } + private static async Task ReadData(IPlc plc, Func> receiveData) + { + return await receiveData(plc); + } - public static IObservable CreateDatatransferWithHandshake(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func> readData) + private static async Task ReadDataAndAcknowlodge(IPlc plc, Func> readData, string ackTriggerAddress) + { + try { - return CreateDatatransferWithHandshake(plc, triggerAddress, ackTriggerAddress, readData, false); + return await ReadData(plc, readData); } - - private static async Task ReadData(IPlc plc, Func> receiveData) + finally { - return await receiveData(plc); - } - - private static async Task ReadDataAndAcknowlodge(IPlc plc, Func> readData, string ackTriggerAddress) - { - try - { - return await ReadData(plc, readData); - } - finally - { - await plc.SetValue(ackTriggerAddress, true); - } + await plc.SetValue(ackTriggerAddress, true); } } -} \ No newline at end of file +} diff --git a/Sharp7.Rx/Interfaces/IPlc.cs b/Sharp7.Rx/Interfaces/IPlc.cs index 683aed9..296e830 100644 --- a/Sharp7.Rx/Interfaces/IPlc.cs +++ b/Sharp7.Rx/Interfaces/IPlc.cs @@ -1,18 +1,15 @@ -using System; -using System.Threading.Tasks; -using JetBrains.Annotations; +using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Sharp7.Rx.Enums; -namespace Sharp7.Rx.Interfaces +namespace Sharp7.Rx.Interfaces; + +[NoReorder] +public interface IPlc : IDisposable { - [NoReorder] - public interface IPlc : IDisposable - { - IObservable CreateNotification(string variableName, TransmissionMode transmissionMode); - Task SetValue(string variableName, TValue value); - Task GetValue(string variableName); - IObservable ConnectionState { get; } - ILogger Logger { get; } - } + IObservable CreateNotification(string variableName, TransmissionMode transmissionMode); + Task SetValue(string variableName, TValue value); + Task GetValue(string variableName); + IObservable ConnectionState { get; } + ILogger Logger { get; } } diff --git a/Sharp7.Rx/Interfaces/IS7Connector.cs b/Sharp7.Rx/Interfaces/IS7Connector.cs index 10aa850..5f9da81 100644 --- a/Sharp7.Rx/Interfaces/IS7Connector.cs +++ b/Sharp7.Rx/Interfaces/IS7Connector.cs @@ -1,26 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using JetBrains.Annotations; +using JetBrains.Annotations; using Sharp7.Rx.Enums; -namespace Sharp7.Rx.Interfaces +namespace Sharp7.Rx.Interfaces; + +[NoReorder] +internal interface IS7Connector : IDisposable { - [NoReorder] - internal interface IS7Connector : IDisposable - { - IObservable ConnectionState { get; } - Task InitializeAsync(); + IObservable ConnectionState { get; } + Task InitializeAsync(); - Task Connect(); - Task Disconnect(); + Task Connect(); + Task Disconnect(); - Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token); + Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token); - Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token); - Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token); + Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token); + Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token); - Task> ExecuteMultiVarRequest(IReadOnlyList variableNames); - } -} \ No newline at end of file + Task> ExecuteMultiVarRequest(IReadOnlyList variableNames); +} diff --git a/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs b/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs index dd8e272..07f4b9e 100644 --- a/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs +++ b/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs @@ -1,7 +1,6 @@ -namespace Sharp7.Rx.Interfaces +namespace Sharp7.Rx.Interfaces; + +internal interface IS7VariableNameParser { - internal interface IS7VariableNameParser - { - S7VariableAddress Parse(string input); - } -} \ No newline at end of file + S7VariableAddress Parse(string input); +} diff --git a/Sharp7.Rx/S7ErrorCodes.cs b/Sharp7.Rx/S7ErrorCodes.cs index db1b5eb..f076bf8 100644 --- a/Sharp7.Rx/S7ErrorCodes.cs +++ b/Sharp7.Rx/S7ErrorCodes.cs @@ -1,27 +1,24 @@ -using System.Collections.Generic; +namespace Sharp7.Rx; -namespace Sharp7.Rx +public static class S7ErrorCodes { - public static class S7ErrorCodes + /// + /// This list is not exhaustive and should be considered work in progress. + /// + private static readonly HashSet notDisconnectedErrorCodes = new HashSet { - /// - /// This list is not exhaustive and should be considered work in progress. - /// - private static readonly HashSet notDisconnectedErrorCodes = new HashSet - { - 0x000000, // OK - 0xC00000, // CPU: Item not available - 0x900000, // CPU: Address out of range - }; + 0x000000, // OK + 0xC00000, // CPU: Item not available + 0x900000, // CPU: Address out of range + }; - /// - /// 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 - /// the DBs range. In this case the driver should not consider the connection to be lost. - /// - public static bool AssumeConnectionLost(int errorCode) - { - return !notDisconnectedErrorCodes.Contains(errorCode); - } + /// + /// 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 + /// the DBs range. In this case the driver should not consider the connection to be lost. + /// + public static bool AssumeConnectionLost(int errorCode) + { + return !notDisconnectedErrorCodes.Contains(errorCode); } -} \ No newline at end of file +} diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index fb6f985..26b86c5 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -1,85 +1,83 @@ -using System; -using System.Buffers.Binary; +using System.Buffers.Binary; using System.Runtime.InteropServices; using System.Text; using Sharp7.Rx.Enums; -namespace Sharp7.Rx +namespace Sharp7.Rx; + +internal static class S7ValueConverter { - internal static class S7ValueConverter + public static TValue ConvertToType(byte[] buffer, S7VariableAddress address) { - public static TValue ConvertToType(byte[] buffer, S7VariableAddress address) + if (typeof(TValue) == typeof(bool)) + return (TValue) (object) (((buffer[0] >> address.Bit) & 1) > 0); + + if (typeof(TValue) == typeof(int)) { - if (typeof(TValue) == typeof(bool)) - return (TValue) (object) (((buffer[0] >> address.Bit) & 1) > 0); + if (address.Length == 2) + return (TValue) (object) (int) BinaryPrimitives.ReadInt16BigEndian(buffer); + if (address.Length == 4) + return (TValue) (object) BinaryPrimitives.ReadInt32BigEndian(buffer); - if (typeof(TValue) == typeof(int)) - { - if (address.Length == 2) - return (TValue) (object) (int) BinaryPrimitives.ReadInt16BigEndian(buffer); - if (address.Length == 4) - return (TValue) (object) BinaryPrimitives.ReadInt32BigEndian(buffer); - - throw new InvalidOperationException($"length must be 2 or 4 but is {address.Length}"); - } - - if (typeof(TValue) == typeof(long)) - return (TValue) (object) BinaryPrimitives.ReadInt64BigEndian(buffer); - - if (typeof(TValue) == typeof(ulong)) - return (TValue) (object) BinaryPrimitives.ReadUInt64BigEndian(buffer); - - if (typeof(TValue) == typeof(short)) - return (TValue) (object) BinaryPrimitives.ReadInt16BigEndian(buffer); - - if (typeof(TValue) == typeof(byte)) - return (TValue) (object) buffer[0]; - if (typeof(TValue) == typeof(char)) - return (TValue) (object) (char) buffer[0]; - - if (typeof(TValue) == typeof(byte[])) - return (TValue) (object) buffer; - - if (typeof(TValue) == typeof(double)) - { - var d = new UInt32SingleMap - { - UInt32 = BinaryPrimitives.ReadUInt32BigEndian(buffer) - }; - return (TValue) (object) (double) d.Single; - } - - if (typeof(TValue) == typeof(float)) - { - var d = new UInt32SingleMap - { - UInt32 = BinaryPrimitives.ReadUInt32BigEndian(buffer) - }; - return (TValue) (object) d.Single; - } - - if (typeof(TValue) == typeof(string)) - if (address.Type == DbType.String) - { - // First byte is maximal length - // Second byte is actual length - // https://cache.industry.siemens.com/dl/files/480/22506480/att_105176/v1/s7_scl_string_parameterzuweisung_e.pdf - - var length = Math.Min(address.Length, buffer[1]); - - return (TValue) (object) Encoding.ASCII.GetString(buffer, 2, length); - } - else - return (TValue) (object) Encoding.ASCII.GetString(buffer).Trim(); - - throw new InvalidOperationException(string.Format("type '{0}' not supported.", typeof(TValue))); + throw new InvalidOperationException($"length must be 2 or 4 but is {address.Length}"); } - [StructLayout(LayoutKind.Explicit)] - private struct UInt32SingleMap + 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)) { - [FieldOffset(0)] public uint UInt32; - [FieldOffset(0)] public float Single; + var d = new UInt32SingleMap + { + UInt32 = BinaryPrimitives.ReadUInt32BigEndian(buffer) + }; + return (TValue) (object) (double) d.Single; } + + if (typeof(TValue) == typeof(float)) + { + var d = new UInt32SingleMap + { + UInt32 = BinaryPrimitives.ReadUInt32BigEndian(buffer) + }; + return (TValue) (object) d.Single; + } + + if (typeof(TValue) == typeof(string)) + if (address.Type == DbType.String) + { + // First byte is maximal length + // Second byte is actual length + // https://cache.industry.siemens.com/dl/files/480/22506480/att_105176/v1/s7_scl_string_parameterzuweisung_e.pdf + + var length = Math.Min(address.Length, buffer[1]); + + return (TValue) (object) Encoding.ASCII.GetString(buffer, 2, length); + } + else + return (TValue) (object) Encoding.ASCII.GetString(buffer).Trim(); + + throw new InvalidOperationException(string.Format("type '{0}' not supported.", typeof(TValue))); } -} \ No newline at end of file + + [StructLayout(LayoutKind.Explicit)] + private struct UInt32SingleMap + { + [FieldOffset(0)] public uint UInt32; + [FieldOffset(0)] public float Single; + } +} diff --git a/Sharp7.Rx/S7VariableAddress.cs b/Sharp7.Rx/S7VariableAddress.cs index 7c751f7..ae5e440 100644 --- a/Sharp7.Rx/S7VariableAddress.cs +++ b/Sharp7.Rx/S7VariableAddress.cs @@ -1,16 +1,15 @@ using JetBrains.Annotations; using Sharp7.Rx.Enums; -namespace Sharp7.Rx +namespace Sharp7.Rx; + +[NoReorder] +internal class S7VariableAddress { - [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; } - } -} \ No newline at end of file + 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; } +} diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index df34b3a..80e9913 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -1,84 +1,81 @@ -using System; -using System.Collections.Generic; -using System.Globalization; +using System.Globalization; using System.Text.RegularExpressions; using Sharp7.Rx.Enums; using Sharp7.Rx.Interfaces; -namespace Sharp7.Rx +namespace Sharp7.Rx; + +internal class S7VariableNameParser : IS7VariableNameParser { - internal class S7VariableNameParser : IS7VariableNameParser + private static readonly Regex regex = new Regex(@"^(?db{1})(?\d{1,4})\.?(?dbx|x|s|string|b|dbb|d|int|dbw|w|dint|dul|dulint|dulong|){1}(?\d+)(\.(?\d+))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly IReadOnlyDictionary types = new Dictionary(StringComparer.OrdinalIgnoreCase) { - private static readonly Regex regex = new Regex(@"^(?db{1})(?\d{1,4})\.?(?dbx|x|s|string|b|dbb|d|int|dbw|w|dint|dul|dulint|dulong|){1}(?\d+)(\.(?\d+))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); + {"x", DbType.Bit}, + {"dbx", DbType.Bit}, + {"s", DbType.String}, + {"string", DbType.String}, + {"b", DbType.Byte}, + {"dbb", DbType.Byte}, + {"d", DbType.Double}, + {"int", DbType.Integer}, + {"dint", DbType.DInteger}, + {"w", DbType.Integer}, + {"dbw", DbType.Integer}, + {"dul", DbType.ULong}, + {"dulint", DbType.ULong}, + {"dulong", DbType.ULong} + }; - private static readonly IReadOnlyDictionary types = new Dictionary(StringComparer.OrdinalIgnoreCase) + public S7VariableAddress Parse(string input) + { + var match = regex.Match(input); + if (match.Success) { - {"x", DbType.Bit}, - {"dbx", DbType.Bit}, - {"s", DbType.String}, - {"string", DbType.String}, - {"b", DbType.Byte}, - {"dbb", DbType.Byte}, - {"d", DbType.Double}, - {"int", DbType.Integer}, - {"dint", DbType.DInteger}, - {"w", DbType.Integer}, - {"dbw", DbType.Integer}, - {"dul", DbType.ULong}, - {"dulint", DbType.ULong}, - {"dulong", DbType.ULong} - }; + var operand = (Operand) Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true); + var dbNr = ushort.Parse(match.Groups["dbNr"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture); + var start = ushort.Parse(match.Groups["start"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture); + if (!types.TryGetValue(match.Groups["type"].Value, out var type)) + return null; - public S7VariableAddress Parse(string input) - { - var match = regex.Match(input); - if (match.Success) + + var s7VariableAddress = new S7VariableAddress { - var operand = (Operand) Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true); - var dbNr = ushort.Parse(match.Groups["dbNr"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture); - var start = ushort.Parse(match.Groups["start"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture); - if (!types.TryGetValue(match.Groups["type"].Value, out var type)) - return null; + Operand = operand, + DbNr = dbNr, + Start = start, + Type = type, + }; - - var s7VariableAddress = new S7VariableAddress - { - Operand = operand, - DbNr = dbNr, - Start = start, - Type = type, - }; - - switch (type) - { - case DbType.Bit: - s7VariableAddress.Length = 1; - s7VariableAddress.Bit = byte.Parse(match.Groups["bitOrLength"].Value); - break; - case DbType.Byte: - s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort) 1; - break; - case DbType.String: - s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort) 0; - break; - case DbType.Integer: - s7VariableAddress.Length = 2; - break; - case DbType.DInteger: - s7VariableAddress.Length = 4; - break; - case DbType.ULong: - s7VariableAddress.Length = 8; - break; - case DbType.Double: - s7VariableAddress.Length = 4; - break; - } - - return s7VariableAddress; + switch (type) + { + case DbType.Bit: + s7VariableAddress.Length = 1; + s7VariableAddress.Bit = byte.Parse(match.Groups["bitOrLength"].Value); + break; + case DbType.Byte: + s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort) 1; + break; + case DbType.String: + s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort) 0; + break; + case DbType.Integer: + s7VariableAddress.Length = 2; + break; + case DbType.DInteger: + s7VariableAddress.Length = 4; + break; + case DbType.ULong: + s7VariableAddress.Length = 8; + break; + case DbType.Double: + s7VariableAddress.Length = 4; + break; } - return null; + return s7VariableAddress; } + + return null; } -} \ No newline at end of file +} diff --git a/Sharp7.Rx/Settings/PlcConnectionSettings.cs b/Sharp7.Rx/Settings/PlcConnectionSettings.cs index 8adffbf..6ec2e0b 100644 --- a/Sharp7.Rx/Settings/PlcConnectionSettings.cs +++ b/Sharp7.Rx/Settings/PlcConnectionSettings.cs @@ -1,10 +1,9 @@ -namespace Sharp7.Rx.Settings +namespace Sharp7.Rx.Settings; + +internal class PlcConnectionSettings { - internal class PlcConnectionSettings - { - public int CpuMpiAddress { get; set; } - public string IpAddress { get; set; } - public int Port { get; set; } - public int RackNumber { get; set; } - } -} \ No newline at end of file + public int CpuMpiAddress { get; set; } + public string IpAddress { get; set; } + public int Port { get; set; } + public int RackNumber { get; set; } +} diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 7adfa2e..e8a078a 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Disposables; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Sharp7.Rx.Basics; using Sharp7.Rx.Enums; @@ -14,266 +9,265 @@ using Sharp7.Rx.Interfaces; using Sharp7.Rx.Resources; using Sharp7.Rx.Settings; -namespace Sharp7.Rx +namespace Sharp7.Rx; + +internal class Sharp7Connector : IS7Connector { - internal class Sharp7Connector : IS7Connector + private readonly BehaviorSubject connectionStateSubject = new BehaviorSubject(Enums.ConnectionState.Initial); + private readonly int cpuSlotNr; + + private readonly CompositeDisposable disposables = new CompositeDisposable(); + private readonly string ipAddress; + private readonly int port; + private readonly int rackNr; + private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism: 1); + private readonly IS7VariableNameParser variableNameParser; + private bool disposed; + + private S7Client sharp7; + + + public Sharp7Connector(PlcConnectionSettings settings, IS7VariableNameParser variableNameParser) { - private readonly BehaviorSubject connectionStateSubject = new BehaviorSubject(Enums.ConnectionState.Initial); - private readonly int cpuSlotNr; + this.variableNameParser = variableNameParser; + ipAddress = settings.IpAddress; + cpuSlotNr = settings.CpuMpiAddress; + port = settings.Port; + rackNr = settings.RackNumber; - private readonly CompositeDisposable disposables = new CompositeDisposable(); - private readonly string ipAddress; - private readonly int port; - private readonly int rackNr; - private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism: 1); - private readonly IS7VariableNameParser variableNameParser; - private bool disposed; + ReconnectDelay = TimeSpan.FromSeconds(5); + } - private S7Client sharp7; + public IObservable ConnectionState => connectionStateSubject.DistinctUntilChanged().AsObservable(); + public ILogger Logger { get; set; } - public Sharp7Connector(PlcConnectionSettings settings, IS7VariableNameParser variableNameParser) + public TimeSpan ReconnectDelay { get; set; } + + private bool IsConnected => connectionStateSubject.Value == Enums.ConnectionState.Connected; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public async Task Connect() + { + if (sharp7 == null) + throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + + try { - this.variableNameParser = variableNameParser; - ipAddress = settings.IpAddress; - cpuSlotNr = settings.CpuMpiAddress; - port = settings.Port; - rackNr = settings.RackNumber; - - ReconnectDelay = TimeSpan.FromSeconds(5); - } - - public IObservable ConnectionState => connectionStateSubject.DistinctUntilChanged().AsObservable(); - - public ILogger Logger { get; set; } - - public TimeSpan ReconnectDelay { get; set; } - - private bool IsConnected => connectionStateSubject.Value == Enums.ConnectionState.Connected; - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public async Task Connect() - { - if (sharp7 == null) - throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); - - try + var errorCode = await Task.Factory.StartNew(() => sharp7.ConnectTo(ipAddress, rackNr, cpuSlotNr), CancellationToken.None, TaskCreationOptions.None, scheduler); + var success = EvaluateErrorCode(errorCode); + if (success) { - var errorCode = await Task.Factory.StartNew(() => sharp7.ConnectTo(ipAddress, rackNr, cpuSlotNr), CancellationToken.None, TaskCreationOptions.None, scheduler); - var success = EvaluateErrorCode(errorCode); - if (success) - { - connectionStateSubject.OnNext(Enums.ConnectionState.Connected); - return true; - } - } - catch (Exception ex) - { - // TODO: - } - - return false; - } - - - public async Task Disconnect() - { - connectionStateSubject.OnNext(Enums.ConnectionState.DisconnectedByUser); - await CloseConnection(); - } - - public async Task> ExecuteMultiVarRequest(IReadOnlyList variableNames) - { - if (variableNames.IsEmpty()) - return new Dictionary(); - - var s7MultiVar = new S7MultiVar(sharp7); - - var buffers = variableNames - .Select(key => new {VariableName = key, Address = variableNameParser.Parse(key)}) - .Select(x => - { - var buffer = new byte[x.Address.Length]; - s7MultiVar.Add(S7Consts.S7AreaDB, S7Consts.S7WLByte, x.Address.DbNr, x.Address.Start, x.Address.Length, ref buffer); - return new {x.VariableName, Buffer = buffer}; - }) - .ToArray(); - - var result = await Task.Factory.StartNew(() => s7MultiVar.Read(), CancellationToken.None, TaskCreationOptions.None, scheduler); - if (result != 0) - { - EvaluateErrorCode(result); - throw new InvalidOperationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)}"); - } - - return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer); - } - - public Task InitializeAsync() - { - try - { - sharp7 = new S7Client(); - sharp7.PLCPort = port; - - var subscription = - ConnectionState - .Where(state => state == Enums.ConnectionState.ConnectionLost) - .Take(1) - .SelectMany(_ => Reconnect()) - .RepeatAfterDelay(ReconnectDelay) - .LogAndRetry(Logger, "Error while reconnecting to S7.") - .Subscribe(); - - disposables.Add(subscription); - } - catch (Exception ex) - { - Logger?.LogError(ex, StringResources.StrErrorS7DriverCouldNotBeInitialized); - } - - return Task.FromResult(true); - } - - public async Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token) - { - EnsureConnectionValid(); - - var buffer = new byte[bytesToRead]; - - - var result = - await Task.Factory.StartNew(() => sharp7.ReadArea(operand.ToArea(), dBNr, startByteAddress, bytesToRead, S7WordLength.Byte, buffer), token, TaskCreationOptions.None, scheduler); - token.ThrowIfCancellationRequested(); - - if (result != 0) - { - EvaluateErrorCode(result); - var errorText = sharp7.ErrorText(result); - throw new InvalidOperationException($"Error reading {operand}{dBNr}:{startByteAddress}->{bytesToRead} ({errorText})"); - } - - return buffer; - } - - public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token) - { - EnsureConnectionValid(); - - var buffer = new[] {value ? (byte) 0xff : (byte) 0}; - - 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); - token.ThrowIfCancellationRequested(); - - if (result != 0) - { - EvaluateErrorCode(result); - return (false); - } - - return (true); - } - - public async Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token) - { - EnsureConnectionValid(); - - var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dBNr, startByteAdress, data.Length, S7WordLength.Byte, data), token, TaskCreationOptions.None, scheduler); - token.ThrowIfCancellationRequested(); - - if (result != 0) - { - EvaluateErrorCode(result); - return 0; - } - - return (ushort) (data.Length); - } - - - protected virtual void Dispose(bool disposing) - { - if (!disposed) - { - if (disposing) - { - disposables.Dispose(); - - if (sharp7 != null) - { - sharp7.Disconnect(); - sharp7 = null; - } - - connectionStateSubject?.OnCompleted(); - connectionStateSubject?.Dispose(); - } - - disposed = true; - } - } - - private async Task CloseConnection() - { - if (sharp7 == null) - throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); - - await Task.Factory.StartNew(() => sharp7.Disconnect(), CancellationToken.None, TaskCreationOptions.None, scheduler); - } - - private void EnsureConnectionValid() - { - if (disposed) - throw new ObjectDisposedException("S7Connector"); - - if (sharp7 == null) - throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); - - if (!IsConnected) - throw new InvalidOperationException("Plc is not connected"); - } - - private bool EvaluateErrorCode(int errorCode) - { - if (errorCode == 0) + connectionStateSubject.OnNext(Enums.ConnectionState.Connected); return true; - - if (sharp7 == null) - throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); - - var errorText = sharp7.ErrorText(errorCode); - Logger?.LogError($"Error Code {errorCode} {errorText}"); - - if (S7ErrorCodes.AssumeConnectionLost(errorCode)) - SetConnectionLostState(); - - return false; + } + } + catch (Exception ex) + { + // TODO: } - private async Task Reconnect() - { - await CloseConnection(); + return false; + } - return await Connect(); + + public async Task Disconnect() + { + connectionStateSubject.OnNext(Enums.ConnectionState.DisconnectedByUser); + await CloseConnection(); + } + + public async Task> ExecuteMultiVarRequest(IReadOnlyList variableNames) + { + if (variableNames.IsEmpty()) + return new Dictionary(); + + var s7MultiVar = new S7MultiVar(sharp7); + + var buffers = variableNames + .Select(key => new {VariableName = key, Address = variableNameParser.Parse(key)}) + .Select(x => + { + var buffer = new byte[x.Address.Length]; + s7MultiVar.Add(S7Consts.S7AreaDB, S7Consts.S7WLByte, x.Address.DbNr, x.Address.Start, x.Address.Length, ref buffer); + return new {x.VariableName, Buffer = buffer}; + }) + .ToArray(); + + var result = await Task.Factory.StartNew(() => s7MultiVar.Read(), CancellationToken.None, TaskCreationOptions.None, scheduler); + if (result != 0) + { + EvaluateErrorCode(result); + throw new InvalidOperationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)}"); } - private void SetConnectionLostState() - { - if (connectionStateSubject.Value == Enums.ConnectionState.ConnectionLost) return; + return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer); + } - connectionStateSubject.OnNext(Enums.ConnectionState.ConnectionLost); + public Task InitializeAsync() + { + try + { + sharp7 = new S7Client(); + sharp7.PLCPort = port; + + var subscription = + ConnectionState + .Where(state => state == Enums.ConnectionState.ConnectionLost) + .Take(1) + .SelectMany(_ => Reconnect()) + .RepeatAfterDelay(ReconnectDelay) + .LogAndRetry(Logger, "Error while reconnecting to S7.") + .Subscribe(); + + disposables.Add(subscription); + } + catch (Exception ex) + { + Logger?.LogError(ex, StringResources.StrErrorS7DriverCouldNotBeInitialized); } - ~Sharp7Connector() + return Task.FromResult(true); + } + + public async Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token) + { + EnsureConnectionValid(); + + var buffer = new byte[bytesToRead]; + + + var result = + await Task.Factory.StartNew(() => sharp7.ReadArea(operand.ToArea(), dBNr, startByteAddress, bytesToRead, S7WordLength.Byte, buffer), token, TaskCreationOptions.None, scheduler); + token.ThrowIfCancellationRequested(); + + if (result != 0) { - Dispose(false); + EvaluateErrorCode(result); + var errorText = sharp7.ErrorText(result); + throw new InvalidOperationException($"Error reading {operand}{dBNr}:{startByteAddress}->{bytesToRead} ({errorText})"); + } + + return buffer; + } + + public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token) + { + EnsureConnectionValid(); + + var buffer = new[] {value ? (byte) 0xff : (byte) 0}; + + 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); + token.ThrowIfCancellationRequested(); + + if (result != 0) + { + EvaluateErrorCode(result); + return (false); + } + + return (true); + } + + public async Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token) + { + EnsureConnectionValid(); + + var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dBNr, startByteAdress, data.Length, S7WordLength.Byte, data), token, TaskCreationOptions.None, scheduler); + token.ThrowIfCancellationRequested(); + + if (result != 0) + { + EvaluateErrorCode(result); + return 0; + } + + return (ushort) (data.Length); + } + + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + disposables.Dispose(); + + if (sharp7 != null) + { + sharp7.Disconnect(); + sharp7 = null; + } + + connectionStateSubject?.OnCompleted(); + connectionStateSubject?.Dispose(); + } + + disposed = true; } } + + private async Task CloseConnection() + { + if (sharp7 == null) + throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + + await Task.Factory.StartNew(() => sharp7.Disconnect(), CancellationToken.None, TaskCreationOptions.None, scheduler); + } + + private void EnsureConnectionValid() + { + if (disposed) + throw new ObjectDisposedException("S7Connector"); + + if (sharp7 == null) + throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + + if (!IsConnected) + throw new InvalidOperationException("Plc is not connected"); + } + + private bool EvaluateErrorCode(int errorCode) + { + if (errorCode == 0) + return true; + + if (sharp7 == null) + throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + + var errorText = sharp7.ErrorText(errorCode); + Logger?.LogError($"Error Code {errorCode} {errorText}"); + + if (S7ErrorCodes.AssumeConnectionLost(errorCode)) + SetConnectionLostState(); + + return false; + } + + private async Task Reconnect() + { + await CloseConnection(); + + return await Connect(); + } + + private void SetConnectionLostState() + { + if (connectionStateSubject.Value == Enums.ConnectionState.ConnectionLost) return; + + connectionStateSubject.OnNext(Enums.ConnectionState.ConnectionLost); + } + + ~Sharp7Connector() + { + Dispose(false); + } } diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 9902224..d57f63c 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -1,13 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; +using System.Diagnostics; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Text; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Sharp7.Rx.Basics; using Sharp7.Rx.Enums; @@ -15,270 +10,269 @@ using Sharp7.Rx.Extensions; using Sharp7.Rx.Interfaces; using Sharp7.Rx.Settings; -namespace Sharp7.Rx +namespace Sharp7.Rx; + +public class Sharp7Plc : IPlc { - public class Sharp7Plc : IPlc + protected readonly CompositeDisposable Disposables = new CompositeDisposable(); + private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new ConcurrentSubjectDictionary(StringComparer.InvariantCultureIgnoreCase); + private readonly List performanceCoutner = new List(1000); + private readonly PlcConnectionSettings plcConnectionSettings; + private readonly IS7VariableNameParser varaibleNameParser = new CacheVariableNameParser(new S7VariableNameParser()); + private bool disposed; + private IS7Connector s7Connector; + + + /// + /// + /// + /// + /// + /// + /// + /// + /// Polling interval used to read multi variable requests from PLC. + /// + /// + /// This is the wait time between two successive reads from PLC and determines the + /// time resolution for all variable reads reated with CreateNotification. + /// + /// + /// Default is 100 ms. The minimum supported time is 5 ms. + /// + /// + public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress, int port = 102, TimeSpan? multiVarRequestCycleTime = null) { - protected readonly CompositeDisposable Disposables = new CompositeDisposable(); - private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new ConcurrentSubjectDictionary(StringComparer.InvariantCultureIgnoreCase); - private readonly List performanceCoutner = new List(1000); - private readonly PlcConnectionSettings plcConnectionSettings; - private readonly IS7VariableNameParser varaibleNameParser = new CacheVariableNameParser(new S7VariableNameParser()); - private bool disposed; - private IS7Connector s7Connector; + plcConnectionSettings = new PlcConnectionSettings {IpAddress = ipAddress, RackNumber = rackNumber, CpuMpiAddress = cpuMpiAddress, Port = port}; + if (multiVarRequestCycleTime != null && multiVarRequestCycleTime > TimeSpan.FromMilliseconds(5)) + MultiVarRequestCycleTime = multiVarRequestCycleTime.Value; + } - /// - /// - /// - /// - /// - /// - /// - /// - /// Polling interval used to read multi variable requests from PLC. - /// - /// - /// This is the wait time between two successive reads from PLC and determines the - /// time resolution for all variable reads reated with CreateNotification. - /// - /// - /// Default is 100 ms. The minimum supported time is 5 ms. - /// - /// - 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}; + public IObservable ConnectionState { get; private set; } + public ILogger Logger { get; set; } + public TimeSpan MultiVarRequestCycleTime { get; } = TimeSpan.FromSeconds(0.1); - if (multiVarRequestCycleTime != null && multiVarRequestCycleTime > TimeSpan.FromMilliseconds(5)) - MultiVarRequestCycleTime = multiVarRequestCycleTime.Value; - } + public int MultiVarRequestMaxItems { get; set; } = 16; - public IObservable ConnectionState { get; private set; } - public ILogger Logger { get; set; } - public TimeSpan MultiVarRequestCycleTime { get; } = TimeSpan.FromSeconds(0.1); + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - public int MultiVarRequestMaxItems { get; set; } = 16; - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public IObservable CreateNotification(string variableName, TransmissionMode transmissionMode) - { - return Observable.Create(observer => - { - var address = varaibleNameParser.Parse(variableName); - if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName)); - - var disposables = new CompositeDisposable(); - var disposeableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName); - disposeableContainer.AddDisposableTo(disposables); - - var observable = disposeableContainer.Observable - .Select(bytes => S7ValueConverter.ConvertToType(bytes, address)); - - if (transmissionMode == TransmissionMode.OnChange) - observable = observable.DistinctUntilChanged(); - - observable.Subscribe(observer) - .AddDisposableTo(disposables); - - return disposables; - }); - } - - public Task GetValue(string variableName) - { - return GetValue(variableName, CancellationToken.None); - } - - - public Task SetValue(string variableName, TValue value) - { - return SetValue(variableName, value, CancellationToken.None); - } - - - public async Task GetValue(string variableName, CancellationToken token) + public IObservable CreateNotification(string variableName, TransmissionMode transmissionMode) + { + return Observable.Create(observer => { var address = varaibleNameParser.Parse(variableName); 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); - return S7ValueConverter.ConvertToType(data, address); - } + var disposables = new CompositeDisposable(); + var disposeableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName); + disposeableContainer.AddDisposableTo(disposables); - public async Task InitializeAsync() - { - s7Connector = new Sharp7Connector(plcConnectionSettings, varaibleNameParser) {Logger = Logger}; - ConnectionState = s7Connector.ConnectionState; + var observable = disposeableContainer.Observable + .Select(bytes => S7ValueConverter.ConvertToType(bytes, address)); - await s7Connector.InitializeAsync(); + if (transmissionMode == TransmissionMode.OnChange) + observable = observable.DistinctUntilChanged(); + + observable.Subscribe(observer) + .AddDisposableTo(disposables); + + return disposables; + }); + } + + public Task GetValue(string variableName) + { + return GetValue(variableName, CancellationToken.None); + } + + + public Task SetValue(string variableName, TValue value) + { + return SetValue(variableName, value, CancellationToken.None); + } + + + public async Task GetValue(string variableName, CancellationToken token) + { + var address = varaibleNameParser.Parse(variableName); + if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName)); + + var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); + return S7ValueConverter.ConvertToType(data, address); + } + + public async Task InitializeAsync() + { + s7Connector = new Sharp7Connector(plcConnectionSettings, varaibleNameParser) {Logger = Logger}; + ConnectionState = s7Connector.ConnectionState; + + await s7Connector.InitializeAsync(); #pragma warning disable 4014 - Task.Run(async () => + Task.Run(async () => + { + try { - try - { - await s7Connector.Connect(); - } - catch (Exception e) - { - Logger?.LogError(e, "Error while connecting to PLC"); - } - }); + await s7Connector.Connect(); + } + catch (Exception e) + { + Logger?.LogError(e, "Error while connecting to PLC"); + } + }); #pragma warning restore 4014 - RunNotifications(s7Connector, MultiVarRequestCycleTime) - .AddDisposableTo(Disposables); + RunNotifications(s7Connector, MultiVarRequestCycleTime) + .AddDisposableTo(Disposables); - return true; - } + return true; + } - public async Task SetValue(string variableName, TValue value, CancellationToken token) + public async Task SetValue(string variableName, TValue value, CancellationToken token) + { + var address = varaibleNameParser.Parse(variableName); + if (address == null) throw new ArgumentException("Input variable name is not valid", "variableName"); + + if (typeof(TValue) == typeof(bool)) { - var address = varaibleNameParser.Parse(variableName); - if (address == null) throw new ArgumentException("Input variable name is not valid", "variableName"); + await s7Connector.WriteBit(address.Operand, address.Start, address.Bit, (bool) (object) value, address.DbNr, token); + } + else if (typeof(TValue) == typeof(int) || typeof(TValue) == typeof(short)) + { + byte[] bytes; + if (address.Length == 4) + bytes = BitConverter.GetBytes((int) (object) value); + else + bytes = BitConverter.GetBytes((short) (object) value); - if (typeof(TValue) == typeof(bool)) - { - await s7Connector.WriteBit(address.Operand, address.Start, address.Bit, (bool) (object) value, address.DbNr, token); - } - else if (typeof(TValue) == typeof(int) || typeof(TValue) == typeof(short)) - { - byte[] bytes; - if (address.Length == 4) - bytes = BitConverter.GetBytes((int) (object) value); - else - bytes = BitConverter.GetBytes((short) (object) value); + Array.Reverse(bytes); - Array.Reverse(bytes); + await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); + } + else if (typeof(TValue) == typeof(byte) || typeof(TValue) == typeof(char)) + { + var bytes = new[] {Convert.ToByte(value)}; + await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); + } + else if (typeof(TValue) == typeof(byte[])) + { + await s7Connector.WriteBytes(address.Operand, address.Start, (byte[]) (object) value, address.DbNr, token); + } + else if (typeof(TValue) == typeof(float)) + { + var buffer = new byte[sizeof(float)]; + buffer.SetRealAt(0, (float) (object) value); + await s7Connector.WriteBytes(address.Operand, address.Start, buffer, address.DbNr, token); + } + else if (typeof(TValue) == typeof(string)) + { + var stringValue = value as string; + if (stringValue == null) throw new ArgumentException("Value must be of type string", "value"); - 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); - var bytes = Encoding.ASCII.GetBytes(stringValue); - Array.Resize(ref bytes, address.Length); - - if (address.Type == DbType.String) + 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 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); + var stringStartAddress = (ushort) (address.Start + 2); token.ThrowIfCancellationRequested(); + await s7Connector.WriteBytes(address.Operand, stringStartAddress, bytes, address.DbNr, token); } } else { - throw new InvalidOperationException($"type '{typeof(TValue)}' not supported."); + await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); + token.ThrowIfCancellationRequested(); } } - - protected virtual void Dispose(bool disposing) + else { - if (disposed) return; - disposed = true; - - if (disposing) - { - Disposables.Dispose(); - - if (s7Connector != null) - { - s7Connector.Disconnect().Wait(); - s7Connector.Dispose(); - s7Connector = null; - } - - multiVariableSubscriptions.Dispose(); - } - } - - private async Task GetAllValues(bool connected, IS7Connector connector) - { - if (!connected) - return Unit.Default; - - if (multiVariableSubscriptions.ExistingKeys.IsEmpty()) - return Unit.Default; - - var stopWatch = Stopwatch.StartNew(); - foreach (var partsOfMultiVarRequest in multiVariableSubscriptions.ExistingKeys.Buffer(MultiVarRequestMaxItems)) - { - var multiVarRequest = await connector.ExecuteMultiVarRequest(partsOfMultiVarRequest as IReadOnlyList); - - foreach (var pair in multiVarRequest) - if (multiVariableSubscriptions.TryGetObserver(pair.Key, out var subject)) - subject.OnNext(pair.Value); - } - - stopWatch.Stop(); - performanceCoutner.Add(stopWatch.ElapsedMilliseconds); - - PrintAndResetPerformanceStatistik(); - - return Unit.Default; - } - - private void PrintAndResetPerformanceStatistik() - { - if (performanceCoutner.Count == performanceCoutner.Capacity) - { - var average = performanceCoutner.Average(); - var min = performanceCoutner.Min(); - var max = performanceCoutner.Max(); - - Logger?.LogTrace("Performance statistic during {0} elements of plc notification. Min: {1}, Max: {2}, Average: {3}, Plc: '{4}', Number of variables: {5}, Batch size: {6}", performanceCoutner.Capacity, min, max, average, plcConnectionSettings.IpAddress, - multiVariableSubscriptions.ExistingKeys.Count(), - MultiVarRequestMaxItems); - performanceCoutner.Clear(); - } - } - - private IDisposable RunNotifications(IS7Connector connector, TimeSpan cycle) - { - return ConnectionState.FirstAsync() - .Select(states => states == Enums.ConnectionState.Connected) - .SelectMany(connected => GetAllValues(connected, connector)) - .RepeatAfterDelay(MultiVarRequestCycleTime) - .LogAndRetryAfterDelay(Logger, cycle, "Error while getting batch notifications from plc") - .Subscribe(); - } - - ~Sharp7Plc() - { - Dispose(false); + throw new InvalidOperationException($"type '{typeof(TValue)}' not supported."); } } -} \ No newline at end of file + + protected virtual void Dispose(bool disposing) + { + if (disposed) return; + disposed = true; + + if (disposing) + { + Disposables.Dispose(); + + if (s7Connector != null) + { + s7Connector.Disconnect().Wait(); + s7Connector.Dispose(); + s7Connector = null; + } + + multiVariableSubscriptions.Dispose(); + } + } + + private async Task GetAllValues(bool connected, IS7Connector connector) + { + if (!connected) + return Unit.Default; + + if (multiVariableSubscriptions.ExistingKeys.IsEmpty()) + return Unit.Default; + + var stopWatch = Stopwatch.StartNew(); + foreach (var partsOfMultiVarRequest in multiVariableSubscriptions.ExistingKeys.Buffer(MultiVarRequestMaxItems)) + { + var multiVarRequest = await connector.ExecuteMultiVarRequest(partsOfMultiVarRequest as IReadOnlyList); + + foreach (var pair in multiVarRequest) + if (multiVariableSubscriptions.TryGetObserver(pair.Key, out var subject)) + subject.OnNext(pair.Value); + } + + stopWatch.Stop(); + performanceCoutner.Add(stopWatch.ElapsedMilliseconds); + + PrintAndResetPerformanceStatistik(); + + return Unit.Default; + } + + private void PrintAndResetPerformanceStatistik() + { + if (performanceCoutner.Count == performanceCoutner.Capacity) + { + var average = performanceCoutner.Average(); + var min = performanceCoutner.Min(); + var max = performanceCoutner.Max(); + + Logger?.LogTrace("Performance statistic during {0} elements of plc notification. Min: {1}, Max: {2}, Average: {3}, Plc: '{4}', Number of variables: {5}, Batch size: {6}", performanceCoutner.Capacity, min, max, average, plcConnectionSettings.IpAddress, + multiVariableSubscriptions.ExistingKeys.Count(), + MultiVarRequestMaxItems); + performanceCoutner.Clear(); + } + } + + private IDisposable RunNotifications(IS7Connector connector, TimeSpan cycle) + { + return ConnectionState.FirstAsync() + .Select(states => states == Enums.ConnectionState.Connected) + .SelectMany(connected => GetAllValues(connected, connector)) + .RepeatAfterDelay(MultiVarRequestCycleTime) + .LogAndRetryAfterDelay(Logger, cycle, "Error while getting batch notifications from plc") + .Subscribe(); + } + + ~Sharp7Plc() + { + Dispose(false); + } +} From cb6e2d91e0a6c7cf4131ca0a5d8ee27b5917cb18 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 10:57:47 +0100 Subject: [PATCH 25/78] Add prerelease build --- .github/workflows/prerelease.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/prerelease.yml diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 0000000..0c4bd81 --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,30 @@ +name: .NET Core Build + +on: + push: + branches: + - prerelease + pull_request: + branches: [ prerelease ] + +jobs: + build: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.0.x + - name: Install NUnit.ConsoleRunner + run: nuget install NUnit.ConsoleRunner -Version 3.17.0 -DirectDownload -OutputDirectory . + - name: Install dependencies + run: dotnet restore + - name: Build + run: dotnet build --configuration Release --no-restore /p:version=2.0.${{ github.run_number }}-prerelease + - name: Tests + run: ./NUnit.ConsoleRunner.3.17.0/tools/nunit3-console.exe "Sharp7.Rx.Tests\bin\Release\net461\Sharp7.Rx.Tests.dll" + - name: NugetPublish + run: dotnet nuget push Sharp7.Rx\bin\Release\Sharp7.Rx.2.0.${{ github.run_number }}-prerelease.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_DEPLOY_KEY }} From 8aaf3c1e7e7b8e7546676aa6893cb43a07a3b42e Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 11:01:11 +0100 Subject: [PATCH 26/78] Fix unit test path --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 0c4bd81..2ea7119 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -25,6 +25,6 @@ jobs: - name: Build run: dotnet build --configuration Release --no-restore /p:version=2.0.${{ github.run_number }}-prerelease - name: Tests - run: ./NUnit.ConsoleRunner.3.17.0/tools/nunit3-console.exe "Sharp7.Rx.Tests\bin\Release\net461\Sharp7.Rx.Tests.dll" + run: ./NUnit.ConsoleRunner.3.17.0/tools/nunit3-console.exe "Sharp7.Rx.Tests\bin\Debug\net8.0\Sharp7.Rx.Tests.dll" - name: NugetPublish run: dotnet nuget push Sharp7.Rx\bin\Release\Sharp7.Rx.2.0.${{ github.run_number }}-prerelease.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_DEPLOY_KEY }} From 1b7a85c7c52cc666d4a5ee69d80cf58ca40c0d0e Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 11:05:26 +0100 Subject: [PATCH 27/78] Skip publish in release for pull requests --- .github/workflows/prerelease.yml | 2 +- .github/workflows/{dotnet-core.yml => release.yml} | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) rename .github/workflows/{dotnet-core.yml => release.yml} (90%) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 2ea7119..0831e60 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -1,4 +1,4 @@ -name: .NET Core Build +name: Prerelease on: push: diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/release.yml similarity index 90% rename from .github/workflows/dotnet-core.yml rename to .github/workflows/release.yml index f11645f..b1b8025 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: .NET Core Build +name: Release on: push: @@ -28,6 +28,7 @@ jobs: run: ./NUnit.ConsoleRunner.3.13.0/tools/nunit3-console.exe "Sharp7.Rx.Tests\bin\Release\net461\Sharp7.Rx.Tests.dll" - name: NugetPublish run: dotnet nuget push Sharp7.Rx\bin\Release\Sharp7.Rx.1.1.${{ github.run_number }}.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_DEPLOY_KEY }} + if: github.event_name != 'pull_request' - name: Create Release id: create_release uses: actions/create-release@v1 @@ -37,4 +38,5 @@ jobs: tag_name: 1.1.${{ github.run_number }} release_name: 1.1.${{ github.run_number }} draft: false - prerelease: true \ No newline at end of file + prerelease: true + if: github.event_name != 'pull_request' \ No newline at end of file From 19154501155c5ca5b9b71e2cdde98572a9ebb33b Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 11:07:09 +0100 Subject: [PATCH 28/78] Fix test path --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 0831e60..848b7f5 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -25,6 +25,6 @@ jobs: - name: Build run: dotnet build --configuration Release --no-restore /p:version=2.0.${{ github.run_number }}-prerelease - name: Tests - run: ./NUnit.ConsoleRunner.3.17.0/tools/nunit3-console.exe "Sharp7.Rx.Tests\bin\Debug\net8.0\Sharp7.Rx.Tests.dll" + run: ./NUnit.ConsoleRunner.3.17.0/tools/nunit3-console.exe "Sharp7.Rx.Tests\bin\Release\net8.0\Sharp7.Rx.Tests.dll" - name: NugetPublish run: dotnet nuget push Sharp7.Rx\bin\Release\Sharp7.Rx.2.0.${{ github.run_number }}-prerelease.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_DEPLOY_KEY }} From c667c113e3c348a882f2d6646df3a588c9c66a0c Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 11:29:48 +0100 Subject: [PATCH 29/78] Set s7Connector in constructor --- Sharp7.Rx/Sharp7Connector.cs | 6 +++--- Sharp7.Rx/Sharp7Plc.cs | 39 +++++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index e8a078a..efdd014 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -13,14 +13,14 @@ namespace Sharp7.Rx; internal class Sharp7Connector : IS7Connector { - private readonly BehaviorSubject connectionStateSubject = new BehaviorSubject(Enums.ConnectionState.Initial); + private readonly BehaviorSubject connectionStateSubject = new(Enums.ConnectionState.Initial); private readonly int cpuSlotNr; - private readonly CompositeDisposable disposables = new CompositeDisposable(); + private readonly CompositeDisposable disposables = new(); private readonly string ipAddress; private readonly int port; private readonly int rackNr; - private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism: 1); + private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new(maxDegreeOfParallelism: 1); private readonly IS7VariableNameParser variableNameParser; private bool disposed; diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index d57f63c..974619d 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -14,13 +14,13 @@ namespace Sharp7.Rx; public class Sharp7Plc : IPlc { - protected readonly CompositeDisposable Disposables = new CompositeDisposable(); - private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new ConcurrentSubjectDictionary(StringComparer.InvariantCultureIgnoreCase); - private readonly List performanceCoutner = new List(1000); + private readonly CompositeDisposable disposables = new(); + private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase); + private readonly List performanceCoutner = new(1000); private readonly PlcConnectionSettings plcConnectionSettings; private readonly IS7VariableNameParser varaibleNameParser = new CacheVariableNameParser(new S7VariableNameParser()); private bool disposed; - private IS7Connector s7Connector; + private Sharp7Connector s7Connector; /// @@ -44,13 +44,26 @@ public class Sharp7Plc : IPlc public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress, int port = 102, TimeSpan? multiVarRequestCycleTime = null) { plcConnectionSettings = new PlcConnectionSettings {IpAddress = ipAddress, RackNumber = rackNumber, CpuMpiAddress = cpuMpiAddress, Port = port}; + s7Connector = new Sharp7Connector(plcConnectionSettings, varaibleNameParser); + ConnectionState = s7Connector.ConnectionState; - if (multiVarRequestCycleTime != null && multiVarRequestCycleTime > TimeSpan.FromMilliseconds(5)) - MultiVarRequestCycleTime = multiVarRequestCycleTime.Value; + if (multiVarRequestCycleTime != null) + { + if (multiVarRequestCycleTime < TimeSpan.FromMilliseconds(5)) + MultiVarRequestCycleTime = TimeSpan.FromMilliseconds(5); + else + MultiVarRequestCycleTime = multiVarRequestCycleTime.Value; + } + } + + public IObservable ConnectionState { get; } + + public ILogger Logger + { + get => s7Connector.Logger; + set => s7Connector.Logger = value; } - public IObservable ConnectionState { get; private set; } - public ILogger Logger { get; set; } public TimeSpan MultiVarRequestCycleTime { get; } = TimeSpan.FromSeconds(0.1); public int MultiVarRequestMaxItems { get; set; } = 16; @@ -108,9 +121,6 @@ public class Sharp7Plc : IPlc public async Task InitializeAsync() { - s7Connector = new Sharp7Connector(plcConnectionSettings, varaibleNameParser) {Logger = Logger}; - ConnectionState = s7Connector.ConnectionState; - await s7Connector.InitializeAsync(); #pragma warning disable 4014 @@ -128,7 +138,7 @@ public class Sharp7Plc : IPlc #pragma warning restore 4014 RunNotifications(s7Connector, MultiVarRequestCycleTime) - .AddDisposableTo(Disposables); + .AddDisposableTo(disposables); return true; } @@ -207,7 +217,7 @@ public class Sharp7Plc : IPlc if (disposing) { - Disposables.Dispose(); + disposables.Dispose(); if (s7Connector != null) { @@ -254,7 +264,8 @@ public class Sharp7Plc : IPlc var min = performanceCoutner.Min(); var max = performanceCoutner.Max(); - Logger?.LogTrace("Performance statistic during {0} elements of plc notification. Min: {1}, Max: {2}, Average: {3}, Plc: '{4}', Number of variables: {5}, Batch size: {6}", performanceCoutner.Capacity, min, max, average, plcConnectionSettings.IpAddress, + Logger?.LogTrace("Performance statistic during {0} elements of plc notification. Min: {1}, Max: {2}, Average: {3}, Plc: '{4}', Number of variables: {5}, Batch size: {6}", + performanceCoutner.Capacity, min, max, average, plcConnectionSettings.IpAddress, multiVariableSubscriptions.ExistingKeys.Count(), MultiVarRequestMaxItems); performanceCoutner.Clear(); From 3c39996f5ef6a6de0ade93019878de9b8d4d6f4c Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 13:33:50 +0100 Subject: [PATCH 30/78] Make CreateNotification for nonexistent variable fail with Exception --- Sharp7.Rx/Sharp7Plc.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 974619d..0265a80 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -85,8 +85,14 @@ public class Sharp7Plc : IPlc var disposeableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName); disposeableContainer.AddDisposableTo(disposables); - var observable = disposeableContainer.Observable - .Select(bytes => S7ValueConverter.ConvertToType(bytes, address)); + var observable = + // Directly read variable first. + // This will propagate any errors due to reading from invalid addresses. + Observable.FromAsync(() => GetValue(variableName)) + .Concat( + disposeableContainer.Observable + .Select(bytes => S7ValueConverter.ConvertToType(bytes, address)) + ); if (transmissionMode == TransmissionMode.OnChange) observable = observable.DistinctUntilChanged(); From d4a8ef9cb32092f152bf4481d2b06e104bf939ee Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 13:34:12 +0100 Subject: [PATCH 31/78] Rename variable --- Sharp7.Rx/Sharp7Plc.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 0265a80..02a975c 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -81,9 +81,9 @@ public class Sharp7Plc : IPlc var address = varaibleNameParser.Parse(variableName); 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); - disposeableContainer.AddDisposableTo(disposables); + disposeableContainer.AddDisposableTo(disp); var observable = // Directly read variable first. @@ -98,9 +98,9 @@ public class Sharp7Plc : IPlc observable = observable.DistinctUntilChanged(); observable.Subscribe(observer) - .AddDisposableTo(disposables); + .AddDisposableTo(disp); - return disposables; + return disp; }); } From 4389e813408f1ebffa9b3c1d594ea0a1745e4b27 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 13:47:13 +0100 Subject: [PATCH 32/78] Add Tests for WriteToBuffer --- .../S7ValueConverterTests.cs | 4 +- .../S7ValueConverterTests/WriteToBuffer.cs | 47 +++++++++++++++++++ Sharp7.Rx/S7ValueConverter.cs | 4 ++ 3 files changed, 53 insertions(+), 2 deletions(-) rename Sharp7.Rx.Tests/{ => S7ValueConverterTests}/S7ValueConverterTests.cs (97%) create mode 100644 Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/S7ValueConverterTests.cs similarity index 97% rename from Sharp7.Rx.Tests/S7ValueConverterTests.cs rename to Sharp7.Rx.Tests/S7ValueConverterTests/S7ValueConverterTests.cs index 032877d..23052d2 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/S7ValueConverterTests.cs @@ -2,10 +2,10 @@ using Sharp7.Rx.Interfaces; using Shouldly; -namespace Sharp7.Rx.Tests; +namespace Sharp7.Rx.Tests.S7ValueConverterTests; [TestFixture] -public class S7ValueConverterTests +public class ConvertToType { static readonly IS7VariableNameParser parser = new S7VariableNameParser(); diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs new file mode 100644 index 0000000..137bd07 --- /dev/null +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs @@ -0,0 +1,47 @@ +using NUnit.Framework; +using Sharp7.Rx.Interfaces; +using Shouldly; + +namespace Sharp7.Rx.Tests.S7ValueConverterTests; + +[TestFixture] +public class WriteToBuffer +{ + 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 Write(T expected, string address, byte[] data) + { + //Arrange + var variableAddress = parser.Parse(address); + var buffer = new byte[variableAddress.Length]; + + //Act + S7ValueConverter.WriteToBuffer(buffer, expected, variableAddress); + + //Assert + buffer.ShouldBe(data); + } +} diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 26b86c5..612e159 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -7,6 +7,10 @@ namespace Sharp7.Rx; internal static class S7ValueConverter { + public static void WriteToBuffer(Span buffer, TValue value, S7VariableAddress address) + { + } + public static TValue ConvertToType(byte[] buffer, S7VariableAddress address) { if (typeof(TValue) == typeof(bool)) From 981a30647881dc4115171c6f03ff87b4c21a54f2 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 15:16:23 +0100 Subject: [PATCH 33/78] Add WriteToBuffer and unify supported types --- .../S7ValueConverterTests/ConvertBothWays.cs | 45 +++++++++ ...alueConverterTests.cs => ConvertToType.cs} | 8 +- .../S7ValueConverterTests/WriteToBuffer.cs | 29 ++++-- Sharp7.Rx/S7ValueConverter.cs | 95 +++++++++++++++---- Sharp7.Rx/S7VariableAddress.cs | 2 + 5 files changed, 148 insertions(+), 31 deletions(-) create mode 100644 Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs rename Sharp7.Rx.Tests/S7ValueConverterTests/{S7ValueConverterTests.cs => ConvertToType.cs} (95%) diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs new file mode 100644 index 0000000..5d264a1 --- /dev/null +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs @@ -0,0 +1,45 @@ +using NUnit.Framework; +using Sharp7.Rx.Interfaces; +using Shouldly; + +namespace Sharp7.Rx.Tests.S7ValueConverterTests; + +[TestFixture] +public class ConvertBothWays +{ + static readonly IS7VariableNameParser parser = new S7VariableNameParser(); + + [TestCase(true, "DB0.DBx0.0")] + [TestCase(false, "DB0.DBx0.0")] + [TestCase(true, "DB0.DBx0.4")] + [TestCase(false, "DB0.DBx0.4")] + [TestCase((byte) 18, "DB0.DBB0")] + [TestCase((short) 4660, "DB0.INT0")] + [TestCase((short)-3532, "DB0.INT0")] + [TestCase(-3532, "DB0.INT0")] + [TestCase(305419879, "DB0.DINT0")] + [TestCase(-231451033, "DB0.DINT0")] + [TestCase(1311768394163015151L, "DB0.dul0")] + [TestCase(-994074615050678801L, "DB0.dul0")] + [TestCase(1311768394163015151uL, "DB0.dul0")] + [TestCase(17452669458658872815uL, "DB0.dul0")] + [TestCase(new byte[] { 0x12, 0x34, 0x56, 0x67 }, "DB0.DBB0.4")] + [TestCase(0.25f, "DB0.D0")] + [TestCase("ABCD", "DB0.string0.4")] + [TestCase("ABCD", "DB0.string0.4")] // Clip to length in Address + [TestCase("ABCD", "DB0.DBB0.4")] + public void Write(T input, string address) + { + //Arrange + var variableAddress = parser.Parse(address); + var buffer = new byte[variableAddress.BufferLength]; + + //Act + S7ValueConverter.WriteToBuffer(buffer, input, variableAddress); + var result = S7ValueConverter.ConvertToType(buffer, variableAddress); + + //Assert + result.ShouldBe(input); + } + +} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/S7ValueConverterTests.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs similarity index 95% rename from Sharp7.Rx.Tests/S7ValueConverterTests/S7ValueConverterTests.cs rename to Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs index 23052d2..caa37a9 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/S7ValueConverterTests.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs @@ -16,7 +16,6 @@ public class ConvertToType [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})] @@ -28,7 +27,6 @@ public class ConvertToType [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})] @@ -44,8 +42,10 @@ public class ConvertToType result.ShouldBe(expected); } + [TestCase((char) 18, "DB0.DBB0", new byte[] {0x12})] [TestCase((ushort) 3532, "DB0.INT0", new byte[] {0xF2, 0x34})] - public void Invalid(T expected, string address, byte[] data) + [TestCase(0.25, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] + public void Invalid(T template, string address, byte[] data) { //Arrange var variableAddress = parser.Parse(address); @@ -55,7 +55,7 @@ public class ConvertToType } [TestCase(3532, "DB0.DINT0", new byte[] {0xF2, 0x34})] - public void Argument(T expected, string address, byte[] data) + public void Argument(T template, string address, byte[] data) { //Arrange var variableAddress = parser.Parse(address); diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs index 137bd07..c33209f 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs @@ -13,10 +13,7 @@ public class WriteToBuffer [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})] @@ -28,20 +25,32 @@ public class WriteToBuffer [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.string0.4", new byte[] {0x04, 0x04, 0x41, 0x42, 0x43, 0x44})] + [TestCase("ABCD", "DB0.string0.8", new byte[] {0x08, 0x04, 0x41, 0x42, 0x43, 0x44, 0x00, 0x00, 0x00, 0x00})] + [TestCase("ABCD", "DB0.string0.2", new byte[] {0x02, 0x02, 0x41, 0x42})] [TestCase("ABCD", "DB0.DBB0.4", new byte[] {0x41, 0x42, 0x43, 0x44})] - public void Write(T expected, string address, byte[] data) + public void Write(T input, string address, byte[] expected) { //Arrange var variableAddress = parser.Parse(address); - var buffer = new byte[variableAddress.Length]; + var buffer = new byte[variableAddress.BufferLength]; //Act - S7ValueConverter.WriteToBuffer(buffer, expected, variableAddress); + S7ValueConverter.WriteToBuffer(buffer, input, variableAddress); //Assert - buffer.ShouldBe(data); + buffer.ShouldBe(expected); + } + + [TestCase((char) 18, "DB0.DBB0")] + [TestCase(0.25, "DB0.D0")] + public void Invalid(T input, string address) + { + //Arrange + var variableAddress = parser.Parse(address); + var buffer = new byte[variableAddress.BufferLength]; + + //Act + Should.Throw(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); } } diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 612e159..247070a 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -7,10 +7,6 @@ namespace Sharp7.Rx; internal static class S7ValueConverter { - public static void WriteToBuffer(Span buffer, TValue value, S7VariableAddress address) - { - } - public static TValue ConvertToType(byte[] buffer, S7VariableAddress address) { if (typeof(TValue) == typeof(bool)) @@ -37,21 +33,10 @@ internal static class S7ValueConverter 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) - }; - return (TValue) (object) (double) d.Single; - } - + if (typeof(TValue) == typeof(float)) { var d = new UInt32SingleMap @@ -75,7 +60,83 @@ internal static class S7ValueConverter else return (TValue) (object) Encoding.ASCII.GetString(buffer).Trim(); - throw new InvalidOperationException(string.Format("type '{0}' not supported.", typeof(TValue))); + throw new InvalidOperationException($"type '{typeof(TValue)}' not supported."); + } + + public static void WriteToBuffer(Span buffer, TValue value, S7VariableAddress address) + { + if (buffer.Length < address.BufferLength) + throw new ArgumentException($"buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer)); + + if (typeof(TValue) == typeof(bool)) + { + var byteValue = (bool) (object) value ? (byte) 1 : (byte) 0; + var shifted = (byte) (byteValue << address.Bit); + buffer[0] = shifted; + } + + else if (typeof(TValue) == typeof(int)) + { + if (address.Length == 2) + BinaryPrimitives.WriteInt16BigEndian(buffer, (short) (int) (object) value); + else + BinaryPrimitives.WriteInt32BigEndian(buffer, (int) (object) value); + } + else if (typeof(TValue) == typeof(short)) + { + if (address.Length == 2) + BinaryPrimitives.WriteInt16BigEndian(buffer, (short) (object) value); + else + BinaryPrimitives.WriteInt32BigEndian(buffer, (short) (object) value); + } + else if (typeof(TValue) == typeof(long)) + BinaryPrimitives.WriteInt64BigEndian(buffer, (long) (object) value); + else if (typeof(TValue) == typeof(ulong)) + BinaryPrimitives.WriteUInt64BigEndian(buffer, (ulong) (object) value); + else if (typeof(TValue) == typeof(byte)) + buffer[0] = (byte) (object) value; + else if (typeof(TValue) == typeof(byte[])) + { + var source = (byte[]) (object) value; + + var length = Math.Min(Math.Min(source.Length, buffer.Length), address.Length); + + source.AsSpan(0, length).CopyTo(buffer); + } + else if (typeof(TValue) == typeof(float)) + { + var map = new UInt32SingleMap + { + Single = (float) (object) value + }; + + BinaryPrimitives.WriteUInt32BigEndian(buffer, map.UInt32); + } + else if (typeof(TValue) == typeof(string)) + { + if (value is not string stringValue) throw new ArgumentException("Value must be of type string", nameof(value)); + + // Todo: Serialize directly to Span, when upgrading to .net + var stringBytes = Encoding.ASCII.GetBytes(stringValue); + + var length = Math.Min(address.Length, stringValue.Length); + + int stringOffset; + if (address.Type == DbType.String) + { + stringOffset = 2; + buffer[0] = (byte) address.Length; + buffer[1] = (byte) length; + } + else + stringOffset = 0; + + stringBytes.AsSpan(0, length).CopyTo(buffer.Slice(stringOffset)); + } + else + { + throw new InvalidOperationException($"type '{typeof(TValue)}' not supported."); + } } [StructLayout(LayoutKind.Explicit)] diff --git a/Sharp7.Rx/S7VariableAddress.cs b/Sharp7.Rx/S7VariableAddress.cs index ae5e440..b12d55f 100644 --- a/Sharp7.Rx/S7VariableAddress.cs +++ b/Sharp7.Rx/S7VariableAddress.cs @@ -12,4 +12,6 @@ internal class S7VariableAddress public ushort Length { get; set; } public byte Bit { get; set; } public DbType Type { get; set; } + + public ushort BufferLength => Type == DbType.String ? (ushort)(Length + 2) : Length; } From 2a694bf9808e726dac151dca1241685df61be81c Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 16:43:18 +0100 Subject: [PATCH 34/78] Use WriteToBuffer in Sharp7Plc --- Sharp7.Rx/Sharp7Plc.cs | 63 +++++------------------------------------- 1 file changed, 7 insertions(+), 56 deletions(-) diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 02a975c..d7eb28d 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -2,7 +2,6 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Text; using Microsoft.Extensions.Logging; using Sharp7.Rx.Basics; using Sharp7.Rx.Enums; @@ -155,64 +154,16 @@ public class Sharp7Plc : IPlc if (address == null) throw new ArgumentException("Input variable name is not valid", "variableName"); if (typeof(TValue) == typeof(bool)) - { + // Special handling for bools, which are written on a by-bit basis. Writing a complete byte would + // overwrite other bits within this byte. await s7Connector.WriteBit(address.Operand, address.Start, address.Bit, (bool) (object) value, address.DbNr, token); - } - else if (typeof(TValue) == typeof(int) || typeof(TValue) == typeof(short)) - { - byte[] bytes; - if (address.Length == 4) - bytes = BitConverter.GetBytes((int) (object) value); - else - bytes = BitConverter.GetBytes((short) (object) value); - - Array.Reverse(bytes); - - await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); - } - else if (typeof(TValue) == typeof(byte) || typeof(TValue) == typeof(char)) - { - var bytes = new[] {Convert.ToByte(value)}; - await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); - } - else if (typeof(TValue) == typeof(byte[])) - { - await s7Connector.WriteBytes(address.Operand, address.Start, (byte[]) (object) value, address.DbNr, token); - } - else if (typeof(TValue) == typeof(float)) - { - var buffer = new byte[sizeof(float)]; - buffer.SetRealAt(0, (float) (object) value); - await s7Connector.WriteBytes(address.Operand, address.Start, buffer, address.DbNr, token); - } - else if (typeof(TValue) == typeof(string)) - { - var stringValue = value as string; - if (stringValue == null) throw new ArgumentException("Value must be of type string", "value"); - - var bytes = Encoding.ASCII.GetBytes(stringValue); - Array.Resize(ref bytes, address.Length); - - if (address.Type == DbType.String) - { - var bytesWritten = await s7Connector.WriteBytes(address.Operand, address.Start, new[] {(byte) address.Length, (byte) bytes.Length}, address.DbNr, token); - token.ThrowIfCancellationRequested(); - if (bytesWritten == 2) - { - var stringStartAddress = (ushort) (address.Start + 2); - token.ThrowIfCancellationRequested(); - await s7Connector.WriteBytes(address.Operand, stringStartAddress, bytes, address.DbNr, token); - } - } - else - { - await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNr, token); - token.ThrowIfCancellationRequested(); - } - } 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); } } From 6492d039daad2d0dc164421cdfab74f5fc09b476 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Wed, 7 Feb 2024 17:40:51 +0100 Subject: [PATCH 35/78] Extend supported variables and improve parser errors --- .../S7VariableAddressTests/MatchesType.cs | 40 ++++ Sharp7.Rx.Tests/S7VariableNameParserTests.cs | 77 ++++++-- Sharp7.Rx/Enums/DbType.cs | 46 ++++- Sharp7.Rx/Exceptions/S7Exception.cs | 27 +++ Sharp7.Rx/Extensions/S7VariableExtensions.cs | 9 + Sharp7.Rx/Interfaces/IS7VariableNameParser.cs | 3 +- Sharp7.Rx/S7VariableAddress.cs | 9 +- Sharp7.Rx/S7VariableNameParser.cs | 171 ++++++++++++------ Sharp7.Rx/Sharp7.Rx.csproj | 4 + Sharp7.Rx/Sharp7.Rx.csproj.DotSettings | 2 + Sharp7.Rx/Sharp7Plc.cs | 8 +- 11 files changed, 323 insertions(+), 73 deletions(-) create mode 100644 Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs create mode 100644 Sharp7.Rx/Exceptions/S7Exception.cs create mode 100644 Sharp7.Rx/Extensions/S7VariableExtensions.cs create mode 100644 Sharp7.Rx/Sharp7.Rx.csproj.DotSettings diff --git a/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs b/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs new file mode 100644 index 0000000..d3d428a --- /dev/null +++ b/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using NUnit.Framework; +using Sharp7.Rx.Extensions; +using Sharp7.Rx.Interfaces; +using Shouldly; + +namespace Sharp7.Rx.Tests.S7VariableAddressTests; + +[TestFixture] +public class MatchesType +{ + static readonly IS7VariableNameParser parser = new S7VariableNameParser(); + + + public void Supported(Type type, string address) + { + Check(type, address, true); + } + + public IEnumerable GetValid() + { + yield return new TestCase(typeof(bool), "DB0.DBx0.0"); + yield return new TestCase(typeof(short), "DB0.INT0"); + yield return new TestCase(typeof(int), "DB0.DINT0"); + yield return new TestCase(typeof(long), "DB0.DUL0"); + yield return new TestCase(typeof(ulong), "DB0.DUL0"); + } + + + 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); +} diff --git a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs index a5fa263..7f4869f 100644 --- a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs +++ b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs @@ -1,13 +1,14 @@ using DeepEqual.Syntax; using NUnit.Framework; using Sharp7.Rx.Enums; +using Shouldly; namespace Sharp7.Rx.Tests; [TestFixture] internal class S7VariableNameParserTests { - [TestCaseSource(nameof(GetTestCases))] + [TestCaseSource(nameof(ValidTestCases))] public void Run(TestCase tc) { var parser = new S7VariableNameParser(); @@ -15,23 +16,71 @@ internal class S7VariableNameParserTests resp.ShouldDeepEqual(tc.Expected); } - public static IEnumerable GetTestCases() + [TestCase("DB506.Bit216", TestName = "Bit without Bit")] + [TestCase("DB506.String216", TestName = "String without Length")] + [TestCase("DB506.WString216", TestName = "WString without Length")] + + [TestCase("DB506.Int216.1", TestName = "Int with Length")] + [TestCase("DB506.UInt216.1", TestName = "UInt with Length")] + [TestCase("DB506.DInt216.1", TestName = "DInt with Length")] + [TestCase("DB506.UDInt216.1", TestName = "UDInt with Length")] + [TestCase("DB506.LInt216.1", TestName = "LInt with Length")] + [TestCase("DB506.ULInt216.1", TestName = "ULInt with Length")] + [TestCase("DB506.Real216.1", TestName = "LReal with Length")] + [TestCase("DB506.LReal216.1", TestName = "LReal with Length")] + + [TestCase("DB506.xx216", TestName = "Invalid type")] + [TestCase("DB506.216", TestName = "No type")] + [TestCase("DB506.Int216.", TestName = "Trailing dot")] + [TestCase("x506.Int216", TestName = "Wrong type")] + [TestCase("506.Int216", TestName = "No type")] + [TestCase("", TestName = "empty")] + [TestCase(" ", TestName = "space")] + [TestCase(" DB506.Int216", TestName = "leading space")] + [TestCase("DB506.Int216 ", TestName = "trailing space")] + [TestCase("DB.Int216 ", TestName = "No db")] + [TestCase("DB5061234.Int216.1", TestName = "DB too large")] + public void Invalid(string? input) { + var parser = new S7VariableNameParser(); + Should.Throw(() => parser.Parse(input)); + } + + public static IEnumerable ValidTestCases() + { + yield return new TestCase("DB506.Bit216.2", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Bit = 2, Type = DbType.Bit}); + + yield return new TestCase("DB506.String216.10", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 10, Type = DbType.String}); + yield return new TestCase("DB506.WString216.10", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 10, Type = DbType.WString}); + + yield return new TestCase("DB506.Byte216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Type = DbType.Byte}); + yield return new TestCase("DB506.Byte216.100", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 100, Type = DbType.Byte}); + yield return new TestCase("DB506.Int216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int}); + yield return new TestCase("DB506.UInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.UInt}); + yield return new TestCase("DB506.DInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.DInt}); + yield return new TestCase("DB506.UDInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.UDInt}); + yield return new TestCase("DB506.LInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.LInt}); + yield return new TestCase("DB506.ULInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); + + yield return new TestCase("DB506.Real216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.Single}); + yield return new TestCase("DB506.LReal216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.Double}); + + + // Legacy yield return new TestCase("DB13.DBX3.1", new S7VariableAddress {Operand = Operand.Db, DbNr = 13, Start = 3, Length = 1, Bit = 1, Type = DbType.Bit}); yield return new TestCase("Db403.X5.2", new S7VariableAddress {Operand = Operand.Db, DbNr = 403, Start = 5, Length = 1, Bit = 2, Type = DbType.Bit}); yield return new TestCase("DB55DBX23.6", new S7VariableAddress {Operand = Operand.Db, DbNr = 55, Start = 23, Length = 1, Bit = 6, Type = DbType.Bit}); - yield return new TestCase("DB1.S255", new S7VariableAddress {Operand = Operand.Db, DbNr = 1, Start = 255, Length = 0, Bit = 0, Type = DbType.String}); - yield return new TestCase("DB1.S255.20", new S7VariableAddress {Operand = Operand.Db, DbNr = 1, Start = 255, Length = 20, Bit = 0, Type = DbType.String}); - yield return new TestCase("DB5.String887.20", new S7VariableAddress {Operand = Operand.Db, DbNr = 5, Start = 887, Length = 20, Bit = 0, Type = DbType.String}); - yield return new TestCase("DB506.B216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Bit = 0, Type = DbType.Byte}); - yield return new TestCase("DB506.DBB216.5", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 5, Bit = 0, Type = DbType.Byte}); - yield return new TestCase("DB506.D216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Bit = 0, Type = DbType.Double}); - yield return new TestCase("DB506.DINT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Bit = 0, Type = DbType.DInteger}); - yield return new TestCase("DB506.INT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Bit = 0, Type = DbType.Integer}); - yield return new TestCase("DB506.DBW216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Bit = 0, Type = DbType.Integer}); - yield return new TestCase("DB506.DUL216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Bit = 0, Type = DbType.ULong}); - yield return new TestCase("DB506.DULINT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Bit = 0, Type = DbType.ULong}); - yield return new TestCase("DB506.DULONG216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Bit = 0, Type = DbType.ULong}); + yield return new TestCase("DB1.S255.20", new S7VariableAddress {Operand = Operand.Db, DbNr = 1, Start = 255, Length = 20, Type = DbType.String}); + yield return new TestCase("DB5.String887.20", new S7VariableAddress {Operand = Operand.Db, DbNr = 5, Start = 887, Length = 20, Type = DbType.String}); + yield return new TestCase("DB506.B216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Type = DbType.Byte}); + yield return new TestCase("DB506.DBB216.5", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 5, Type = DbType.Byte}); + yield return new TestCase("DB506.D216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.Single}); + yield return new TestCase("DB506.DINT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.DInt}); + yield return new TestCase("DB506.INT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int}); + yield return new TestCase("DB506.DBW216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int}); + yield return new TestCase("DB506.DUL216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); + yield return new TestCase("DB506.DULINT216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); + yield return new TestCase("DB506.DULONG216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); } public record TestCase(string Input, S7VariableAddress Expected) diff --git a/Sharp7.Rx/Enums/DbType.cs b/Sharp7.Rx/Enums/DbType.cs index 07066f8..30691d6 100644 --- a/Sharp7.Rx/Enums/DbType.cs +++ b/Sharp7.Rx/Enums/DbType.cs @@ -1,12 +1,52 @@ namespace Sharp7.Rx.Enums; +// see https://support.industry.siemens.com/cs/mdm/109747174?c=88343664523&lc=de-DE internal enum DbType { Bit, + + /// + /// ASCII string + /// String, + + /// + /// UTF16 string + /// + WString, + Byte, + + /// + /// Int16 + /// + Int, + + /// + /// UInt16 + /// + UInt, + + /// + /// Int32 + /// + DInt, + + /// + /// UInt32 + /// + UDInt, + + /// + /// Int64 + /// + LInt, + + /// + /// UInt64 + /// + ULInt, + + Single, Double, - Integer, - DInteger, - ULong } diff --git a/Sharp7.Rx/Exceptions/S7Exception.cs b/Sharp7.Rx/Exceptions/S7Exception.cs new file mode 100644 index 0000000..e9cc1d3 --- /dev/null +++ b/Sharp7.Rx/Exceptions/S7Exception.cs @@ -0,0 +1,27 @@ +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 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; private set; } +} diff --git a/Sharp7.Rx/Extensions/S7VariableExtensions.cs b/Sharp7.Rx/Extensions/S7VariableExtensions.cs new file mode 100644 index 0000000..a2b0a39 --- /dev/null +++ b/Sharp7.Rx/Extensions/S7VariableExtensions.cs @@ -0,0 +1,9 @@ +namespace Sharp7.Rx.Extensions; + +internal static class S7VariableAddressExtensions +{ + public static bool MatchesType(this S7VariableAddress address, Type type) + { + return false; + } +} diff --git a/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs b/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs index 07f4b9e..ae81d67 100644 --- a/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs +++ b/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs @@ -1,4 +1,5 @@ -namespace Sharp7.Rx.Interfaces; +#nullable enable +namespace Sharp7.Rx.Interfaces; internal interface IS7VariableNameParser { diff --git a/Sharp7.Rx/S7VariableAddress.cs b/Sharp7.Rx/S7VariableAddress.cs index b12d55f..0172c06 100644 --- a/Sharp7.Rx/S7VariableAddress.cs +++ b/Sharp7.Rx/S7VariableAddress.cs @@ -10,8 +10,13 @@ internal class S7VariableAddress public ushort DbNr { get; set; } public ushort Start { get; set; } public ushort Length { get; set; } - public byte Bit { get; set; } + public byte? Bit { get; set; } public DbType Type { get; set; } - public ushort BufferLength => Type == DbType.String ? (ushort)(Length + 2) : Length; + public ushort BufferLength => Type switch + { + DbType.String => (ushort) (Length + 2), + DbType.WString => (ushort) (Length * 2 + 4), + _ => Length + }; } diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index 80e9913..683ec18 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -7,75 +7,142 @@ namespace Sharp7.Rx; internal class S7VariableNameParser : IS7VariableNameParser { - private static readonly Regex regex = new Regex(@"^(?db{1})(?\d{1,4})\.?(?dbx|x|s|string|b|dbb|d|int|dbw|w|dint|dul|dulint|dulong|){1}(?\d+)(\.(?\d+))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex regex = new Regex(@"^(?db)(?\d+)\.?(?[a-z]+)(?\d+)(\.(?\d+))?$", + RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly IReadOnlyDictionary types = new Dictionary(StringComparer.OrdinalIgnoreCase) { - {"x", DbType.Bit}, - {"dbx", DbType.Bit}, - {"s", DbType.String}, + {"bit", DbType.Bit}, + {"string", DbType.String}, + {"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}, + + // used for legacy compatability {"b", DbType.Byte}, + {"d", DbType.Single}, {"dbb", DbType.Byte}, - {"d", DbType.Double}, - {"int", DbType.Integer}, - {"dint", DbType.DInteger}, - {"w", DbType.Integer}, - {"dbw", DbType.Integer}, - {"dul", DbType.ULong}, - {"dulint", DbType.ULong}, - {"dulong", DbType.ULong} + {"dbw", DbType.Int}, + {"dbx", DbType.Bit}, + {"dul", DbType.ULInt}, + {"dulint", DbType.ULInt}, + {"dulong", DbType.ULInt}, + {"s", DbType.String}, + {"w", DbType.Int}, + {"x", DbType.Bit}, }; public S7VariableAddress Parse(string input) { + if (input == null) + throw new ArgumentNullException(nameof(input)); + var match = regex.Match(input); - if (match.Success) + if (!match.Success) + throw new InvalidS7AddressException($"Invalid S7 address: \"{input}\"", input); + + var operand = (Operand) Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true); + + if (!ushort.TryParse(match.Groups["dbNo"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var dbNr)) + throw new InvalidS7AddressException($"\"{match.Groups["dbNo"].Value}\" is an invalid DB number in \"{input}\"", input); + + if (!ushort.TryParse(match.Groups["start"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var start)) + throw new InvalidS7AddressException($"\"{match.Groups["start"].Value}\" is an invalid start bit in \"{input}\"", input); + + if (!types.TryGetValue(match.Groups["type"].Value, out var type)) + throw new InvalidS7AddressException($"\"{match.Groups["type"].Value}\" is an invalid type in \"{input}\"", input); + + ushort length = type switch { - var operand = (Operand) Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true); - var dbNr = ushort.Parse(match.Groups["dbNr"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture); - var start = ushort.Parse(match.Groups["start"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture); - if (!types.TryGetValue(match.Groups["type"].Value, out var type)) - return null; + DbType.Bit => 1, + DbType.String => GetLength(), + DbType.WString => GetLength(), - var s7VariableAddress = new S7VariableAddress - { - Operand = operand, - DbNr = dbNr, - Start = start, - Type = type, - }; + DbType.Byte => GetLength(1), - switch (type) - { - case DbType.Bit: - s7VariableAddress.Length = 1; - s7VariableAddress.Bit = byte.Parse(match.Groups["bitOrLength"].Value); - break; - case DbType.Byte: - s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort) 1; - break; - case DbType.String: - s7VariableAddress.Length = match.Groups["bitOrLength"].Success ? ushort.Parse(match.Groups["bitOrLength"].Value) : (ushort) 0; - break; - case DbType.Integer: - s7VariableAddress.Length = 2; - break; - case DbType.DInteger: - s7VariableAddress.Length = 4; - break; - case DbType.ULong: - s7VariableAddress.Length = 8; - break; - case DbType.Double: - s7VariableAddress.Length = 4; - break; - } + DbType.Int => 2, + DbType.DInt => 4, + DbType.ULInt => 8, + DbType.UInt => 2, + DbType.UDInt => 4, + DbType.LInt => 8, - return s7VariableAddress; + DbType.Single => 4, + DbType.Double => 8, + _ => throw new ArgumentOutOfRangeException($"DbType {type} is not supported") + }; + + switch (type) + { + case DbType.Bit: + case DbType.String: + case DbType.WString: + case DbType.Byte: + break; + case DbType.Int: + case DbType.UInt: + case DbType.DInt: + case DbType.UDInt: + case DbType.LInt: + case DbType.ULInt: + case DbType.Single: + case DbType.Double: + default: + if (match.Groups["bitOrLength"].Success) + throw new InvalidS7AddressException($"{type} address must not have a length: \"{input}\"", input); + break; } - return null; + byte? bit = type == DbType.Bit ? GetBit() : null; + + + var s7VariableAddress = new S7VariableAddress + { + Operand = operand, + DbNr = dbNr, + Start = start, + Type = type, + Length = length, + Bit = bit + }; + + return s7VariableAddress; + + ushort GetLength(ushort? defaultValue = null) + { + if (!match.Groups["bitOrLength"].Success) + { + if (defaultValue.HasValue) + return defaultValue.Value; + throw new InvalidS7AddressException($"Variable of type {type} must have a length set \"{input}\"", input); + } + + if (!ushort.TryParse(match.Groups["bitOrLength"].Value, out var result)) + throw new InvalidS7AddressException($"\"{match.Groups["bitOrLength"].Value}\" is an invalid length in \"{input}\"", input); + + return result; + } + + byte GetBit() + { + if (!match.Groups["bitOrLength"].Success) + throw new InvalidS7AddressException($"Variable of type {type} must have a bit number set \"{input}\"", input); + + if (!byte.TryParse(match.Groups["bitOrLength"].Value, out var result)) + throw new InvalidS7AddressException($"\"{match.Groups["bitOrLength"].Value}\" is an invalid bit number in \"{input}\"", input); + + return result; + } } } diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 3b73209..2e5a601 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -42,4 +42,8 @@ + + + + diff --git a/Sharp7.Rx/Sharp7.Rx.csproj.DotSettings b/Sharp7.Rx/Sharp7.Rx.csproj.DotSettings new file mode 100644 index 0000000..374f4af --- /dev/null +++ b/Sharp7.Rx/Sharp7.Rx.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index d7eb28d..c8401f1 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -154,9 +154,15 @@ public class Sharp7Plc : IPlc if (address == null) throw new ArgumentException("Input variable name is not valid", "variableName"); if (typeof(TValue) == typeof(bool)) + { // Special handling for bools, which are written on a by-bit basis. Writing a complete byte would // overwrite other bits within this byte. - await s7Connector.WriteBit(address.Operand, address.Start, address.Bit, (bool) (object) value, address.DbNr, token); + + if (address.Bit == null) + throw new InvalidOperationException("Address must have a Bit to write a bool."); + + await s7Connector.WriteBit(address.Operand, address.Start, address.Bit.Value, (bool) (object) value, address.DbNr, token); + } else { // TODO: Use ArrayPool.Rent() once we drop Framwework support From d1ec075aa7927722d9db26e492a07daf0c1fb446 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 10:12:13 +0100 Subject: [PATCH 36/78] Restructure and extens tests for converter --- .../S7ValueConverterTests/ConvertBothWays.cs | 40 +++------ .../S7ValueConverterTests/ConvertToType.cs | 66 -------------- .../ConverterTestBase.cs | 86 +++++++++++++++++++ .../S7ValueConverterTests/ReadFromBuffer.cs | 51 +++++++++++ .../S7ValueConverterTests/WriteToBuffer.cs | 37 ++------ Sharp7.Rx/S7ValueConverter.cs | 2 +- Sharp7.Rx/S7VariableNameParser.cs | 9 +- Sharp7.Rx/Sharp7Plc.cs | 4 +- 8 files changed, 164 insertions(+), 131 deletions(-) delete mode 100644 Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs create mode 100644 Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs create mode 100644 Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs index 5d264a1..b3956ac 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs @@ -1,45 +1,25 @@ using NUnit.Framework; -using Sharp7.Rx.Interfaces; using Shouldly; namespace Sharp7.Rx.Tests.S7ValueConverterTests; [TestFixture] -public class ConvertBothWays +internal class ConvertBothWays : ConverterTestBase { - static readonly IS7VariableNameParser parser = new S7VariableNameParser(); - - [TestCase(true, "DB0.DBx0.0")] - [TestCase(false, "DB0.DBx0.0")] - [TestCase(true, "DB0.DBx0.4")] - [TestCase(false, "DB0.DBx0.4")] - [TestCase((byte) 18, "DB0.DBB0")] - [TestCase((short) 4660, "DB0.INT0")] - [TestCase((short)-3532, "DB0.INT0")] - [TestCase(-3532, "DB0.INT0")] - [TestCase(305419879, "DB0.DINT0")] - [TestCase(-231451033, "DB0.DINT0")] - [TestCase(1311768394163015151L, "DB0.dul0")] - [TestCase(-994074615050678801L, "DB0.dul0")] - [TestCase(1311768394163015151uL, "DB0.dul0")] - [TestCase(17452669458658872815uL, "DB0.dul0")] - [TestCase(new byte[] { 0x12, 0x34, 0x56, 0x67 }, "DB0.DBB0.4")] - [TestCase(0.25f, "DB0.D0")] - [TestCase("ABCD", "DB0.string0.4")] - [TestCase("ABCD", "DB0.string0.4")] // Clip to length in Address - [TestCase("ABCD", "DB0.DBB0.4")] - public void Write(T input, string address) + [TestCaseSource(nameof(GetValidTestCases))] + public void Convert(ConverterTestCase tc) { //Arrange - var variableAddress = parser.Parse(address); - var buffer = new byte[variableAddress.BufferLength]; + var buffer = new byte[tc.VariableAddress.BufferLength]; + + var write = CreateWriteMethod(tc); + var read = CreateReadMethod(tc); //Act - S7ValueConverter.WriteToBuffer(buffer, input, variableAddress); - var result = S7ValueConverter.ConvertToType(buffer, variableAddress); + write.Invoke(null, [buffer, tc.Value, tc.VariableAddress]); + var result = read.Invoke(null, [buffer, tc.VariableAddress]); //Assert - result.ShouldBe(input); + result.ShouldBe(tc.Value); } - } diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs deleted file mode 100644 index caa37a9..0000000 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertToType.cs +++ /dev/null @@ -1,66 +0,0 @@ -using NUnit.Framework; -using Sharp7.Rx.Interfaces; -using Shouldly; - -namespace Sharp7.Rx.Tests.S7ValueConverterTests; - -[TestFixture] -public class ConvertToType -{ - 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((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("ABCD", "DB0.string0.4", new byte[] {0x00, 0x04, 0x41, 0x42, 0x43, 0x44})] - [TestCase("ABCD", "DB0.string0.4", new byte[] {0x00, 0xF0, 0x41, 0x42, 0x43, 0x44})] // Clip to length in Address - [TestCase("ABCD", "DB0.DBB0.4", new byte[] {0x41, 0x42, 0x43, 0x44})] - public void Parse(T expected, string address, byte[] data) - { - //Arrange - var variableAddress = parser.Parse(address); - - //Act - var result = S7ValueConverter.ConvertToType(data, variableAddress); - - //Assert - result.ShouldBe(expected); - } - - [TestCase((char) 18, "DB0.DBB0", new byte[] {0x12})] - [TestCase((ushort) 3532, "DB0.INT0", new byte[] {0xF2, 0x34})] - [TestCase(0.25, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] - public void Invalid(T template, string address, byte[] data) - { - //Arrange - var variableAddress = parser.Parse(address); - - //Act - Should.Throw(() => S7ValueConverter.ConvertToType(data, variableAddress)); - } - - [TestCase(3532, "DB0.DINT0", new byte[] {0xF2, 0x34})] - public void Argument(T template, string address, byte[] data) - { - //Arrange - var variableAddress = parser.Parse(address); - - //Act - Should.Throw(() => S7ValueConverter.ConvertToType(data, variableAddress)); - } -} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs new file mode 100644 index 0000000..273029c --- /dev/null +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs @@ -0,0 +1,86 @@ +using System.Reflection; +using Sharp7.Rx.Interfaces; + +namespace Sharp7.Rx.Tests.S7ValueConverterTests; + +internal abstract class ConverterTestBase +{ + protected static readonly IS7VariableNameParser Parser = new S7VariableNameParser(); + + public static MethodInfo CreateReadMethod(ConverterTestCase tc) + { + var convertMi = typeof(S7ValueConverter).GetMethod(nameof(S7ValueConverter.ReadFromBuffer)); + var convert = convertMi!.MakeGenericMethod(tc.Value.GetType()); + return convert; + } + + public static MethodInfo CreateWriteMethod(ConverterTestCase tc) + { + var writeMi = typeof(ConverterTestBase).GetMethod(nameof(WriteToBuffer)); + var write = writeMi!.MakeGenericMethod(tc.Value.GetType()); + return write; + } + + public static IEnumerable GetValidTestCases() + { + yield return new ConverterTestCase(true, "DB99.bit5.4", [0x10]); + yield return new ConverterTestCase(false, "DB99.bit5.4", [0x00]); + + yield return new ConverterTestCase((byte) 18, "DB99.Byte5", [0x12]); + yield return new ConverterTestCase((short) 4660, "DB99.Int5", [0x12, 0x34]); + yield return new ConverterTestCase((short) -3532, "DB99.Int5", [0xF2, 0x34]); + yield return new ConverterTestCase((ushort) 4660, "DB99.UInt5", [0x12, 0x34]); + yield return new ConverterTestCase((ushort) 3532, "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(231451033u, "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", [0x3E, 0x80, 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(-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(1311768394163015151L, "DB99.dul0", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + yield return new ConverterTestCase(-994074615050678801L, "DB99.dul0", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + yield return new ConverterTestCase(1311768394163015151uL, "DB99.dul0", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + yield return new ConverterTestCase(17452669458658872815uL, "DB99.dul0", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); + yield return new ConverterTestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB99.DBB0.4", [0x12, 0x34, 0x56, 0x67]); + yield return new ConverterTestCase(0.25f, "DB99.D0", [0x3E, 0x80, 0x00, 0x00]); + } + + /// + /// This helper method exists, since I could not manage to invoke a generic method + /// accepring a Span<T> as parameter. + /// + public static void WriteToBuffer(byte[] buffer, TValue value, S7VariableAddress address) + { + S7ValueConverter.WriteToBuffer(buffer, value, address); + } + + public record ConverterTestCase(object Value, string Address, byte[] Data) + { + public S7VariableAddress VariableAddress => Parser.Parse(Address); + + public override string ToString() => $"{Address} {Value} ({Value.GetType().Name})"; + } +} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs new file mode 100644 index 0000000..cf9cc4d --- /dev/null +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs @@ -0,0 +1,51 @@ +using NUnit.Framework; +using Shouldly; + +namespace Sharp7.Rx.Tests.S7ValueConverterTests; + +[TestFixture] +internal class ReadFromBuffer : ConverterTestBase +{ + [TestCaseSource(nameof(GetValidTestCases))] + [TestCaseSource(nameof(GetAdditinalReadTestCases))] + public void Read(ConverterTestCase tc) + { + //Arrange + var convert = CreateReadMethod(tc); + + //Act + var result = convert.Invoke(null, [tc.Data, tc.VariableAddress]); + + //Assert + result.ShouldBe(tc.Value); + } + + public static IEnumerable GetAdditinalReadTestCases() + { + yield return new ConverterTestCase(true, "DB0.DBx0.4", [0x1F]); + yield return new ConverterTestCase(false, "DB0.DBx0.4", [0xEF]); + yield return new ConverterTestCase("ABCD", "DB0.string0.10", [0x04, 0x04, 0x41, 0x42, 0x43, 0x44]); // Length in address exceeds PLC string length + } + + [TestCase((char) 18, "DB0.DBB0", new byte[] {0x12})] + [TestCase((ushort) 3532, "DB0.INT0", new byte[] {0xF2, 0x34})] + [TestCase(0.25, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] + public void Invalid(T template, string address, byte[] data) + { + //Arrange + var variableAddress = Parser.Parse(address); + + //Act + Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); + } + + [TestCase(3532, "DB0.DINT0", new byte[] {0xF2, 0x34})] + public void Argument(T template, string address, byte[] data) + { + //Arrange + var variableAddress = Parser.Parse(address); + + //Act + Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); + } +} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs index c33209f..4a93719 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs @@ -5,41 +5,20 @@ using Shouldly; namespace Sharp7.Rx.Tests.S7ValueConverterTests; [TestFixture] -public class WriteToBuffer +internal class WriteToBuffer:ConverterTestBase { - 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((byte) 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("ABCD", "DB0.string0.4", new byte[] {0x04, 0x04, 0x41, 0x42, 0x43, 0x44})] - [TestCase("ABCD", "DB0.string0.8", new byte[] {0x08, 0x04, 0x41, 0x42, 0x43, 0x44, 0x00, 0x00, 0x00, 0x00})] - [TestCase("ABCD", "DB0.string0.2", new byte[] {0x02, 0x02, 0x41, 0x42})] - [TestCase("ABCD", "DB0.DBB0.4", new byte[] {0x41, 0x42, 0x43, 0x44})] - public void Write(T input, string address, byte[] expected) + [TestCaseSource(nameof(GetValidTestCases))] + public void Write(ConverterTestCase tc) { //Arrange - var variableAddress = parser.Parse(address); - var buffer = new byte[variableAddress.BufferLength]; + var buffer = new byte[tc.VariableAddress.BufferLength]; + var write = CreateWriteMethod(tc); //Act - S7ValueConverter.WriteToBuffer(buffer, input, variableAddress); + write.Invoke(null, [buffer, tc.Value, tc.VariableAddress]); //Assert - buffer.ShouldBe(expected); + buffer.ShouldBe(tc.Data); } [TestCase((char) 18, "DB0.DBB0")] @@ -47,7 +26,7 @@ public class WriteToBuffer public void Invalid(T input, string address) { //Arrange - var variableAddress = parser.Parse(address); + var variableAddress = Parser.Parse(address); var buffer = new byte[variableAddress.BufferLength]; //Act diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 247070a..9618a70 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -7,7 +7,7 @@ namespace Sharp7.Rx; internal static class S7ValueConverter { - public static TValue ConvertToType(byte[] buffer, S7VariableAddress address) + public static TValue ReadFromBuffer(byte[] buffer, S7VariableAddress address) { if (typeof(TValue) == typeof(bool)) return (TValue) (object) (((buffer[0] >> address.Bit) & 1) > 0); diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index 683ec18..bf5d7ca 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -28,12 +28,15 @@ internal class S7VariableNameParser : IS7VariableNameParser {"real", DbType.Single}, {"lreal", DbType.Double}, - // used for legacy compatability - {"b", DbType.Byte}, - {"d", DbType.Single}, + // S7 notation {"dbb", DbType.Byte}, {"dbw", DbType.Int}, {"dbx", DbType.Bit}, + {"dbd", DbType.DInt}, + + // used for legacy compatability + {"b", DbType.Byte}, + {"d", DbType.Single}, {"dul", DbType.ULInt}, {"dulint", DbType.ULInt}, {"dulong", DbType.ULInt}, diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index c8401f1..46936ef 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -90,7 +90,7 @@ public class Sharp7Plc : IPlc Observable.FromAsync(() => GetValue(variableName)) .Concat( disposeableContainer.Observable - .Select(bytes => S7ValueConverter.ConvertToType(bytes, address)) + .Select(bytes => S7ValueConverter.ReadFromBuffer(bytes, address)) ); if (transmissionMode == TransmissionMode.OnChange) @@ -121,7 +121,7 @@ public class Sharp7Plc : IPlc 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); - return S7ValueConverter.ConvertToType(data, address); + return S7ValueConverter.ReadFromBuffer(data, address); } public async Task InitializeAsync() From 314542643235e0f3440001851aaaa1443f32da01 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 11:02:16 +0100 Subject: [PATCH 37/78] Improve error message --- Sharp7.Rx/S7VariableNameParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index bf5d7ca..2355b24 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -52,7 +52,7 @@ internal class S7VariableNameParser : IS7VariableNameParser var match = regex.Match(input); if (!match.Success) - throw new InvalidS7AddressException($"Invalid S7 address: \"{input}\"", input); + throw new InvalidS7AddressException($"Invalid S7 address \"{input}\". Expect format \"DB.(.)\".", input); var operand = (Operand) Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true); From 3c592c6d466135879b7dbc54c54c77cf0780b4da Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 12:57:36 +0100 Subject: [PATCH 38/78] Add new types to S7ValueConverter.ReadFromBuffer --- .../ConverterTestBase.cs | 9 +- Sharp7.Rx/Exceptions/S7Exception.cs | 40 ++++- Sharp7.Rx/S7ValueConverter.cs | 137 +++++++++++------- Sharp7.Rx/S7VariableAddress.cs | 10 ++ 4 files changed, 141 insertions(+), 55 deletions(-) diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs index 273029c..a4fb1b4 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs @@ -30,17 +30,17 @@ internal abstract class ConverterTestBase 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) 3532, "DB99.UInt5", [0xF2, 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(231451033u, "DB99.UDInt5", [0xF2, 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", [0x3E, 0x80, 0x00, 0x00, 0x00, 0x00, 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]); @@ -57,7 +57,6 @@ internal abstract class ConverterTestBase 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(-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(1311768394163015151L, "DB99.dul0", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); @@ -81,6 +80,6 @@ internal abstract class ConverterTestBase { public S7VariableAddress VariableAddress => Parser.Parse(Address); - public override string ToString() => $"{Address} {Value} ({Value.GetType().Name})"; + public override string ToString() => $"{Value.GetType().Name}, {Address}: {Value}"; } } diff --git a/Sharp7.Rx/Exceptions/S7Exception.cs b/Sharp7.Rx/Exceptions/S7Exception.cs index e9cc1d3..fb836f9 100644 --- a/Sharp7.Rx/Exceptions/S7Exception.cs +++ b/Sharp7.Rx/Exceptions/S7Exception.cs @@ -11,6 +11,44 @@ public abstract class S7Exception : Exception } } +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) @@ -23,5 +61,5 @@ public class InvalidS7AddressException : S7Exception Input = input; } - public string Input { get; private set; } + public string Input { get; } } diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 9618a70..90b21d8 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -7,60 +7,92 @@ namespace Sharp7.Rx; internal static class S7ValueConverter { + private static readonly Dictionary> readFunctions = new() + { + {typeof(bool), (buffer, address) => (buffer[0] >> address.Bit & 1) > 0}, + + {typeof(byte), (buffer, address) => buffer[0]}, + {typeof(byte[]), (buffer, address) => buffer}, + + {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), + _ => throw new DataTypeMissmatchException($"Cannot read string from {address.Type}", typeof(string), address) + }; + + string ParseString() + { + // First byte is maximal length + // Second byte is actual length + // https://support.industry.siemens.com/cs/mdm/109747174?c=94063831435&lc=de-DE + + var length = Math.Min(address.Length, buffer[1]); + + return Encoding.ASCII.GetString(buffer, 2, length); + } + + string ParseWString() + { + // First 2 bytes are maximal length + // Second 2 bytes are actual length + // https://support.industry.siemens.com/cs/mdm/109747174?c=94063855243&lc=de-DE + + // the length of the string is two bytes per + var length = Math.Min(address.Length, BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(2,2))) * 2; + + return Encoding.BigEndianUnicode.GetString(buffer, 4, length); + } + } + }, + }; + public static TValue ReadFromBuffer(byte[] buffer, S7VariableAddress address) { - if (typeof(TValue) == typeof(bool)) - return (TValue) (object) (((buffer[0] >> address.Bit) & 1) > 0); + // Todo: Change to Span when switched to newer .net - if (typeof(TValue) == typeof(int)) - { - if (address.Length == 2) - return (TValue) (object) (int) BinaryPrimitives.ReadInt16BigEndian(buffer); - if (address.Length == 4) - return (TValue) (object) BinaryPrimitives.ReadInt32BigEndian(buffer); + var type = typeof(TValue); - throw new InvalidOperationException($"length must be 2 or 4 but is {address.Length}"); - } + if (!readFunctions.TryGetValue(type, out var readFunc)) + throw new UnsupportedS7TypeException($"{type.Name} is not supported. {address}", type, address); - 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(byte[])) - return (TValue) (object) buffer; - - 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($"type '{typeof(TValue)}' not supported."); + var result = readFunc(buffer, address); + return (TValue) result; } public static void WriteToBuffer(Span buffer, TValue value, S7VariableAddress address) @@ -145,4 +177,11 @@ internal static class S7ValueConverter [FieldOffset(0)] public uint UInt32; [FieldOffset(0)] public float Single; } + + [StructLayout(LayoutKind.Explicit)] + private struct UInt64DoubleMap + { + [FieldOffset(0)] public ulong UInt64; + [FieldOffset(0)] public double Double; + } } diff --git a/Sharp7.Rx/S7VariableAddress.cs b/Sharp7.Rx/S7VariableAddress.cs index 0172c06..f04bd34 100644 --- a/Sharp7.Rx/S7VariableAddress.cs +++ b/Sharp7.Rx/S7VariableAddress.cs @@ -19,4 +19,14 @@ internal class S7VariableAddress 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}", + }; } From 1001303b8c26d44dfcc938a30d0b84e917493cd9 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 16:45:48 +0100 Subject: [PATCH 39/78] Improve WriteToBuffer implementation and tests --- .../S7ValueConverterTests/ReadFromBuffer.cs | 14 +- .../S7ValueConverterTests/WriteToBuffer.cs | 28 ++- Sharp7.Rx.Tests/S7VariableNameParserTests.cs | 1 + Sharp7.Rx/S7ValueConverter.cs | 181 +++++++++++------- 4 files changed, 140 insertions(+), 84 deletions(-) diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs index cf9cc4d..cbb4542 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs @@ -24,23 +24,23 @@ internal class ReadFromBuffer : ConverterTestBase { 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.10", [0x04, 0x04, 0x41, 0x42, 0x43, 0x44]); // Length in address exceeds PLC string length + 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})] - [TestCase((ushort) 3532, "DB0.INT0", new byte[] {0xF2, 0x34})] - [TestCase(0.25, "DB0.D0", new byte[] {0x3E, 0x80, 0x00, 0x00})] - public void Invalid(T template, string address, byte[] data) + public void UnsupportedType(T template, string address, byte[] data) { //Arrange var variableAddress = Parser.Parse(address); //Act - Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); + Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); } - [TestCase(3532, "DB0.DINT0", new byte[] {0xF2, 0x34})] - public void Argument(T template, string address, byte[] data) + [TestCase(123, "DB12.DINT3", new byte[] {0x01, 0x02, 0x03})] + [TestCase((short) 123, "DB12.INT3", new byte[] {0xF2})] + [TestCase("ABC", "DB0.string0.6", new byte[] {0x01, 0x02, 0x03})] + public void BufferTooSmall(T template, string address, byte[] data) { //Arrange var variableAddress = Parser.Parse(address); diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs index 4a93719..4e364fb 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs @@ -1,13 +1,13 @@ using NUnit.Framework; -using Sharp7.Rx.Interfaces; using Shouldly; namespace Sharp7.Rx.Tests.S7ValueConverterTests; [TestFixture] -internal class WriteToBuffer:ConverterTestBase +internal class WriteToBuffer : ConverterTestBase { [TestCaseSource(nameof(GetValidTestCases))] + [TestCaseSource(nameof(GetAdditinalWriteTestCases))] public void Write(ConverterTestCase tc) { //Arrange @@ -21,15 +21,33 @@ internal class WriteToBuffer:ConverterTestBase buffer.ShouldBe(tc.Data); } + public static IEnumerable GetAdditinalWriteTestCases() + { + yield return new ConverterTestCase("aaaaBCDE", "DB0.string0.4", [0x04, 0x04, 0x61, 0x61, 0x61, 0x61]); // Length in address exceeds PLC string length + yield return new ConverterTestCase("aaaaBCDE", "DB0.WString0.4", [0x00, 0x04, 0x00, 0x04, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61, 0x00, 0x61]); // Length in address exceeds PLC string length + } + + [TestCase(18, "DB0.DInt12", 3)] + [TestCase(0.25f, "DB0.Real1", 3)] + [TestCase("test", "DB0.String1.10", 9)] + public void BufferToSmall(T input, string address, int bufferSize) + { + //Arrange + var variableAddress = Parser.Parse(address); + var buffer = new byte[bufferSize]; + + //Act + Should.Throw(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); + } + [TestCase((char) 18, "DB0.DBB0")] - [TestCase(0.25, "DB0.D0")] - public void Invalid(T input, string address) + public void UnsupportedType(T input, string address) { //Arrange var variableAddress = Parser.Parse(address); var buffer = new byte[variableAddress.BufferLength]; //Act - Should.Throw(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); + Should.Throw(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); } } diff --git a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs index 7f4869f..50a460a 100644 --- a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs +++ b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs @@ -17,6 +17,7 @@ internal class S7VariableNameParserTests } [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")] diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/S7ValueConverter.cs index 90b21d8..4bc23f8 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/S7ValueConverter.cs @@ -7,12 +7,105 @@ namespace Sharp7.Rx; internal static class S7ValueConverter { - private static readonly Dictionary> readFunctions = new() + private static readonly Dictionary writeFunctions = new() + { + { + typeof(bool), (data, address, value) => + { + var byteValue = (bool) value ? (byte) 1 : (byte) 0; + var shifted = (byte) (byteValue << address.Bit!); + data[0] = shifted; + } + }, + + {typeof(byte), (data, address, value) => data[0] = (byte) value}, + { + typeof(byte[]), (data, address, value) => + { + var source = (byte[]) value; + + var length = Math.Min(Math.Min(source.Length, data.Length), address.Length); + + source.AsSpan(0, length).CopyTo(data); + } + }, + + {typeof(short), (data, address, value) => BinaryPrimitives.WriteInt16BigEndian(data, (short) value)}, + {typeof(ushort), (data, address, value) => BinaryPrimitives.WriteUInt16BigEndian(data, (ushort) value)}, + {typeof(int), (data, address, value) => BinaryPrimitives.WriteInt32BigEndian(data, (int) value)}, + {typeof(uint), (data, address, value) => BinaryPrimitives.WriteUInt32BigEndian(data, (uint) value)}, + {typeof(long), (data, address, value) => BinaryPrimitives.WriteInt64BigEndian(data, (long) value)}, + {typeof(ulong), (data, address, value) => BinaryPrimitives.WriteUInt64BigEndian(data, (ulong) value)}, + + { + typeof(float), (data, address, value) => + { + var map = new UInt32SingleMap + { + Single = (float) value + }; + + BinaryPrimitives.WriteUInt32BigEndian(data, map.UInt32); + } + }, + { + typeof(double), (data, address, value) => + { + var map = new UInt64DoubleMap + { + Double = (double) value + }; + + BinaryPrimitives.WriteUInt64BigEndian(data, map.UInt64); + } + }, + + { + typeof(string), (data, address, value) => + { + if (value is not string stringValue) throw new ArgumentException("Value must be of type string", nameof(value)); + + var length = Math.Min(address.Length, stringValue.Length); + + switch (address.Type) + { + case DbType.String: + data[0] = (byte) address.Length; + data[1] = (byte) length; + + // Todo: Serialize directly to Span, when upgrading to .net + Encoding.ASCII.GetBytes(stringValue) + .AsSpan(0, length) + .CopyTo(data.Slice(2)); + return; + case DbType.WString: + BinaryPrimitives.WriteUInt16BigEndian(data, address.Length); + BinaryPrimitives.WriteUInt16BigEndian(data.Slice(2), (ushort) length); + + // Todo: Serialize directly to Span, when upgrading to .net + Encoding.BigEndianUnicode.GetBytes(stringValue) + .AsSpan(0, length * 2) + .CopyTo(data.Slice(4)); + return; + case DbType.Byte: + // Todo: Serialize directly to Span, when upgrading to .net + Encoding.ASCII.GetBytes(stringValue) + .AsSpan(0, length) + .CopyTo(data); + return; + default: + throw new DataTypeMissmatchException($"Cannot write string to {address.Type}", typeof(string), address); + } + } + } + }; + + private static readonly Dictionary readFunctions = new() { {typeof(bool), (buffer, address) => (buffer[0] >> address.Bit & 1) > 0}, {typeof(byte), (buffer, address) => buffer[0]}, - {typeof(byte[]), (buffer, address) => buffer}, + {typeof(byte[]), (buffer, address) => buffer.ToArray()}, {typeof(short), (buffer, address) => BinaryPrimitives.ReadInt16BigEndian(buffer)}, {typeof(ushort), (buffer, address) => BinaryPrimitives.ReadUInt16BigEndian(buffer)}, @@ -52,7 +145,7 @@ internal static class S7ValueConverter { DbType.String => ParseString(), DbType.WString => ParseWString(), - DbType.Byte => Encoding.ASCII.GetString(buffer), + DbType.Byte => Encoding.ASCII.GetString(buffer.ToArray()), _ => throw new DataTypeMissmatchException($"Cannot read string from {address.Type}", typeof(string), address) }; @@ -74,7 +167,7 @@ internal static class S7ValueConverter // 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; + var length = Math.Min(address.Length, BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(2, 2))) * 2; return Encoding.BigEndianUnicode.GetString(buffer, 4, length); } @@ -86,6 +179,9 @@ internal static class S7ValueConverter { // Todo: Change to Span when switched to newer .net + if (buffer.Length < address.BufferLength) + throw new ArgumentException($"Buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer)); + var type = typeof(TValue); if (!readFunctions.TryGetValue(type, out var readFunc)) @@ -98,79 +194,18 @@ internal static class S7ValueConverter public static void WriteToBuffer(Span buffer, TValue value, S7VariableAddress address) { if (buffer.Length < address.BufferLength) - throw new ArgumentException($"buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer)); + throw new ArgumentException($"Buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer)); - if (typeof(TValue) == typeof(bool)) - { - var byteValue = (bool) (object) value ? (byte) 1 : (byte) 0; - var shifted = (byte) (byteValue << address.Bit); - buffer[0] = shifted; - } + var type = typeof(TValue); - else if (typeof(TValue) == typeof(int)) - { - if (address.Length == 2) - BinaryPrimitives.WriteInt16BigEndian(buffer, (short) (int) (object) value); - else - BinaryPrimitives.WriteInt32BigEndian(buffer, (int) (object) value); - } - else if (typeof(TValue) == typeof(short)) - { - if (address.Length == 2) - BinaryPrimitives.WriteInt16BigEndian(buffer, (short) (object) value); - else - BinaryPrimitives.WriteInt32BigEndian(buffer, (short) (object) value); - } - else if (typeof(TValue) == typeof(long)) - BinaryPrimitives.WriteInt64BigEndian(buffer, (long) (object) value); - else if (typeof(TValue) == typeof(ulong)) - BinaryPrimitives.WriteUInt64BigEndian(buffer, (ulong) (object) value); - else if (typeof(TValue) == typeof(byte)) - buffer[0] = (byte) (object) value; - else if (typeof(TValue) == typeof(byte[])) - { - var source = (byte[]) (object) value; + if (!writeFunctions.TryGetValue(type, out var writeFunc)) + throw new UnsupportedS7TypeException($"{type.Name} is not supported. {address}", type, address); - var length = Math.Min(Math.Min(source.Length, buffer.Length), address.Length); - - source.AsSpan(0, length).CopyTo(buffer); - } - else if (typeof(TValue) == typeof(float)) - { - var map = new UInt32SingleMap - { - Single = (float) (object) value - }; - - BinaryPrimitives.WriteUInt32BigEndian(buffer, map.UInt32); - } - else if (typeof(TValue) == typeof(string)) - { - if (value is not string stringValue) throw new ArgumentException("Value must be of type string", nameof(value)); - - // Todo: Serialize directly to Span, when upgrading to .net - var stringBytes = Encoding.ASCII.GetBytes(stringValue); - - var length = Math.Min(address.Length, stringValue.Length); - - int stringOffset; - if (address.Type == DbType.String) - { - stringOffset = 2; - buffer[0] = (byte) address.Length; - buffer[1] = (byte) length; - } - else - stringOffset = 0; - - stringBytes.AsSpan(0, length).CopyTo(buffer.Slice(stringOffset)); - } - else - { - throw new InvalidOperationException($"type '{typeof(TValue)}' not supported."); - } + writeFunc(buffer, address, value); } + delegate object ReadFunc(byte[] data, S7VariableAddress address); + [StructLayout(LayoutKind.Explicit)] private struct UInt32SingleMap { @@ -184,4 +219,6 @@ internal static class S7ValueConverter [FieldOffset(0)] public ulong UInt64; [FieldOffset(0)] public double Double; } + + delegate void WriteFunc(Span data, S7VariableAddress address, object value); } From fdc25d281763043eba24915ee7965bacdc50fd3a Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 16:47:10 +0100 Subject: [PATCH 40/78] Ensure bit size --- Sharp7.Rx/S7VariableNameParser.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index 2355b24..a7029a1 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -145,6 +145,11 @@ internal class S7VariableNameParser : IS7VariableNameParser 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; } } From 829dee14afa236803604eff098ea00ace0fa37ab Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 17:54:15 +0100 Subject: [PATCH 41/78] Implement MatchesType --- .../ConverterTestBase.cs | 2 - .../S7VariableAddressTests/MatchesType.cs | 69 +++++++++++++++---- Sharp7.Rx/Extensions/S7VariableExtensions.cs | 24 +++++-- Sharp7.Rx/S7VariableNameParser.cs | 2 - 4 files changed, 77 insertions(+), 20 deletions(-) diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs index a4fb1b4..f344256 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs +++ b/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs @@ -59,8 +59,6 @@ internal abstract class ConverterTestBase 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(1311768394163015151L, "DB99.dul0", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); - yield return new ConverterTestCase(-994074615050678801L, "DB99.dul0", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]); 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]); diff --git a/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs b/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs index d3d428a..458c17d 100644 --- a/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs +++ b/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs @@ -1,7 +1,7 @@ -using System.Reflection; -using NUnit.Framework; +using NUnit.Framework; using Sharp7.Rx.Extensions; using Sharp7.Rx.Interfaces; +using Sharp7.Rx.Tests.S7ValueConverterTests; using Shouldly; namespace Sharp7.Rx.Tests.S7VariableAddressTests; @@ -11,19 +11,61 @@ public class MatchesType { static readonly IS7VariableNameParser parser = new S7VariableNameParser(); - - public void Supported(Type type, string address) + private static readonly IReadOnlyList typeList = new[] { - Check(type, address, true); + typeof(byte), + typeof(byte[]), + + typeof(bool), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + + typeof(float), + typeof(double), + + typeof(string), + + typeof(int[]), + typeof(float[]), + typeof(DateTime[]), + typeof(object), + }; + + [TestCaseSource(nameof(GetValid))] + public void Supported(TestCase tc) => Check(tc.Type, tc.Address, true); + + [TestCaseSource(nameof(GetInvalid))] + public void Unsupported(TestCase tc) => Check(tc.Type, tc.Address, false); + + + public static IEnumerable GetValid() + { + return + ConverterTestBase.GetValidTestCases() + .Select(tc => new TestCase(tc.Value.GetType(), tc.Address)); } - public IEnumerable GetValid() + public static IEnumerable GetInvalid() { - yield return new TestCase(typeof(bool), "DB0.DBx0.0"); - yield return new TestCase(typeof(short), "DB0.INT0"); - yield return new TestCase(typeof(int), "DB0.DINT0"); - yield return new TestCase(typeof(long), "DB0.DUL0"); - yield return new TestCase(typeof(ulong), "DB0.DUL0"); + 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") + )) + ; } @@ -36,5 +78,8 @@ public class MatchesType variableAddress.MatchesType(type).ShouldBe(expected); } - public record TestCase(Type Type, string Address); + public record TestCase(Type Type, string Address) + { + public override string ToString() => $"{Type.Name} {Address}"; + } } diff --git a/Sharp7.Rx/Extensions/S7VariableExtensions.cs b/Sharp7.Rx/Extensions/S7VariableExtensions.cs index a2b0a39..40c8eed 100644 --- a/Sharp7.Rx/Extensions/S7VariableExtensions.cs +++ b/Sharp7.Rx/Extensions/S7VariableExtensions.cs @@ -1,9 +1,25 @@ -namespace Sharp7.Rx.Extensions; +using Sharp7.Rx.Enums; + +namespace Sharp7.Rx.Extensions; internal static class S7VariableAddressExtensions { - public static bool MatchesType(this S7VariableAddress address, Type type) + private static readonly Dictionary> supportedTypeMap = new() { - return false; - } + {typeof(bool), a => a.Type == DbType.Bit}, + {typeof(string), a => a.Type is DbType.String or DbType.WString or DbType.Byte }, + {typeof(byte), a => a.Type==DbType.Byte && a.Length == 1}, + {typeof(short), a => a.Type==DbType.Int}, + {typeof(ushort), a => a.Type==DbType.UInt}, + {typeof(int), a => a.Type==DbType.DInt}, + {typeof(uint), a => a.Type==DbType.UDInt}, + {typeof(long), a => a.Type==DbType.LInt}, + {typeof(ulong), a => a.Type==DbType.ULInt}, + {typeof(float), a => a.Type==DbType.Single}, + {typeof(double), a => a.Type==DbType.Double}, + {typeof(byte[]), a => a.Type==DbType.Byte}, + }; + + public static bool MatchesType(this S7VariableAddress address, Type type) => + supportedTypeMap.TryGetValue(type, out var map) && map(address); } diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index a7029a1..38cecc3 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -148,8 +148,6 @@ internal class S7VariableNameParser : IS7VariableNameParser if (result > 7) throw new InvalidS7AddressException($"Bit must be between 0 and 7 but is {result} in \"{input}\"", input); - - return result; } } From 5d8582316770fc6555f019a03183bb9a00f87175 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 18:06:55 +0100 Subject: [PATCH 42/78] Verify data types --- Sharp7.Rx/S7VariableNameParser.cs | 7 ++++--- Sharp7.Rx/Sharp7Plc.cs | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/S7VariableNameParser.cs index 38cecc3..b53fcc9 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/S7VariableNameParser.cs @@ -1,4 +1,5 @@ -using System.Globalization; +#nullable enable +using System.Globalization; using System.Text.RegularExpressions; using Sharp7.Rx.Enums; using Sharp7.Rx.Interfaces; @@ -7,8 +8,8 @@ namespace Sharp7.Rx; internal class S7VariableNameParser : IS7VariableNameParser { - private static readonly Regex regex = new Regex(@"^(?db)(?\d+)\.?(?[a-z]+)(?\d+)(\.(?\d+))?$", - RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex regex = new(@"^(?db)(?\d+)\.?(?[a-z]+)(?\d+)(\.(?\d+))?$", + RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly IReadOnlyDictionary types = new Dictionary(StringComparer.OrdinalIgnoreCase) { diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 46936ef..ec6504a 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -77,8 +77,7 @@ public class Sharp7Plc : IPlc { return Observable.Create(observer => { - var address = varaibleNameParser.Parse(variableName); - if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName)); + var address = ParseAndVerify(variableName, typeof(TValue)); var disp = new CompositeDisposable(); var disposeableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName); @@ -103,6 +102,15 @@ 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 GetValue(string variableName) { return GetValue(variableName, CancellationToken.None); @@ -117,8 +125,7 @@ public class Sharp7Plc : IPlc public async Task GetValue(string variableName, CancellationToken token) { - var address = varaibleNameParser.Parse(variableName); - if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName)); + var address = ParseAndVerify(variableName, typeof(TValue)); var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.Length, address.DbNr, token); return S7ValueConverter.ReadFromBuffer(data, address); @@ -150,8 +157,7 @@ public class Sharp7Plc : IPlc public async Task SetValue(string variableName, TValue value, CancellationToken token) { - var address = varaibleNameParser.Parse(variableName); - if (address == null) throw new ArgumentException("Input variable name is not valid", "variableName"); + var address = ParseAndVerify(variableName, typeof(TValue)); if (typeof(TValue) == typeof(bool)) { From b43a595e13ae8bf21cbb58126039e210f72567b3 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 18:30:49 +0100 Subject: [PATCH 43/78] Return S7CommunicationException --- Sharp7.Rx/Exceptions/S7Exception.cs | 18 ++++++ Sharp7.Rx/Interfaces/IS7Connector.cs | 6 +- .../Resources/StringResources.Designer.cs | 20 +------ Sharp7.Rx/Resources/StringResources.resx | 6 -- Sharp7.Rx/Sharp7Connector.cs | 60 +++++++++---------- Sharp7.Rx/Sharp7Plc.cs | 5 +- 6 files changed, 53 insertions(+), 62 deletions(-) diff --git a/Sharp7.Rx/Exceptions/S7Exception.cs b/Sharp7.Rx/Exceptions/S7Exception.cs index fb836f9..2d8fb44 100644 --- a/Sharp7.Rx/Exceptions/S7Exception.cs +++ b/Sharp7.Rx/Exceptions/S7Exception.cs @@ -11,6 +11,24 @@ public abstract class S7Exception : Exception } } +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) diff --git a/Sharp7.Rx/Interfaces/IS7Connector.cs b/Sharp7.Rx/Interfaces/IS7Connector.cs index 5f9da81..fe365c1 100644 --- a/Sharp7.Rx/Interfaces/IS7Connector.cs +++ b/Sharp7.Rx/Interfaces/IS7Connector.cs @@ -12,10 +12,10 @@ internal interface IS7Connector : IDisposable Task Connect(); Task Disconnect(); - Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token); + Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dbNo, CancellationToken token); - Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token); - Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token); + Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNo, CancellationToken token); + Task WriteBytes(Operand operand, ushort startByteAddress, byte[] data, ushort dbNo, CancellationToken token); Task> ExecuteMultiVarRequest(IReadOnlyList variableNames); } diff --git a/Sharp7.Rx/Resources/StringResources.Designer.cs b/Sharp7.Rx/Resources/StringResources.Designer.cs index f1d725d..dd88d85 100644 --- a/Sharp7.Rx/Resources/StringResources.Designer.cs +++ b/Sharp7.Rx/Resources/StringResources.Designer.cs @@ -19,7 +19,7 @@ namespace Sharp7.Rx.Resources { // 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.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class StringResources { @@ -60,24 +60,6 @@ namespace Sharp7.Rx.Resources { } } - /// - /// Looks up a localized string similar to S7 driver could not be initialized. - /// - internal static string StrErrorS7DriverCouldNotBeInitialized { - get { - return ResourceManager.GetString("StrErrorS7DriverCouldNotBeInitialized", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to S7 driver is not initialized.. - /// - internal static string StrErrorS7DriverNotInitialized { - get { - return ResourceManager.GetString("StrErrorS7DriverNotInitialized", resourceCulture); - } - } - /// /// Looks up a localized string similar to TCP/IP connection established.. /// diff --git a/Sharp7.Rx/Resources/StringResources.resx b/Sharp7.Rx/Resources/StringResources.resx index 3eff273..ee55570 100644 --- a/Sharp7.Rx/Resources/StringResources.resx +++ b/Sharp7.Rx/Resources/StringResources.resx @@ -123,16 +123,10 @@ Communication error discovered. Reconnect is in progress... - - S7 driver is not initialized. - Trying to connect to PLC ({2}) '{0}', CPU slot {1}... TCP/IP connection established. - - S7 driver could not be initialized - \ No newline at end of file diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index efdd014..98f9c74 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -55,21 +55,26 @@ internal class Sharp7Connector : IS7Connector public async Task Connect() { if (sharp7 == null) - throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + throw new InvalidOperationException("S7 driver is not initialized."); try { var errorCode = await Task.Factory.StartNew(() => sharp7.ConnectTo(ipAddress, rackNr, cpuSlotNr), CancellationToken.None, TaskCreationOptions.None, scheduler); - var success = EvaluateErrorCode(errorCode); - if (success) + if (errorCode == 0) { connectionStateSubject.OnNext(Enums.ConnectionState.Connected); return true; } + else + { + var errorText = EvaluateErrorCode(errorCode); + Logger.LogError("Failed to establish initial connection: {Error}", errorText); + } } catch (Exception ex) { - // TODO: + connectionStateSubject.OnNext(Enums.ConnectionState.ConnectionLost); + Logger.LogError(ex, "Failed to establish initial connection."); } return false; @@ -102,8 +107,8 @@ internal class Sharp7Connector : IS7Connector var result = await Task.Factory.StartNew(() => s7MultiVar.Read(), CancellationToken.None, TaskCreationOptions.None, scheduler); if (result != 0) { - EvaluateErrorCode(result); - throw new InvalidOperationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)}"); + var errorText = EvaluateErrorCode(result); + throw new S7CommunicationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)} ({errorText})", result, errorText); } return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer); @@ -129,13 +134,13 @@ internal class Sharp7Connector : IS7Connector } catch (Exception ex) { - Logger?.LogError(ex, StringResources.StrErrorS7DriverCouldNotBeInitialized); + Logger?.LogError(ex, "S7 driver could not be initialized"); } return Task.FromResult(true); } - public async Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token) + public async Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dbNo, CancellationToken token) { EnsureConnectionValid(); @@ -143,20 +148,19 @@ internal class Sharp7Connector : IS7Connector var result = - await Task.Factory.StartNew(() => sharp7.ReadArea(operand.ToArea(), dBNr, startByteAddress, bytesToRead, S7WordLength.Byte, buffer), token, TaskCreationOptions.None, scheduler); + await Task.Factory.StartNew(() => sharp7.ReadArea(operand.ToArea(), dbNo, startByteAddress, bytesToRead, S7WordLength.Byte, buffer), token, TaskCreationOptions.None, scheduler); token.ThrowIfCancellationRequested(); if (result != 0) { - EvaluateErrorCode(result); - var errorText = sharp7.ErrorText(result); - throw new InvalidOperationException($"Error reading {operand}{dBNr}:{startByteAddress}->{bytesToRead} ({errorText})"); + var errorText = EvaluateErrorCode(result); + throw new S7CommunicationException($"Error reading {operand}{dbNo}:{startByteAddress}->{bytesToRead} ({errorText})", result, errorText); } return buffer; } - public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token) + public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNo, CancellationToken token) { EnsureConnectionValid(); @@ -164,32 +168,28 @@ internal class Sharp7Connector : IS7Connector var offsetStart = (startByteAddress * 8) + bitAdress; - var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dbNr, offsetStart, 1, S7WordLength.Bit, buffer), token, TaskCreationOptions.None, scheduler); + var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dbNo, offsetStart, 1, S7WordLength.Bit, buffer), token, TaskCreationOptions.None, scheduler); token.ThrowIfCancellationRequested(); if (result != 0) { - EvaluateErrorCode(result); - return (false); + var errorText = EvaluateErrorCode(result); + throw new S7CommunicationException($"Error writing {operand}{dbNo}:{startByteAddress} bit {bitAdress} ({errorText})", result, errorText); } - - return (true); } - public async Task WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token) + public async Task WriteBytes(Operand operand, ushort startByteAddress, byte[] data, ushort dbNo, CancellationToken token) { EnsureConnectionValid(); - var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dBNr, startByteAdress, data.Length, S7WordLength.Byte, data), token, TaskCreationOptions.None, scheduler); + var result = await Task.Factory.StartNew(() => sharp7.WriteArea(operand.ToArea(), dbNo, startByteAddress, data.Length, S7WordLength.Byte, data), token, TaskCreationOptions.None, scheduler); token.ThrowIfCancellationRequested(); if (result != 0) { - EvaluateErrorCode(result); - return 0; + var errorText = EvaluateErrorCode(result); + throw new S7CommunicationException($"Error writing {operand}{dbNo}:{startByteAddress}.{data.Length} ({errorText})", result, errorText); } - - return (ushort) (data.Length); } @@ -218,7 +218,7 @@ internal class Sharp7Connector : IS7Connector private async Task CloseConnection() { if (sharp7 == null) - throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + throw new InvalidOperationException("S7 driver is not initialized."); await Task.Factory.StartNew(() => sharp7.Disconnect(), CancellationToken.None, TaskCreationOptions.None, scheduler); } @@ -229,19 +229,19 @@ internal class Sharp7Connector : IS7Connector throw new ObjectDisposedException("S7Connector"); if (sharp7 == null) - throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + throw new InvalidOperationException("S7 driver is not initialized."); if (!IsConnected) throw new InvalidOperationException("Plc is not connected"); } - private bool EvaluateErrorCode(int errorCode) + private string EvaluateErrorCode(int errorCode) { if (errorCode == 0) - return true; + return null; if (sharp7 == null) - throw new InvalidOperationException(StringResources.StrErrorS7DriverNotInitialized); + throw new InvalidOperationException("S7 driver is not initialized."); var errorText = sharp7.ErrorText(errorCode); Logger?.LogError($"Error Code {errorCode} {errorText}"); @@ -249,7 +249,7 @@ internal class Sharp7Connector : IS7Connector if (S7ErrorCodes.AssumeConnectionLost(errorCode)) SetConnectionLostState(); - return false; + return errorText; } private async Task Reconnect() diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index ec6504a..bada2e2 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -164,10 +164,7 @@ public class Sharp7Plc : IPlc // Special handling for bools, which are written on a by-bit basis. Writing a complete byte would // overwrite other bits within this byte. - if (address.Bit == null) - throw new InvalidOperationException("Address must have a Bit to write a bool."); - - 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.DbNr, token); } else { From 32a7d7cd24b91970c9e898b19e4aa86d540c19bc Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 8 Feb 2024 18:31:23 +0100 Subject: [PATCH 44/78] Remove unused localization --- .../Resources/StringResources.Designer.cs | 99 ------------- Sharp7.Rx/Resources/StringResources.resx | 132 ------------------ Sharp7.Rx/Sharp7.Rx.csproj | 19 --- Sharp7.Rx/Sharp7Connector.cs | 1 - 4 files changed, 251 deletions(-) delete mode 100644 Sharp7.Rx/Resources/StringResources.Designer.cs delete mode 100644 Sharp7.Rx/Resources/StringResources.resx diff --git a/Sharp7.Rx/Resources/StringResources.Designer.cs b/Sharp7.Rx/Resources/StringResources.Designer.cs deleted file mode 100644 index dd88d85..0000000 --- a/Sharp7.Rx/Resources/StringResources.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Sharp7.Rx.Resources { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class StringResources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal StringResources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Sharp7.Rx.Resources.StringResources", typeof(StringResources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to TCP/IP connection established.. - /// - internal static string StrInfoConnectionEstablished { - get { - return ResourceManager.GetString("StrInfoConnectionEstablished", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Trying to connect to PLC ({2}) '{0}', CPU slot {1}.... - /// - internal static string StrInfoTryConnecting { - get { - return ResourceManager.GetString("StrInfoTryConnecting", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Error while reading data from plc.. - /// - internal static string StrLogErrorReadingDataFromPlc { - get { - return ResourceManager.GetString("StrLogErrorReadingDataFromPlc", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Communication error discovered. Reconnect is in progress.... - /// - internal static string StrLogWarningCommunictionErrorReconnecting { - get { - return ResourceManager.GetString("StrLogWarningCommunictionErrorReconnecting", resourceCulture); - } - } - } -} diff --git a/Sharp7.Rx/Resources/StringResources.resx b/Sharp7.Rx/Resources/StringResources.resx deleted file mode 100644 index ee55570..0000000 --- a/Sharp7.Rx/Resources/StringResources.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Error while reading data from plc. - - - Communication error discovered. Reconnect is in progress... - - - Trying to connect to PLC ({2}) '{0}', CPU slot {1}... - - - TCP/IP connection established. - - \ No newline at end of file diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 2e5a601..faf60fe 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -27,23 +27,4 @@ - - - True - True - StringResources.resx - - - - - - ResXFileCodeGenerator - StringResources.Designer.cs - - - - - - - diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 98f9c74..951ff0c 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -6,7 +6,6 @@ using Sharp7.Rx.Basics; using Sharp7.Rx.Enums; using Sharp7.Rx.Extensions; using Sharp7.Rx.Interfaces; -using Sharp7.Rx.Resources; using Sharp7.Rx.Settings; namespace Sharp7.Rx; From 25bcfea835aa1d2c24636d3343f88c60278ae1eb Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 09:58:16 +0100 Subject: [PATCH 45/78] Add connectionState.Disposed --- Sharp7.Rx/Enums/ConnectionState.cs | 3 ++- Sharp7.Rx/Sharp7Connector.cs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Sharp7.Rx/Enums/ConnectionState.cs b/Sharp7.Rx/Enums/ConnectionState.cs index 5cbedc5..ada7634 100644 --- a/Sharp7.Rx/Enums/ConnectionState.cs +++ b/Sharp7.Rx/Enums/ConnectionState.cs @@ -5,5 +5,6 @@ public enum ConnectionState Initial, Connected, DisconnectedByUser, - ConnectionLost + ConnectionLost, + Disposed } diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 951ff0c..3fca9a7 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -206,6 +206,7 @@ internal class Sharp7Connector : IS7Connector sharp7 = null; } + connectionStateSubject?.OnNext(Enums.ConnectionState.Disposed); connectionStateSubject?.OnCompleted(); connectionStateSubject?.Dispose(); } From f3a92addaaa0175c3f02cf5b7b201a39fe534323 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 10:26:23 +0100 Subject: [PATCH 46/78] Fix buffer length --- Sharp7.Rx/Sharp7Plc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index bada2e2..501b578 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -127,7 +127,7 @@ public class Sharp7Plc : IPlc { 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.DbNr, token); return S7ValueConverter.ReadFromBuffer(data, address); } From bfc9c93c803754d21754eafbfd06be979a364084 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 10:26:54 +0100 Subject: [PATCH 47/78] improve error messages --- Sharp7.Rx/Sharp7Connector.cs | 38 +++++++++++++++--------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 3fca9a7..843794d 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -104,11 +104,8 @@ internal class Sharp7Connector : IS7Connector .ToArray(); var result = await Task.Factory.StartNew(() => s7MultiVar.Read(), CancellationToken.None, TaskCreationOptions.None, scheduler); - if (result != 0) - { - var errorText = EvaluateErrorCode(result); - throw new S7CommunicationException($"Error in MultiVar request for variables: {string.Join(",", variableNames)} ({errorText})", result, errorText); - } + + EnsureSuccessOrThrow(result, $"Error in MultiVar request for variables: {string.Join(",", variableNames)}"); 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); token.ThrowIfCancellationRequested(); - if (result != 0) - { - var errorText = EvaluateErrorCode(result); - throw new S7CommunicationException($"Error reading {operand}{dbNo}:{startByteAddress}->{bytesToRead} ({errorText})", result, errorText); - } + EnsureSuccessOrThrow(result, $"Error reading {operand}{dbNo}:{startByteAddress}->{bytesToRead}"); 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); token.ThrowIfCancellationRequested(); - if (result != 0) - { - var errorText = EvaluateErrorCode(result); - throw new S7CommunicationException($"Error writing {operand}{dbNo}:{startByteAddress} bit {bitAdress} ({errorText})", result, errorText); - } + EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress} bit {bitAdress}"); } public async Task WriteBytes(Operand operand, ushort startByteAddress, byte[] data, ushort dbNo, CancellationToken token) @@ -184,11 +173,16 @@ 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); token.ThrowIfCancellationRequested(); - if (result != 0) - { - var errorText = EvaluateErrorCode(result); - throw new S7CommunicationException($"Error writing {operand}{dbNo}:{startByteAddress}.{data.Length} ({errorText})", result, errorText); - } + EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress}.{data.Length}"); + } + + private void EnsureSuccessOrThrow(int result, string message) + { + if (result == 0) return; + + var errorText = EvaluateErrorCode(result); + // 0x40000: Maybe the DB is optimized or PUT/GET communication is not enabled. + throw new S7CommunicationException($"{message} ({errorText})", result, errorText); } @@ -243,8 +237,8 @@ internal class Sharp7Connector : IS7Connector if (sharp7 == null) throw new InvalidOperationException("S7 driver is not initialized."); - var errorText = sharp7.ErrorText(errorCode); - Logger?.LogError($"Error Code {errorCode} {errorText}"); + var errorText = $"0x{errorCode:X}: {sharp7.ErrorText(errorCode)}"; + Logger?.LogError($"S7 Error {errorText}"); if (S7ErrorCodes.AssumeConnectionLost(errorCode)) SetConnectionLostState(); From 280a894b1fc4a22696ad4ec4ad4bc0b7ef45a265 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 11:18:48 +0100 Subject: [PATCH 48/78] Add additional hints to errors --- Sharp7.Rx/Sharp7Connector.cs | 36 ++++++++++++++++++++++++++---------- Sharp7.Rx/Sharp7Plc.cs | 18 +++++++++--------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 843794d..e7b4c52 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -12,6 +12,18 @@ namespace Sharp7.Rx; internal class Sharp7Connector : IS7Connector { + private static readonly IReadOnlyDictionary additionalErrorTexts = new Dictionary + { + {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. + """ + } + }; + private readonly BehaviorSubject connectionStateSubject = new(Enums.ConnectionState.Initial); private readonly int cpuSlotNr; @@ -176,15 +188,6 @@ internal class Sharp7Connector : IS7Connector EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress}.{data.Length}"); } - private void EnsureSuccessOrThrow(int result, string message) - { - if (result == 0) return; - - var errorText = EvaluateErrorCode(result); - // 0x40000: Maybe the DB is optimized or PUT/GET communication is not enabled. - throw new S7CommunicationException($"{message} ({errorText})", result, errorText); - } - protected virtual void Dispose(bool disposing) { @@ -229,6 +232,19 @@ internal class Sharp7Connector : IS7Connector 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}"; + + if (additionalErrorTexts.TryGetValue(result, out var additionalErrorText)) + completeMessage += Environment.NewLine + additionalErrorText; + + throw new S7CommunicationException(completeMessage, result, errorText); + } + private string EvaluateErrorCode(int errorCode) { if (errorCode == 0) @@ -237,7 +253,7 @@ internal class Sharp7Connector : IS7Connector if (sharp7 == null) throw new InvalidOperationException("S7 driver is not initialized."); - var errorText = $"0x{errorCode:X}: {sharp7.ErrorText(errorCode)}"; + var errorText = $"0x{errorCode:X}, {sharp7.ErrorText(errorCode)}"; Logger?.LogError($"S7 Error {errorText}"); if (S7ErrorCodes.AssumeConnectionLost(errorCode)) diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 501b578..904294b 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -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 GetValue(string variableName) { return GetValue(variableName, CancellationToken.None); @@ -222,6 +213,15 @@ public class Sharp7Plc : IPlc return Unit.Default; } + 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; + } + private void PrintAndResetPerformanceStatistik() { if (performanceCoutner.Count == performanceCoutner.Capacity) From dd0af702625c0a55203d3b013a3b7dfff78041c5 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 11:31:08 +0100 Subject: [PATCH 49/78] Fix multithreadign issue with lingering subscriptions --- .../Basics/ConcurrentSubjectDictionary.cs | 41 +++++++++++-------- Sharp7.Rx/Interfaces/IS7Connector.cs | 2 +- Sharp7.Rx/Sharp7Connector.cs | 2 +- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs b/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs index 3ea9ef0..332e03c 100644 --- a/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs +++ b/Sharp7.Rx/Basics/ConcurrentSubjectDictionary.cs @@ -53,11 +53,14 @@ internal class ConcurrentSubjectDictionary : IDisposable { lock (dictionaryLock) { - var subject = dictionary.AddOrUpdate(key, k => new SubjectWithRefCounter {Counter = 1, Subject = CreateSubject(k)}, (key1, counter) => - { - counter.Counter = counter.Counter + 1; - return counter; - }); + var subject = dictionary.AddOrUpdate( + key, + k => new SubjectWithRefCounter(CreateSubject(k)), + (_, subjectWithRefCounter) => + { + subjectWithRefCounter.IncreaseCount(); + return subjectWithRefCounter; + }); return new DisposableItem(subject.Subject.AsObservable(), () => RemoveIfNoLongerInUse(key)); } @@ -65,8 +68,7 @@ internal class ConcurrentSubjectDictionary : IDisposable public bool TryGetObserver(TKey key, out IObserver subject) { - SubjectWithRefCounter subjectWithRefCount; - if (dictionary.TryGetValue(key, out subjectWithRefCount)) + if (dictionary.TryGetValue(key, out var subjectWithRefCount)) { subject = subjectWithRefCount.Subject.AsObserver(); return true; @@ -101,15 +103,9 @@ internal class ConcurrentSubjectDictionary : IDisposable private void RemoveIfNoLongerInUse(TKey variableName) { lock (dictionaryLock) - { - SubjectWithRefCounter subjectWithRefCount; - if (dictionary.TryGetValue(variableName, out subjectWithRefCount)) - { - if (subjectWithRefCount.Counter == 1) - dictionary.TryRemove(variableName, out subjectWithRefCount); - else subjectWithRefCount.Counter--; - } - } + if (dictionary.TryGetValue(variableName, out var subjectWithRefCount)) + if (subjectWithRefCount.DecreaseCount() < 1) + dictionary.TryRemove(variableName, out _); } ~ConcurrentSubjectDictionary() @@ -119,7 +115,16 @@ internal class ConcurrentSubjectDictionary : IDisposable class SubjectWithRefCounter { - public int Counter { get; set; } - public ISubject Subject { get; set; } + private int counter = 1; + + public SubjectWithRefCounter(ISubject subject) + { + Subject = subject; + } + + public ISubject Subject { get; } + + public int DecreaseCount() => Interlocked.Decrement(ref counter); + public int IncreaseCount() => Interlocked.Increment(ref counter); } } diff --git a/Sharp7.Rx/Interfaces/IS7Connector.cs b/Sharp7.Rx/Interfaces/IS7Connector.cs index fe365c1..66566ac 100644 --- a/Sharp7.Rx/Interfaces/IS7Connector.cs +++ b/Sharp7.Rx/Interfaces/IS7Connector.cs @@ -17,5 +17,5 @@ internal interface IS7Connector : IDisposable Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNo, CancellationToken token); Task WriteBytes(Operand operand, ushort startByteAddress, byte[] data, ushort dbNo, CancellationToken token); - Task> ExecuteMultiVarRequest(IReadOnlyList variableNames); + Task> ExecuteMultiVarRequest(IReadOnlyList variableNames); } diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index e7b4c52..e1684cf 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -98,7 +98,7 @@ internal class Sharp7Connector : IS7Connector await CloseConnection(); } - public async Task> ExecuteMultiVarRequest(IReadOnlyList variableNames) + public async Task> ExecuteMultiVarRequest(IReadOnlyList variableNames) { if (variableNames.IsEmpty()) return new Dictionary(); From 4701a224a7bbcde6de4ddc43b3f952b27e33f000 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 12:33:24 +0100 Subject: [PATCH 50/78] Remove "S7" from some class names --- Sharp7.Rx.Tests/S7VariableNameParserTests.cs | 91 ------------------- .../ConvertBothWays.cs | 2 +- .../ConverterTestBase.cs | 12 +-- .../ReadFromBuffer.cs | 6 +- .../WriteToBuffer.cs | 6 +- .../MatchesType.cs | 6 +- Sharp7.Rx.Tests/VariableNameParserTests.cs | 91 +++++++++++++++++++ Sharp7.Rx/CacheVariableNameParser.cs | 10 +- Sharp7.Rx/Exceptions/S7Exception.cs | 8 +- Sharp7.Rx/Extensions/S7VariableExtensions.cs | 6 +- Sharp7.Rx/Interfaces/IS7VariableNameParser.cs | 7 -- Sharp7.Rx/Interfaces/IVariableNameParser.cs | 7 ++ Sharp7.Rx/Sharp7Connector.cs | 4 +- Sharp7.Rx/Sharp7Plc.cs | 10 +- ...{S7ValueConverter.cs => ValueConverter.cs} | 10 +- ...7VariableAddress.cs => VariableAddress.cs} | 2 +- ...bleNameParser.cs => VariableNameParser.cs} | 6 +- 17 files changed, 142 insertions(+), 142 deletions(-) delete mode 100644 Sharp7.Rx.Tests/S7VariableNameParserTests.cs rename Sharp7.Rx.Tests/{S7ValueConverterTests => ValueConverterTests}/ConvertBothWays.cs (92%) rename Sharp7.Rx.Tests/{S7ValueConverterTests => ValueConverterTests}/ConverterTestBase.cs (91%) rename Sharp7.Rx.Tests/{S7ValueConverterTests => ValueConverterTests}/ReadFromBuffer.cs (84%) rename Sharp7.Rx.Tests/{S7ValueConverterTests => ValueConverterTests}/WriteToBuffer.cs (84%) rename Sharp7.Rx.Tests/{S7VariableAddressTests => VariableAddressTests}/MatchesType.cs (92%) create mode 100644 Sharp7.Rx.Tests/VariableNameParserTests.cs delete mode 100644 Sharp7.Rx/Interfaces/IS7VariableNameParser.cs create mode 100644 Sharp7.Rx/Interfaces/IVariableNameParser.cs rename Sharp7.Rx/{S7ValueConverter.cs => ValueConverter.cs} (96%) rename Sharp7.Rx/{S7VariableAddress.cs => VariableAddress.cs} (96%) rename Sharp7.Rx/{S7VariableNameParser.cs => VariableNameParser.cs} (96%) diff --git a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs b/Sharp7.Rx.Tests/S7VariableNameParserTests.cs deleted file mode 100644 index 50a460a..0000000 --- a/Sharp7.Rx.Tests/S7VariableNameParserTests.cs +++ /dev/null @@ -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(() => parser.Parse(input)); - } - - public static IEnumerable ValidTestCases() - { - yield return new TestCase("DB506.Bit216.2", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Bit = 2, Type = DbType.Bit}); - - yield return new TestCase("DB506.String216.10", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 10, Type = DbType.String}); - yield return new TestCase("DB506.WString216.10", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 10, Type = DbType.WString}); - - yield return new TestCase("DB506.Byte216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Type = DbType.Byte}); - yield return new TestCase("DB506.Byte216.100", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 100, Type = DbType.Byte}); - yield return new TestCase("DB506.Int216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int}); - yield return new TestCase("DB506.UInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.UInt}); - yield return new TestCase("DB506.DInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.DInt}); - yield return new TestCase("DB506.UDInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.UDInt}); - yield return new TestCase("DB506.LInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.LInt}); - yield return new TestCase("DB506.ULInt216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); - - yield return new TestCase("DB506.Real216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.Single}); - yield return new TestCase("DB506.LReal216", new S7VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.Double}); - - - // Legacy - yield return new TestCase("DB13.DBX3.1", new S7VariableAddress {Operand = Operand.Db, DbNr = 13, Start = 3, Length = 1, Bit = 1, Type = DbType.Bit}); - yield return new TestCase("Db403.X5.2", new S7VariableAddress {Operand = Operand.Db, DbNr = 403, Start = 5, Length = 1, Bit = 2, Type = DbType.Bit}); - yield return new TestCase("DB55DBX23.6", new S7VariableAddress {Operand = Operand.Db, DbNr = 55, Start = 23, Length = 1, Bit = 6, Type = DbType.Bit}); - yield return new TestCase("DB1.S255.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; - } -} diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs b/Sharp7.Rx.Tests/ValueConverterTests/ConvertBothWays.cs similarity index 92% rename from Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs rename to Sharp7.Rx.Tests/ValueConverterTests/ConvertBothWays.cs index b3956ac..39c9be3 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ConvertBothWays.cs +++ b/Sharp7.Rx.Tests/ValueConverterTests/ConvertBothWays.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using Shouldly; -namespace Sharp7.Rx.Tests.S7ValueConverterTests; +namespace Sharp7.Rx.Tests.ValueConverterTests; [TestFixture] internal class ConvertBothWays : ConverterTestBase diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs b/Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs similarity index 91% rename from Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs rename to Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs index f344256..d09c3ed 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ConverterTestBase.cs +++ b/Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs @@ -1,15 +1,15 @@ using System.Reflection; using Sharp7.Rx.Interfaces; -namespace Sharp7.Rx.Tests.S7ValueConverterTests; +namespace Sharp7.Rx.Tests.ValueConverterTests; internal abstract class ConverterTestBase { - protected static readonly IS7VariableNameParser Parser = new S7VariableNameParser(); + protected static readonly IVariableNameParser Parser = new VariableNameParser(); 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()); return convert; } @@ -69,14 +69,14 @@ internal abstract class ConverterTestBase /// This helper method exists, since I could not manage to invoke a generic method /// accepring a Span<T> as parameter. /// - public static void WriteToBuffer(byte[] buffer, TValue value, S7VariableAddress address) + public static void WriteToBuffer(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 S7VariableAddress VariableAddress => Parser.Parse(Address); + public VariableAddress VariableAddress => Parser.Parse(Address); public override string ToString() => $"{Value.GetType().Name}, {Address}: {Value}"; } diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs b/Sharp7.Rx.Tests/ValueConverterTests/ReadFromBuffer.cs similarity index 84% rename from Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs rename to Sharp7.Rx.Tests/ValueConverterTests/ReadFromBuffer.cs index cbb4542..66f08aa 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/ReadFromBuffer.cs +++ b/Sharp7.Rx.Tests/ValueConverterTests/ReadFromBuffer.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using Shouldly; -namespace Sharp7.Rx.Tests.S7ValueConverterTests; +namespace Sharp7.Rx.Tests.ValueConverterTests; [TestFixture] internal class ReadFromBuffer : ConverterTestBase @@ -34,7 +34,7 @@ internal class ReadFromBuffer : ConverterTestBase var variableAddress = Parser.Parse(address); //Act - Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); + Should.Throw(() => ValueConverter.ReadFromBuffer(data, variableAddress)); } [TestCase(123, "DB12.DINT3", new byte[] {0x01, 0x02, 0x03})] @@ -46,6 +46,6 @@ internal class ReadFromBuffer : ConverterTestBase var variableAddress = Parser.Parse(address); //Act - Should.Throw(() => S7ValueConverter.ReadFromBuffer(data, variableAddress)); + Should.Throw(() => ValueConverter.ReadFromBuffer(data, variableAddress)); } } diff --git a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs b/Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs similarity index 84% rename from Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs rename to Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs index 4e364fb..0d790f5 100644 --- a/Sharp7.Rx.Tests/S7ValueConverterTests/WriteToBuffer.cs +++ b/Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using Shouldly; -namespace Sharp7.Rx.Tests.S7ValueConverterTests; +namespace Sharp7.Rx.Tests.ValueConverterTests; [TestFixture] internal class WriteToBuffer : ConverterTestBase @@ -37,7 +37,7 @@ internal class WriteToBuffer : ConverterTestBase var buffer = new byte[bufferSize]; //Act - Should.Throw(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); + Should.Throw(() => ValueConverter.WriteToBuffer(buffer, input, variableAddress)); } [TestCase((char) 18, "DB0.DBB0")] @@ -48,6 +48,6 @@ internal class WriteToBuffer : ConverterTestBase var buffer = new byte[variableAddress.BufferLength]; //Act - Should.Throw(() => S7ValueConverter.WriteToBuffer(buffer, input, variableAddress)); + Should.Throw(() => ValueConverter.WriteToBuffer(buffer, input, variableAddress)); } } diff --git a/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs b/Sharp7.Rx.Tests/VariableAddressTests/MatchesType.cs similarity index 92% rename from Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs rename to Sharp7.Rx.Tests/VariableAddressTests/MatchesType.cs index 458c17d..abaeae5 100644 --- a/Sharp7.Rx.Tests/S7VariableAddressTests/MatchesType.cs +++ b/Sharp7.Rx.Tests/VariableAddressTests/MatchesType.cs @@ -1,15 +1,15 @@ using NUnit.Framework; using Sharp7.Rx.Extensions; using Sharp7.Rx.Interfaces; -using Sharp7.Rx.Tests.S7ValueConverterTests; +using Sharp7.Rx.Tests.ValueConverterTests; using Shouldly; -namespace Sharp7.Rx.Tests.S7VariableAddressTests; +namespace Sharp7.Rx.Tests.VariableAddressTests; [TestFixture] public class MatchesType { - static readonly IS7VariableNameParser parser = new S7VariableNameParser(); + static readonly IVariableNameParser parser = new VariableNameParser(); private static readonly IReadOnlyList typeList = new[] { diff --git a/Sharp7.Rx.Tests/VariableNameParserTests.cs b/Sharp7.Rx.Tests/VariableNameParserTests.cs new file mode 100644 index 0000000..577f7d0 --- /dev/null +++ b/Sharp7.Rx.Tests/VariableNameParserTests.cs @@ -0,0 +1,91 @@ +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(() => parser.Parse(input)); + } + + public static IEnumerable ValidTestCases() + { + yield return new TestCase("DB506.Bit216.2", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Bit = 2, Type = DbType.Bit}); + + yield return new TestCase("DB506.String216.10", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 10, Type = DbType.String}); + yield return new TestCase("DB506.WString216.10", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 10, Type = DbType.WString}); + + yield return new TestCase("DB506.Byte216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Type = DbType.Byte}); + yield return new TestCase("DB506.Byte216.100", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 100, Type = DbType.Byte}); + yield return new TestCase("DB506.Int216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int}); + yield return new TestCase("DB506.UInt216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.UInt}); + yield return new TestCase("DB506.DInt216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.DInt}); + yield return new TestCase("DB506.UDInt216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.UDInt}); + yield return new TestCase("DB506.LInt216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.LInt}); + yield return new TestCase("DB506.ULInt216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); + + yield return new TestCase("DB506.Real216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.Single}); + yield return new TestCase("DB506.LReal216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.Double}); + + + // Legacy + yield return new TestCase("DB13.DBX3.1", new VariableAddress {Operand = Operand.Db, DbNr = 13, Start = 3, Length = 1, Bit = 1, Type = DbType.Bit}); + yield return new TestCase("Db403.X5.2", new VariableAddress {Operand = Operand.Db, DbNr = 403, Start = 5, Length = 1, Bit = 2, Type = DbType.Bit}); + yield return new TestCase("DB55DBX23.6", new VariableAddress {Operand = Operand.Db, DbNr = 55, Start = 23, Length = 1, Bit = 6, Type = DbType.Bit}); + yield return new TestCase("DB1.S255.20", new VariableAddress {Operand = Operand.Db, DbNr = 1, Start = 255, Length = 20, Type = DbType.String}); + yield return new TestCase("DB5.String887.20", new VariableAddress {Operand = Operand.Db, DbNr = 5, Start = 887, Length = 20, Type = DbType.String}); + yield return new TestCase("DB506.B216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Type = DbType.Byte}); + yield return new TestCase("DB506.DBB216.5", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 5, Type = DbType.Byte}); + yield return new TestCase("DB506.D216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.Single}); + yield return new TestCase("DB506.DINT216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.DInt}); + yield return new TestCase("DB506.INT216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int}); + yield return new TestCase("DB506.DBW216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int}); + yield return new TestCase("DB506.DUL216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); + yield return new TestCase("DB506.DULINT216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); + yield return new TestCase("DB506.DULONG216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); + } + + public record TestCase(string Input, VariableAddress Expected) + { + public override string ToString() => Input; + } +} diff --git a/Sharp7.Rx/CacheVariableNameParser.cs b/Sharp7.Rx/CacheVariableNameParser.cs index d24ef98..c4d42a9 100644 --- a/Sharp7.Rx/CacheVariableNameParser.cs +++ b/Sharp7.Rx/CacheVariableNameParser.cs @@ -3,16 +3,16 @@ using Sharp7.Rx.Interfaces; namespace Sharp7.Rx; -internal class CacheVariableNameParser : IS7VariableNameParser +internal class CacheVariableNameParser : IVariableNameParser { - private static readonly ConcurrentDictionary addressCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary addressCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - private readonly IS7VariableNameParser inner; + private readonly IVariableNameParser inner; - public CacheVariableNameParser(IS7VariableNameParser inner) + public CacheVariableNameParser(IVariableNameParser inner) { this.inner = inner; } - public S7VariableAddress Parse(string input) => addressCache.GetOrAdd(input, inner.Parse); + public VariableAddress Parse(string input) => addressCache.GetOrAdd(input, inner.Parse); } diff --git a/Sharp7.Rx/Exceptions/S7Exception.cs b/Sharp7.Rx/Exceptions/S7Exception.cs index 2d8fb44..7fd9915 100644 --- a/Sharp7.Rx/Exceptions/S7Exception.cs +++ b/Sharp7.Rx/Exceptions/S7Exception.cs @@ -31,13 +31,13 @@ public class S7CommunicationException : 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; 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; Address = address.ToString(); @@ -50,13 +50,13 @@ public class DataTypeMissmatchException : 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; 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; Address = address.ToString(); diff --git a/Sharp7.Rx/Extensions/S7VariableExtensions.cs b/Sharp7.Rx/Extensions/S7VariableExtensions.cs index 40c8eed..ae04de3 100644 --- a/Sharp7.Rx/Extensions/S7VariableExtensions.cs +++ b/Sharp7.Rx/Extensions/S7VariableExtensions.cs @@ -2,9 +2,9 @@ namespace Sharp7.Rx.Extensions; -internal static class S7VariableAddressExtensions +internal static class VariableAddressExtensions { - private static readonly Dictionary> supportedTypeMap = new() + private static readonly Dictionary> supportedTypeMap = new() { {typeof(bool), a => a.Type == DbType.Bit}, {typeof(string), a => a.Type is DbType.String or DbType.WString or DbType.Byte }, @@ -20,6 +20,6 @@ internal static class S7VariableAddressExtensions {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); } diff --git a/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs b/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs deleted file mode 100644 index ae81d67..0000000 --- a/Sharp7.Rx/Interfaces/IS7VariableNameParser.cs +++ /dev/null @@ -1,7 +0,0 @@ -#nullable enable -namespace Sharp7.Rx.Interfaces; - -internal interface IS7VariableNameParser -{ - S7VariableAddress Parse(string input); -} diff --git a/Sharp7.Rx/Interfaces/IVariableNameParser.cs b/Sharp7.Rx/Interfaces/IVariableNameParser.cs new file mode 100644 index 0000000..d6f8219 --- /dev/null +++ b/Sharp7.Rx/Interfaces/IVariableNameParser.cs @@ -0,0 +1,7 @@ +#nullable enable +namespace Sharp7.Rx.Interfaces; + +internal interface IVariableNameParser +{ + VariableAddress Parse(string input); +} diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index e1684cf..695e20d 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -32,13 +32,13 @@ internal class Sharp7Connector : IS7Connector private readonly int port; private readonly int rackNr; private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new(maxDegreeOfParallelism: 1); - private readonly IS7VariableNameParser variableNameParser; + private readonly IVariableNameParser variableNameParser; private bool disposed; private S7Client sharp7; - public Sharp7Connector(PlcConnectionSettings settings, IS7VariableNameParser variableNameParser) + public Sharp7Connector(PlcConnectionSettings settings, IVariableNameParser variableNameParser) { this.variableNameParser = variableNameParser; ipAddress = settings.IpAddress; diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 904294b..b799924 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -17,7 +17,7 @@ public class Sharp7Plc : IPlc private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase); private readonly List performanceCoutner = new(1000); private readonly PlcConnectionSettings plcConnectionSettings; - private readonly IS7VariableNameParser varaibleNameParser = new CacheVariableNameParser(new S7VariableNameParser()); + private readonly IVariableNameParser varaibleNameParser = new CacheVariableNameParser(new VariableNameParser()); private bool disposed; private Sharp7Connector s7Connector; @@ -89,7 +89,7 @@ public class Sharp7Plc : IPlc Observable.FromAsync(() => GetValue(variableName)) .Concat( disposeableContainer.Observable - .Select(bytes => S7ValueConverter.ReadFromBuffer(bytes, address)) + .Select(bytes => ValueConverter.ReadFromBuffer(bytes, address)) ); if (transmissionMode == TransmissionMode.OnChange) @@ -119,7 +119,7 @@ public class Sharp7Plc : IPlc var address = ParseAndVerify(variableName, typeof(TValue)); var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.BufferLength, address.DbNr, token); - return S7ValueConverter.ReadFromBuffer(data, address); + return ValueConverter.ReadFromBuffer(data, address); } public async Task InitializeAsync() @@ -161,7 +161,7 @@ public class Sharp7Plc : IPlc { // TODO: Use ArrayPool.Rent() once we drop Framwework support 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); } @@ -213,7 +213,7 @@ public class Sharp7Plc : IPlc return Unit.Default; } - private S7VariableAddress ParseAndVerify(string variableName, Type type) + private VariableAddress ParseAndVerify(string variableName, Type type) { var address = varaibleNameParser.Parse(variableName); if (!address.MatchesType(type)) diff --git a/Sharp7.Rx/S7ValueConverter.cs b/Sharp7.Rx/ValueConverter.cs similarity index 96% rename from Sharp7.Rx/S7ValueConverter.cs rename to Sharp7.Rx/ValueConverter.cs index 4bc23f8..83078c2 100644 --- a/Sharp7.Rx/S7ValueConverter.cs +++ b/Sharp7.Rx/ValueConverter.cs @@ -5,7 +5,7 @@ using Sharp7.Rx.Enums; namespace Sharp7.Rx; -internal static class S7ValueConverter +internal static class ValueConverter { private static readonly Dictionary writeFunctions = new() { @@ -175,7 +175,7 @@ internal static class S7ValueConverter }, }; - public static TValue ReadFromBuffer(byte[] buffer, S7VariableAddress address) + public static TValue ReadFromBuffer(byte[] buffer, VariableAddress address) { // Todo: Change to Span when switched to newer .net @@ -191,7 +191,7 @@ internal static class S7ValueConverter return (TValue) result; } - public static void WriteToBuffer(Span buffer, TValue value, S7VariableAddress address) + public static void WriteToBuffer(Span buffer, TValue value, VariableAddress address) { if (buffer.Length < address.BufferLength) 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); } - delegate object ReadFunc(byte[] data, S7VariableAddress address); + delegate object ReadFunc(byte[] data, VariableAddress address); [StructLayout(LayoutKind.Explicit)] private struct UInt32SingleMap @@ -220,5 +220,5 @@ internal static class S7ValueConverter [FieldOffset(0)] public double Double; } - delegate void WriteFunc(Span data, S7VariableAddress address, object value); + delegate void WriteFunc(Span data, VariableAddress address, object value); } diff --git a/Sharp7.Rx/S7VariableAddress.cs b/Sharp7.Rx/VariableAddress.cs similarity index 96% rename from Sharp7.Rx/S7VariableAddress.cs rename to Sharp7.Rx/VariableAddress.cs index f04bd34..7e116e7 100644 --- a/Sharp7.Rx/S7VariableAddress.cs +++ b/Sharp7.Rx/VariableAddress.cs @@ -4,7 +4,7 @@ using Sharp7.Rx.Enums; namespace Sharp7.Rx; [NoReorder] -internal class S7VariableAddress +internal class VariableAddress { public Operand Operand { get; set; } public ushort DbNr { get; set; } diff --git a/Sharp7.Rx/S7VariableNameParser.cs b/Sharp7.Rx/VariableNameParser.cs similarity index 96% rename from Sharp7.Rx/S7VariableNameParser.cs rename to Sharp7.Rx/VariableNameParser.cs index b53fcc9..8c2bef7 100644 --- a/Sharp7.Rx/S7VariableNameParser.cs +++ b/Sharp7.Rx/VariableNameParser.cs @@ -6,7 +6,7 @@ using Sharp7.Rx.Interfaces; namespace Sharp7.Rx; -internal class S7VariableNameParser : IS7VariableNameParser +internal class VariableNameParser : IVariableNameParser { private static readonly Regex regex = new(@"^(?db)(?\d+)\.?(?[a-z]+)(?\d+)(\.(?\d+))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); @@ -46,7 +46,7 @@ internal class S7VariableNameParser : IS7VariableNameParser {"x", DbType.Bit}, }; - public S7VariableAddress Parse(string input) + public VariableAddress Parse(string input) { if (input == null) throw new ArgumentNullException(nameof(input)); @@ -111,7 +111,7 @@ internal class S7VariableNameParser : IS7VariableNameParser byte? bit = type == DbType.Bit ? GetBit() : null; - var s7VariableAddress = new S7VariableAddress + var s7VariableAddress = new VariableAddress { Operand = operand, DbNr = dbNr, From 996706df2f1fea585deb65d788b6959d8c3341ec Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 12:38:16 +0100 Subject: [PATCH 51/78] Move GetAdditionalErrorText to S7ErrorCodes --- Sharp7.Rx/S7ErrorCodes.cs | 19 ++++++++++++++++++- Sharp7.Rx/Sharp7Connector.cs | 15 ++------------- Sharp7.Rx/Sharp7Plc.cs | 2 +- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Sharp7.Rx/S7ErrorCodes.cs b/Sharp7.Rx/S7ErrorCodes.cs index f076bf8..5e54922 100644 --- a/Sharp7.Rx/S7ErrorCodes.cs +++ b/Sharp7.Rx/S7ErrorCodes.cs @@ -1,4 +1,6 @@ -namespace Sharp7.Rx; +#nullable enable + +namespace Sharp7.Rx; public static class S7ErrorCodes { @@ -12,6 +14,18 @@ public static class S7ErrorCodes 0x900000, // CPU: Address out of range }; + private static readonly IReadOnlyDictionary additionalErrorTexts = new Dictionary + { + {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. + """ + } + }; + /// /// 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 @@ -21,4 +35,7 @@ public static class S7ErrorCodes { return !notDisconnectedErrorCodes.Contains(errorCode); } + + public static string? GetAdditionalErrorText(int errorCode) => + additionalErrorTexts.TryGetValue(errorCode, out var text) ? text : null; } diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 695e20d..9e4ceca 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -12,18 +12,6 @@ namespace Sharp7.Rx; internal class Sharp7Connector : IS7Connector { - private static readonly IReadOnlyDictionary additionalErrorTexts = new Dictionary - { - {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. - """ - } - }; - private readonly BehaviorSubject connectionStateSubject = new(Enums.ConnectionState.Initial); private readonly int cpuSlotNr; @@ -239,7 +227,8 @@ internal class Sharp7Connector : IS7Connector var errorText = EvaluateErrorCode(result); var completeMessage = $"{message}: {errorText}"; - if (additionalErrorTexts.TryGetValue(result, out var additionalErrorText)) + var additionalErrorText = S7ErrorCodes.GetAdditionalErrorText(result); + if (additionalErrorText != null) completeMessage += Environment.NewLine + additionalErrorText; throw new S7CommunicationException(completeMessage, result, errorText); diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index b799924..8eff549 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -17,7 +17,7 @@ public class Sharp7Plc : IPlc private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase); private readonly List performanceCoutner = new(1000); private readonly PlcConnectionSettings plcConnectionSettings; - private readonly IVariableNameParser varaibleNameParser = new CacheVariableNameParser(new VariableNameParser()); + private readonly CacheVariableNameParser varaibleNameParser = new CacheVariableNameParser(new VariableNameParser()); private bool disposed; private Sharp7Connector s7Connector; From 56b300b3ab9f95409131428ce23e2b02c82787b3 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 12:46:45 +0100 Subject: [PATCH 52/78] Convert VariableAddress to record --- .../VariableAddressTests/MatchesType.cs | 8 +-- Sharp7.Rx.Tests/VariableNameParserTests.cs | 56 +++++++++---------- Sharp7.Rx/AssemblyInfo.cs | 2 +- Sharp7.Rx/Extensions/OperandExtensions.cs | 1 - Sharp7.Rx/Extensions/PlcExtensions.cs | 3 +- Sharp7.Rx/Extensions/S7VariableExtensions.cs | 22 ++++---- Sharp7.Rx/Sharp7Connector.cs | 2 +- Sharp7.Rx/Sharp7Plc.cs | 6 +- Sharp7.Rx/VariableAddress.cs | 24 ++++---- Sharp7.Rx/VariableNameParser.cs | 10 +--- 10 files changed, 62 insertions(+), 72 deletions(-) diff --git a/Sharp7.Rx.Tests/VariableAddressTests/MatchesType.cs b/Sharp7.Rx.Tests/VariableAddressTests/MatchesType.cs index abaeae5..0feffb3 100644 --- a/Sharp7.Rx.Tests/VariableAddressTests/MatchesType.cs +++ b/Sharp7.Rx.Tests/VariableAddressTests/MatchesType.cs @@ -61,10 +61,10 @@ public class MatchesType // 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") - )) + (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") + )) ; } diff --git a/Sharp7.Rx.Tests/VariableNameParserTests.cs b/Sharp7.Rx.Tests/VariableNameParserTests.cs index 577f7d0..4ecf1c5 100644 --- a/Sharp7.Rx.Tests/VariableNameParserTests.cs +++ b/Sharp7.Rx.Tests/VariableNameParserTests.cs @@ -20,7 +20,6 @@ internal class VariableNameParserTests [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")] @@ -29,7 +28,6 @@ internal class VariableNameParserTests [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")] @@ -49,39 +47,39 @@ internal class VariableNameParserTests public static IEnumerable ValidTestCases() { - yield return new TestCase("DB506.Bit216.2", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Bit = 2, Type = DbType.Bit}); + 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, DbNr = 506, Start = 216, Length = 10, Type = DbType.String}); - yield return new TestCase("DB506.WString216.10", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 10, Type = DbType.WString}); + 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, DbNr = 506, Start = 216, Length = 1, Type = DbType.Byte}); - yield return new TestCase("DB506.Byte216.100", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 100, Type = DbType.Byte}); - yield return new TestCase("DB506.Int216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int}); - yield return new TestCase("DB506.UInt216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.UInt}); - yield return new TestCase("DB506.DInt216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.DInt}); - yield return new TestCase("DB506.UDInt216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.UDInt}); - yield return new TestCase("DB506.LInt216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.LInt}); - yield return new TestCase("DB506.ULInt216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); + 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, DbNr = 506, Start = 216, Length = 4, Type = DbType.Single}); - yield return new TestCase("DB506.LReal216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.Double}); + 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, DbNr = 13, Start = 3, Length = 1, Bit = 1, Type = DbType.Bit}); - yield return new TestCase("Db403.X5.2", new VariableAddress {Operand = Operand.Db, DbNr = 403, Start = 5, Length = 1, Bit = 2, Type = DbType.Bit}); - yield return new TestCase("DB55DBX23.6", new VariableAddress {Operand = Operand.Db, DbNr = 55, Start = 23, Length = 1, Bit = 6, Type = DbType.Bit}); - yield return new TestCase("DB1.S255.20", new VariableAddress {Operand = Operand.Db, DbNr = 1, Start = 255, Length = 20, Type = DbType.String}); - yield return new TestCase("DB5.String887.20", new VariableAddress {Operand = Operand.Db, DbNr = 5, Start = 887, Length = 20, Type = DbType.String}); - yield return new TestCase("DB506.B216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 1, Type = DbType.Byte}); - yield return new TestCase("DB506.DBB216.5", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 5, Type = DbType.Byte}); - yield return new TestCase("DB506.D216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.Single}); - yield return new TestCase("DB506.DINT216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 4, Type = DbType.DInt}); - yield return new TestCase("DB506.INT216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int}); - yield return new TestCase("DB506.DBW216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 2, Type = DbType.Int}); - yield return new TestCase("DB506.DUL216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); - yield return new TestCase("DB506.DULINT216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); - yield return new TestCase("DB506.DULONG216", new VariableAddress {Operand = Operand.Db, DbNr = 506, Start = 216, Length = 8, Type = DbType.ULInt}); + 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) diff --git a/Sharp7.Rx/AssemblyInfo.cs b/Sharp7.Rx/AssemblyInfo.cs index e92998f..d9d730c 100644 --- a/Sharp7.Rx/AssemblyInfo.cs +++ b/Sharp7.Rx/AssemblyInfo.cs @@ -1,3 +1,3 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Sharp7.Rx.Tests")] \ No newline at end of file +[assembly: InternalsVisibleTo("Sharp7.Rx.Tests")] diff --git a/Sharp7.Rx/Extensions/OperandExtensions.cs b/Sharp7.Rx/Extensions/OperandExtensions.cs index c78cfa5..b2c6744 100644 --- a/Sharp7.Rx/Extensions/OperandExtensions.cs +++ b/Sharp7.Rx/Extensions/OperandExtensions.cs @@ -13,5 +13,4 @@ internal static class OperandExtensions Operand.Db => S7Area.DB, _ => throw new ArgumentOutOfRangeException(nameof(operand), operand, null) }; - } diff --git a/Sharp7.Rx/Extensions/PlcExtensions.cs b/Sharp7.Rx/Extensions/PlcExtensions.cs index b1f1cd2..af2738d 100644 --- a/Sharp7.Rx/Extensions/PlcExtensions.cs +++ b/Sharp7.Rx/Extensions/PlcExtensions.cs @@ -9,7 +9,8 @@ namespace Sharp7.Rx.Extensions; public static class PlcExtensions { - public static IObservable CreateDatatransferWithHandshake(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func> readData, bool initialTransfer) + public static IObservable CreateDatatransferWithHandshake(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func> readData, + bool initialTransfer) { return Observable.Create(async observer => { diff --git a/Sharp7.Rx/Extensions/S7VariableExtensions.cs b/Sharp7.Rx/Extensions/S7VariableExtensions.cs index ae04de3..fa1c87f 100644 --- a/Sharp7.Rx/Extensions/S7VariableExtensions.cs +++ b/Sharp7.Rx/Extensions/S7VariableExtensions.cs @@ -7,17 +7,17 @@ internal static class VariableAddressExtensions private static readonly Dictionary> supportedTypeMap = new() { {typeof(bool), a => a.Type == DbType.Bit}, - {typeof(string), a => a.Type is DbType.String or DbType.WString or DbType.Byte }, - {typeof(byte), a => a.Type==DbType.Byte && a.Length == 1}, - {typeof(short), a => a.Type==DbType.Int}, - {typeof(ushort), a => a.Type==DbType.UInt}, - {typeof(int), a => a.Type==DbType.DInt}, - {typeof(uint), a => a.Type==DbType.UDInt}, - {typeof(long), a => a.Type==DbType.LInt}, - {typeof(ulong), a => a.Type==DbType.ULInt}, - {typeof(float), a => a.Type==DbType.Single}, - {typeof(double), a => a.Type==DbType.Double}, - {typeof(byte[]), a => a.Type==DbType.Byte}, + {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 VariableAddress address, Type type) => diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 9e4ceca..f33c55b 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -98,7 +98,7 @@ internal class Sharp7Connector : IS7Connector .Select(x => { 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}; }) .ToArray(); diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 8eff549..a50d24d 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -118,7 +118,7 @@ public class Sharp7Plc : IPlc { var address = ParseAndVerify(variableName, typeof(TValue)); - var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.BufferLength, address.DbNr, token); + var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.BufferLength, address.DbNo, token); return ValueConverter.ReadFromBuffer(data, address); } @@ -155,7 +155,7 @@ public class Sharp7Plc : IPlc // Special handling for bools, which are written on a by-bit basis. Writing a complete byte would // 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 { @@ -163,7 +163,7 @@ public class Sharp7Plc : IPlc var bytes = new byte[address.BufferLength]; 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); } } diff --git a/Sharp7.Rx/VariableAddress.cs b/Sharp7.Rx/VariableAddress.cs index 7e116e7..8405916 100644 --- a/Sharp7.Rx/VariableAddress.cs +++ b/Sharp7.Rx/VariableAddress.cs @@ -4,14 +4,14 @@ using Sharp7.Rx.Enums; namespace Sharp7.Rx; [NoReorder] -internal class VariableAddress +internal record VariableAddress(Operand Operand, ushort DbNo, DbType Type, ushort Start, ushort Length, byte? Bit = null) { - 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 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 { @@ -23,10 +23,10 @@ internal class VariableAddress 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}", + 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}", }; } diff --git a/Sharp7.Rx/VariableNameParser.cs b/Sharp7.Rx/VariableNameParser.cs index 8c2bef7..734c33e 100644 --- a/Sharp7.Rx/VariableNameParser.cs +++ b/Sharp7.Rx/VariableNameParser.cs @@ -111,15 +111,7 @@ internal class VariableNameParser : IVariableNameParser byte? bit = type == DbType.Bit ? GetBit() : null; - var s7VariableAddress = new VariableAddress - { - Operand = operand, - DbNr = dbNr, - Start = start, - Type = type, - Length = length, - Bit = bit - }; + var s7VariableAddress = new VariableAddress(Operand: operand, DbNo: dbNr, Type: type, Start: start, Length: length, Bit: bit); return s7VariableAddress; From 9a1d0f70f02328c7228e01b32d713d09072dbbac Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 13:17:10 +0100 Subject: [PATCH 53/78] Adapt readme --- README.md | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a5f27b9..a80d530 100644 --- a/README.md +++ b/README.md @@ -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. -## S7 Addressing rules +## Addressing rules Sharp7Reactive uses a syntax for identifying addresses similar to official siemens syntax. -Every address has the form (case unsensible) `DB..`. -
i.e.: `DB42.DBX0.7` => (means) Datablock 42, bit (DBX), Start: 0, Position: 7 -
or
-`DB42.DBB4.25` => (means) Datablock 42, bytes (DBB), Start: 4, Length: 25. +Every address has the form (case unsensitive) `DB..`. -Following types are supported: -- `DBX` => Bit (bool) -- `DBB` => byte or byte[] -- `INT` -- `DINT` -- `DUL` => LINT -- `D` => REAL +| Example | Explaination | +| ------------------------------------ | ----------------------------------------------------------------- | +| `DB42.Int4` or
`DB42.DBD4` | Datablock 42, 16 bit integer from bytes 4 to 5 (zero based index) | +| `DB42.Bit0.7` or
`DB42.DBX0.7` | Datablock 42, bit from byte 0, position 7 | +| `DB42.Byte4.25` or
`DB42.DBB4.25` | Datablock 42, 25 bytes from byte 4 to 29 (zero based index) | + +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? From b6966cf526cf9a65fecbabc4c8a5e97268479a65 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 13:41:59 +0100 Subject: [PATCH 54/78] Fix buffer length for string notifications --- Sharp7.Rx/Sharp7Connector.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index f33c55b..cf95ab4 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -97,8 +97,8 @@ internal class Sharp7Connector : IS7Connector .Select(key => new {VariableName = key, Address = variableNameParser.Parse(key)}) .Select(x => { - var buffer = new byte[x.Address.Length]; - s7MultiVar.Add(S7Consts.S7AreaDB, S7Consts.S7WLByte, x.Address.DbNo, x.Address.Start, x.Address.Length, ref buffer); + var buffer = new byte[x.Address.BufferLength]; + s7MultiVar.Add(S7Consts.S7AreaDB, S7Consts.S7WLByte, x.Address.DbNo, x.Address.Start, x.Address.BufferLength, ref buffer); return new {x.VariableName, Buffer = buffer}; }) .ToArray(); From 096435f4d16fe834b20f6dabc2bf1a1cda04f837 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 14:09:34 +0100 Subject: [PATCH 55/78] Add linqpad samples --- Sharp7.Rx/Sharp7.Rx.csproj | 6 +++ .../linqpad-samples/Create Notification.linq | 37 +++++++++++++++ .../linqpad-samples/Establish connection.linq | 39 +++++++++++++++ Sharp7.Rx/linqpad-samples/FileOrder.txt | 4 ++ .../Multiple notifications.linq | 47 +++++++++++++++++++ .../linqpad-samples/Write and read value.linq | 35 ++++++++++++++ 6 files changed, 168 insertions(+) create mode 100644 Sharp7.Rx/linqpad-samples/Create Notification.linq create mode 100644 Sharp7.Rx/linqpad-samples/Establish connection.linq create mode 100644 Sharp7.Rx/linqpad-samples/FileOrder.txt create mode 100644 Sharp7.Rx/linqpad-samples/Multiple notifications.linq create mode 100644 Sharp7.Rx/linqpad-samples/Write and read value.linq diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index faf60fe..16d6add 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -27,4 +27,10 @@ + + + true + linqpad-samples\;content + + diff --git a/Sharp7.Rx/linqpad-samples/Create Notification.linq b/Sharp7.Rx/linqpad-samples/Create Notification.linq new file mode 100644 index 0000000..57d9720 --- /dev/null +++ b/Sharp7.Rx/linqpad-samples/Create Notification.linq @@ -0,0 +1,37 @@ + + Sharp7.Rx + Sharp7.Rx + System.Reactive.Linq + System.Reactive.Threading.Tasks + System.Threading.Tasks + + +var ip = "10.30.3.221"; // Set IP address of S7 +var db = 3; // Set to an existing DB + +// For rack number and cpu mpi address see +// https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot +var rackNumber = 0; +var cpuMpiAddress = 0; + +using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress); + +await plc.InitializeAsync(); +await plc.ConnectionState + .FirstAsync(c => c == Sharp7.Rx.Enums.ConnectionState.Connected) + .ToTask(); + +"Connection established".Dump(); + +// create an IObservable +var observable = plc.CreateNotification($"DB{db}.Int6", Sharp7.Rx.Enums.TransmissionMode.OnChange); + +observable.Dump(); + +for (int i = 0; i < 10; i++) +{ + await plc.SetValue($"DB{db}.Int6", (short)i); + await Task.Delay(300); +} + + diff --git a/Sharp7.Rx/linqpad-samples/Establish connection.linq b/Sharp7.Rx/linqpad-samples/Establish connection.linq new file mode 100644 index 0000000..33f6f69 --- /dev/null +++ b/Sharp7.Rx/linqpad-samples/Establish connection.linq @@ -0,0 +1,39 @@ + + Sharp7.Rx + Sharp7.Rx + System.Reactive.Linq + System.Reactive.Threading.Tasks + System.Threading.Tasks + + +// Set IP address of S7 +var ip = "10.30.3.221"; + +// For rack number and cpu mpi address see +// https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot +var rackNumber = 0; +var cpuMpiAddress = 0; + +// Create Sharp7Plc +using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress); + +// Initialize connection +await plc.InitializeAsync(); + +// wait for connection to be established +await plc.ConnectionState + .FirstAsync(c => c == Sharp7.Rx.Enums.ConnectionState.Connected) + .ToTask(); + +"Connection established".Dump(); + +try +{ + await Task.Delay(Timeout.Infinite, this.QueryCancelToken); +} +catch (TaskCanceledException) +{ + "Script stopped by user. Disconnecting by disposing plc.".Dump(); +} + + diff --git a/Sharp7.Rx/linqpad-samples/FileOrder.txt b/Sharp7.Rx/linqpad-samples/FileOrder.txt new file mode 100644 index 0000000..6d18ca6 --- /dev/null +++ b/Sharp7.Rx/linqpad-samples/FileOrder.txt @@ -0,0 +1,4 @@ +Establish connection.linq +Write and read value.linq +Create Notification.linq +Multiple notifications.linq \ No newline at end of file diff --git a/Sharp7.Rx/linqpad-samples/Multiple notifications.linq b/Sharp7.Rx/linqpad-samples/Multiple notifications.linq new file mode 100644 index 0000000..ec8db01 --- /dev/null +++ b/Sharp7.Rx/linqpad-samples/Multiple notifications.linq @@ -0,0 +1,47 @@ + + Sharp7.Rx + Sharp7.Rx + System.Reactive.Linq + System.Reactive.Threading.Tasks + System.Threading.Tasks + + +var ip = "10.30.3.221"; // Set IP address of S7 +var db = 3; // Set to an existing DB + +// For rack number and cpu mpi address see +// https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot +var rackNumber = 0; +var cpuMpiAddress = 0; + +using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress); + +plc.ConnectionState.Dump(); + +await plc.InitializeAsync(); +await plc.ConnectionState + .FirstAsync(c => c == Sharp7.Rx.Enums.ConnectionState.Connected) + .ToTask(); + +// create an IObservable +plc.CreateNotification($"DB{db}.Int6", Sharp7.Rx.Enums.TransmissionMode.OnChange).Dump("Int 6"); +plc.CreateNotification($"DB{db}.Real10", Sharp7.Rx.Enums.TransmissionMode.OnChange).Dump("Real 10"); + + + +for (int i = 0; i < 15; i++) +{ + switch (i%3) + { + case 0: + await plc.SetValue($"DB{db}.Int6", (short)i); + break; + case 1: + await plc.SetValue($"DB{db}.Real10", i * 0.123f); + break; + } + + await Task.Delay(300); +} + + diff --git a/Sharp7.Rx/linqpad-samples/Write and read value.linq b/Sharp7.Rx/linqpad-samples/Write and read value.linq new file mode 100644 index 0000000..ee5780e --- /dev/null +++ b/Sharp7.Rx/linqpad-samples/Write and read value.linq @@ -0,0 +1,35 @@ + + Sharp7.Rx + Sharp7.Rx + System.Reactive.Linq + System.Reactive.Threading.Tasks + System.Threading.Tasks + + +var ip = "10.30.3.221"; // Set IP address of S7 +var db = 3; // Set to an existing DB + +// For rack number and cpu mpi address see +// https://github.com/fbarresi/Sharp7/wiki/Connection#rack-and-slot +var rackNumber = 0; +var cpuMpiAddress = 0; + +using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress); + +await plc.InitializeAsync(); +await plc.ConnectionState + .FirstAsync(c => c == Sharp7.Rx.Enums.ConnectionState.Connected) + .ToTask(); + +"Connection established".Dump(); + +for (int i = 0; i < 10; i++) +{ + await plc.SetValue($"DB{db}.Int6", (short)i); + var value = await plc.GetValue($"DB{db}.Int6"); + value.Dump(); + + await Task.Delay(200); +} + + From c5edb4c6953078e9d4fe27d0c4785f3cd1d146af Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 14:11:40 +0100 Subject: [PATCH 56/78] Add linqpad-samples package tag --- Sharp7.Rx/Sharp7.Rx.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 16d6add..db981fa 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -12,7 +12,8 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb evopro system engineering AG evopro system engineering AG - Reactive framework for Sharp7, the Ethernet S7 PLC communication suite + Reactive framework for Sharp7, the Ethernet S7 PLC communication suite. Handling RFC1006 connections to Siemens S7 300, 1200 and 1500. + linqpad-samples https://github.com/evopro-ag/Sharp7Reactive https://raw.githubusercontent.com/evopro-ag/Sharp7Reactive/master/LICENSE true From 3338f5566ed892c703bf601406d1291e77f4d684 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 14:21:09 +0100 Subject: [PATCH 57/78] Add SourceLink --- Sharp7.Rx/Sharp7.Rx.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index db981fa..993a2fc 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -28,6 +28,10 @@ + + + + true From 332329dc68ab6f819b2e24ae72f3cab2a0f7a282 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 14:34:06 +0100 Subject: [PATCH 58/78] Explicitly state license --- Sharp7.Rx/Sharp7.Rx.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 993a2fc..4e15262 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -15,7 +15,7 @@ Reactive framework for Sharp7, the Ethernet S7 PLC communication suite. Handling RFC1006 connections to Siemens S7 300, 1200 and 1500. linqpad-samples https://github.com/evopro-ag/Sharp7Reactive - https://raw.githubusercontent.com/evopro-ag/Sharp7Reactive/master/LICENSE + Apache-2.0 true snupkg From c001ac1ea71af71e99ec19b3cd0fc56dc4acbfe6 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 14:34:16 +0100 Subject: [PATCH 59/78] Document multi variable reads --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index a80d530..e1da309 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,21 @@ Here is a table of supported data types: > Identifiers marked with * are kept for compatability reasons and might be removed in the future. +## Performance considerations + +Frequent reads of variables using `GetValue` can cause performance pressure on the S7 PLC, resulting in an increase of cycle time. + +If you frequently read variables, like polling triggers, use `CreateNotification`. Internally all variable polling initialized with `CreateNotification` is pooled to a single (or some) multi-variable-reads. + +You can provide a cycle time (delay between consecutive multi variable reads) in the `Sharp7Plc` constructor: + +```csharp +public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress, int port = 102, TimeSpan? multiVarRequestCycleTime = null) +``` + +The default value for `multiVarRequestCycleTime` is 100 ms, the minimal value is 5 ms. + + ## Would you like to contribute? Yes, please! From 1e52327a3d837f0c8eaaeeba4800b970d2903666 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 9 Feb 2024 15:05:40 +0100 Subject: [PATCH 60/78] Fix some typos --- Sharp7.Rx/Sharp7Plc.cs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index a50d24d..3727bc9 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -15,9 +15,9 @@ public class Sharp7Plc : IPlc { private readonly CompositeDisposable disposables = new(); private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase); - private readonly List performanceCoutner = new(1000); + private readonly List performanceCounter = new(1000); private readonly PlcConnectionSettings plcConnectionSettings; - private readonly CacheVariableNameParser varaibleNameParser = new CacheVariableNameParser(new VariableNameParser()); + private readonly CacheVariableNameParser variableNameParser = new CacheVariableNameParser(new VariableNameParser()); private bool disposed; private Sharp7Connector s7Connector; @@ -30,11 +30,11 @@ public class Sharp7Plc : IPlc /// /// /// - /// Polling interval used to read multi variable requests from PLC. + /// Polling interval for multi variable read from PLC. /// /// /// This is the wait time between two successive reads from PLC and determines the - /// time resolution for all variable reads reated with CreateNotification. + /// time resolution for all variable reads related with CreateNotification. /// /// /// Default is 100 ms. The minimum supported time is 5 ms. @@ -43,7 +43,7 @@ public class Sharp7Plc : IPlc public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress, int port = 102, TimeSpan? multiVarRequestCycleTime = null) { plcConnectionSettings = new PlcConnectionSettings {IpAddress = ipAddress, RackNumber = rackNumber, CpuMpiAddress = cpuMpiAddress, Port = port}; - s7Connector = new Sharp7Connector(plcConnectionSettings, varaibleNameParser); + s7Connector = new Sharp7Connector(plcConnectionSettings, variableNameParser); ConnectionState = s7Connector.ConnectionState; if (multiVarRequestCycleTime != null) @@ -80,15 +80,15 @@ public class Sharp7Plc : IPlc var address = ParseAndVerify(variableName, typeof(TValue)); var disp = new CompositeDisposable(); - var disposeableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName); - disposeableContainer.AddDisposableTo(disp); + var disposableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName); + disposableContainer.AddDisposableTo(disp); var observable = - // Directly read variable first. + // Read variable with GetValue first. // This will propagate any errors due to reading from invalid addresses. Observable.FromAsync(() => GetValue(variableName)) .Concat( - disposeableContainer.Observable + disposableContainer.Observable .Select(bytes => ValueConverter.ReadFromBuffer(bytes, address)) ); @@ -206,7 +206,7 @@ public class Sharp7Plc : IPlc } stopWatch.Stop(); - performanceCoutner.Add(stopWatch.ElapsedMilliseconds); + performanceCounter.Add(stopWatch.ElapsedMilliseconds); PrintAndResetPerformanceStatistik(); @@ -215,7 +215,7 @@ public class Sharp7Plc : IPlc private VariableAddress ParseAndVerify(string variableName, Type type) { - var address = varaibleNameParser.Parse(variableName); + var address = variableNameParser.Parse(variableName); if (!address.MatchesType(type)) throw new DataTypeMissmatchException($"Address \"{variableName}\" does not match type {type}.", type, address); @@ -224,17 +224,17 @@ public class Sharp7Plc : IPlc private void PrintAndResetPerformanceStatistik() { - if (performanceCoutner.Count == performanceCoutner.Capacity) + if (performanceCounter.Count == performanceCounter.Capacity) { - var average = performanceCoutner.Average(); - var min = performanceCoutner.Min(); - var max = performanceCoutner.Max(); + var average = performanceCounter.Average(); + var min = performanceCounter.Min(); + var max = performanceCounter.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, + performanceCounter.Capacity, min, max, average, plcConnectionSettings.IpAddress, multiVariableSubscriptions.ExistingKeys.Count(), MultiVarRequestMaxItems); - performanceCoutner.Clear(); + performanceCounter.Clear(); } } From 1672ca906d828b6357cf820602f71f864d909664 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 25 Apr 2024 10:04:56 +0200 Subject: [PATCH 61/78] Update to .net 6 --- Sharp7.Rx/Sharp7.Rx.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 4e15262..196f263 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net6.0 12.0 disable From 87d69fb61473c3483ae3448936ba34d9b8bd8741 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 25 Apr 2024 10:19:17 +0200 Subject: [PATCH 62/78] Fix or ignore build warnings --- .../LimitedConcurrencyLevelTaskScheduler.cs | 2 +- Sharp7.Rx/S7ErrorCodes.cs | 18 +++++++++--------- Sharp7.Rx/Sharp7.Rx.csproj | 7 +++++++ Sharp7.Rx/Sharp7Connector.cs | 2 ++ Sharp7.Rx/Sharp7Plc.cs | 5 +++-- Sharp7.Rx/VariableNameParser.cs | 3 +-- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Sharp7.Rx/Basics/LimitedConcurrencyLevelTaskScheduler.cs b/Sharp7.Rx/Basics/LimitedConcurrencyLevelTaskScheduler.cs index 98c24c6..56ca27b 100644 --- a/Sharp7.Rx/Basics/LimitedConcurrencyLevelTaskScheduler.cs +++ b/Sharp7.Rx/Basics/LimitedConcurrencyLevelTaskScheduler.cs @@ -26,7 +26,7 @@ internal class LimitedConcurrencyLevelTaskScheduler : TaskScheduler /// The maximum degree of parallelism provided by this scheduler. public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism) { - if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException("maxDegreeOfParallelism"); + if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism)); this.maxDegreeOfParallelism = maxDegreeOfParallelism; } diff --git a/Sharp7.Rx/S7ErrorCodes.cs b/Sharp7.Rx/S7ErrorCodes.cs index 5e54922..7734149 100644 --- a/Sharp7.Rx/S7ErrorCodes.cs +++ b/Sharp7.Rx/S7ErrorCodes.cs @@ -1,5 +1,7 @@ #nullable enable +using System.Diagnostics.CodeAnalysis; + namespace Sharp7.Rx; public static class S7ErrorCodes @@ -7,12 +9,12 @@ public static class S7ErrorCodes /// /// This list is not exhaustive and should be considered work in progress. /// - private static readonly HashSet notDisconnectedErrorCodes = new HashSet - { + private static readonly HashSet notDisconnectedErrorCodes = + [ 0x000000, // OK 0xC00000, // CPU: Item not available - 0x900000, // CPU: Address out of range - }; + 0x900000 // CPU: Address out of range + ]; private static readonly IReadOnlyDictionary additionalErrorTexts = new Dictionary { @@ -31,11 +33,9 @@ public static class S7ErrorCodes /// Other error codes indicate a user error, like reading from an unavailable DB or exceeding /// the DBs range. In this case the driver should not consider the connection to be lost. ///
- public static bool AssumeConnectionLost(int errorCode) - { - return !notDisconnectedErrorCodes.Contains(errorCode); - } + public static bool AssumeConnectionLost(int errorCode) => + !notDisconnectedErrorCodes.Contains(errorCode); public static string? GetAdditionalErrorText(int errorCode) => - additionalErrorTexts.TryGetValue(errorCode, out var text) ? text : null; + additionalErrorTexts.GetValueOrDefault(errorCode); } diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 196f263..210e8d5 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -18,6 +18,13 @@ Apache-2.0 true snupkg + + + $(NoWarn);CA1848;CA2254;CA1859
diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index cf95ab4..d04af97 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -98,7 +98,9 @@ internal class Sharp7Connector : IS7Connector .Select(x => { var buffer = new byte[x.Address.BufferLength]; +#pragma warning disable CS0618 // Type or member is obsolete, no matching overload. s7MultiVar.Add(S7Consts.S7AreaDB, S7Consts.S7WLByte, x.Address.DbNo, x.Address.Start, x.Address.BufferLength, ref buffer); +#pragma warning restore CS0618 return new {x.VariableName, Buffer = buffer}; }) .ToArray(); diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 3727bc9..a9e7c62 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -230,8 +230,9 @@ public class Sharp7Plc : IPlc var min = performanceCounter.Min(); var max = performanceCounter.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}", - performanceCounter.Capacity, min, max, average, plcConnectionSettings.IpAddress, + Logger?.LogTrace("PLC {Plc} notification perf: {Elements} calls, min {Min}, max {Max}, avg {Avg}, variables {Vars}, batch size {BatchSize}", + plcConnectionSettings.IpAddress, + performanceCounter.Capacity, min, max, average, multiVariableSubscriptions.ExistingKeys.Count(), MultiVarRequestMaxItems); performanceCounter.Clear(); diff --git a/Sharp7.Rx/VariableNameParser.cs b/Sharp7.Rx/VariableNameParser.cs index 734c33e..7191a48 100644 --- a/Sharp7.Rx/VariableNameParser.cs +++ b/Sharp7.Rx/VariableNameParser.cs @@ -48,8 +48,7 @@ internal class VariableNameParser : IVariableNameParser public VariableAddress Parse(string input) { - if (input == null) - throw new ArgumentNullException(nameof(input)); + ArgumentNullException.ThrowIfNull(input); var match = regex.Match(input); if (!match.Success) From 478a2060ba87857dbd9525e5893fe1ce17553e08 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 25 Apr 2024 10:23:51 +0200 Subject: [PATCH 63/78] Fix or ignore build warnings in tests --- Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj | 6 ++++++ Sharp7.Rx.Tests/VariableNameParserTests.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj b/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj index d1de65b..2c16b68 100644 --- a/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj +++ b/Sharp7.Rx.Tests/Sharp7.Rx.Tests.csproj @@ -6,6 +6,12 @@ enable enable latest-Recommended + + + $(NoWarn);CA1859;CA1852 diff --git a/Sharp7.Rx.Tests/VariableNameParserTests.cs b/Sharp7.Rx.Tests/VariableNameParserTests.cs index 4ecf1c5..c3e0405 100644 --- a/Sharp7.Rx.Tests/VariableNameParserTests.cs +++ b/Sharp7.Rx.Tests/VariableNameParserTests.cs @@ -42,7 +42,7 @@ internal class VariableNameParserTests public void Invalid(string? input) { var parser = new VariableNameParser(); - Should.Throw(() => parser.Parse(input)); + Should.Throw(() => parser.Parse(input!)); } public static IEnumerable ValidTestCases() From c5a6b1284342b5cb96ff2c6dfcacd4d23e8bb7f7 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 25 Apr 2024 10:24:14 +0200 Subject: [PATCH 64/78] Use ArrayPool as a write buffer --- Sharp7.Rx/Sharp7Plc.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index a9e7c62..f250ad8 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System.Buffers; +using System.Diagnostics; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -20,6 +21,7 @@ public class Sharp7Plc : IPlc private readonly CacheVariableNameParser variableNameParser = new CacheVariableNameParser(new VariableNameParser()); private bool disposed; private Sharp7Connector s7Connector; + private static readonly ArrayPool arrayPool = ArrayPool.Shared; /// @@ -159,11 +161,17 @@ public class Sharp7Plc : IPlc } else { - // TODO: Use ArrayPool.Rent() once we drop Framwework support - var bytes = new byte[address.BufferLength]; - ValueConverter.WriteToBuffer(bytes, value, address); + var buffer = arrayPool.Rent(address.BufferLength); + try + { + ValueConverter.WriteToBuffer(buffer, value, address); - await s7Connector.WriteBytes(address.Operand, address.Start, bytes, address.DbNo, token); + await s7Connector.WriteBytes(address.Operand, address.Start, buffer, address.DbNo, token); + } + finally + { + ArrayPool.Shared.Return(buffer); + } } } From b400a7215aa4726a756bac125f638c88231cab01 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 25 Apr 2024 12:16:49 +0200 Subject: [PATCH 65/78] Switch ReadFromBuffer to span --- .../ValueConverterTests/ConverterTestBase.cs | 15 +++++++---- Sharp7.Rx/ValueConverter.cs | 26 +++++++++---------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs b/Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs index d09c3ed..41fe7ac 100644 --- a/Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs +++ b/Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs @@ -9,7 +9,7 @@ internal abstract class ConverterTestBase public static MethodInfo CreateReadMethod(ConverterTestCase tc) { - var convertMi = typeof(ValueConverter).GetMethod(nameof(ValueConverter.ReadFromBuffer)); + var convertMi = typeof(ConverterTestBase).GetMethod(nameof(ReadFromBuffer)); var convert = convertMi!.MakeGenericMethod(tc.Value.GetType()); return convert; } @@ -67,12 +67,17 @@ internal abstract class ConverterTestBase /// /// This helper method exists, since I could not manage to invoke a generic method - /// accepring a Span<T> as parameter. + /// with a Span<T> parameter. /// - public static void WriteToBuffer(byte[] buffer, TValue value, VariableAddress address) - { + public static void WriteToBuffer(byte[] buffer, TValue value, VariableAddress address) => ValueConverter.WriteToBuffer(buffer, value, address); - } + + /// + /// This helper method exists, since I could not manage to invoke a generic method + /// with a Span<T> parameter. + /// + public static TValue ReadFromBuffer(byte[] buffer, VariableAddress address) => + ValueConverter.ReadFromBuffer(buffer, address); public record ConverterTestCase(object Value, string Address, byte[] Data) { diff --git a/Sharp7.Rx/ValueConverter.cs b/Sharp7.Rx/ValueConverter.cs index 83078c2..42c65b5 100644 --- a/Sharp7.Rx/ValueConverter.cs +++ b/Sharp7.Rx/ValueConverter.cs @@ -143,42 +143,40 @@ internal static class ValueConverter { return address.Type switch { - DbType.String => ParseString(), - DbType.WString => ParseWString(), - DbType.Byte => Encoding.ASCII.GetString(buffer.ToArray()), + DbType.String => ParseString(buffer), + DbType.WString => ParseWString(buffer), + DbType.Byte => Encoding.ASCII.GetString(buffer), _ => throw new DataTypeMissmatchException($"Cannot read string from {address.Type}", typeof(string), address) }; - string ParseString() + string ParseString(Span data) { // 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]); + var length = Math.Min(address.Length, data[1]); - return Encoding.ASCII.GetString(buffer, 2, length); + return Encoding.ASCII.GetString(data.Slice(2, length)); } - string ParseWString() + string ParseWString(Span data) { // 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; + // the length of the string is two bytes per character + var length = Math.Min(address.Length, BinaryPrimitives.ReadUInt16BigEndian(data.Slice(2, 2))) * 2; - return Encoding.BigEndianUnicode.GetString(buffer, 4, length); + return Encoding.BigEndianUnicode.GetString(data.Slice(4, length)); } } }, }; - public static TValue ReadFromBuffer(byte[] buffer, VariableAddress address) + public static TValue ReadFromBuffer(Span buffer, VariableAddress address) { - // Todo: Change to Span when switched to newer .net - if (buffer.Length < address.BufferLength) throw new ArgumentException($"Buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer)); @@ -204,7 +202,7 @@ internal static class ValueConverter writeFunc(buffer, address, value); } - delegate object ReadFunc(byte[] data, VariableAddress address); + delegate object ReadFunc(Span data, VariableAddress address); [StructLayout(LayoutKind.Explicit)] private struct UInt32SingleMap From 08f572b582ae04162d16dbe4f0ea5969959b8bea Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 25 Apr 2024 13:33:05 +0200 Subject: [PATCH 66/78] Encode without array allocation --- .../ValueConverterTests/ConverterTestBase.cs | 10 +- .../ValueConverterTests/WriteToBuffer.cs | 4 + Sharp7.Rx/ValueConverter.cs | 143 ++++++------------ 3 files changed, 56 insertions(+), 101 deletions(-) diff --git a/Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs b/Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs index 41fe7ac..31dfccb 100644 --- a/Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs +++ b/Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs @@ -49,6 +49,8 @@ internal abstract class ConverterTestBase 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("A\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80A", "DB99.WString10.10", + [0x0, 0xA, 0x0, 0x9, 0x0, 0x41, 0xD8, 0x3D, 0xDC, 0x69, 0xD8, 0x3C, 0xDF, 0xFD, 0x20, 0xD, 0xD8, 0x3D, 0xDE, 0x80, 0x0, 0x41, 0x0, 0x0]); yield return new ConverterTestCase(true, "DB99.DBx0.0", [0x01]); yield return new ConverterTestCase(false, "DB99.DBx0.0", [0x00]); @@ -69,15 +71,15 @@ internal abstract class ConverterTestBase /// This helper method exists, since I could not manage to invoke a generic method /// with a Span<T> parameter. /// - public static void WriteToBuffer(byte[] buffer, TValue value, VariableAddress address) => - ValueConverter.WriteToBuffer(buffer, value, address); + public static TValue ReadFromBuffer(byte[] buffer, VariableAddress address) => + ValueConverter.ReadFromBuffer(buffer, address); /// /// This helper method exists, since I could not manage to invoke a generic method /// with a Span<T> parameter. /// - public static TValue ReadFromBuffer(byte[] buffer, VariableAddress address) => - ValueConverter.ReadFromBuffer(buffer, address); + public static void WriteToBuffer(byte[] buffer, TValue value, VariableAddress address) => + ValueConverter.WriteToBuffer(buffer, value, address); public record ConverterTestCase(object Value, string Address, byte[] Data) { diff --git a/Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs b/Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs index 0d790f5..0e8e15d 100644 --- a/Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs +++ b/Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs @@ -25,6 +25,10 @@ internal class WriteToBuffer : ConverterTestBase { 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 + yield return new ConverterTestCase("aaaaBCDE", "DB0.DBB0.4", [0x61, 0x61, 0x61, 0x61]); // Length in address exceeds PLC array length + yield return new ConverterTestCase("\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "DB0.WString0.2", [0x00, 0x02, 0x00, 0x02, 0xD8, 0x3D, 0xDC, 0x69]); // Length in address exceeds PLC string length, multi char unicode point + yield return new ConverterTestCase("\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "DB0.String0.2", [0x02, 0x02, 0x3F, 0x3F]); // Length in address exceeds PLC string length, multi char unicode point + yield return new ConverterTestCase("\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "DB0.DBB0.4", [0x3F, 0x3F, 0x3F, 0x3F]); // Length in address exceeds PLC string length, multi char unicode point } [TestCase(18, "DB0.DInt12", 3)] diff --git a/Sharp7.Rx/ValueConverter.cs b/Sharp7.Rx/ValueConverter.cs index 42c65b5..97d4666 100644 --- a/Sharp7.Rx/ValueConverter.cs +++ b/Sharp7.Rx/ValueConverter.cs @@ -1,5 +1,4 @@ using System.Buffers.Binary; -using System.Runtime.InteropServices; using System.Text; using Sharp7.Rx.Enums; @@ -18,7 +17,7 @@ internal static class ValueConverter } }, - {typeof(byte), (data, address, value) => data[0] = (byte) value}, + {typeof(byte), (data, _, value) => data[0] = (byte) value}, { typeof(byte[]), (data, address, value) => { @@ -30,72 +29,58 @@ internal static class ValueConverter } }, - {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(short), (data, _, value) => BinaryPrimitives.WriteInt16BigEndian(data, (short) value)}, + {typeof(ushort), (data, _, value) => BinaryPrimitives.WriteUInt16BigEndian(data, (ushort) value)}, + {typeof(int), (data, _, value) => BinaryPrimitives.WriteInt32BigEndian(data, (int) value)}, + {typeof(uint), (data, _, value) => BinaryPrimitives.WriteUInt32BigEndian(data, (uint) value)}, + {typeof(long), (data, _, value) => BinaryPrimitives.WriteInt64BigEndian(data, (long) value)}, + {typeof(ulong), (data, _, 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(float), (data, _, value) => BinaryPrimitives.WriteSingleBigEndian(data, (float) value)}, + {typeof(double), (data, _, value) => BinaryPrimitives.WriteDoubleBigEndian(data, (double) value)}, { 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)); + EncodeString(data); 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)); + EncodeWString(data); return; case DbType.Byte: - // Todo: Serialize directly to Span, when upgrading to .net - Encoding.ASCII.GetBytes(stringValue) - .AsSpan(0, length) - .CopyTo(data); + Encoding.ASCII.GetBytes(stringValue.AsSpan(0, address.Length), data); return; default: throw new DataTypeMissmatchException($"Cannot write string to {address.Type}", typeof(string), address); } + + void EncodeString(Span span) + { + var encodedLength = Encoding.ASCII.GetByteCount(stringValue); + var length = Math.Min(address.Length, encodedLength); + + span[0] = (byte) address.Length; + span[1] = (byte) length; + + Encoding.ASCII.GetBytes(stringValue.AsSpan(0, length), span[2..]); + } + + void EncodeWString(Span span) + { + var length = Math.Min(address.Length, stringValue.Length); + + BinaryPrimitives.WriteUInt16BigEndian(span, address.Length); + BinaryPrimitives.WriteUInt16BigEndian(span[2..], (ushort) length); + + var readOnlySpan = stringValue.AsSpan(0, length); + Encoding.BigEndianUnicode.GetBytes(readOnlySpan, span[4..]); + } } } }; @@ -104,39 +89,17 @@ internal static class ValueConverter { {typeof(bool), (buffer, address) => (buffer[0] >> address.Bit & 1) > 0}, - {typeof(byte), (buffer, address) => buffer[0]}, - {typeof(byte[]), (buffer, address) => buffer.ToArray()}, + {typeof(byte), (buffer, _) => buffer[0]}, + {typeof(byte[]), (buffer, _) => 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(short), (buffer, _) => BinaryPrimitives.ReadInt16BigEndian(buffer)}, + {typeof(ushort), (buffer, _) => BinaryPrimitives.ReadUInt16BigEndian(buffer)}, + {typeof(int), (buffer, _) => BinaryPrimitives.ReadInt32BigEndian(buffer)}, + {typeof(uint), (buffer, _) => BinaryPrimitives.ReadUInt32BigEndian(buffer)}, + {typeof(long), (buffer, _) => BinaryPrimitives.ReadInt64BigEndian(buffer)}, + {typeof(ulong), (buffer, _) => BinaryPrimitives.ReadUInt64BigEndian(buffer)}, + {typeof(float), (buffer, _) => BinaryPrimitives.ReadSingleBigEndian(buffer)}, + {typeof(double), (buffer, _) => BinaryPrimitives.ReadDoubleBigEndian(buffer)}, { typeof(string), (buffer, address) => @@ -202,21 +165,7 @@ internal static class ValueConverter writeFunc(buffer, address, value); } - delegate object ReadFunc(Span data, VariableAddress address); + private delegate object ReadFunc(Span data, VariableAddress address); - [StructLayout(LayoutKind.Explicit)] - private struct UInt32SingleMap - { - [FieldOffset(0)] public uint UInt32; - [FieldOffset(0)] public float Single; - } - - [StructLayout(LayoutKind.Explicit)] - private struct UInt64DoubleMap - { - [FieldOffset(0)] public ulong UInt64; - [FieldOffset(0)] public double Double; - } - - delegate void WriteFunc(Span data, VariableAddress address, object value); + private delegate void WriteFunc(Span data, VariableAddress address, object value); } From 07009801171f3f8d45d8b0911bc540b470ac2acf Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 25 Apr 2024 13:47:24 +0200 Subject: [PATCH 67/78] Fix string to byte array encoding --- .../ValueConverterTests/WriteToBuffer.cs | 25 ++++++++++++++++--- Sharp7.Rx/ValueConverter.cs | 4 ++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs b/Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs index 0e8e15d..b316ae0 100644 --- a/Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs +++ b/Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs @@ -23,12 +23,31 @@ internal class WriteToBuffer : ConverterTestBase public static IEnumerable GetAdditinalWriteTestCases() { + yield return new ConverterTestCase("a", "DB0.Byte80.3", [0x61, 0x00, 0x00]); // short string + yield return new ConverterTestCase("abc", "DB0.Byte80.3", [0x61, 0x62, 0x63]); // matching string + yield return new ConverterTestCase("abcxx", "DB0.Byte80.3", [0x61, 0x62, 0x63]); // long string + + yield return new ConverterTestCase("a", "DB0.string0.3", [0x03, 0x01, 0x61, 0x00, 0x00]); // short string + yield return new ConverterTestCase("abc", "DB0.string0.3", [0x03, 0x03, 0x61, 0x62, 0x63]); // matching string + yield return new ConverterTestCase("abcxx", "DB0.string0.3", [0x03, 0x03, 0x61, 0x62, 0x63]); // long string + + yield return new ConverterTestCase("a", "DB0.wstring0.3", [0x00, 0x03, 0x00, 0x01, 0x00, 0x61, 0x00, 0x00, 0x00, 0x00]); // short string + yield return new ConverterTestCase("abc", "DB0.wstring0.3", [0x00, 0x03, 0x00, 0x03, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63]); // matching string + yield return new ConverterTestCase("abcxx", "DB0.wstring0.3", [0x00, 0x03, 0x00, 0x03, 0x00, 0x61, 0x00, 0x62, 0x00, 0x63]); // long string + + 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 yield return new ConverterTestCase("aaaaBCDE", "DB0.DBB0.4", [0x61, 0x61, 0x61, 0x61]); // Length in address exceeds PLC array length - yield return new ConverterTestCase("\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "DB0.WString0.2", [0x00, 0x02, 0x00, 0x02, 0xD8, 0x3D, 0xDC, 0x69]); // Length in address exceeds PLC string length, multi char unicode point - yield return new ConverterTestCase("\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "DB0.String0.2", [0x02, 0x02, 0x3F, 0x3F]); // Length in address exceeds PLC string length, multi char unicode point - yield return new ConverterTestCase("\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "DB0.DBB0.4", [0x3F, 0x3F, 0x3F, 0x3F]); // Length in address exceeds PLC string length, multi char unicode point + + // Length in address exceeds PLC string length, multi char unicode point + yield return new ConverterTestCase("\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "DB0.WString0.2", [0x00, 0x02, 0x00, 0x02, 0xD8, 0x3D, 0xDC, 0x69]); + + // Length in address exceeds PLC string length, multi char unicode point + yield return new ConverterTestCase("\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "DB0.String0.2", [0x02, 0x02, 0x3F, 0x3F]); + + // Length in address exceeds PLC string length, multi char unicode point + yield return new ConverterTestCase("\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "DB0.DBB0.4", [0x3F, 0x3F, 0x3F, 0x3F]); } [TestCase(18, "DB0.DInt12", 3)] diff --git a/Sharp7.Rx/ValueConverter.cs b/Sharp7.Rx/ValueConverter.cs index 97d4666..24f307b 100644 --- a/Sharp7.Rx/ValueConverter.cs +++ b/Sharp7.Rx/ValueConverter.cs @@ -54,7 +54,9 @@ internal static class ValueConverter EncodeWString(data); return; case DbType.Byte: - Encoding.ASCII.GetBytes(stringValue.AsSpan(0, address.Length), data); + + var readOnlySpan = stringValue.AsSpan(0, Math.Min(address.Length, stringValue.Length)); + Encoding.ASCII.GetBytes(readOnlySpan, data); return; default: throw new DataTypeMissmatchException($"Cannot write string to {address.Type}", typeof(string), address); From 99cf9cbc125be03fb915f9a54f320a84623963c3 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Thu, 25 Apr 2024 13:49:10 +0200 Subject: [PATCH 68/78] refactor --- Sharp7.Rx/ValueConverter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sharp7.Rx/ValueConverter.cs b/Sharp7.Rx/ValueConverter.cs index 24f307b..0bd2059 100644 --- a/Sharp7.Rx/ValueConverter.cs +++ b/Sharp7.Rx/ValueConverter.cs @@ -132,7 +132,8 @@ internal static class ValueConverter // https://support.industry.siemens.com/cs/mdm/109747174?c=94063855243&lc=de-DE // the length of the string is two bytes per character - var length = Math.Min(address.Length, BinaryPrimitives.ReadUInt16BigEndian(data.Slice(2, 2))) * 2; + var statedStringLength = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(2, 2)); + var length = Math.Min(address.Length, statedStringLength) * 2; return Encoding.BigEndianUnicode.GetString(data.Slice(4, length)); } From f5a51c074fa834e66a7ad8c31099b6428f055be5 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 26 Apr 2024 09:23:51 +0200 Subject: [PATCH 69/78] Add non-generic GetValue and improve documentation --- Sharp7.Rx/Extensions/S7VariableExtensions.cs | 19 ++ Sharp7.Rx/Interfaces/IPlc.cs | 7 +- Sharp7.Rx/Sharp7Plc.cs | 192 +++++++++++++++---- 3 files changed, 175 insertions(+), 43 deletions(-) diff --git a/Sharp7.Rx/Extensions/S7VariableExtensions.cs b/Sharp7.Rx/Extensions/S7VariableExtensions.cs index fa1c87f..f125bb6 100644 --- a/Sharp7.Rx/Extensions/S7VariableExtensions.cs +++ b/Sharp7.Rx/Extensions/S7VariableExtensions.cs @@ -22,4 +22,23 @@ internal static class VariableAddressExtensions public static bool MatchesType(this VariableAddress address, Type type) => supportedTypeMap.TryGetValue(type, out var map) && map(address); + + public static Type GetClrType(this VariableAddress address) => + address.Type switch + { + DbType.Bit => typeof(bool), + DbType.String => typeof(string), + DbType.WString => typeof(string), + DbType.Byte => address.Length == 1 ? typeof(byte) : typeof(byte[]), + DbType.Int => typeof(short), + DbType.UInt => typeof(ushort), + DbType.DInt => typeof(int), + DbType.UDInt => typeof(uint), + DbType.LInt => typeof(long), + DbType.ULInt => typeof(ulong), + DbType.Single => typeof(float), + DbType.Double => typeof(double), + _ => throw new ArgumentOutOfRangeException(nameof(address)) + }; + } diff --git a/Sharp7.Rx/Interfaces/IPlc.cs b/Sharp7.Rx/Interfaces/IPlc.cs index 296e830..31be636 100644 --- a/Sharp7.Rx/Interfaces/IPlc.cs +++ b/Sharp7.Rx/Interfaces/IPlc.cs @@ -8,8 +8,11 @@ namespace Sharp7.Rx.Interfaces; public interface IPlc : IDisposable { IObservable CreateNotification(string variableName, TransmissionMode transmissionMode); - Task SetValue(string variableName, TValue value); - Task GetValue(string variableName); + Task SetValue(string variableName, TValue value, CancellationToken token = default); + Task GetValue(string variableName, CancellationToken token = default); IObservable ConnectionState { get; } + + Task GetValue(string variableName, CancellationToken token = default); + ILogger Logger { get; } } diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index f250ad8..032cdbb 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -3,6 +3,8 @@ using System.Diagnostics; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; +using System.Reflection; using Microsoft.Extensions.Logging; using Sharp7.Rx.Basics; using Sharp7.Rx.Enums; @@ -14,6 +16,11 @@ namespace Sharp7.Rx; public class Sharp7Plc : IPlc { + private static readonly ArrayPool arrayPool = ArrayPool.Shared; + + private static readonly MethodInfo getValueMethod = typeof(Sharp7Plc).GetMethods() + .Single(m => m.Name == nameof(GetValue) && m.GetGenericArguments().Length == 1); + private readonly CompositeDisposable disposables = new(); private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase); private readonly List performanceCounter = new(1000); @@ -21,8 +28,6 @@ public class Sharp7Plc : IPlc private readonly CacheVariableNameParser variableNameParser = new CacheVariableNameParser(new VariableNameParser()); private bool disposed; private Sharp7Connector s7Connector; - private static readonly ArrayPool arrayPool = ArrayPool.Shared; - /// /// @@ -75,6 +80,14 @@ public class Sharp7Plc : IPlc GC.SuppressFinalize(this); } + /// + /// Create an Observable for a given variable. Multiple notifications are automatically combined into a multi-variable subscription to + /// reduce network trafic and PLC workload. + /// + /// + /// + /// + /// public IObservable CreateNotification(string variableName, TransmissionMode transmissionMode) { return Observable.Create(observer => @@ -104,19 +117,14 @@ public class Sharp7Plc : IPlc }); } - public Task GetValue(string variableName) - { - return GetValue(variableName, CancellationToken.None); - } - - - public Task SetValue(string variableName, TValue value) - { - return SetValue(variableName, value, CancellationToken.None); - } - - - public async Task GetValue(string variableName, CancellationToken token) + /// + /// Read PLC variable as generic variable. + /// + /// + /// + /// + /// + public async Task GetValue(string variableName, CancellationToken token = default) { var address = ParseAndVerify(variableName, typeof(TValue)); @@ -124,31 +132,38 @@ public class Sharp7Plc : IPlc return ValueConverter.ReadFromBuffer(data, address); } - public async Task InitializeAsync() + /// + /// Read PLC variable as object. + /// + /// + /// + /// + public async Task GetValue(string variableName, CancellationToken token = default) { - await s7Connector.InitializeAsync(); + var address = variableNameParser.Parse(variableName); + var clrType = address.GetClrType(); -#pragma warning disable 4014 - Task.Run(async () => - { - try - { - await s7Connector.Connect(); - } - catch (Exception e) - { - Logger?.LogError(e, "Error while connecting to PLC"); - } - }); -#pragma warning restore 4014 + var genericGetValue = getValueMethod!.MakeGenericMethod(clrType); - RunNotifications(s7Connector, MultiVarRequestCycleTime) - .AddDisposableTo(disposables); + var task = genericGetValue.Invoke(this, [variableName, token]) as Task; - return true; + await task!; + var taskType = typeof(Task<>).MakeGenericType(clrType); + var propertyInfo = taskType.GetProperty(nameof(Task.Result)); + var result = propertyInfo!.GetValue(task); + + return result; } - public async Task SetValue(string variableName, TValue value, CancellationToken token) + /// + /// Write value to the PLC. + /// + /// + /// + /// + /// + /// + public async Task SetValue(string variableName, TValue value, CancellationToken token = default) { var address = ParseAndVerify(variableName, typeof(TValue)); @@ -170,11 +185,105 @@ public class Sharp7Plc : IPlc } finally { - ArrayPool.Shared.Return(buffer); + ArrayPool.Shared.Return(buffer); } } } + /// + /// Trigger PLC connection and start notification loop. + /// + /// This method returns immediately and does not wait for the connection to be established. + /// + /// + /// Always true + [Obsolete("Use InitializeConnection.")] + public async Task InitializeAsync() + { + await s7Connector.InitializeAsync(); + +#pragma warning disable 4014 + Task.Run(async () => + { + try + { + await s7Connector.Connect(); + } + catch (Exception e) + { + Logger?.LogError(e, "Error while connecting to PLC"); + } + }); +#pragma warning restore 4014 + + RunNotifications(); + + return true; + } + + /// + /// Initialize PLC connection and wait for connection to be established. + /// + /// + /// + public async Task TriggerInitialize(CancellationToken token = default) + { + await s7Connector.InitializeAsync(); + + // Triger connection. + // The initial connection might fail. In this case a reconnect is initiated. + // So we ignore any errors and wait for ConnectionState Connected afterward. + _ = Task.Run(async () => + { + try + { + await s7Connector.Connect(); + } + catch (Exception e) + { + Logger?.LogError(e, "Error while connecting to PLC"); + } + }, token); + + await s7Connector.ConnectionState + .FirstAsync(c => c == Enums.ConnectionState.Connected) + .ToTask(token); + + RunNotifications(); + } + + + /// + /// Initialize PLC connection and wait for connection to be established. + /// + /// + /// + public async Task InitializeConnection(CancellationToken token = default) + { + await s7Connector.InitializeAsync(); + + // Triger connection. + // The initial connection might fail. In this case a reconnect is initiated. + // So we ignore any errors and wait for ConnectionState Connected afterward. + _ = Task.Run(async () => + { + try + { + await s7Connector.Connect(); + } + catch (Exception e) + { + Logger?.LogError(e, "Error while connecting to PLC"); + } + }, token); + + await s7Connector.ConnectionState + .FirstAsync(c => c == Enums.ConnectionState.Connected) + .ToTask(token); + + RunNotifications(); + } + protected virtual void Dispose(bool disposing) { if (disposed) return; @@ -240,21 +349,22 @@ public class Sharp7Plc : IPlc Logger?.LogTrace("PLC {Plc} notification perf: {Elements} calls, min {Min}, max {Max}, avg {Avg}, variables {Vars}, batch size {BatchSize}", plcConnectionSettings.IpAddress, - performanceCounter.Capacity, min, max, average, + performanceCounter.Capacity, min, max, average, multiVariableSubscriptions.ExistingKeys.Count(), MultiVarRequestMaxItems); performanceCounter.Clear(); } } - private IDisposable RunNotifications(IS7Connector connector, TimeSpan cycle) + private void RunNotifications() { - return ConnectionState.FirstAsync() + ConnectionState.FirstAsync() .Select(states => states == Enums.ConnectionState.Connected) - .SelectMany(connected => GetAllValues(connected, connector)) + .SelectMany(connected => GetAllValues(connected, s7Connector)) .RepeatAfterDelay(MultiVarRequestCycleTime) - .LogAndRetryAfterDelay(Logger, cycle, "Error while getting batch notifications from plc") - .Subscribe(); + .LogAndRetryAfterDelay(Logger, MultiVarRequestCycleTime, "Error while getting batch notifications from plc") + .Subscribe() + .AddDisposableTo(disposables); } ~Sharp7Plc() From e52c81683b75e9f718dce028abb14086b36dd4fd Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 26 Apr 2024 09:59:43 +0200 Subject: [PATCH 70/78] Improve robustness of connection --- Sharp7.Rx/Sharp7Connector.cs | 2 +- Sharp7.Rx/Sharp7Plc.cs | 145 ++++++++++++++--------------------- 2 files changed, 59 insertions(+), 88 deletions(-) diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index d04af97..995f4a9 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -213,7 +213,7 @@ internal class Sharp7Connector : IS7Connector private void EnsureConnectionValid() { if (disposed) - throw new ObjectDisposedException("S7Connector"); + throw new ObjectDisposedException(nameof(Sharp7Connector)); if (sharp7 == null) throw new InvalidOperationException("S7 driver is not initialized."); diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 032cdbb..6cde0b2 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -21,12 +21,13 @@ public class Sharp7Plc : IPlc private static readonly MethodInfo getValueMethod = typeof(Sharp7Plc).GetMethods() .Single(m => m.Name == nameof(GetValue) && m.GetGenericArguments().Length == 1); - private readonly CompositeDisposable disposables = new(); + private IDisposable notificationSubscription; private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase); private readonly List performanceCounter = new(1000); private readonly PlcConnectionSettings plcConnectionSettings; private readonly CacheVariableNameParser variableNameParser = new CacheVariableNameParser(new VariableNameParser()); private bool disposed; + private int initialized; private Sharp7Connector s7Connector; /// @@ -197,92 +198,27 @@ public class Sharp7Plc : IPlc /// /// /// Always true - [Obsolete("Use InitializeConnection.")] + [Obsolete($"Use {nameof(InitializeConnection)} or {nameof(TriggerConnection)}.")] public async Task InitializeAsync() { - await s7Connector.InitializeAsync(); - -#pragma warning disable 4014 - Task.Run(async () => - { - try - { - await s7Connector.Connect(); - } - catch (Exception e) - { - Logger?.LogError(e, "Error while connecting to PLC"); - } - }); -#pragma warning restore 4014 - - RunNotifications(); - + await TriggerConnection(); return true; } - /// - /// Initialize PLC connection and wait for connection to be established. - /// - /// - /// - public async Task TriggerInitialize(CancellationToken token = default) - { - await s7Connector.InitializeAsync(); - - // Triger connection. - // The initial connection might fail. In this case a reconnect is initiated. - // So we ignore any errors and wait for ConnectionState Connected afterward. - _ = Task.Run(async () => - { - try - { - await s7Connector.Connect(); - } - catch (Exception e) - { - Logger?.LogError(e, "Error while connecting to PLC"); - } - }, token); - - await s7Connector.ConnectionState - .FirstAsync(c => c == Enums.ConnectionState.Connected) - .ToTask(token); - - RunNotifications(); - } - /// /// Initialize PLC connection and wait for connection to be established. /// /// /// - public async Task InitializeConnection(CancellationToken token = default) - { - await s7Connector.InitializeAsync(); + public async Task InitializeConnection(CancellationToken token = default) => await DoInitializeConnection(true, token); - // Triger connection. - // The initial connection might fail. In this case a reconnect is initiated. - // So we ignore any errors and wait for ConnectionState Connected afterward. - _ = Task.Run(async () => - { - try - { - await s7Connector.Connect(); - } - catch (Exception e) - { - Logger?.LogError(e, "Error while connecting to PLC"); - } - }, token); - - await s7Connector.ConnectionState - .FirstAsync(c => c == Enums.ConnectionState.Connected) - .ToTask(token); - - RunNotifications(); - } + /// + /// Initialize PLC and trigger connection. This method will not wait for the connection to be established. + /// + /// + /// + public async Task TriggerConnection(CancellationToken token = default) => await DoInitializeConnection(false, token); protected virtual void Dispose(bool disposing) { @@ -291,7 +227,8 @@ public class Sharp7Plc : IPlc if (disposing) { - disposables.Dispose(); + notificationSubscription?.Dispose(); + notificationSubscription = null; if (s7Connector != null) { @@ -304,11 +241,37 @@ public class Sharp7Plc : IPlc } } - private async Task GetAllValues(bool connected, IS7Connector connector) + private async Task DoInitializeConnection(bool waitForConnection, CancellationToken token) { - if (!connected) - return Unit.Default; + if (Interlocked.Exchange(ref initialized, 1) == 1) return; + await s7Connector.InitializeAsync(); + + // Triger connection. + // The initial connection might fail. In this case a reconnect is initiated. + // So we ignore any errors and wait for ConnectionState Connected afterward. + _ = Task.Run(async () => + { + try + { + await s7Connector.Connect(); + } + catch (Exception e) + { + Logger?.LogError(e, "Intiial PLC connection failed."); + } + }, token); + + if (waitForConnection) + await s7Connector.ConnectionState + .FirstAsync(c => c == Enums.ConnectionState.Connected) + .ToTask(token); + + StartNotificationLoop(); + } + + private async Task GetAllValues(IS7Connector connector) + { if (multiVariableSubscriptions.ExistingKeys.IsEmpty()) return Unit.Default; @@ -356,15 +319,23 @@ public class Sharp7Plc : IPlc } } - private void RunNotifications() + private void StartNotificationLoop() { - ConnectionState.FirstAsync() - .Select(states => states == Enums.ConnectionState.Connected) - .SelectMany(connected => GetAllValues(connected, s7Connector)) - .RepeatAfterDelay(MultiVarRequestCycleTime) - .LogAndRetryAfterDelay(Logger, MultiVarRequestCycleTime, "Error while getting batch notifications from plc") - .Subscribe() - .AddDisposableTo(disposables); + if (notificationSubscription != null) + // notification loop already running + return; + + var subscription = + ConnectionState + .FirstAsync(states => states == Enums.ConnectionState.Connected) + .SelectMany(_ => GetAllValues(s7Connector)) + .RepeatAfterDelay(MultiVarRequestCycleTime) + .LogAndRetryAfterDelay(Logger, MultiVarRequestCycleTime, "Error while getting batch notifications from plc") + .Subscribe(); + + if (Interlocked.CompareExchange(ref notificationSubscription, subscription, null) != null) + // Subscription has already been created (race condition). Dispose new subscription. + subscription.Dispose(); } ~Sharp7Plc() From 6917295cf129510b0a6181537508dfb80ea0dc6a Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 26 Apr 2024 10:01:58 +0200 Subject: [PATCH 71/78] Update LinqPad samples --- Sharp7.Rx/linqpad-samples/Create Notification.linq | 5 +---- Sharp7.Rx/linqpad-samples/Establish connection.linq | 2 +- Sharp7.Rx/linqpad-samples/Multiple notifications.linq | 5 +---- Sharp7.Rx/linqpad-samples/Write and read value.linq | 5 +---- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/Sharp7.Rx/linqpad-samples/Create Notification.linq b/Sharp7.Rx/linqpad-samples/Create Notification.linq index 57d9720..9357004 100644 --- a/Sharp7.Rx/linqpad-samples/Create Notification.linq +++ b/Sharp7.Rx/linqpad-samples/Create Notification.linq @@ -16,10 +16,7 @@ var cpuMpiAddress = 0; using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress); -await plc.InitializeAsync(); -await plc.ConnectionState - .FirstAsync(c => c == Sharp7.Rx.Enums.ConnectionState.Connected) - .ToTask(); +await plc.InitializeConnection(); "Connection established".Dump(); diff --git a/Sharp7.Rx/linqpad-samples/Establish connection.linq b/Sharp7.Rx/linqpad-samples/Establish connection.linq index 33f6f69..8b96b56 100644 --- a/Sharp7.Rx/linqpad-samples/Establish connection.linq +++ b/Sharp7.Rx/linqpad-samples/Establish connection.linq @@ -18,7 +18,7 @@ var cpuMpiAddress = 0; using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress); // Initialize connection -await plc.InitializeAsync(); +await plc.InitializeConnection(); // wait for connection to be established await plc.ConnectionState diff --git a/Sharp7.Rx/linqpad-samples/Multiple notifications.linq b/Sharp7.Rx/linqpad-samples/Multiple notifications.linq index ec8db01..6078d74 100644 --- a/Sharp7.Rx/linqpad-samples/Multiple notifications.linq +++ b/Sharp7.Rx/linqpad-samples/Multiple notifications.linq @@ -18,10 +18,7 @@ using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress); plc.ConnectionState.Dump(); -await plc.InitializeAsync(); -await plc.ConnectionState - .FirstAsync(c => c == Sharp7.Rx.Enums.ConnectionState.Connected) - .ToTask(); +await plc.InitializeConnection(); // create an IObservable plc.CreateNotification($"DB{db}.Int6", Sharp7.Rx.Enums.TransmissionMode.OnChange).Dump("Int 6"); diff --git a/Sharp7.Rx/linqpad-samples/Write and read value.linq b/Sharp7.Rx/linqpad-samples/Write and read value.linq index ee5780e..302bb3a 100644 --- a/Sharp7.Rx/linqpad-samples/Write and read value.linq +++ b/Sharp7.Rx/linqpad-samples/Write and read value.linq @@ -16,10 +16,7 @@ var cpuMpiAddress = 0; using var plc = new Sharp7Plc(ip, rackNumber, cpuMpiAddress); -await plc.InitializeAsync(); -await plc.ConnectionState - .FirstAsync(c => c == Sharp7.Rx.Enums.ConnectionState.Connected) - .ToTask(); +await plc.InitializeConnection(); "Connection established".Dump(); From 57ff276489695bea16f245b71bff3453b1e0b3c0 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 26 Apr 2024 10:45:14 +0200 Subject: [PATCH 72/78] Add non-generic CreateNotification method --- Sharp7.Rx/Interfaces/IPlc.cs | 10 ++++----- Sharp7.Rx/Sharp7.Rx.csproj | 2 +- Sharp7.Rx/Sharp7Plc.cs | 29 +++++++++++++++++++++++++-- Sharp7.Rx/Utils/SignatureConverter.cs | 21 +++++++++++++++++++ 4 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 Sharp7.Rx/Utils/SignatureConverter.cs diff --git a/Sharp7.Rx/Interfaces/IPlc.cs b/Sharp7.Rx/Interfaces/IPlc.cs index 31be636..b2c4aca 100644 --- a/Sharp7.Rx/Interfaces/IPlc.cs +++ b/Sharp7.Rx/Interfaces/IPlc.cs @@ -1,5 +1,4 @@ using JetBrains.Annotations; -using Microsoft.Extensions.Logging; using Sharp7.Rx.Enums; namespace Sharp7.Rx.Interfaces; @@ -7,12 +6,13 @@ namespace Sharp7.Rx.Interfaces; [NoReorder] public interface IPlc : IDisposable { - IObservable CreateNotification(string variableName, TransmissionMode transmissionMode); - Task SetValue(string variableName, TValue value, CancellationToken token = default); - Task GetValue(string variableName, CancellationToken token = default); IObservable ConnectionState { get; } + Task SetValue(string variableName, TValue value, CancellationToken token = default); + + Task GetValue(string variableName, CancellationToken token = default); Task GetValue(string variableName, CancellationToken token = default); - ILogger Logger { get; } + IObservable CreateNotification(string variableName, TransmissionMode transmissionMode); + IObservable CreateNotification(string variableName, TransmissionMode transmissionMode); } diff --git a/Sharp7.Rx/Sharp7.Rx.csproj b/Sharp7.Rx/Sharp7.Rx.csproj index 210e8d5..3067b4e 100644 --- a/Sharp7.Rx/Sharp7.Rx.csproj +++ b/Sharp7.Rx/Sharp7.Rx.csproj @@ -36,7 +36,7 @@ - + diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 6cde0b2..3f0b839 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -11,6 +11,7 @@ using Sharp7.Rx.Enums; using Sharp7.Rx.Extensions; using Sharp7.Rx.Interfaces; using Sharp7.Rx.Settings; +using Sharp7.Rx.Utils; namespace Sharp7.Rx; @@ -21,13 +22,17 @@ public class Sharp7Plc : IPlc private static readonly MethodInfo getValueMethod = typeof(Sharp7Plc).GetMethods() .Single(m => m.Name == nameof(GetValue) && m.GetGenericArguments().Length == 1); - private IDisposable notificationSubscription; + private static readonly MethodInfo createNotificationMethod = typeof(Sharp7Plc).GetMethods() + .Single(m => m.Name == nameof(CreateNotification) && m.GetGenericArguments().Length == 1); + private readonly ConcurrentSubjectDictionary multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase); private readonly List performanceCounter = new(1000); private readonly PlcConnectionSettings plcConnectionSettings; private readonly CacheVariableNameParser variableNameParser = new CacheVariableNameParser(new VariableNameParser()); private bool disposed; private int initialized; + + private IDisposable notificationSubscription; private Sharp7Connector s7Connector; /// @@ -135,10 +140,11 @@ public class Sharp7Plc : IPlc /// /// Read PLC variable as object. + /// The return type is automatically infered from the variable name. /// /// /// - /// + /// The actual return type is infered from the variable name. public async Task GetValue(string variableName, CancellationToken token = default) { var address = variableNameParser.Parse(variableName); @@ -191,6 +197,25 @@ public class Sharp7Plc : IPlc } } + /// + /// Creates an observable of object for a variable. + /// The return type is automatically infered from the variable name. + /// + /// + /// + /// The return type is infered from the variable name. + public IObservable CreateNotification(string variableName, TransmissionMode transmissionMode) + { + var address = variableNameParser.Parse(variableName); + var clrType = address.GetClrType(); + + var genericCreateNotification = createNotificationMethod!.MakeGenericMethod(clrType); + + var genericNotification = genericCreateNotification.Invoke(this, [variableName, transmissionMode]); + + return SignatureConverter.ConvertToObjectObservable(genericNotification, clrType); + } + /// /// Trigger PLC connection and start notification loop. /// diff --git a/Sharp7.Rx/Utils/SignatureConverter.cs b/Sharp7.Rx/Utils/SignatureConverter.cs new file mode 100644 index 0000000..64f4bf0 --- /dev/null +++ b/Sharp7.Rx/Utils/SignatureConverter.cs @@ -0,0 +1,21 @@ +using System.Reactive.Linq; +using System.Reflection; + +namespace Sharp7.Rx.Utils; + +internal static class SignatureConverter +{ + private static readonly MethodInfo convertToObjectObservableMethod = + typeof(SignatureConverter) + .GetMethods(BindingFlags.Public | BindingFlags.Static) + .Single(m => m.Name == nameof(ConvertToObjectObservable) && m.GetGenericArguments().Length == 1); + + public static IObservable ConvertToObjectObservable(IObservable obs) => obs.Select(o => (object) o); + + public static IObservable ConvertToObjectObservable(object observable, Type sourceType) + { + var convertGeneric = convertToObjectObservableMethod.MakeGenericMethod(sourceType); + + return convertGeneric.Invoke(null, [observable]) as IObservable; + } +} From c3ccdad31cb34d9d9f14c6f8046d2f16a7b3d09b Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 26 Apr 2024 15:19:58 +0200 Subject: [PATCH 73/78] Remove IS7Connector interface --- Sharp7.Rx/Interfaces/IS7Connector.cs | 21 --------------------- Sharp7.Rx/Sharp7Connector.cs | 2 +- 2 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 Sharp7.Rx/Interfaces/IS7Connector.cs diff --git a/Sharp7.Rx/Interfaces/IS7Connector.cs b/Sharp7.Rx/Interfaces/IS7Connector.cs deleted file mode 100644 index 66566ac..0000000 --- a/Sharp7.Rx/Interfaces/IS7Connector.cs +++ /dev/null @@ -1,21 +0,0 @@ -using JetBrains.Annotations; -using Sharp7.Rx.Enums; - -namespace Sharp7.Rx.Interfaces; - -[NoReorder] -internal interface IS7Connector : IDisposable -{ - IObservable ConnectionState { get; } - Task InitializeAsync(); - - Task Connect(); - Task Disconnect(); - - Task ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, 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> ExecuteMultiVarRequest(IReadOnlyList variableNames); -} diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 995f4a9..83d9bac 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -10,7 +10,7 @@ using Sharp7.Rx.Settings; namespace Sharp7.Rx; -internal class Sharp7Connector : IS7Connector +internal class Sharp7Connector: IDisposable { private readonly BehaviorSubject connectionStateSubject = new(Enums.ConnectionState.Initial); private readonly int cpuSlotNr; From e2a278940e0b875f19ce21afb71e28bbfc5d857d Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 26 Apr 2024 15:20:51 +0200 Subject: [PATCH 74/78] Fix bug with writing too many bytes --- Sharp7.Rx/Sharp7Connector.cs | 4 ++-- Sharp7.Rx/Sharp7Plc.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sharp7.Rx/Sharp7Connector.cs b/Sharp7.Rx/Sharp7Connector.cs index 83d9bac..440fb09 100644 --- a/Sharp7.Rx/Sharp7Connector.cs +++ b/Sharp7.Rx/Sharp7Connector.cs @@ -168,11 +168,11 @@ internal class Sharp7Connector: IDisposable EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress} bit {bitAdress}"); } - 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, ushort bytesToWrite, CancellationToken token) { EnsureConnectionValid(); - 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, bytesToWrite, S7WordLength.Byte, data), token, TaskCreationOptions.None, scheduler); token.ThrowIfCancellationRequested(); EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress}.{data.Length}"); diff --git a/Sharp7.Rx/Sharp7Plc.cs b/Sharp7.Rx/Sharp7Plc.cs index 3f0b839..bc98040 100644 --- a/Sharp7.Rx/Sharp7Plc.cs +++ b/Sharp7.Rx/Sharp7Plc.cs @@ -188,7 +188,7 @@ public class Sharp7Plc : IPlc { ValueConverter.WriteToBuffer(buffer, value, address); - await s7Connector.WriteBytes(address.Operand, address.Start, buffer, address.DbNo, token); + await s7Connector.WriteBytes(address.Operand, address.Start, buffer, address.DbNo, address.BufferLength, token); } finally { @@ -295,7 +295,7 @@ public class Sharp7Plc : IPlc StartNotificationLoop(); } - private async Task GetAllValues(IS7Connector connector) + private async Task GetAllValues(Sharp7Connector connector) { if (multiVariableSubscriptions.ExistingKeys.IsEmpty()) return Unit.Default; From fa59a6731d263f4d9ea748d58cc35f761d1dc404 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 26 Apr 2024 15:21:06 +0200 Subject: [PATCH 75/78] Adapt example in Readme --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e1da309..dba0a3e 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,8 @@ using (var disposables = new CompositeDisposable()) var plc = new Sharp7Plc("10.30.3.10", 0, 2); disposables.Add(plc); - // initialize the plc - await plc.InitializeAsync(); - - //wait for the plc to get connected - await plc.ConnectionState - .FirstAsync(c => c == Sharp7.Rx.Enums.ConnectionState.Connected) - .ToTask(); + // initialize and connect to the plc + await plc.InitializeConnection(); await plc.SetValue("DB2.DBX0.4", true); // set a bit var bit = await plc.GetValue("DB2.int4"); // get a bit From bc14f2934b8d6efa4fc9cb96d52f0297a5fa72ab Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 26 Apr 2024 15:45:27 +0200 Subject: [PATCH 76/78] Unify build process --- .github/workflows/prerelease.yml | 30 ------------------------------ .github/workflows/release.yml | 31 +++++++++++++------------------ Sharp7.Rx.sln | 21 ++++++++++++++++++--- 3 files changed, 31 insertions(+), 51 deletions(-) delete mode 100644 .github/workflows/prerelease.yml diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml deleted file mode 100644 index 848b7f5..0000000 --- a/.github/workflows/prerelease.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Prerelease - -on: - push: - branches: - - prerelease - pull_request: - branches: [ prerelease ] - -jobs: - build: - - runs-on: windows-latest - - steps: - - uses: actions/checkout@v2 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 8.0.x - - name: Install NUnit.ConsoleRunner - run: nuget install NUnit.ConsoleRunner -Version 3.17.0 -DirectDownload -OutputDirectory . - - name: Install dependencies - run: dotnet restore - - name: Build - run: dotnet build --configuration Release --no-restore /p:version=2.0.${{ github.run_number }}-prerelease - - name: Tests - run: ./NUnit.ConsoleRunner.3.17.0/tools/nunit3-console.exe "Sharp7.Rx.Tests\bin\Release\net8.0\Sharp7.Rx.Tests.dll" - - name: NugetPublish - run: dotnet nuget push Sharp7.Rx\bin\Release\Sharp7.Rx.2.0.${{ github.run_number }}-prerelease.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_DEPLOY_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1b8025..2685dcb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,42 +1,37 @@ -name: Release +name: Release on: push: branches: - master + - prerelease pull_request: branches: [ master ] + jobs: build: runs-on: windows-latest + env: + version: 2.0.${{ github.run_number }}${{ github.ref == 'refs/heads/master' && '' || 'prerelease' }} steps: - uses: actions/checkout@v2 - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 8.0.x - name: Install NUnit.ConsoleRunner - run: nuget install NUnit.ConsoleRunner -Version 3.13.0 -DirectDownload -OutputDirectory . + run: nuget install NUnit.ConsoleRunner -Version 3.17.0 -DirectDownload -OutputDirectory . + - name: Install dependencies + run: dotnet restore - name: Install dependencies run: dotnet restore - name: Build - run: dotnet build --configuration Release --no-restore /p:version=1.1.${{ github.run_number }} + run: dotnet build --configuration Release --no-restore /p:version=${{ env.version }} - name: Tests - run: ./NUnit.ConsoleRunner.3.13.0/tools/nunit3-console.exe "Sharp7.Rx.Tests\bin\Release\net461\Sharp7.Rx.Tests.dll" + run: ./NUnit.ConsoleRunner.3.17.0/tools/nunit3-console.exe "Sharp7.Rx.Tests\bin\Release\net8.0\Sharp7.Rx.Tests.dll" - name: NugetPublish - run: dotnet nuget push Sharp7.Rx\bin\Release\Sharp7.Rx.1.1.${{ github.run_number }}.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_DEPLOY_KEY }} - if: github.event_name != 'pull_request' - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: 1.1.${{ github.run_number }} - release_name: 1.1.${{ github.run_number }} - draft: false - prerelease: true - if: github.event_name != 'pull_request' \ No newline at end of file + if: github.event_name == 'push' + run: dotnet nuget push Sharp7.Rx\bin\Release\Sharp7.Rx.2.0.${{ env.version }}.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_DEPLOY_KEY }} diff --git a/Sharp7.Rx.sln b/Sharp7.Rx.sln index 033c14e..e211d65 100644 --- a/Sharp7.Rx.sln +++ b/Sharp7.Rx.sln @@ -1,11 +1,23 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28010.2041 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sharp7.Rx", "Sharp7.Rx\Sharp7.Rx.csproj", "{690A7E0E-BE95-49AC-AF2F-7FEA2F63204A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sharp7.Rx.Tests", "Sharp7.Rx.Tests\Sharp7.Rx.Tests.csproj", "{1BDD07D2-6540-4ACF-81E7-98300421073B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sharp7.Rx.Tests", "Sharp7.Rx.Tests\Sharp7.Rx.Tests.csproj", "{1BDD07D2-6540-4ACF-81E7-98300421073B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{3A9DEBA7-8F53-4554-869C-7C99F0A4932E}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflow", "Workflow", "{1CFDA2EA-49CF-4B96-A9C9-B12B21B3D78E}" + ProjectSection(SolutionItems) = preProject + .github\workflows\release.yml = .github\workflows\release.yml + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -25,6 +37,9 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1CFDA2EA-49CF-4B96-A9C9-B12B21B3D78E} = {3A9DEBA7-8F53-4554-869C-7C99F0A4932E} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {ABA1FD47-15EE-4588-9BA7-0116C635BFC4} EndGlobalSection From bebbea899d7f12ff85d3a150cdb70963d1148339 Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Fri, 26 Apr 2024 15:50:20 +0200 Subject: [PATCH 77/78] fix prerelease version string --- .github/workflows/release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2685dcb..3b7efd5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: runs-on: windows-latest env: - version: 2.0.${{ github.run_number }}${{ github.ref == 'refs/heads/master' && '' || 'prerelease' }} + version: 2.0.${{ github.run_number }}${{ github.ref == 'refs/heads/master' && '' || '-prerelease' }} steps: - uses: actions/checkout@v2 @@ -34,4 +34,5 @@ jobs: run: ./NUnit.ConsoleRunner.3.17.0/tools/nunit3-console.exe "Sharp7.Rx.Tests\bin\Release\net8.0\Sharp7.Rx.Tests.dll" - name: NugetPublish if: github.event_name == 'push' - run: dotnet nuget push Sharp7.Rx\bin\Release\Sharp7.Rx.2.0.${{ env.version }}.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_DEPLOY_KEY }} + run: dotnet nuget push Sharp7.Rx\bin\Release\Sharp7.Rx.${{ env.version }}.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_DEPLOY_KEY }} + From bdfac4bd221fdf5448a90d0fc21fd0e6d2f4019d Mon Sep 17 00:00:00 2001 From: Peter Butzhammer Date: Tue, 7 May 2024 08:40:51 +0200 Subject: [PATCH 78/78] Extend readme --- README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dba0a3e..49b2256 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ It combines the S7 communication library with the power of System.Reactive. - Threadsafe (Sharp7 is basically not threadsafe) ## Quick start + +Install the [Sharp7.Rx Nuget package](https://www.nuget.org/packages/Sharp7.Rx). + The example below shows you how to create and use the Sharp7Rx PLC. ```csharp @@ -28,10 +31,11 @@ using (var disposables = new CompositeDisposable()) await plc.InitializeConnection(); await plc.SetValue("DB2.DBX0.4", true); // set a bit - var bit = await plc.GetValue("DB2.int4"); // get a bit - + var value = await plc.GetValue("DB2.Int4"); // get an 16 bit integer + Console.WriteLine(value) + // create a nofication for data change in the plc - var notification = plc.CreateNotification("DB1.DBX0.2", TransmissionMode.OnChange, TimeSpan.FromMilliseconds(100)) + var notification = plc.CreateNotification("DB1.DBX0.2", TransmissionMode.OnChange) .Where(b => b) //select rising edge .Do(_ => doStuff()) .Subscribe(); @@ -43,7 +47,13 @@ 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. + +## Examples + +This library comes with integrated [LinqPad](https://www.linqpad.net/) examples - even for the free edition. Just download the [Sharp7.Rx Nuget package](https://www.nuget.org/packages/Sharp7.Rx) after pressing `Ctrl + Shift + P` and browse the "Samples". + +[Sharp7Monitor](https://github.com/Peter-B-/Sharp7.Monitor) is a console application for monitoring S7 variables over RFC1006, based on this library. ## Addressing rules @@ -91,7 +101,6 @@ public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress, int port = The default value for `multiVarRequestCycleTime` is 100 ms, the minimal value is 5 ms. - ## Would you like to contribute? Yes, please!