91 Commits

Author SHA1 Message Date
Peter Butzhammer
8836c14e2b Update nuget packages
Some checks failed
Release / build (push) Has been cancelled
2024-07-22 14:21:15 +02:00
Peter Butzhammer
e912d088eb Include xml doc in package 2024-07-22 14:20:32 +02:00
Peter Butzhammer
2655d1ec41 Remove duplicate build step 2024-07-22 14:18:21 +02:00
Peter Butzhammer
f90ad5f9be Improve error message 2024-07-22 14:18:03 +02:00
Peter Butzhammer
636c56d252 Use correct ArrayPool reference 2024-07-22 14:17:43 +02:00
Peter Butzhammer
f1734ebd73 Extend list of possible errors 2024-05-13 10:02:01 +02:00
Peter Butzhammer
0c1d5067c2 Remove unused enum 2024-05-13 10:01:44 +02:00
Peter Butzhammer
18659454f5 Fix badge 2024-05-07 09:19:24 +02:00
Peter Butzhammer
921360757f Try to fix build by swapping truthy value 2024-05-07 09:17:35 +02:00
Peter Butzhammer
d9f1ad62b4 Merge pull request #5 from evopro-ag/prerelease
2.0 Release
2024-05-07 08:48:46 +02:00
Peter Butzhammer
bdfac4bd22 Extend readme 2024-05-07 08:40:51 +02:00
Peter Butzhammer
bebbea899d fix prerelease version string 2024-04-26 15:56:15 +02:00
Peter Butzhammer
bc14f2934b Unify build process 2024-04-26 15:47:22 +02:00
Peter Butzhammer
fa59a6731d Adapt example in Readme 2024-04-26 15:21:06 +02:00
Peter Butzhammer
e2a278940e Fix bug with writing too many bytes 2024-04-26 15:20:51 +02:00
Peter Butzhammer
c3ccdad31c Remove IS7Connector interface 2024-04-26 15:19:58 +02:00
Peter Butzhammer
57ff276489 Add non-generic CreateNotification method 2024-04-26 10:45:14 +02:00
Peter Butzhammer
6917295cf1 Update LinqPad samples 2024-04-26 10:01:58 +02:00
Peter Butzhammer
e52c81683b Improve robustness of connection 2024-04-26 09:59:43 +02:00
Peter Butzhammer
f5a51c074f Add non-generic GetValue and improve documentation 2024-04-26 09:23:51 +02:00
Peter Butzhammer
99cf9cbc12 refactor 2024-04-25 13:49:10 +02:00
Peter Butzhammer
0700980117 Fix string to byte array encoding 2024-04-25 13:47:24 +02:00
Peter Butzhammer
08f572b582 Encode without array allocation 2024-04-25 13:33:05 +02:00
Peter Butzhammer
b400a7215a Switch ReadFromBuffer to span 2024-04-25 12:16:54 +02:00
Peter Butzhammer
c5a6b12843 Use ArrayPool as a write buffer 2024-04-25 10:24:14 +02:00
Peter Butzhammer
478a2060ba Fix or ignore build warnings in tests 2024-04-25 10:23:51 +02:00
Peter Butzhammer
87d69fb614 Fix or ignore build warnings 2024-04-25 10:19:17 +02:00
Peter Butzhammer
1672ca906d Update to .net 6 2024-04-25 10:04:56 +02:00
Peter Butzhammer
1e52327a3d Fix some typos 2024-02-09 15:05:40 +01:00
Peter Butzhammer
c001ac1ea7 Document multi variable reads 2024-02-09 14:34:16 +01:00
Peter Butzhammer
332329dc68 Explicitly state license 2024-02-09 14:34:06 +01:00
Peter Butzhammer
3338f5566e Add SourceLink 2024-02-09 14:21:09 +01:00
Peter Butzhammer
2a5dec3075 Merge branch 'feature/performanceImprovements' into prerelease 2024-02-09 14:13:00 +01:00
Peter Butzhammer
c5edb4c695 Add linqpad-samples package tag 2024-02-09 14:11:40 +01:00
Peter Butzhammer
096435f4d1 Add linqpad samples 2024-02-09 14:09:34 +01:00
Peter Butzhammer
b6966cf526 Fix buffer length for string notifications 2024-02-09 13:41:59 +01:00
Peter Butzhammer
9a51a407ec Merge branch 'feature/performanceImprovements' into prerelease 2024-02-09 13:17:19 +01:00
Peter Butzhammer
9a1d0f70f0 Adapt readme 2024-02-09 13:17:10 +01:00
Peter Butzhammer
56b300b3ab Convert VariableAddress to record 2024-02-09 12:46:45 +01:00
Peter Butzhammer
996706df2f Move GetAdditionalErrorText to S7ErrorCodes 2024-02-09 12:38:16 +01:00
Peter Butzhammer
4701a224a7 Remove "S7" from some class names 2024-02-09 12:33:24 +01:00
Peter Butzhammer
dd0af70262 Fix multithreadign issue with lingering subscriptions 2024-02-09 11:31:08 +01:00
Peter Butzhammer
280a894b1f Add additional hints to errors 2024-02-09 11:18:48 +01:00
Peter Butzhammer
bfc9c93c80 improve error messages 2024-02-09 10:26:54 +01:00
Peter Butzhammer
f3a92addaa Fix buffer length 2024-02-09 10:26:23 +01:00
Peter Butzhammer
25bcfea835 Add connectionState.Disposed 2024-02-09 09:58:16 +01:00
Peter Butzhammer
527d5a094c Merge branch 'feature/performanceImprovements' into prerelease 2024-02-08 18:32:35 +01:00
Peter Butzhammer
32a7d7cd24 Remove unused localization 2024-02-08 18:31:23 +01:00
Peter Butzhammer
b43a595e13 Return S7CommunicationException 2024-02-08 18:30:49 +01:00
Peter Butzhammer
5d85823167 Verify data types 2024-02-08 18:10:23 +01:00
Peter Butzhammer
829dee14af Implement MatchesType 2024-02-08 17:54:15 +01:00
Peter Butzhammer
fdc25d2817 Ensure bit size 2024-02-08 16:47:10 +01:00
Peter Butzhammer
1001303b8c Improve WriteToBuffer implementation and tests 2024-02-08 16:45:48 +01:00
Peter Butzhammer
3c592c6d46 Add new types to S7ValueConverter.ReadFromBuffer 2024-02-08 12:57:36 +01:00
Peter Butzhammer
3145426432 Improve error message 2024-02-08 11:02:16 +01:00
Peter Butzhammer
d1ec075aa7 Restructure and extens tests for converter 2024-02-08 11:02:09 +01:00
Peter Butzhammer
6492d039da Extend supported variables and improve parser errors 2024-02-07 17:40:51 +01:00
Peter Butzhammer
2a694bf980 Use WriteToBuffer in Sharp7Plc 2024-02-07 16:43:18 +01:00
Peter Butzhammer
981a306478 Add WriteToBuffer and unify supported types 2024-02-07 15:16:23 +01:00
Peter Butzhammer
4389e81340 Add Tests for WriteToBuffer 2024-02-07 13:47:13 +01:00
Peter Butzhammer
d4a8ef9cb3 Rename variable 2024-02-07 13:34:12 +01:00
Peter Butzhammer
3c39996f5e Make CreateNotification for nonexistent variable fail with Exception 2024-02-07 13:33:50 +01:00
Peter Butzhammer
c667c113e3 Set s7Connector in constructor 2024-02-07 11:29:58 +01:00
Peter Butzhammer
1915450115 Fix test path 2024-02-07 11:07:09 +01:00
Peter Butzhammer
1b7a85c7c5 Skip publish in release for pull requests 2024-02-07 11:05:26 +01:00
Peter Butzhammer
8aaf3c1e7e Fix unit test path 2024-02-07 11:01:11 +01:00
Peter Butzhammer
cb6e2d91e0 Add prerelease build 2024-02-07 10:57:47 +01:00
Peter Butzhammer
e6cea1e04f Use file scoped namespaces 2024-02-07 09:54:18 +01:00
Peter Butzhammer
bacfbf05e2 Move ToArea to extension method 2024-02-07 09:53:43 +01:00
Peter Butzhammer
d6dafebfe7 Set langversion to 12 and enable nullables 2024-02-07 09:53:30 +01:00
Peter Butzhammer
662ba7cb6b Add .editorconfig 2024-02-07 09:36:20 +01:00
Peter Butzhammer
babbb1a6bc Cleanup 2024-02-07 09:35:21 +01:00
Peter Butzhammer
c79e07be33 Add multiVarRequestCycleTime parameter 2024-02-07 09:31:11 +01:00
Peter Butzhammer
4432f3c0d4 Remove unused cycleTime 2024-02-07 09:30:52 +01:00
Peter Butzhammer
956f39cc66 Complete Notifications when Plc is disposed 2024-02-07 08:51:40 +01:00
Peter Butzhammer
d678924b6e Complete ConnectionState on dispose 2024-02-06 18:21:31 +01:00
Peter Butzhammer
e7176c26e7 Do not consider connection lost on some error codes 2024-02-06 18:19:15 +01:00
Peter Butzhammer
d422249955 Fix all conversions 2024-02-06 17:51:53 +01:00
Peter Butzhammer
baef1afac1 Use BinaryPrimitives 2024-02-06 17:47:00 +01:00
Peter Butzhammer
b87480c4f9 Fix String conversion 2024-02-06 17:19:23 +01:00
Peter Butzhammer
f45e2c8b1f Add unit tests for S7ValueConverter 2024-02-06 16:10:42 +01:00
Peter Butzhammer
5c2f0c0a09 Update nuget packages 2024-02-06 15:33:39 +01:00
Peter Butzhammer
173b576ad9 Extract S7ValueConverter 2024-02-06 14:45:39 +01:00
Peter Butzhammer
5b86b3e984 Use ConvertToType for both GetValue and CreateNotification 2024-02-06 14:43:50 +01:00
Peter Butzhammer
ffa4ee6236 Remove ReadBit
Using ReadBytes to allow for later unification of Byte conversion logic
2024-02-06 14:36:32 +01:00
Peter Butzhammer
fe68b1d565 Remove extra byte array 2024-02-06 14:01:15 +01:00
Peter Butzhammer
81eb5e42aa Remove unused logger from internal interface 2024-02-06 13:56:00 +01:00
Peter Butzhammer
9b0749baae Cache S7 variable names 2024-02-06 13:39:55 +01:00
Peter Butzhammer
49fe1968d9 Optimize dictionary access 2024-02-06 13:19:32 +01:00
Peter Butzhammer
55050dccd6 Make regex static and compiled 2024-02-05 15:30:56 +01:00
Peter Butzhammer
8d8d5617d1 Move unit tests from specflow to NUnit 2024-02-05 15:25:29 +01:00
54 changed files with 2398 additions and 2015 deletions

