mirror of
https://github.com/evopro-ag/Sharp7Reactive.git
synced 2025-12-15 11:22:52 +00:00
Compare commits
91 Commits
1.1.8
...
8836c14e2b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8836c14e2b | ||
|
|
e912d088eb | ||
|
|
2655d1ec41 | ||
|
|
f90ad5f9be | ||
|
|
636c56d252 | ||
|
|
f1734ebd73 | ||
|
|
0c1d5067c2 | ||
|
|
18659454f5 | ||
|
|
921360757f | ||
|
|
d9f1ad62b4 | ||
|
|
bdfac4bd22 | ||
|
|
bebbea899d | ||
|
|
bc14f2934b | ||
|
|
fa59a6731d | ||
|
|
e2a278940e | ||
|
|
c3ccdad31c | ||
|
|
57ff276489 | ||
|
|
6917295cf1 | ||
|
|
e52c81683b | ||
|
|
f5a51c074f | ||
|
|
99cf9cbc12 | ||
|
|
0700980117 | ||
|
|
08f572b582 | ||
|
|
b400a7215a | ||
|
|
c5a6b12843 | ||
|
|
478a2060ba | ||
|
|
87d69fb614 | ||
|
|
1672ca906d | ||
|
|
1e52327a3d | ||
|
|
c001ac1ea7 | ||
|
|
332329dc68 | ||
|
|
3338f5566e | ||
|
|
2a5dec3075 | ||
|
|
c5edb4c695 | ||
|
|
096435f4d1 | ||
|
|
b6966cf526 | ||
|
|
9a51a407ec | ||
|
|
9a1d0f70f0 | ||
|
|
56b300b3ab | ||
|
|
996706df2f | ||
|
|
4701a224a7 | ||
|
|
dd0af70262 | ||
|
|
280a894b1f | ||
|
|
bfc9c93c80 | ||
|
|
f3a92addaa | ||
|
|
25bcfea835 | ||
|
|
527d5a094c | ||
|
|
32a7d7cd24 | ||
|
|
b43a595e13 | ||
|
|
5d85823167 | ||
|
|
829dee14af | ||
|
|
fdc25d2817 | ||
|
|
1001303b8c | ||
|
|
3c592c6d46 | ||
|
|
3145426432 | ||
|
|
d1ec075aa7 | ||
|
|
6492d039da | ||
|
|
2a694bf980 | ||
|
|
981a306478 | ||
|
|
4389e81340 | ||
|
|
d4a8ef9cb3 | ||
|
|
3c39996f5e | ||
|
|
c667c113e3 | ||
|
|
1915450115 | ||
|
|
1b7a85c7c5 | ||
|
|
8aaf3c1e7e | ||
|
|
cb6e2d91e0 | ||
|
|
e6cea1e04f | ||
|
|
bacfbf05e2 | ||
|
|
d6dafebfe7 | ||
|
|
662ba7cb6b | ||
|
|
babbb1a6bc | ||
|
|
c79e07be33 | ||
|
|
4432f3c0d4 | ||
|
|
956f39cc66 | ||
|
|
d678924b6e | ||
|
|
e7176c26e7 | ||
|
|
d422249955 | ||
|
|
baef1afac1 | ||
|
|
b87480c4f9 | ||
|
|
f45e2c8b1f | ||
|
|
5c2f0c0a09 | ||
|
|
173b576ad9 | ||
|
|
5b86b3e984 | ||
|
|
ffa4ee6236 | ||
|
|
fe68b1d565 | ||
|
|
81eb5e42aa | ||
|
|
9b0749baae | ||
|
|
49fe1968d9 | ||
|
|
55050dccd6 | ||
|
|
8d8d5617d1 |
20
.editorconfig
Normal file
20
.editorconfig
Normal 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
|
||||
40
.github/workflows/dotnet-core.yml
vendored
40
.github/workflows/dotnet-core.yml
vendored
@@ -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
36
.github/workflows/release.yml
vendored
Normal 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 }}
|
||||
|
||||
83
README.md
83
README.md
@@ -1,6 +1,6 @@
|
||||
# Sharp7Reactive
|
||||
|
||||
[](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/dotnet-core.yml)
|
||||
[](https://github.com/evopro-ag/Sharp7Reactive/actions/workflows/release.yml)
|
||||

|
||||
[](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?
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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 |
|
||||
242
Sharp7.Rx.Tests/ParsingS7VariableName.feature.cs
generated
242
Sharp7.Rx.Tests/ParsingS7VariableName.feature.cs
generated
@@ -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
|
||||
@@ -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>
|
||||
|
||||
25
Sharp7.Rx.Tests/ValueConverterTests/ConvertBothWays.cs
Normal file
25
Sharp7.Rx.Tests/ValueConverterTests/ConvertBothWays.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
90
Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs
Normal file
90
Sharp7.Rx.Tests/ValueConverterTests/ConverterTestBase.cs
Normal 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<T> 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<T> 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}";
|
||||
}
|
||||
}
|
||||
51
Sharp7.Rx.Tests/ValueConverterTests/ReadFromBuffer.cs
Normal file
51
Sharp7.Rx.Tests/ValueConverterTests/ReadFromBuffer.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
76
Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs
Normal file
76
Sharp7.Rx.Tests/ValueConverterTests/WriteToBuffer.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
85
Sharp7.Rx.Tests/VariableAddressTests/MatchesType.cs
Normal file
85
Sharp7.Rx.Tests/VariableAddressTests/MatchesType.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
89
Sharp7.Rx.Tests/VariableNameParserTests.cs
Normal file
89
Sharp7.Rx.Tests/VariableNameParserTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Sharp7.Rx.Tests")]
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
18
Sharp7.Rx/CacheVariableNameParser.cs
Normal file
18
Sharp7.Rx/CacheVariableNameParser.cs
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Sharp7.Rx.Enums
|
||||
{
|
||||
internal enum CpuType
|
||||
{
|
||||
S7_300,
|
||||
S7_400,
|
||||
S7_1200,
|
||||
S7_1500
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
83
Sharp7.Rx/Exceptions/S7Exception.cs
Normal file
83
Sharp7.Rx/Exceptions/S7Exception.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
namespace Sharp7.Rx;
|
||||
|
||||
public abstract class S7Exception : Exception
|
||||
{
|
||||
protected S7Exception(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
protected S7Exception(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class S7CommunicationException : S7Exception
|
||||
{
|
||||
public S7CommunicationException(string message, int s7ErrorCode, string s7ErrorText) : base(message)
|
||||
{
|
||||
S7ErrorCode = s7ErrorCode;
|
||||
S7ErrorText = s7ErrorText;
|
||||
}
|
||||
|
||||
public S7CommunicationException(string message, Exception innerException, int s7ErrorCode, string s7ErrorText) : base(message, innerException)
|
||||
{
|
||||
S7ErrorCode = s7ErrorCode;
|
||||
S7ErrorText = s7ErrorText;
|
||||
}
|
||||
|
||||
public int S7ErrorCode { get; }
|
||||
public string S7ErrorText { get; }
|
||||
}
|
||||
|
||||
public class DataTypeMissmatchException : S7Exception
|
||||
{
|
||||
internal DataTypeMissmatchException(string message, Type type, 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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
16
Sharp7.Rx/Extensions/OperandExtensions.cs
Normal file
16
Sharp7.Rx/Extensions/OperandExtensions.cs
Normal 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)
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
44
Sharp7.Rx/Extensions/S7VariableExtensions.cs
Normal file
44
Sharp7.Rx/Extensions/S7VariableExtensions.cs
Normal 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))
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Sharp7.Rx.Interfaces
|
||||
{
|
||||
internal interface IS7VariableNameParser
|
||||
{
|
||||
S7VariableAddress Parse(string input);
|
||||
}
|
||||
}
|
||||
7
Sharp7.Rx/Interfaces/IVariableNameParser.cs
Normal file
7
Sharp7.Rx/Interfaces/IVariableNameParser.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
#nullable enable
|
||||
namespace Sharp7.Rx.Interfaces;
|
||||
|
||||
internal interface IVariableNameParser
|
||||
{
|
||||
VariableAddress Parse(string input);
|
||||
}
|
||||
117
Sharp7.Rx/Resources/StringResources.Designer.cs
generated
117
Sharp7.Rx/Resources/StringResources.Designer.cs
generated
@@ -1,117 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Sharp7.Rx.Resources {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class StringResources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal StringResources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Sharp7.Rx.Resources.StringResources", typeof(StringResources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to S7 driver could not be initialized.
|
||||
/// </summary>
|
||||
internal static string StrErrorS7DriverCouldNotBeInitialized {
|
||||
get {
|
||||
return ResourceManager.GetString("StrErrorS7DriverCouldNotBeInitialized", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to S7 driver is not initialized..
|
||||
/// </summary>
|
||||
internal static string StrErrorS7DriverNotInitialized {
|
||||
get {
|
||||
return ResourceManager.GetString("StrErrorS7DriverNotInitialized", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to TCP/IP connection established..
|
||||
/// </summary>
|
||||
internal static string StrInfoConnectionEstablished {
|
||||
get {
|
||||
return ResourceManager.GetString("StrInfoConnectionEstablished", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Trying to connect to PLC ({2}) '{0}', CPU slot {1}....
|
||||
/// </summary>
|
||||
internal static string StrInfoTryConnecting {
|
||||
get {
|
||||
return ResourceManager.GetString("StrInfoTryConnecting", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Error while reading data from plc..
|
||||
/// </summary>
|
||||
internal static string StrLogErrorReadingDataFromPlc {
|
||||
get {
|
||||
return ResourceManager.GetString("StrLogErrorReadingDataFromPlc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Communication error discovered. Reconnect is in progress....
|
||||
/// </summary>
|
||||
internal static string StrLogWarningCommunictionErrorReconnecting {
|
||||
get {
|
||||
return ResourceManager.GetString("StrLogWarningCommunictionErrorReconnecting", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="StrLogErrorReadingDataFromPlc" xml:space="preserve">
|
||||
<value>Error while reading data from plc.</value>
|
||||
</data>
|
||||
<data name="StrLogWarningCommunictionErrorReconnecting" xml:space="preserve">
|
||||
<value>Communication error discovered. Reconnect is in progress...</value>
|
||||
</data>
|
||||
<data name="StrErrorS7DriverNotInitialized" xml:space="preserve">
|
||||
<value>S7 driver is not initialized.</value>
|
||||
</data>
|
||||
<data name="StrInfoTryConnecting" xml:space="preserve">
|
||||
<value>Trying to connect to PLC ({2}) '{0}', CPU slot {1}...</value>
|
||||
</data>
|
||||
<data name="StrInfoConnectionEstablished" xml:space="preserve">
|
||||
<value>TCP/IP connection established.</value>
|
||||
</data>
|
||||
<data name="StrErrorS7DriverCouldNotBeInitialized" xml:space="preserve">
|
||||
<value>S7 driver could not be initialized</value>
|
||||
</data>
|
||||
</root>
|
||||
39
Sharp7.Rx/S7ErrorCodes.cs
Normal file
39
Sharp7.Rx/S7ErrorCodes.cs
Normal 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);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
2
Sharp7.Rx/Sharp7.Rx.csproj.DotSettings
Normal file
2
Sharp7.Rx/Sharp7.Rx.csproj.DotSettings
Normal file
@@ -0,0 +1,2 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=exceptions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
21
Sharp7.Rx/Utils/SignatureConverter.cs
Normal file
21
Sharp7.Rx/Utils/SignatureConverter.cs
Normal 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
174
Sharp7.Rx/ValueConverter.cs
Normal 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);
|
||||
}
|
||||
32
Sharp7.Rx/VariableAddress.cs
Normal file
32
Sharp7.Rx/VariableAddress.cs
Normal 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}",
|
||||
};
|
||||
}
|
||||
146
Sharp7.Rx/VariableNameParser.cs
Normal file
146
Sharp7.Rx/VariableNameParser.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Sharp7.Rx/linqpad-samples/Create Notification.linq
Normal file
34
Sharp7.Rx/linqpad-samples/Create Notification.linq
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
39
Sharp7.Rx/linqpad-samples/Establish connection.linq
Normal file
39
Sharp7.Rx/linqpad-samples/Establish connection.linq
Normal 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();
|
||||
}
|
||||
|
||||
|
||||
4
Sharp7.Rx/linqpad-samples/FileOrder.txt
Normal file
4
Sharp7.Rx/linqpad-samples/FileOrder.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
Establish connection.linq
|
||||
Write and read value.linq
|
||||
Create Notification.linq
|
||||
Multiple notifications.linq
|
||||
44
Sharp7.Rx/linqpad-samples/Multiple notifications.linq
Normal file
44
Sharp7.Rx/linqpad-samples/Multiple notifications.linq
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
32
Sharp7.Rx/linqpad-samples/Write and read value.linq
Normal file
32
Sharp7.Rx/linqpad-samples/Write and read value.linq
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user