20
.editorconfig Normal file
View File

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

View File

@@ -1,40 +0,0 @@
name: .NET Core Build
on:
push:
branches:
- master
pull_request:
branches: [ master ]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
- name: Install NUnit.ConsoleRunner
run: nuget install NUnit.ConsoleRunner -Version 3.13.0 -DirectDownload -OutputDirectory .
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore /p:version=1.1.${{ github.run_number }}
- name: Tests
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 }}
- 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

36
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
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: 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=${{ env.version }}
- 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
if: github.event_name == 'push'
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 }}

View File

@@ -1,6 +1,6 @@
# Sharp7Reactive
[![.NET Core Build](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/dotnet-core.yml/badge.svg)](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/dotnet-core.yml)
[![Release](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/release.yml/badge.svg?branch=master)](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/release.yml)
![Licence](https://img.shields.io/github/license/evopro-ag/Sharp7Reactive.svg)
[![Nuget Version](https://img.shields.io/nuget/v/Sharp7.Rx.svg)](https://www.nuget.org/packages/Sharp7.Rx/)
@@ -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
@@ -24,19 +27,15 @@ 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<bool>("DB2.DBX0.4", true); // set a bit
var bit = await plc.GetValue<int>("DB2.int4"); // get a bit
var value = await plc.GetValue<short>("DB2.Int4"); // get an 16 bit integer
Console.WriteLine(value)
// create a nofication for data change in the plc
var notification = plc.CreateNotification<bool>("DB1.DBX0.2", TransmissionMode.OnChange, TimeSpan.FromMilliseconds(100))
var notification = plc.CreateNotification<bool>("DB1.DBX0.2", TransmissionMode.OnChange)
.Where(b => b) //select rising edge
.Do(_ => doStuff())
.Subscribe();
@@ -48,23 +47,59 @@ using (var disposables = new CompositeDisposable())
}
```
the best way to test your PLC application is running your [SoftPLC](https://github.com/fbarresi/softplc) locally.
The best way to test your PLC application is running your [SoftPLC](https://github.com/fbarresi/softplc) locally.
## S7 Addressing rules
## 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
Sharp7Reactive uses a syntax for identifying addresses similar to official siemens syntax.
Every address has the form (case unsensible) `DB<number>.<TYPE><Start>.<Length/Position>`.
<br/>i.e.: `DB42.DBX0.7` => (means) Datablock 42, bit (DBX), Start: 0, Position: 7
<br/>or<br/>
`DB42.DBB4.25` => (means) Datablock 42, bytes (DBB), Start: 4, Length: 25.
Every address has the form (case unsensitive) `DB<number>.<TYPE><Start>.<Length/Position>`.
Following types are supported:
- `DBX` => Bit (bool)
- `DBB` => byte or byte[]
- `INT`
- `DINT`
- `DUL` => LINT
- `D` => REAL
| Example | Explaination |
| ------------------------------------ | ----------------------------------------------------------------- |
| `DB42.Int4` or<br> `DB42.DBD4` | Datablock 42, 16 bit integer from bytes 4 to 5 (zero based index) |
| `DB42.Bit0.7` or<br>`DB42.DBX0.7` | Datablock 42, bit from byte 0, position 7 |
| `DB42.Byte4.25` or<br>`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.
## 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?

View File

@@ -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<Vars>();
ScenarioContext.Current.Set(names);
}
[When(@"I parse the var name")]
public void WhenIParseTheVarName()
{
var names = ScenarioContext.Current.Get<IEnumerable<Vars>>();
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<S7VariableAddress[]>();
table.CompareToSet(addresses);
}
}
class Vars
{
public string VarName { get; set; }
}
}

View File

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

View File

@@ -1,242 +0,0 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
// ------------------------------------------------------------------------------
#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>(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

View File

@@ -1,32 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net461</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
<!--
CA1859: Change type of field 'xxx' from interface to type for performance reasons
CA1852: Type 'ReadFromBuffer' can be sealed
-->
<NoWarn>$(NoWarn);CA1859;CA1852</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="SpecFlow" Version="2.4.0" />
<PackageReference Include="DeepEqual" Version="5.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="Shouldly" Version="4.2.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sharp7.Rx\Sharp7.Rx.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="ParsingS7VariableName.feature.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>ParsingS7VariableName.feature</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="ParsingS7VariableName.feature">
<Generator>SpecFlowSingleFileGenerator</Generator>
<LastGenOutput>ParsingS7VariableName.feature.cs</LastGenOutput>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,25 @@
using NUnit.Framework;
using Shouldly;
namespace Sharp7.Rx.Tests.ValueConverterTests;
[TestFixture]
internal class ConvertBothWays : ConverterTestBase
{
[TestCaseSource(nameof(GetValidTestCases))]
public void Convert(ConverterTestCase tc)
{
//Arrange
var buffer = new byte[tc.VariableAddress.BufferLength];
var write = CreateWriteMethod(tc);
var read = CreateReadMethod(tc);
//Act
write.Invoke(null, [buffer, tc.Value, tc.VariableAddress]);
var result = read.Invoke(null, [buffer, tc.VariableAddress]);
//Assert
result.ShouldBe(tc.Value);
}
}

View File

@@ -0,0 +1,90 @@
using System.Reflection;
using Sharp7.Rx.Interfaces;
namespace Sharp7.Rx.Tests.ValueConverterTests;
internal abstract class ConverterTestBase
{
protected static readonly IVariableNameParser Parser = new VariableNameParser();
public static MethodInfo CreateReadMethod(ConverterTestCase tc)
{
var convertMi = typeof(ConverterTestBase).GetMethod(nameof(ReadFromBuffer));
var convert = convertMi!.MakeGenericMethod(tc.Value.GetType());
return convert;
}
public static MethodInfo CreateWriteMethod(ConverterTestCase tc)
{
var writeMi = typeof(ConverterTestBase).GetMethod(nameof(WriteToBuffer));
var write = writeMi!.MakeGenericMethod(tc.Value.GetType());
return write;
}
public static IEnumerable<ConverterTestCase> GetValidTestCases()
{
yield return new ConverterTestCase(true, "DB99.bit5.4", [0x10]);
yield return new ConverterTestCase(false, "DB99.bit5.4", [0x00]);
yield return new ConverterTestCase((byte) 18, "DB99.Byte5", [0x12]);
yield return new ConverterTestCase((short) 4660, "DB99.Int5", [0x12, 0x34]);
yield return new ConverterTestCase((short) -3532, "DB99.Int5", [0xF2, 0x34]);
yield return new ConverterTestCase((ushort) 4660, "DB99.UInt5", [0x12, 0x34]);
yield return new ConverterTestCase((ushort) 62004, "DB99.UInt5", [0xF2, 0x34]);
yield return new ConverterTestCase(305419879, "DB99.DInt5", [0x12, 0x34, 0x56, 0x67]);
yield return new ConverterTestCase(-231451033, "DB99.DInt5", [0xF2, 0x34, 0x56, 0x67]);
yield return new ConverterTestCase(305419879u, "DB99.UDInt5", [0x12, 0x34, 0x56, 0x67]);
yield return new ConverterTestCase(4063516263u, "DB99.UDInt5", [0xF2, 0x34, 0x56, 0x67]);
yield return new ConverterTestCase(1311768394163015151L, "DB99.LInt5", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
yield return new ConverterTestCase(-994074615050678801L, "DB99.LInt5", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
yield return new ConverterTestCase(1311768394163015151uL, "DB99.ULInt5", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
yield return new ConverterTestCase(17452669458658872815uL, "DB99.ULInt5", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
yield return new ConverterTestCase(0.25f, "DB99.Real5", [0x3E, 0x80, 0x00, 0x00]);
yield return new ConverterTestCase(0.25, "DB99.LReal5", [0x3F, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
yield return new ConverterTestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB99.Byte5.4", [0x12, 0x34, 0x56, 0x67]);
yield return new ConverterTestCase("ABCD", "DB99.String10.4", [0x04, 0x04, 0x41, 0x42, 0x43, 0x44]);
yield return new ConverterTestCase("ABCD", "DB99.String10.6", [0x06, 0x04, 0x41, 0x42, 0x43, 0x44, 0x00, 0x00]);
yield return new ConverterTestCase("ABCD", "DB99.WString10.4", [0x00, 0x04, 0x00, 0x04, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44]);
yield return new ConverterTestCase("ABCD", "DB99.WString10.6", [0x00, 0x06, 0x00, 0x04, 0x00, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00]);
yield return new ConverterTestCase("ABCD", "DB99.Byte5.4", [0x41, 0x42, 0x43, 0x44]);
yield return new ConverterTestCase("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]);
yield return new ConverterTestCase(true, "DB99.DBx0.4", [0x10]);
yield return new ConverterTestCase(false, "DB99.DBx0.4", [0]);
yield return new ConverterTestCase((byte) 18, "DB99.DBB0", [0x12]);
yield return new ConverterTestCase((short) 4660, "DB99.INT0", [0x12, 0x34]);
yield return new ConverterTestCase((short) -3532, "DB99.INT0", [0xF2, 0x34]);
yield return new ConverterTestCase(305419879, "DB99.DINT0", [0x12, 0x34, 0x56, 0x67]);
yield return new ConverterTestCase(-231451033, "DB99.DINT0", [0xF2, 0x34, 0x56, 0x67]);
yield return new ConverterTestCase(1311768394163015151uL, "DB99.dul0", [0x12, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
yield return new ConverterTestCase(17452669458658872815uL, "DB99.dul0", [0xF2, 0x34, 0x56, 0x67, 0x89, 0xAB, 0xCD, 0xEF]);
yield return new ConverterTestCase(new byte[] {0x12, 0x34, 0x56, 0x67}, "DB99.DBB0.4", [0x12, 0x34, 0x56, 0x67]);
yield return new ConverterTestCase(0.25f, "DB99.D0", [0x3E, 0x80, 0x00, 0x00]);
}
/// <summary>
/// This helper method exists, since I could not manage to invoke a generic method
/// with a Span&lt;T&gt; parameter.
/// </summary>
public static TValue ReadFromBuffer<TValue>(byte[] buffer, VariableAddress address) =>
ValueConverter.ReadFromBuffer<TValue>(buffer, address);
/// <summary>
/// This helper method exists, since I could not manage to invoke a generic method
/// with a Span&lt;T&gt; parameter.
/// </summary>
public static void WriteToBuffer<TValue>(byte[] buffer, TValue value, VariableAddress address) =>
ValueConverter.WriteToBuffer(buffer, value, address);
public record ConverterTestCase(object Value, string Address, byte[] Data)
{
public VariableAddress VariableAddress => Parser.Parse(Address);
public override string ToString() => $"{Value.GetType().Name}, {Address}: {Value}";
}
}

View File

@@ -0,0 +1,51 @@
using NUnit.Framework;
using Shouldly;
namespace Sharp7.Rx.Tests.ValueConverterTests;
[TestFixture]
internal class ReadFromBuffer : ConverterTestBase
{
[TestCaseSource(nameof(GetValidTestCases))]
[TestCaseSource(nameof(GetAdditinalReadTestCases))]
public void Read(ConverterTestCase tc)
{
//Arrange
var convert = CreateReadMethod(tc);
//Act
var result = convert.Invoke(null, [tc.Data, tc.VariableAddress]);
//Assert
result.ShouldBe(tc.Value);
}
public static IEnumerable<ConverterTestCase> GetAdditinalReadTestCases()
{
yield return new ConverterTestCase(true, "DB0.DBx0.4", [0x1F]);
yield return new ConverterTestCase(false, "DB0.DBx0.4", [0xEF]);
yield return new ConverterTestCase("ABCD", "DB0.string0.6", [0x04, 0x04, 0x41, 0x42, 0x43, 0x44, 0x00, 0x00]); // Length in address exceeds PLC string length
}
[TestCase((char) 18, "DB0.DBB0", new byte[] {0x12})]
public void UnsupportedType<T>(T template, string address, byte[] data)
{
//Arrange
var variableAddress = Parser.Parse(address);
//Act
Should.Throw<UnsupportedS7TypeException>(() => ValueConverter.ReadFromBuffer<T>(data, variableAddress));
}
[TestCase(123, "DB12.DINT3", new byte[] {0x01, 0x02, 0x03})]
[TestCase((short) 123, "DB12.INT3", new byte[] {0xF2})]
[TestCase("ABC", "DB0.string0.6", new byte[] {0x01, 0x02, 0x03})]
public void BufferTooSmall<T>(T template, string address, byte[] data)
{
//Arrange
var variableAddress = Parser.Parse(address);
//Act
Should.Throw<ArgumentException>(() => ValueConverter.ReadFromBuffer<T>(data, variableAddress));
}
}

View File

@@ -0,0 +1,76 @@
using NUnit.Framework;
using Shouldly;
namespace Sharp7.Rx.Tests.ValueConverterTests;
[TestFixture]
internal class WriteToBuffer : ConverterTestBase
{
[TestCaseSource(nameof(GetValidTestCases))]
[TestCaseSource(nameof(GetAdditinalWriteTestCases))]
public void Write(ConverterTestCase tc)
{
//Arrange
var buffer = new byte[tc.VariableAddress.BufferLength];
var write = CreateWriteMethod(tc);
//Act
write.Invoke(null, [buffer, tc.Value, tc.VariableAddress]);
//Assert
buffer.ShouldBe(tc.Data);
}
public static IEnumerable<ConverterTestCase> GetAdditinalWriteTestCases()
{
yield return new ConverterTestCase("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
// 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)]
[TestCase(0.25f, "DB0.Real1", 3)]
[TestCase("test", "DB0.String1.10", 9)]
public void BufferToSmall<T>(T input, string address, int bufferSize)
{
//Arrange
var variableAddress = Parser.Parse(address);
var buffer = new byte[bufferSize];
//Act
Should.Throw<ArgumentException>(() => ValueConverter.WriteToBuffer(buffer, input, variableAddress));
}
[TestCase((char) 18, "DB0.DBB0")]
public void UnsupportedType<T>(T input, string address)
{
//Arrange
var variableAddress = Parser.Parse(address);
var buffer = new byte[variableAddress.BufferLength];
//Act
Should.Throw<UnsupportedS7TypeException>(() => ValueConverter.WriteToBuffer(buffer, input, variableAddress));
}
}

View File

@@ -0,0 +1,85 @@
using NUnit.Framework;
using Sharp7.Rx.Extensions;
using Sharp7.Rx.Interfaces;
using Sharp7.Rx.Tests.ValueConverterTests;
using Shouldly;
namespace Sharp7.Rx.Tests.VariableAddressTests;
[TestFixture]
public class MatchesType
{
static readonly IVariableNameParser parser = new VariableNameParser();
private static readonly IReadOnlyList<Type> typeList = new[]
{
typeof(byte),
typeof(byte[]),
typeof(bool),
typeof(short),
typeof(ushort),
typeof(int),
typeof(uint),
typeof(long),
typeof(ulong),
typeof(float),
typeof(double),
typeof(string),
typeof(int[]),
typeof(float[]),
typeof(DateTime[]),
typeof(object),
};
[TestCaseSource(nameof(GetValid))]
public void Supported(TestCase tc) => Check(tc.Type, tc.Address, true);
[TestCaseSource(nameof(GetInvalid))]
public void Unsupported(TestCase tc) => Check(tc.Type, tc.Address, false);
public static IEnumerable<TestCase> GetValid()
{
return
ConverterTestBase.GetValidTestCases()
.Select(tc => new TestCase(tc.Value.GetType(), tc.Address));
}
public static IEnumerable<TestCase> GetInvalid()
{
return
ConverterTestBase.GetValidTestCases()
.DistinctBy(tc => tc.Value.GetType())
.SelectMany(tc =>
typeList.Where(type => type != tc.Value.GetType())
.Select(type => new TestCase(type, tc.Address))
)
// Explicitly remove some valid combinations
.Where(tc => !(
(tc.Type == typeof(string) && tc.Address == "DB99.Byte5") ||
(tc.Type == typeof(string) && tc.Address == "DB99.Byte5.4") ||
(tc.Type == typeof(byte[]) && tc.Address == "DB99.Byte5")
))
;
}
private static void Check(Type type, string address, bool expected)
{
//Arrange
var variableAddress = parser.Parse(address);
//Act
variableAddress.MatchesType(type).ShouldBe(expected);
}
public record TestCase(Type Type, string Address)
{
public override string ToString() => $"{Type.Name} {Address}";
}
}

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Sharp7.Rx.Tests")]

View File

@@ -1,128 +1,130 @@
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;
using Sharp7.Rx.Extensions;
namespace Sharp7.Rx.Basics
namespace Sharp7.Rx.Basics;
internal class ConcurrentSubjectDictionary<TKey, TValue> : IDisposable
{
internal class ConcurrentSubjectDictionary<TKey, TValue> : IDisposable
private readonly object dictionaryLock = new object();
private readonly Func<TKey, TValue> valueFactory;
private ConcurrentDictionary<TKey, SubjectWithRefCounter> dictionary;
public ConcurrentSubjectDictionary()
{
private readonly object dictionaryLock = new object();
private readonly Func<TKey, TValue> valueFactory;
private ConcurrentDictionary<TKey, SubjectWithRefCounter> dictionary;
dictionary = new ConcurrentDictionary<TKey, SubjectWithRefCounter>();
}
public ConcurrentSubjectDictionary()
public ConcurrentSubjectDictionary(IEqualityComparer<TKey> comparer)
{
dictionary = new ConcurrentDictionary<TKey, SubjectWithRefCounter>(comparer);
}
public ConcurrentSubjectDictionary(TValue initialValue, IEqualityComparer<TKey> comparer)
{
valueFactory = _ => initialValue;
dictionary = new ConcurrentDictionary<TKey, SubjectWithRefCounter>(comparer);
}
public ConcurrentSubjectDictionary(TValue initialValue)
{
valueFactory = _ => initialValue;
dictionary = new ConcurrentDictionary<TKey, SubjectWithRefCounter>();
}
public ConcurrentSubjectDictionary(Func<TKey, TValue> valueFactory = null)
{
this.valueFactory = valueFactory;
dictionary = new ConcurrentDictionary<TKey, SubjectWithRefCounter>();
}
public IEnumerable<TKey> ExistingKeys => dictionary.Keys;
public bool IsDisposed { get; private set; }
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public DisposableItem<TValue> GetOrCreateObservable(TKey key)
{
lock (dictionaryLock)
{
dictionary = new ConcurrentDictionary<TKey, SubjectWithRefCounter>();
}
public ConcurrentSubjectDictionary(IEqualityComparer<TKey> comparer)
{
dictionary = new ConcurrentDictionary<TKey, SubjectWithRefCounter>(comparer);
}
public ConcurrentSubjectDictionary(TValue initialValue, IEqualityComparer<TKey> comparer)
{
valueFactory = _ => initialValue;
dictionary = new ConcurrentDictionary<TKey, SubjectWithRefCounter>(comparer);
}
public ConcurrentSubjectDictionary(TValue initialValue)
{
valueFactory = _ => initialValue;
dictionary = new ConcurrentDictionary<TKey, SubjectWithRefCounter>();
}
public ConcurrentSubjectDictionary(Func<TKey, TValue> valueFactory = null)
{
this.valueFactory = valueFactory;
dictionary = new ConcurrentDictionary<TKey, SubjectWithRefCounter>();
}
public IEnumerable<TKey> ExistingKeys => dictionary.Keys;
public bool IsDisposed { get; private set; }
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public DisposableItem<TValue> 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(CreateSubject(k)),
(_, subjectWithRefCounter) =>
{
counter.Counter = counter.Counter + 1;
return counter;
subjectWithRefCounter.IncreaseCount();
return subjectWithRefCounter;
});
return new DisposableItem<TValue>(subject.Subject.AsObservable(), () => RemoveIfNoLongerInUse(key));
}
}
public bool TryGetObserver(TKey key, out IObserver<TValue> 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)
{
dictionary.Values.DisposeItems();
dictionary.Clear();
dictionary = null;
}
IsDisposed = true;
}
private ISubject<TValue> CreateSubject(TKey key)
{
if (valueFactory == null)
return new Subject<TValue>();
return new BehaviorSubject<TValue>(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<TValue> Subject { get; set; }
return new DisposableItem<TValue>(subject.Subject.AsObservable(), () => RemoveIfNoLongerInUse(key));
}
}
}
public bool TryGetObserver(TKey key, out IObserver<TValue> subject)
{
if (dictionary.TryGetValue(key, out var 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<TValue> CreateSubject(TKey key)
{
if (valueFactory == null)
return new Subject<TValue>();
return new BehaviorSubject<TValue>(valueFactory(key));
}
private void RemoveIfNoLongerInUse(TKey variableName)
{
lock (dictionaryLock)
if (dictionary.TryGetValue(variableName, out var subjectWithRefCount))
if (subjectWithRefCount.DecreaseCount() < 1)
dictionary.TryRemove(variableName, out _);
}
~ConcurrentSubjectDictionary()
{
Dispose(false);
}
class SubjectWithRefCounter
{
private int counter = 1;
public SubjectWithRefCounter(ISubject<TValue> subject)
{
Subject = subject;
}
public ISubject<TValue> Subject { get; }
public int DecreaseCount() => Interlocked.Decrement(ref counter);
public int IncreaseCount() => Interlocked.Increment(ref counter);
}
}

View File

@@ -1,38 +1,34 @@
using System;
using System.Linq;
namespace Sharp7.Rx.Basics;
namespace Sharp7.Rx.Basics
internal class DisposableItem<TValue> : IDisposable
{
internal class DisposableItem<TValue> : IDisposable
private readonly Action disposeAction;
bool disposed;
public DisposableItem(IObservable<TValue> observable, Action disposeAction)
{
private readonly Action disposeAction;
bool disposed;
public DisposableItem(IObservable<TValue> observable, Action disposeAction)
{
this.disposeAction = disposeAction;
Observable = observable;
}
public IObservable<TValue> 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;
}
}
public IObservable<TValue> Observable { get; }
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed) return;
if (disposing)
{
disposeAction();
}
disposed = true;
}
}

View File

@@ -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
/// <summary>
/// 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
/// </summary>
internal class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
{
/// <summary>Whether the current thread is processing work items.</summary>
[ThreadStatic] private static bool currentThreadIsProcessingItems;
/// <summary>The maximum concurrency level allowed by this scheduler.</summary>
private readonly int maxDegreeOfParallelism;
/// <summary>The list of tasks to be executed.</summary>
private readonly LinkedList<Task> tasks = new LinkedList<Task>(); // protected by lock(_tasks)
/// <summary>Whether the scheduler is currently processing work items.</summary>
private int delegatesQueuedOrRunning; // protected by lock(_tasks)
/// <summary>
/// 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.
/// </summary>
internal class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
/// <param name="maxDegreeOfParallelism">The maximum degree of parallelism provided by this scheduler.</param>
public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism)
{
/// <summary>Whether the current thread is processing work items.</summary>
[ThreadStatic] private static bool currentThreadIsProcessingItems;
if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism));
this.maxDegreeOfParallelism = maxDegreeOfParallelism;
}
/// <summary>The maximum concurrency level allowed by this scheduler.</summary>
private readonly int maxDegreeOfParallelism;
/// <summary>Gets the maximum concurrency level supported by this scheduler.</summary>
public sealed override int MaximumConcurrencyLevel => maxDegreeOfParallelism;
/// <summary>The list of tasks to be executed.</summary>
private readonly LinkedList<Task> tasks = new LinkedList<Task>(); // protected by lock(_tasks)
/// <summary>Whether the scheduler is currently processing work items.</summary>
private int delegatesQueuedOrRunning; // protected by lock(_tasks)
/// <summary>
/// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the
/// specified degree of parallelism.
/// </summary>
/// <param name="maxDegreeOfParallelism">The maximum degree of parallelism provided by this scheduler.</param>
public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism)
/// <summary>Gets an enumerable of the tasks currently scheduled on this scheduler.</summary>
/// <returns>An enumerable of the tasks currently scheduled.</returns>
protected sealed override IEnumerable<Task> 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();
}
/// <summary>Gets the maximum concurrency level supported by this scheduler.</summary>
public sealed override int MaximumConcurrencyLevel => maxDegreeOfParallelism;
/// <summary>Gets an enumerable of the tasks currently scheduled on this scheduler.</summary>
/// <returns>An enumerable of the tasks currently scheduled.</returns>
protected sealed override IEnumerable<Task> 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);
}
}
/// <summary>Queues a task to the scheduler.</summary>
/// <param name="task">The task to be queued.</param>
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();
}
}
}
/// <summary>Attempts to remove a previously scheduled task from the scheduler.</summary>
/// <param name="task">The task to be removed.</param>
/// <returns>Whether the task could be found and removed.</returns>
protected sealed override bool TryDequeue(Task task)
{
lock (tasks)
{
return tasks.Remove(task);
}
}
/// <summary>Attempts to execute the specified task on the current thread.</summary>
/// <param name="task">The task to be executed.</param>
/// <param name="taskWasPreviouslyQueued"></param>
/// <returns>Whether the task could be executed on the current thread.</returns>
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);
}
/// <summary>
/// Informs the ThreadPool that there's work to be executed for this scheduler.
/// </summary>
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);
}
}
}
/// <summary>Queues a task to the scheduler.</summary>
/// <param name="task">The task to be queued.</param>
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();
}
}
}
/// <summary>Attempts to remove a previously scheduled task from the scheduler.</summary>
/// <param name="task">The task to be removed.</param>
/// <returns>Whether the task could be found and removed.</returns>
protected sealed override bool TryDequeue(Task task)
{
lock (tasks)
{
return tasks.Remove(task);
}
}
/// <summary>Attempts to execute the specified task on the current thread.</summary>
/// <param name="task">The task to be executed.</param>
/// <param name="taskWasPreviouslyQueued"></param>
/// <returns>Whether the task could be executed on the current thread.</returns>
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);
}
/// <summary>
/// Informs the ThreadPool that there's work to be executed for this scheduler.
/// </summary>
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);
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Concurrent;
using Sharp7.Rx.Interfaces;
namespace Sharp7.Rx;
internal class CacheVariableNameParser : IVariableNameParser
{
private static readonly ConcurrentDictionary<string, VariableAddress> addressCache = new ConcurrentDictionary<string, VariableAddress>(StringComparer.OrdinalIgnoreCase);
private readonly IVariableNameParser inner;
public CacheVariableNameParser(IVariableNameParser inner)
{
this.inner = inner;
}
public VariableAddress Parse(string input) => addressCache.GetOrAdd(input, inner.Parse);
}

View File

@@ -1,10 +1,10 @@
namespace Sharp7.Rx.Enums
namespace Sharp7.Rx.Enums;
public enum ConnectionState
{
public enum ConnectionState
{
Initial,
Connected,
DisconnectedByUser,
ConnectionLost
}
Initial,
Connected,
DisconnectedByUser,
ConnectionLost,
Disposed
}

View File

@@ -1,10 +0,0 @@
namespace Sharp7.Rx.Enums
{
internal enum CpuType
{
S7_300,
S7_400,
S7_1200,
S7_1500
}
}

View File

@@ -1,13 +1,52 @@
namespace Sharp7.Rx.Enums
namespace Sharp7.Rx.Enums;
// see https://support.industry.siemens.com/cs/mdm/109747174?c=88343664523&lc=de-DE
internal enum DbType
{
internal enum DbType
{
Bit,
String,
Byte,
Double,
Integer,
DInteger,
ULong
}
Bit,
/// <summary>
/// ASCII string
/// </summary>
String,
/// <summary>
/// UTF16 string
/// </summary>
WString,
Byte,
/// <summary>
/// Int16
/// </summary>
Int,
/// <summary>
/// UInt16
/// </summary>
UInt,
/// <summary>
/// Int32
/// </summary>
DInt,
/// <summary>
/// UInt32
/// </summary>
UDInt,
/// <summary>
/// Int64
/// </summary>
LInt,
/// <summary>
/// UInt64
/// </summary>
ULInt,
Single,
Double,
}

View File

@@ -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,
}
Input = 69,
Output = 65,
Marker = 77,
Db = 68,
}

View File

@@ -1,8 +1,7 @@
namespace Sharp7.Rx.Enums
namespace Sharp7.Rx.Enums;
public enum TransmissionMode
{
public enum TransmissionMode
{
Cyclic = 3,
OnChange = 4,
}
Cyclic = 3,
OnChange = 4,
}

View File

@@ -0,0 +1,83 @@
namespace Sharp7.Rx;
public abstract class S7Exception : Exception
{
protected S7Exception(string message) : base(message)
{
}
protected S7Exception(string message, Exception innerException) : base(message, innerException)
{
}
}
public class S7CommunicationException : S7Exception
{
public S7CommunicationException(string message, int s7ErrorCode, string s7ErrorText) : base(message)
{
S7ErrorCode = s7ErrorCode;
S7ErrorText = s7ErrorText;
}
public S7CommunicationException(string message, Exception innerException, int s7ErrorCode, string s7ErrorText) : base(message, innerException)
{
S7ErrorCode = s7ErrorCode;
S7ErrorText = s7ErrorText;
}
public int S7ErrorCode { get; }
public string S7ErrorText { get; }
}
public class DataTypeMissmatchException : S7Exception
{
internal DataTypeMissmatchException(string message, Type type, VariableAddress address) : base(message)
{
Type = type;
Address = address.ToString();
}
internal DataTypeMissmatchException(string message, Exception innerException, Type type, VariableAddress 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, VariableAddress address) : base(message)
{
Type = type;
Address = address.ToString();
}
internal UnsupportedS7TypeException(string message, Exception innerException, Type type, VariableAddress address) : base(message, innerException)
{
Type = type;
Address = address.ToString();
}
public string Address { get; }
public Type Type { get; }
}
public class InvalidS7AddressException : S7Exception
{
public InvalidS7AddressException(string message, string input) : base(message)
{
Input = input;
}
public InvalidS7AddressException(string message, Exception innerException, string input) : base(message, innerException)
{
Input = input;
}
public string Input { get; }
}

View File

@@ -1,21 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
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);
}
public static void DisposeItems(this IEnumerable<object> disposables)
{
foreach (IDisposable disposable in disposables.OfType<IDisposable>())
disposable?.Dispose();
}
compositeDisposable.Add(disposable);
}
}

View File

@@ -1,91 +1,81 @@
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.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
namespace Sharp7.Rx.Extensions;
internal static class ObservableExtensions
{
internal static class ObservableExtensions
{
public static IObservable<T> LogAndRetry<T>(this IObservable<T> source, ILogger logger, string message)
{
return source
.Do(
_ => { },
ex => logger?.LogError(ex, message))
.Retry();
}
public static IObservable<T> DisposeMany<T>(this IObservable<T> source)
{
return Observable.Create<T>(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<T> RetryAfterDelay<T>(
this IObservable<T> source,
TimeSpan retryDelay,
int retryCount = -1,
IScheduler scheduler = null)
{
return RedoAfterDelay(source, retryDelay, retryCount, scheduler, Observable.Retry, Observable.Retry);
}
public static IObservable<T> LogAndRetry<T>(this IObservable<T> source, ILogger logger, string message)
{
return source
.Do(
_ => { },
ex => logger?.LogError(ex, message))
.Retry();
}
public static IObservable<T> RepeatAfterDelay<T>(
this IObservable<T> source,
TimeSpan retryDelay,
int repeatCount = -1,
IScheduler scheduler = null)
{
return RedoAfterDelay(source, retryDelay, repeatCount, scheduler, Observable.Repeat, Observable.Repeat);
}
public static IObservable<T> LogAndRetryAfterDelay<T>(
this IObservable<T> 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<T> LogAndRetryAfterDelay<T>(
this IObservable<T> 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<T> RepeatAfterDelay<T>(
this IObservable<T> source,
TimeSpan retryDelay,
int repeatCount = -1,
IScheduler scheduler = null)
{
return RedoAfterDelay(source, retryDelay, repeatCount, scheduler, Observable.Repeat, Observable.Repeat);
}
private static IObservable<T> RedoAfterDelay<T>(IObservable<T> source, TimeSpan retryDelay, int retryCount, IScheduler scheduler, Func<IObservable<T>, IObservable<T>> reDo,
Func<IObservable<T>, int, IObservable<T>> reDoCount)
{
scheduler = scheduler ?? TaskPoolScheduler.Default;
var attempt = 0;
public static IObservable<T> RetryAfterDelay<T>(
this IObservable<T> 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<T> RedoAfterDelay<T>(IObservable<T> source, TimeSpan retryDelay, int retryCount, IScheduler scheduler, Func<IObservable<T>, IObservable<T>> reDo,
Func<IObservable<T>, int, IObservable<T>> reDoCount)
{
scheduler = scheduler ?? TaskPoolScheduler.Default;
var attempt = 0;
public static IObservable<T> DisposeMany<T>(this IObservable<T> source)
{
return Observable.Create<T>(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);
}
}

View File

@@ -0,0 +1,16 @@
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)
};
}

View File

@@ -1,74 +1,72 @@
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<TReturn> CreateDatatransferWithHandshake<TReturn>(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func<IPlc, Task<TReturn>> readData,
bool initialTransfer)
{
public static IObservable<TReturn> CreateDatatransferWithHandshake<TReturn>(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func<IPlc, Task<TReturn>> readData, bool initialTransfer)
return Observable.Create<TReturn>(async observer =>
{
return Observable.Create<TReturn>(async observer =>
var subscriptions = new CompositeDisposable();
var notification = plc
.CreateNotification<bool>(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<bool>(triggerAddress, TransmissionMode.OnChange, TimeSpan.Zero)
.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<TReturn> CreateDatatransferWithHandshake<TReturn>(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func<IPlc, Task<TReturn>> readData)
{
return CreateDatatransferWithHandshake(plc, triggerAddress, ackTriggerAddress, readData, false);
}
return subscriptions;
});
}
private static async Task<TReturn> ReadData<TReturn>(IPlc plc, Func<IPlc, Task<TReturn>> receiveData)
{
return await receiveData(plc);
}
public static IObservable<TReturn> CreateDatatransferWithHandshake<TReturn>(this IPlc plc, string triggerAddress, string ackTriggerAddress, Func<IPlc, Task<TReturn>> readData)
private static async Task<TReturn> ReadDataAndAcknowlodge<TReturn>(IPlc plc, Func<IPlc, Task<TReturn>> readData, string ackTriggerAddress)
{
try
{
return CreateDatatransferWithHandshake(plc, triggerAddress, ackTriggerAddress, readData, false);
return await ReadData(plc, readData);
}
private static async Task<TReturn> ReadData<TReturn>(IPlc plc, Func<IPlc, Task<TReturn>> receiveData)
finally
{
return await receiveData(plc);
}
private static async Task<TReturn> ReadDataAndAcknowlodge<TReturn>(IPlc plc, Func<IPlc, Task<TReturn>> readData, string ackTriggerAddress)
{
try
{
return await ReadData(plc, readData);
}
finally
{
await plc.SetValue(ackTriggerAddress, true);
}
await plc.SetValue(ackTriggerAddress, true);
}
}
}

View File

@@ -0,0 +1,44 @@
using Sharp7.Rx.Enums;
namespace Sharp7.Rx.Extensions;
internal static class VariableAddressExtensions
{
private static readonly Dictionary<Type, Func<VariableAddress, bool>> supportedTypeMap = new()
{
{typeof(bool), a => a.Type == DbType.Bit},
{typeof(string), a => a.Type is DbType.String or DbType.WString or DbType.Byte},
{typeof(byte), a => a.Type == DbType.Byte && a.Length == 1},
{typeof(short), a => a.Type == DbType.Int},
{typeof(ushort), a => a.Type == DbType.UInt},
{typeof(int), a => a.Type == DbType.DInt},
{typeof(uint), a => a.Type == DbType.UDInt},
{typeof(long), a => a.Type == DbType.LInt},
{typeof(ulong), a => a.Type == DbType.ULInt},
{typeof(float), a => a.Type == DbType.Single},
{typeof(double), a => a.Type == DbType.Double},
{typeof(byte[]), a => a.Type == DbType.Byte},
};
public static bool MatchesType(this 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))
};
}

View File

@@ -1,16 +1,18 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using JetBrains.Annotations;
using Sharp7.Rx.Enums;
namespace Sharp7.Rx.Interfaces
namespace Sharp7.Rx.Interfaces;
[NoReorder]
public interface IPlc : IDisposable
{
public interface IPlc : IDisposable
{
IObservable<TValue> CreateNotification<TValue>(string variableName, TransmissionMode transmissionMode, TimeSpan cycleSpan);
Task SetValue<TValue>(string variableName, TValue value);
Task<TValue> GetValue<TValue>(string variableName);
IObservable<ConnectionState> ConnectionState { get; }
ILogger Logger { get; }
}
IObservable<ConnectionState> ConnectionState { get; }
Task SetValue<TValue>(string variableName, TValue value, CancellationToken token = default);
Task<TValue> GetValue<TValue>(string variableName, CancellationToken token = default);
Task<object> GetValue(string variableName, CancellationToken token = default);
IObservable<TValue> CreateNotification<TValue>(string variableName, TransmissionMode transmissionMode);
IObservable<object> CreateNotification(string variableName, TransmissionMode transmissionMode);
}

View File

@@ -1,26 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Sharp7.Rx.Enums;
namespace Sharp7.Rx.Interfaces
{
internal interface IS7Connector : IDisposable
{
IObservable<ConnectionState> ConnectionState { get; }
Task InitializeAsync();
Task<bool> Connect();
Task Disconnect();
Task<bool> ReadBit(Operand operand, ushort byteAddress, byte bitAdress, ushort dbNr, CancellationToken token);
Task<byte[]> ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token);
Task<bool> WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNr, CancellationToken token);
Task<ushort> WriteBytes(Operand operand, ushort startByteAdress, byte[] data, ushort dBNr, CancellationToken token);
ILogger Logger { get; }
Task<Dictionary<string, byte[]>> ExecuteMultiVarRequest(IEnumerable<string> variableNames);
}
}

View File

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

View File

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

View File

@@ -1,117 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Sharp7.Rx.Resources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class StringResources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal StringResources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Sharp7.Rx.Resources.StringResources", typeof(StringResources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to S7 driver could not be initialized.
/// </summary>
internal static string StrErrorS7DriverCouldNotBeInitialized {
get {
return ResourceManager.GetString("StrErrorS7DriverCouldNotBeInitialized", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to S7 driver is not initialized..
/// </summary>
internal static string StrErrorS7DriverNotInitialized {
get {
return ResourceManager.GetString("StrErrorS7DriverNotInitialized", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to TCP/IP connection established..
/// </summary>
internal static string StrInfoConnectionEstablished {
get {
return ResourceManager.GetString("StrInfoConnectionEstablished", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Trying to connect to PLC ({2}) &apos;{0}&apos;, CPU slot {1}....
/// </summary>
internal static string StrInfoTryConnecting {
get {
return ResourceManager.GetString("StrInfoTryConnecting", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Error while reading data from plc..
/// </summary>
internal static string StrLogErrorReadingDataFromPlc {
get {
return ResourceManager.GetString("StrLogErrorReadingDataFromPlc", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Communication error discovered. Reconnect is in progress....
/// </summary>
internal static string StrLogWarningCommunictionErrorReconnecting {
get {
return ResourceManager.GetString("StrLogWarningCommunictionErrorReconnecting", resourceCulture);
}
}
}
}

View File

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

39
Sharp7.Rx/S7ErrorCodes.cs Normal file
View File

@@ -0,0 +1,39 @@
#nullable enable
namespace Sharp7.Rx;
public static class S7ErrorCodes
{
/// <summary>
/// This list is not exhaustive and should be considered work in progress.
/// </summary>
private static readonly HashSet<int> notDisconnectedErrorCodes =
[
0x000000, // OK
0xC00000, // CPU: Item not available
0x900000 // CPU: Address out of range
];
private static readonly IReadOnlyDictionary<int, string> additionalErrorTexts = new Dictionary<int, string>
{
{0xC00000, "This happens when the DB does not exist."},
{0x900000, "This happens when the DB is not long enough."},
{
0x40000, """
This can happen when the cpu MPI address or rack is wrong, the DB is "optimized", or "PUT/GET communication" is not enabled.
See https://snap7.sourceforge.net/snap7_client.html#target_compatibility.
"""
}
};
/// <summary>
/// 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.
/// </summary>
public static bool AssumeConnectionLost(int errorCode) =>
!notDisconnectedErrorCodes.Contains(errorCode);
public static string? GetAdditionalErrorText(int errorCode) =>
additionalErrorTexts.GetValueOrDefault(errorCode);
}

View File

@@ -1,14 +0,0 @@
using Sharp7.Rx.Enums;
namespace Sharp7.Rx
{
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; }
}
}

View File

@@ -1,96 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Sharp7.Rx.Enums;
using Sharp7.Rx.Interfaces;
namespace Sharp7.Rx
{
internal class S7VariableNameParser : IS7VariableNameParser
{
private readonly Regex regex = new Regex(@"^(?<operand>db{1})(?<dbNr>\d{1,4})\.?(?<type>dbx|x|s|string|b|dbb|d|int|dbw|w|dint|dul|dulint|dulong|){1}(?<start>\d+)(\.(?<bitOrLength>\d+))?$", RegexOptions.IgnoreCase);
private readonly Dictionary<string, DbType> types = new Dictionary<string, DbType>
{
{"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 }
};
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 s7VariableAddress = new S7VariableAddress
{
Operand = operand,
DbNr = dbNr,
Start = start,
Type = type,
};
if (type == DbType.Bit)
{
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;
}
return s7VariableAddress;
}
return null;
}
private DbType ParseType(string value)
{
return types
.Where(pair => pair.Key.Equals(value, StringComparison.InvariantCultureIgnoreCase))
.Select(pair => pair.Value)
.FirstOrDefault();
}
}
}

View File

@@ -1,10 +1,9 @@
namespace Sharp7.Rx.Settings
namespace Sharp7.Rx.Settings;
internal class PlcConnectionSettings
{
internal class PlcConnectionSettings
{
public string IpAddress { get; set; }
public int RackNumber { get; set; }
public int CpuMpiAddress { get; set; }
public int Port { get; set; }
}
}
public int CpuMpiAddress { get; set; }
public string IpAddress { get; set; }
public int Port { get; set; }
public int RackNumber { get; set; }
}

View File

@@ -1,38 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<Authors>evopro system engineering AG</Authors>
<Company>evopro system engineering AG</Company>
<Description>Reactive framework for Sharp7, the Ethernet S7 PLC communication suite</Description>
<Description>Reactive framework for Sharp7, the Ethernet S7 PLC communication suite. Handling RFC1006 connections to Siemens S7 300, 1200 and 1500.</Description>
<PackageTags>linqpad-samples</PackageTags>
<PackageProjectUrl>https://github.com/evopro-ag/Sharp7Reactive</PackageProjectUrl>
<PackageLicenseUrl>https://raw.githubusercontent.com/evopro-ag/Sharp7Reactive/master/LICENSE</PackageLicenseUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<!--
CA1848: For improved performance, use the LoggerMessage delegates
CA2254: The logging message template should not vary between calls
CA1859: Change type of field 'xxx' from interface to type for performance reasons
CS1591: Missing XML comment for publicly visible type or member
-->
<NoWarn>$(NoWarn);CA1848;CA2254;CA1859;CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.0.0" />
<PackageReference Include="Sharp7" Version="1.0.50" />
<PackageReference Include="System.Interactive" Version="4.0.0" />
<PackageReference Include="System.Reactive" Version="4.1.0" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Sharp7" Version="1.1.84" />
<PackageReference Include="System.Interactive" Version="6.0.1" />
<PackageReference Include="System.Reactive" Version="6.0.1" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\StringResources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>StringResources.resx</DependentUpon>
</Compile>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\StringResources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>StringResources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<Content Include="linqpad-samples/**/*.*">
<Pack>true</Pack>
<PackagePath>linqpad-samples\;content</PackagePath>
</Content>
</ItemGroup>
</Project>

View File

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

View File

@@ -1,312 +1,274 @@
using System;
using System.Collections.Concurrent;
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;
using Sharp7.Rx.Extensions;
using Sharp7.Rx.Interfaces;
using Sharp7.Rx.Resources;
using Sharp7.Rx.Settings;
namespace Sharp7.Rx
namespace Sharp7.Rx;
internal class Sharp7Connector: IDisposable
{
internal class Sharp7Connector : IS7Connector
private readonly BehaviorSubject<ConnectionState> connectionStateSubject = new(Enums.ConnectionState.Initial);
private readonly int cpuSlotNr;
private readonly CompositeDisposable disposables = new();
private readonly string ipAddress;
private readonly int port;
private readonly int rackNr;
private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new(maxDegreeOfParallelism: 1);
private readonly IVariableNameParser variableNameParser;
private bool disposed;
private S7Client sharp7;
public Sharp7Connector(PlcConnectionSettings settings, IVariableNameParser variableNameParser)
{
private readonly IS7VariableNameParser variableNameParser;
private readonly BehaviorSubject<ConnectionState> connectionStateSubject = new BehaviorSubject<ConnectionState>(Enums.ConnectionState.Initial);
private ConcurrentDictionary<string, S7VariableAddress> s7VariableAddresses = new ConcurrentDictionary<string, S7VariableAddress>();
this.variableNameParser = variableNameParser;
ipAddress = settings.IpAddress;
cpuSlotNr = settings.CpuMpiAddress;
port = settings.Port;
rackNr = settings.RackNumber;
private readonly CompositeDisposable disposables = new CompositeDisposable();
private readonly LimitedConcurrencyLevelTaskScheduler scheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism:1);
private readonly string ipAddress;
private readonly int rackNr;
private readonly int cpuSlotNr;
private readonly int port;
ReconnectDelay = TimeSpan.FromSeconds(5);
}
private S7Client sharp7;
private bool disposed;
public IObservable<ConnectionState> ConnectionState => connectionStateSubject.DistinctUntilChanged().AsObservable();
public ILogger Logger { get; set; }
public async Task<Dictionary<string, byte[]>> ExecuteMultiVarRequest(IEnumerable<string> variableNames)
{
var enumerable = variableNames as string[] ?? variableNames.ToArray();
if (enumerable.IsEmpty())
return new Dictionary<string, byte[]>();
var s7MultiVar = new S7MultiVar(sharp7);
var buffers = enumerable.Select(key => new {VariableName = key, Address = s7VariableAddresses.GetOrAdd(key, s => variableNameParser.Parse(s))})
.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)
{
await EvaluateErrorCode(result);
throw new InvalidOperationException($"Error in MultiVar request for variables: {string.Join(",", enumerable)}");
}
return buffers.ToDictionary(arg => arg.VariableName, arg => arg.Buffer);
}
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;
ReconnectDelay = TimeSpan.FromSeconds(5);
}
public TimeSpan ReconnectDelay { get; set; }
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public async Task<bool> 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 = await EvaluateErrorCode(errorCode);
if (success)
{
connectionStateSubject.OnNext(Enums.ConnectionState.Connected);
return true;
}
}
catch (Exception ex)
{
// TODO:
}
return false;
}
public IObservable<ConnectionState> ConnectionState => connectionStateSubject.DistinctUntilChanged().AsObservable();
public async Task Disconnect()
{
connectionStateSubject.OnNext(Enums.ConnectionState.DisconnectedByUser);
await CloseConnection();
}
public Task InitializeAsync()
{
try
{
sharp7 = new S7Client();
sharp7.PLCPort = this.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);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
disposables.Dispose();
if (sharp7 != null)
{
sharp7.Disconnect();
sharp7 = null;
}
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 async Task<bool> EvaluateErrorCode(int errorCode)
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<bool> Connect()
{
if (sharp7 == null)
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);
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}");
await SetConnectionLostState();
return false;
}
else
{
var errorText = EvaluateErrorCode(errorCode);
Logger.LogError("Failed to establish initial connection: {Error}", errorText);
}
}
private async Task<bool> Reconnect()
catch (Exception ex)
{
await CloseConnection();
return await Connect();
}
private async Task SetConnectionLostState()
{
var state = await connectionStateSubject.FirstAsync();
if (state == Enums.ConnectionState.ConnectionLost) return;
connectionStateSubject.OnNext(Enums.ConnectionState.ConnectionLost);
Logger.LogError(ex, "Failed to establish initial connection.");
}
~Sharp7Connector()
{
Dispose(false);
}
return false;
}
private bool IsConnected => connectionStateSubject.Value == Enums.ConnectionState.Connected;
public async Task<byte[]> ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dBNr, CancellationToken token)
{
EnsureConnectionValid();
public async Task Disconnect()
{
connectionStateSubject.OnNext(Enums.ConnectionState.DisconnectedByUser);
await CloseConnection();
}
var buffer = new byte[bytesToRead];
public async Task<IReadOnlyDictionary<string, byte[]>> ExecuteMultiVarRequest(IReadOnlyList<string> variableNames)
{
if (variableNames.IsEmpty())
return new Dictionary<string, byte[]>();
var area = FromOperand(operand);
var s7MultiVar = new S7MultiVar(sharp7);
var result =
await Task.Factory.StartNew(() => sharp7.ReadArea(area, dBNr, startByteAddress, bytesToRead, S7Consts.S7WLByte, buffer), token, TaskCreationOptions.None, scheduler);
token.ThrowIfCancellationRequested();
if (result != 0)
var buffers = variableNames
.Select(key => new {VariableName = key, Address = variableNameParser.Parse(key)})
.Select(x =>
{
await EvaluateErrorCode(result);
var errorText = this.sharp7.ErrorText(result);
throw new InvalidOperationException($"Error reading {operand}{dBNr}:{startByteAddress}->{bytesToRead} ({errorText})");
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();
var result = await Task.Factory.StartNew(() => s7MultiVar.Read(), CancellationToken.None, TaskCreationOptions.None, scheduler);
EnsureSuccessOrThrow(result, $"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, "S7 driver could not be initialized");
}
return Task.FromResult(true);
}
public async Task<byte[]> ReadBytes(Operand operand, ushort startByteAddress, ushort bytesToRead, ushort dbNo, CancellationToken token)
{
EnsureConnectionValid();
var buffer = new byte[bytesToRead];
var result =
await Task.Factory.StartNew(() => sharp7.ReadArea(operand.ToArea(), dbNo, startByteAddress, bytesToRead, S7WordLength.Byte, buffer), token, TaskCreationOptions.None, scheduler);
token.ThrowIfCancellationRequested();
EnsureSuccessOrThrow(result, $"Error reading {operand}{dbNo}:{startByteAddress}->{bytesToRead}");
return buffer;
}
public async Task WriteBit(Operand operand, ushort startByteAddress, byte bitAdress, bool value, ushort dbNo, 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(), dbNo, offsetStart, 1, S7WordLength.Bit, buffer), token, TaskCreationOptions.None, scheduler);
token.ThrowIfCancellationRequested();
EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress} bit {bitAdress}");
}
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, bytesToWrite, S7WordLength.Byte, data), token, TaskCreationOptions.None, scheduler);
token.ThrowIfCancellationRequested();
EnsureSuccessOrThrow(result, $"Error writing {operand}{dbNo}:{startByteAddress}.{data.Length}");
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
disposables.Dispose();
if (sharp7 != null)
{
sharp7.Disconnect();
sharp7 = null;
}
connectionStateSubject?.OnNext(Enums.ConnectionState.Disposed);
connectionStateSubject?.OnCompleted();
connectionStateSubject?.Dispose();
}
var retBuffer = new byte[bytesToRead];
Array.Copy(buffer, 0, retBuffer, 0, bytesToRead);
return (retBuffer);
}
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<ushort> 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)
{
await EvaluateErrorCode(result);
return (0);
}
return (ushort)(data.Length);
}
public async Task<bool> 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<bool> 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)
{
await EvaluateErrorCode(result);
return (false);
}
return (true);
disposed = true;
}
}
}
private async Task CloseConnection()
{
if (sharp7 == null)
throw new InvalidOperationException("S7 driver is not initialized.");
await Task.Factory.StartNew(() => sharp7.Disconnect(), CancellationToken.None, TaskCreationOptions.None, scheduler);
}
private void EnsureConnectionValid()
{
if (disposed)
throw new ObjectDisposedException(nameof(Sharp7Connector));
if (sharp7 == null)
throw new InvalidOperationException("S7 driver is not initialized.");
if (!IsConnected)
throw new InvalidOperationException("Plc is not connected");
}
private void EnsureSuccessOrThrow(int result, string message)
{
if (result == 0) return;
var errorText = EvaluateErrorCode(result);
var completeMessage = $"{message}: {errorText}";
var additionalErrorText = S7ErrorCodes.GetAdditionalErrorText(result);
if (additionalErrorText != null)
completeMessage += Environment.NewLine + additionalErrorText;
throw new S7CommunicationException(completeMessage, result, errorText);
}
private string EvaluateErrorCode(int errorCode)
{
if (errorCode == 0)
return null;
if (sharp7 == null)
throw new InvalidOperationException("S7 driver is not initialized.");
var errorText = $"0x{errorCode:X}, {sharp7.ErrorText(errorCode)}";
Logger?.LogError($"S7 Error {errorText}");
if (S7ErrorCodes.AssumeConnectionLost(errorCode))
SetConnectionLostState();
return errorText;
}
private async Task<bool> Reconnect()
{
await CloseConnection();
return await Connect();
}
private void SetConnectionLostState()
{
if (connectionStateSubject.Value == Enums.ConnectionState.ConnectionLost) return;
connectionStateSubject.OnNext(Enums.ConnectionState.ConnectionLost);
}
~Sharp7Connector()
{
Dispose(false);
}
}

View File

@@ -1,441 +1,370 @@
using System;
using System.Collections.Generic;
using System.Buffers;
using System.Diagnostics;
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;
using System.Reactive.Threading.Tasks;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Sharp7.Rx.Basics;
using Sharp7.Rx.Enums;
using Sharp7.Rx.Extensions;
using Sharp7.Rx.Interfaces;
using Sharp7.Rx.Settings;
using Sharp7.Rx.Utils;
namespace Sharp7.Rx
namespace Sharp7.Rx;
public class Sharp7Plc : IPlc
{
public class Sharp7Plc : IPlc
private static readonly ArrayPool<byte> arrayPool = ArrayPool<byte>.Shared;
private static readonly MethodInfo getValueMethod = typeof(Sharp7Plc).GetMethods()
.Single(m => m.Name == nameof(GetValue) && m.GetGenericArguments().Length == 1);
private static readonly MethodInfo createNotificationMethod = typeof(Sharp7Plc).GetMethods()
.Single(m => m.Name == nameof(CreateNotification) && m.GetGenericArguments().Length == 1);
private readonly ConcurrentSubjectDictionary<string, byte[]> multiVariableSubscriptions = new(StringComparer.InvariantCultureIgnoreCase);
private readonly List<long> 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;
/// <summary>
/// </summary>
/// <param name="ipAddress"></param>
/// <param name="rackNumber"></param>
/// <param name="cpuMpiAddress"></param>
/// <param name="port"></param>
/// <param name="multiVarRequestCycleTime">
/// <para>
/// Polling interval for multi variable read from PLC.
/// </para>
/// <para>
/// This is the wait time between two successive reads from PLC and determines the
/// time resolution for all variable reads related with CreateNotification.
/// </para>
/// <para>
/// Default is 100 ms. The minimum supported time is 5 ms.
/// </para>
/// </param>
public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress, int port = 102, TimeSpan? multiVarRequestCycleTime = null)
{
private readonly string ipAddress;
private readonly int rackNumber;
private readonly int cpuMpiAddress;
private readonly int port;
private readonly IS7VariableNameParser varaibleNameParser;
private bool disposed;
private ISubject<Unit> disposingSubject = new Subject<Unit>();
private IS7Connector s7Connector;
private readonly PlcConnectionSettings plcConnectionSettings;
private readonly ConcurrentSubjectDictionary<string, byte[]> multiVariableSubscriptions = new ConcurrentSubjectDictionary<string, byte[]>(StringComparer.InvariantCultureIgnoreCase);
protected readonly CompositeDisposable Disposables = new CompositeDisposable();
private readonly List<long> performanceCoutner = new List<long>(1000);
plcConnectionSettings = new PlcConnectionSettings {IpAddress = ipAddress, RackNumber = rackNumber, CpuMpiAddress = cpuMpiAddress, Port = port};
s7Connector = new Sharp7Connector(plcConnectionSettings, variableNameParser);
ConnectionState = s7Connector.ConnectionState;
public Sharp7Plc(string ipAddress, int rackNumber, int cpuMpiAddress, int port = 102)
if (multiVarRequestCycleTime != null)
{
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> ConnectionState { get; private set; }
public ILogger Logger { get; set; }
public async Task<bool> 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, TimeSpan.FromMilliseconds(100))
.AddDisposableTo(Disposables);
return true;
}
public Task<TValue> GetValue<TValue>(string variableName)
{
return GetValue<TValue>(variableName, CancellationToken.None);
}
private TValue ConvertToType<TValue>(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<TValue> GetValue<TValue>(string variableName, CancellationToken token)
{
var address = varaibleNameParser.Parse(variableName);
if (address == null) throw new ArgumentException("Input variable name is not valid", nameof(variableName));
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;
}
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)));
}
public Task SetValue<TValue>(string variableName, TValue value)
{
return SetValue(variableName, value, CancellationToken.None);
}
public async Task SetValue<TValue>(string variableName, TValue value, CancellationToken token)
{
var address = varaibleNameParser.Parse(variableName);
if (address == null) throw new ArgumentException("Input variable name is not valid", "variableName");
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();
}
}
if (multiVarRequestCycleTime < TimeSpan.FromMilliseconds(5))
MultiVarRequestCycleTime = TimeSpan.FromMilliseconds(5);
else
MultiVarRequestCycleTime = multiVarRequestCycleTime.Value;
}
}
public IObservable<ConnectionState> ConnectionState { get; }
public ILogger Logger
{
get => s7Connector.Logger;
set => s7Connector.Logger = value;
}
public TimeSpan MultiVarRequestCycleTime { get; } = TimeSpan.FromSeconds(0.1);
public int MultiVarRequestMaxItems { get; set; } = 16;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Create an Observable for a given variable. Multiple notifications are automatically combined into a multi-variable subscription to
/// reduce network trafic and PLC workload.
/// </summary>
/// <typeparam name="TValue"></typeparam>
/// <param name="variableName"></param>
/// <param name="transmissionMode"></param>
/// <returns></returns>
public IObservable<TValue> CreateNotification<TValue>(string variableName, TransmissionMode transmissionMode)
{
return Observable.Create<TValue>(observer =>
{
var address = ParseAndVerify(variableName, typeof(TValue));
var disp = new CompositeDisposable();
var disposableContainer = multiVariableSubscriptions.GetOrCreateObservable(variableName);
disposableContainer.AddDisposableTo(disp);
var observable =
// Read variable with GetValue first.
// This will propagate any errors due to reading from invalid addresses.
Observable.FromAsync(() => GetValue<TValue>(variableName))
.Concat(
disposableContainer.Observable
.Select(bytes => ValueConverter.ReadFromBuffer<TValue>(bytes, address))
);
if (transmissionMode == TransmissionMode.OnChange)
observable = observable.DistinctUntilChanged();
observable.Subscribe(observer)
.AddDisposableTo(disp);
return disp;
});
}
/// <summary>
/// Read PLC variable as generic variable.
/// </summary>
/// <typeparam name="TValue"></typeparam>
/// <param name="variableName"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task<TValue> GetValue<TValue>(string variableName, CancellationToken token = default)
{
var address = ParseAndVerify(variableName, typeof(TValue));
var data = await s7Connector.ReadBytes(address.Operand, address.Start, address.BufferLength, address.DbNo, token);
return ValueConverter.ReadFromBuffer<TValue>(data, address);
}
/// <summary>
/// Read PLC variable as object.
/// The return type is automatically infered from the variable name.
/// </summary>
/// <param name="variableName"></param>
/// <param name="token"></param>
/// <returns>The actual return type is infered from the variable name.</returns>
public async Task<object> GetValue(string variableName, CancellationToken token = default)
{
var address = variableNameParser.Parse(variableName);
var clrType = address.GetClrType();
var genericGetValue = getValueMethod!.MakeGenericMethod(clrType);
var task = genericGetValue.Invoke(this, [variableName, token]) as Task;
await task!;
var taskType = typeof(Task<>).MakeGenericType(clrType);
var propertyInfo = taskType.GetProperty(nameof(Task<object>.Result));
var result = propertyInfo!.GetValue(task);
return result;
}
/// <summary>
/// Write value to the PLC.
/// </summary>
/// <typeparam name="TValue"></typeparam>
/// <param name="variableName"></param>
/// <param name="value"></param>
/// <param name="token"></param>
/// <returns></returns>
public async Task SetValue<TValue>(string variableName, TValue value, CancellationToken token = default)
{
var address = ParseAndVerify(variableName, typeof(TValue));
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!.Value, (bool) (object) value, address.DbNo, token);
}
else
{
var buffer = arrayPool.Rent(address.BufferLength);
try
{
throw new InvalidOperationException($"type '{typeof(TValue)}' not supported.");
ValueConverter.WriteToBuffer(buffer, value, address);
await s7Connector.WriteBytes(address.Operand, address.Start, buffer, address.DbNo, address.BufferLength, token);
}
finally
{
arrayPool.Return(buffer);
}
}
}
public IObservable<TValue> CreateNotification<TValue>(string variableName, TransmissionMode transmissionMode, TimeSpan cycleTime)
/// <summary>
/// Creates an observable of object for a variable.
/// The return type is automatically infered from the variable name.
/// </summary>
/// <param name="variableName"></param>
/// <param name="transmissionMode"></param>
/// <returns>The return type is infered from the variable name.</returns>
public IObservable<object> 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);
}
/// <summary>
/// Trigger PLC connection and start notification loop.
/// <para>
/// This method returns immediately and does not wait for the connection to be established.
/// </para>
/// </summary>
/// <returns>Always true</returns>
[Obsolete($"Use {nameof(InitializeConnection)} or {nameof(TriggerConnection)}.")]
public async Task<bool> InitializeAsync()
{
await TriggerConnection();
return true;
}
/// <summary>
/// Initialize PLC connection and wait for connection to be established.
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public async Task InitializeConnection(CancellationToken token = default) => await DoInitializeConnection(true, token);
/// <summary>
/// Initialize PLC and trigger connection. This method will not wait for the connection to be established.
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public async Task TriggerConnection(CancellationToken token = default) => await DoInitializeConnection(false, token);
protected virtual void Dispose(bool disposing)
{
if (disposed) return;
disposed = true;
if (disposing)
{
return Observable.Create<TValue>(observer =>
notificationSubscription?.Dispose();
notificationSubscription = null;
if (s7Connector != null)
{
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 => ConvertToType<TValue>(bytes, address));
if (transmissionMode == TransmissionMode.OnChange)
observable = observable.DistinctUntilChanged();
observable.Subscribe(observer)
.AddDisposableTo(disposables);
return disposables;
});
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
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;
}
}
disposed = true;
}
}
~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(cycle)
.LogAndRetryAfterDelay(Logger, cycle, "Error while getting batch notifications from plc")
.TakeUntil(disposingSubject)
.Subscribe();
}
private async Task<Unit> 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);
foreach (var pair in multiVarRequest)
{
if (multiVariableSubscriptions.TryGetObserver(pair.Key, out var subject))
{
subject.OnNext(pair.Value);
}
}
s7Connector.Disconnect().Wait();
s7Connector.Dispose();
s7Connector = null;
}
stopWatch.Stop();
performanceCoutner.Add(stopWatch.ElapsedMilliseconds);
multiVariableSubscriptions.Dispose();
}
}
PrintAndResetPerformanceStatistik();
private async Task DoInitializeConnection(bool waitForConnection, CancellationToken token)
{
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<Unit> GetAllValues(Sharp7Connector connector)
{
if (multiVariableSubscriptions.ExistingKeys.IsEmpty())
return Unit.Default;
}
private void PrintAndResetPerformanceStatistik()
var stopWatch = Stopwatch.StartNew();
foreach (var partsOfMultiVarRequest in multiVariableSubscriptions.ExistingKeys.Buffer(MultiVarRequestMaxItems))
{
if (performanceCoutner.Count == performanceCoutner.Capacity)
{
var average = performanceCoutner.Average();
var min = performanceCoutner.Min();
var max = performanceCoutner.Max();
var multiVarRequest = await connector.ExecuteMultiVarRequest(partsOfMultiVarRequest as IReadOnlyList<string>);
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();
}
foreach (var pair in multiVarRequest)
if (multiVariableSubscriptions.TryGetObserver(pair.Key, out var subject))
subject.OnNext(pair.Value);
}
public int MultiVarRequestMaxItems { get; set; } = 16;
stopWatch.Stop();
performanceCounter.Add(stopWatch.ElapsedMilliseconds);
PrintAndResetPerformanceStatistik();
return Unit.Default;
}
private VariableAddress ParseAndVerify(string variableName, Type type)
{
var address = variableNameParser.Parse(variableName);
if (!address.MatchesType(type))
throw new DataTypeMissmatchException($"Address \"{variableName}\" does not match type {type}.", type, address);
return address;
}
private void PrintAndResetPerformanceStatistik()
{
if (performanceCounter.Count == performanceCounter.Capacity)
{
var average = performanceCounter.Average();
var min = performanceCounter.Min();
var max = performanceCounter.Max();
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();
}
}
private void StartNotificationLoop()
{
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()
{
Dispose(false);
}
}

View File

@@ -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<object> ConvertToObjectObservable<T>(IObservable<T> obs) => obs.Select(o => (object) o);
public static IObservable<object> ConvertToObjectObservable(object observable, Type sourceType)
{
var convertGeneric = convertToObjectObservableMethod.MakeGenericMethod(sourceType);
return convertGeneric.Invoke(null, [observable]) as IObservable<object>;
}
}

174
Sharp7.Rx/ValueConverter.cs Normal file
View File

@@ -0,0 +1,174 @@
using System.Buffers.Binary;
using System.Text;
using Sharp7.Rx.Enums;
namespace Sharp7.Rx;
internal static class ValueConverter
{
private static readonly Dictionary<Type, WriteFunc> 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, _, 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, _, 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, _, 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));
switch (address.Type)
{
case DbType.String:
EncodeString(data);
return;
case DbType.WString:
EncodeWString(data);
return;
case DbType.Byte:
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);
}
void EncodeString(Span<byte> 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<byte> 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..]);
}
}
}
};
private static readonly Dictionary<Type, ReadFunc> readFunctions = new()
{
{typeof(bool), (buffer, address) => (buffer[0] >> address.Bit & 1) > 0},
{typeof(byte), (buffer, _) => buffer[0]},
{typeof(byte[]), (buffer, _) => buffer.ToArray()},
{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) =>
{
return address.Type switch
{
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(Span<byte> 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, data[1]);
return Encoding.ASCII.GetString(data.Slice(2, length));
}
string ParseWString(Span<byte> 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 character
var statedStringLength = BinaryPrimitives.ReadUInt16BigEndian(data.Slice(2, 2));
var length = Math.Min(address.Length, statedStringLength) * 2;
return Encoding.BigEndianUnicode.GetString(data.Slice(4, length));
}
}
},
};
public static TValue ReadFromBuffer<TValue>(Span<byte> buffer, VariableAddress address)
{
if (buffer.Length < address.BufferLength)
throw new ArgumentException($"Buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer));
var type = typeof(TValue);
if (!readFunctions.TryGetValue(type, out var readFunc))
throw new UnsupportedS7TypeException($"{type.Name} is not supported. {address}", type, address);
var result = readFunc(buffer, address);
return (TValue) result;
}
public static void WriteToBuffer<TValue>(Span<byte> buffer, TValue value, VariableAddress address)
{
if (buffer.Length < address.BufferLength)
throw new ArgumentException($"Buffer must be at least {address.BufferLength} bytes long for {address}", nameof(buffer));
var type = typeof(TValue);
if (!writeFunctions.TryGetValue(type, out var writeFunc))
throw new UnsupportedS7TypeException($"{type.Name} is not supported. {address}", type, address);
writeFunc(buffer, address, value);
}
private delegate object ReadFunc(Span<byte> data, VariableAddress address);
private delegate void WriteFunc(Span<byte> data, VariableAddress address, object value);
}

View File

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

View File

@@ -0,0 +1,146 @@
#nullable enable
using System.Globalization;
using System.Text.RegularExpressions;
using Sharp7.Rx.Enums;
using Sharp7.Rx.Interfaces;
namespace Sharp7.Rx;
internal class VariableNameParser : IVariableNameParser
{
private static readonly Regex regex = new(@"^(?<operand>db)(?<dbNo>\d+)\.?(?<type>[a-z]+)(?<start>\d+)(\.(?<bitOrLength>\d+))?$",
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly IReadOnlyDictionary<string, DbType> types = new Dictionary<string, DbType>(StringComparer.OrdinalIgnoreCase)
{
{"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},
// 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},
{"s", DbType.String},
{"w", DbType.Int},
{"x", DbType.Bit},
};
public VariableAddress Parse(string input)
{
ArgumentNullException.ThrowIfNull(input);
var match = regex.Match(input);
if (!match.Success)
throw new InvalidS7AddressException($"Invalid S7 address \"{input}\". Expect format \"DB<dbNo>.<type><startByte>(.<length>)\".", input);
var operand = (Operand) Enum.Parse(typeof(Operand), match.Groups["operand"].Value, true);
if (!ushort.TryParse(match.Groups["dbNo"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var dbNr))
throw new InvalidS7AddressException($"\"{match.Groups["dbNo"].Value}\" is an invalid DB number in \"{input}\"", input);
if (!ushort.TryParse(match.Groups["start"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var start))
throw new InvalidS7AddressException($"\"{match.Groups["start"].Value}\" is an invalid start bit in \"{input}\"", input);
if (!types.TryGetValue(match.Groups["type"].Value, out var type))
throw new InvalidS7AddressException($"\"{match.Groups["type"].Value}\" is an invalid type in \"{input}\"", input);
ushort length = type switch
{
DbType.Bit => 1,
DbType.String => GetLength(),
DbType.WString => GetLength(),
DbType.Byte => GetLength(1),
DbType.Int => 2,
DbType.DInt => 4,
DbType.ULInt => 8,
DbType.UInt => 2,
DbType.UDInt => 4,
DbType.LInt => 8,
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;
}
byte? bit = type == DbType.Bit ? GetBit() : null;
var s7VariableAddress = new VariableAddress(Operand: operand, DbNo: dbNr, Type: type, Start: start, 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. Example \"db12.byte10.3\", found \"{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. Example \"db12.bit10.3\", found \"{input}\"", input);
if (!byte.TryParse(match.Groups["bitOrLength"].Value, out var result))
throw new InvalidS7AddressException($"\"{match.Groups["bitOrLength"].Value}\" is an invalid bit number in \"{input}\"", input);
if (result > 7)
throw new InvalidS7AddressException($"Bit must be between 0 and 7 but is {result} in \"{input}\"", input);
return result;
}
}
}

View File

@@ -0,0 +1,34 @@
<Query Kind="Statements">
<NuGetReference Prerelease="true">Sharp7.Rx</NuGetReference>
<Namespace>Sharp7.Rx</Namespace>
<Namespace>System.Reactive.Linq</Namespace>
<Namespace>System.Reactive.Threading.Tasks</Namespace>
<Namespace>System.Threading.Tasks</Namespace>
</Query>
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.InitializeConnection();
"Connection established".Dump();
// create an IObservable
var observable = plc.CreateNotification<short>($"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);
}

View File

@@ -0,0 +1,39 @@
<Query Kind="Statements">
<NuGetReference Prerelease="true">Sharp7.Rx</NuGetReference>
<Namespace>Sharp7.Rx</Namespace>
<Namespace>System.Reactive.Linq</Namespace>
<Namespace>System.Reactive.Threading.Tasks</Namespace>
<Namespace>System.Threading.Tasks</Namespace>
</Query>
// 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.InitializeConnection();
// 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();
}

View File

@@ -0,0 +1,4 @@
Establish connection.linq
Write and read value.linq
Create Notification.linq
Multiple notifications.linq

View File

@@ -0,0 +1,44 @@
<Query Kind="Statements">
<NuGetReference Prerelease="true">Sharp7.Rx</NuGetReference>
<Namespace>Sharp7.Rx</Namespace>
<Namespace>System.Reactive.Linq</Namespace>
<Namespace>System.Reactive.Threading.Tasks</Namespace>
<Namespace>System.Threading.Tasks</Namespace>
</Query>
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.InitializeConnection();
// create an IObservable
plc.CreateNotification<short>($"DB{db}.Int6", Sharp7.Rx.Enums.TransmissionMode.OnChange).Dump("Int 6");
plc.CreateNotification<float>($"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);
}

View File

@@ -0,0 +1,32 @@
<Query Kind="Statements">
<NuGetReference Prerelease="true">Sharp7.Rx</NuGetReference>
<Namespace>Sharp7.Rx</Namespace>
<Namespace>System.Reactive.Linq</Namespace>
<Namespace>System.Reactive.Threading.Tasks</Namespace>
<Namespace>System.Threading.Tasks</Namespace>
</Query>
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.InitializeConnection();
"Connection established".Dump();
for (int i = 0; i < 10; i++)
{
await plc.SetValue($"DB{db}.Int6", (short)i);
var value = await plc.GetValue<short>($"DB{db}.Int6");
value.Dump();
await Task.Delay(200);
}