mirror of
https://github.com/S7NetPlus/s7netplus.git
synced 2026-02-17 22:38:27 +08:00
Compare commits
295 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab6308eacd | ||
|
|
130eeadbd8 | ||
|
|
76a7ea04f7 | ||
|
|
4764c997ed | ||
|
|
cb24e9a046 | ||
|
|
10315b4b4c | ||
|
|
0774e124bf | ||
|
|
1ebffe08e7 | ||
|
|
f419df4d73 | ||
|
|
1969aac1b2 | ||
|
|
2f2dcf7281 | ||
|
|
07325db2fa | ||
|
|
eada47cd24 | ||
|
|
5e1ac8c7bf | ||
|
|
13544a1bcf | ||
|
|
6fc526b886 | ||
|
|
f227ad4b53 | ||
|
|
e4cc42fa51 | ||
|
|
689e7ffd96 | ||
|
|
8087b8d315 | ||
|
|
a55ceba679 | ||
|
|
eb1fad9333 | ||
|
|
0de9364dee | ||
|
|
9380ea85c3 | ||
|
|
22451bc440 | ||
|
|
e98ce005c5 | ||
|
|
11a40cc5e3 | ||
|
|
f79286b2d0 | ||
|
|
fadd7d0cb3 | ||
|
|
652ff3a9bb | ||
|
|
9c0fea721a | ||
|
|
2ec73224c1 | ||
|
|
a8ef47b475 | ||
|
|
55aa06a1fc | ||
|
|
7e631a713f | ||
|
|
0797c5858f | ||
|
|
f1ae0ea084 | ||
|
|
addf6068bb | ||
|
|
970e9d4395 | ||
|
|
c3934c3493 | ||
|
|
e5823f2806 | ||
|
|
97e27ccc2b | ||
|
|
9b1faa0123 | ||
|
|
54dadec75a | ||
|
|
8b8ad13464 | ||
|
|
714ac62ab1 | ||
|
|
088cd0a4a8 | ||
|
|
361db8be9d | ||
|
|
e26860b0c0 | ||
|
|
6e103cea63 | ||
|
|
c5023c10e4 | ||
|
|
b61ac32913 | ||
|
|
b27e1c9083 | ||
|
|
71f7f8b400 | ||
|
|
4aca9e4e53 | ||
|
|
0bb7c5351a | ||
|
|
c3f86c32a2 | ||
|
|
3d0dd693ba | ||
|
|
8ad25033d5 | ||
|
|
12e180ea2d | ||
|
|
5891a30c5d | ||
|
|
b3077b27e7 | ||
|
|
8126018afd | ||
|
|
4e4071f07f | ||
|
|
534d9fd69d | ||
|
|
8da292ad2f | ||
|
|
019aeb26dc | ||
|
|
670fb70b78 | ||
|
|
aa15145184 | ||
|
|
12ea402769 | ||
|
|
18402604d1 | ||
|
|
53f651a482 | ||
|
|
7558b9a691 | ||
|
|
3185d1fccf | ||
|
|
0d9ccea11b | ||
|
|
1fc6899905 | ||
|
|
18c3883dc0 | ||
|
|
1f26833244 | ||
|
|
7d212134e3 | ||
|
|
38b26e0ce1 | ||
|
|
cf94f8ad11 | ||
|
|
8becc562a8 | ||
|
|
296ead69c7 | ||
|
|
ebf3da6280 | ||
|
|
42194aa788 | ||
|
|
9c8b453326 | ||
|
|
49e4d3369a | ||
|
|
ee06bec0fb | ||
|
|
05ccb05f3a | ||
|
|
0d2817661e | ||
|
|
e869d19587 | ||
|
|
e7194bc470 | ||
|
|
5bc2c6c5e7 | ||
|
|
77dcb1778b | ||
|
|
14053e342a | ||
|
|
ab3bd87701 | ||
|
|
bc7c27e1d4 | ||
|
|
f0256fd0cb | ||
|
|
209148ab02 | ||
|
|
2fc9eaade3 | ||
|
|
ab70bfb041 | ||
|
|
e277cf6e6c | ||
|
|
43b29825a4 | ||
|
|
6aa0133081 | ||
|
|
868e719b78 | ||
|
|
3833d29c0e | ||
|
|
144f814794 | ||
|
|
82aaf7e2cb | ||
|
|
f47918946d | ||
|
|
142f1ba90e | ||
|
|
d99d0d0e6f | ||
|
|
ce9f9f9e08 | ||
|
|
ea1140314b | ||
|
|
e3fad0b94f | ||
|
|
7f76d4fc5a | ||
|
|
ec554ddb59 | ||
|
|
2ecd2c6b49 | ||
|
|
d808ea5eb6 | ||
|
|
5d3f01e59e | ||
|
|
9c3f95ce73 | ||
|
|
12281ec802 | ||
|
|
8df1a9c8cb | ||
|
|
946536c2d6 | ||
|
|
b475aee2e7 | ||
|
|
82a745c972 | ||
|
|
74fecad48d | ||
|
|
37c63cea8e | ||
|
|
36c4564f5e | ||
|
|
0c0200d12d | ||
|
|
6e861d2f00 | ||
|
|
372f4a5dcb | ||
|
|
0d87dbf3c6 | ||
|
|
15f94cd7bf | ||
|
|
36b59a2926 | ||
|
|
8d081859da | ||
|
|
2302819650 | ||
|
|
d65b83660f | ||
|
|
77b2ecfd45 | ||
|
|
b0a6a2375f | ||
|
|
14823bca96 | ||
|
|
53045c5952 | ||
|
|
677d2941e1 | ||
|
|
66b693676c | ||
|
|
4542bbedb2 | ||
|
|
a4b6a360fe | ||
|
|
0bef6bc9ff | ||
|
|
cbe04fbfb4 | ||
|
|
0b4c79cb08 | ||
|
|
5c24e801fd | ||
|
|
bcde65120c | ||
|
|
4541a7ebb7 | ||
|
|
fc9c33fdaf | ||
|
|
a23408d67e | ||
|
|
adb55dc80f | ||
|
|
2fae2c01d5 | ||
|
|
6465e3c8c7 | ||
|
|
fcd61c1236 | ||
|
|
616dc1094c | ||
|
|
70bc1499ef | ||
|
|
fd9aeb5b3b | ||
|
|
0750ee006f | ||
|
|
5318f94dd7 | ||
|
|
ea3beff481 | ||
|
|
f67b1e773f | ||
|
|
e93a656312 | ||
|
|
8035f71a16 | ||
|
|
df4f258290 | ||
|
|
5636b93a53 | ||
|
|
8ed1b840bc | ||
|
|
2afed88231 | ||
|
|
1ded47971b | ||
|
|
ced10b4eca | ||
|
|
632e1c14ac | ||
|
|
aa50280233 | ||
|
|
3a794e8a46 | ||
|
|
0b8bd66bf7 | ||
|
|
e66d21af05 | ||
|
|
44ee651ac4 | ||
|
|
a1b4694ef6 | ||
|
|
d10c15b80f | ||
|
|
5225c8bffd | ||
|
|
aa03400350 | ||
|
|
2b4ec6d9dd | ||
|
|
54f3de6c9f | ||
|
|
e6d14587d3 | ||
|
|
e63d92c61c | ||
|
|
b4b94e1777 | ||
|
|
13c25fc20b | ||
|
|
fff6f3458f | ||
|
|
82e29837a2 | ||
|
|
1afb07774b | ||
|
|
051091919f | ||
|
|
478c1aed52 | ||
|
|
924eb9c48f | ||
|
|
eb8e188c86 | ||
|
|
37384d2a92 | ||
|
|
fdd4519f64 | ||
|
|
52c60f6eaf | ||
|
|
926d74f1d2 | ||
|
|
9b89acfb91 | ||
|
|
de0a9e64dc | ||
|
|
dfcc4c7408 | ||
|
|
c9a98fba95 | ||
|
|
40edecad43 | ||
|
|
0b04c86cb9 | ||
|
|
f03ba93a96 | ||
|
|
9cd63d906f | ||
|
|
d051b93bdc | ||
|
|
ae70f31af2 | ||
|
|
fb44b56c16 | ||
|
|
ce97fcf335 | ||
|
|
81208c0f03 | ||
|
|
c9bab7523a | ||
|
|
a7608e3cb7 | ||
|
|
4d37679c75 | ||
|
|
e2a0ed548d | ||
|
|
4124bae1bc | ||
|
|
2bcc5e6b9c | ||
|
|
33981ab4f9 | ||
|
|
de60a7b6b0 | ||
|
|
de87409458 | ||
|
|
ca89736c7c | ||
|
|
4a72c3596b | ||
|
|
786e012179 | ||
|
|
f46833606f | ||
|
|
b95b71e9aa | ||
|
|
1069641606 | ||
|
|
36a9ecb2c8 | ||
|
|
243e868488 | ||
|
|
ff65f06b7d | ||
|
|
065b1fbdf8 | ||
|
|
8f3c701a2f | ||
|
|
023530322e | ||
|
|
9198fc1686 | ||
|
|
12a2e3c0b1 | ||
|
|
b088fe276b | ||
|
|
80ad95372b | ||
|
|
106e9912ab | ||
|
|
af39659944 | ||
|
|
e5bdb10ce3 | ||
|
|
faea428e4c | ||
|
|
4da76c7f6d | ||
|
|
6c4d4605f0 | ||
|
|
730ccbf9fc | ||
|
|
b36c4a98ec | ||
|
|
70c3f8e996 | ||
|
|
cf1b71220a | ||
|
|
88c45bd995 | ||
|
|
cf493d47f0 | ||
|
|
9e2f17fdf3 | ||
|
|
1919b0083a | ||
|
|
fd4bc0fe84 | ||
|
|
9ff73ff3f7 | ||
|
|
c31353bed2 | ||
|
|
b16097092b | ||
|
|
eca2ed6474 | ||
|
|
b92242f911 | ||
|
|
c99c3d745a | ||
|
|
3b2dbd1148 | ||
|
|
64c781ec8b | ||
|
|
e8a9983367 | ||
|
|
3c91aa02b0 | ||
|
|
edfa208c3e | ||
|
|
d11f46eedb | ||
|
|
09c8b18d3d | ||
|
|
10e5562706 | ||
|
|
7d570f93c1 | ||
|
|
c79ae13ea1 | ||
|
|
09851ec30b | ||
|
|
592d21c3aa | ||
|
|
d530f1e422 | ||
|
|
ba3dd084cb | ||
|
|
a047c5bba4 | ||
|
|
2bb7ac7d2a | ||
|
|
783c456dc9 | ||
|
|
688d4e2a28 | ||
|
|
bb0b57c574 | ||
|
|
2f07d43062 | ||
|
|
4ef037881c | ||
|
|
fbd8a13c6c | ||
|
|
2a451bc049 | ||
|
|
324ae95c42 | ||
|
|
bd8177d39e | ||
|
|
e68ca64596 | ||
|
|
647d4c7ae2 | ||
|
|
c7ef055be2 | ||
|
|
7035d22506 | ||
|
|
fc6781c37f | ||
|
|
3555436c04 | ||
|
|
3258c84fbc | ||
|
|
28257f28b3 | ||
|
|
a1d87de2d9 | ||
|
|
4be5765fc9 | ||
|
|
6614c7330a | ||
|
|
5d59c8284d |
134
.github/workflows/dotnet.yml
vendored
Normal file
134
.github/workflows/dotnet.yml
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
|
||||
name: .NET
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow running the workflow manually from the GitHub UI
|
||||
push:
|
||||
branches:
|
||||
- 'main' # Run the workflow when pushing to the main branch
|
||||
pull_request:
|
||||
branches:
|
||||
- '*' # Run the workflow for all pull requests
|
||||
release:
|
||||
types:
|
||||
- published # Run the workflow when a new GitHub release is published
|
||||
|
||||
env:
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
|
||||
DOTNET_NOLOGO: true
|
||||
NuGetDirectory: ${{ github.workspace}}/nuget
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: pwsh
|
||||
|
||||
jobs:
|
||||
create_nuget:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Get all history to allow automatic versioning
|
||||
|
||||
- name: Install GitVersion
|
||||
uses: gittools/actions/gitversion/setup@v0
|
||||
with:
|
||||
versionSpec: '6.x'
|
||||
includePrerelease: true
|
||||
preferLatestVersion: true
|
||||
|
||||
- name: Determine Version
|
||||
id: gitversion
|
||||
uses: gittools/actions/gitversion/execute@v0
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
|
||||
- run: >
|
||||
dotnet pack
|
||||
--configuration Release
|
||||
/p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }}
|
||||
/p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }}
|
||||
/p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
|
||||
/p:PackageVersion=${{ steps.gitversion.outputs.semVer }}
|
||||
--output ${{ env.NuGetDirectory }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: nuget
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
path: |
|
||||
${{ env.NuGetDirectory }}/*.nupkg
|
||||
${{ env.NuGetDirectory }}/*.snupkg
|
||||
|
||||
run_test:
|
||||
name: test-${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
configuration: Release
|
||||
artifacts: ${{ github.workspace }}/artifacts
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-20.04, macos-latest]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Snap7 Linux
|
||||
if: ${{ matrix.os == 'ubuntu-20.04' }}
|
||||
run: |
|
||||
sudo add-apt-repository ppa:gijzelaar/snap7
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsnap7-1 libsnap7-dev
|
||||
|
||||
- name: Install Snap7 MacOs
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
run: |
|
||||
brew install snap7
|
||||
|
||||
- name: Setup Dotnet
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: |
|
||||
6.x
|
||||
7.x
|
||||
|
||||
- name: Nuget Cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
# Look to see if there is a cache hit for the corresponding requirements file
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --nologo --verbosity normal --logger GitHubActions
|
||||
|
||||
deploy:
|
||||
# Publish only when creating a GitHub Release
|
||||
# https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository
|
||||
# You can update this logic if you want to manage releases differently
|
||||
if: github.event_name == 'release'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ create_nuget, run_test ]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: nuget
|
||||
path: ${{ env.NuGetDirectory }}
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3
|
||||
|
||||
# Publish all NuGet packages to NuGet.org
|
||||
# Use --skip-duplicate to prevent errors if a package with the same version already exists.
|
||||
# If you retry a failed workflow, already published packages will be skipped without error.
|
||||
- name: Publish NuGet package
|
||||
run: |
|
||||
foreach($file in (Get-ChildItem "${{ env.NuGetDirectory }}" -Recurse -Include *.nupkg)) {
|
||||
dotnet nuget push $file --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
assembly-informational-format: '{NuGetVersion}'
|
||||
mode: ContinuousDeployment
|
||||
branches:
|
||||
master:
|
||||
tag: rc
|
||||
increment: Minor
|
||||
feature:
|
||||
regex: features?[/-]
|
||||
tag: rc-{BranchName}
|
||||
increment: Minor
|
||||
pull-request:
|
||||
regex: (pull|pull\-requests|pr)[/-]
|
||||
tag: rc-pr-{BranchName}
|
||||
increment: Minor
|
||||
hotfix:
|
||||
regex: hotfix(es)?[/-]
|
||||
tag: rc
|
||||
increment: Patch
|
||||
develop:
|
||||
regex: dev(elop)?(ment)?$
|
||||
tag: b
|
||||
@@ -12,8 +12,6 @@ to my request for committing code, I decided to pick up where he left off here o
|
||||
## Documentation
|
||||
Check the Wiki and feel free to edit it: https://github.com/killnine/s7netplus/wiki
|
||||
|
||||
S7.Net Plus has a [User Manual](https://github.com/killnine/s7netplus/blob/master/Documentation/Documentation.pdf), check it out.
|
||||
|
||||
## Supported PLC
|
||||
|
||||
+ Compatible S7 PLC (S7-200, S7-300, S7-400, S7-1200, S7-1500)
|
||||
@@ -36,6 +34,5 @@ PM> Install-Package S7netplus
|
||||
|
||||
## Running the tests
|
||||
|
||||
Unit tests use Snap7 server, so port 102 must be not in use.
|
||||
If you have Siemens Step7 installed, the service s7oiehsx64 is stopped when running unit tests.
|
||||
You have to restart the service manually if you need it.
|
||||
Unit tests use Snap7 server.
|
||||
On Windows, the DLL is included with the test project. On other platforms, Snap7 must be installed manually before running tests.
|
||||
|
||||
259
S7.Net.UnitTest/CommunicationTests/Clock.cs
Normal file
259
S7.Net.UnitTest/CommunicationTests/Clock.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using S7.Net.Protocol;
|
||||
|
||||
namespace S7.Net.UnitTest.CommunicationTests;
|
||||
|
||||
[TestClass]
|
||||
public class Clock
|
||||
{
|
||||
[TestMethod, Timeout(1000)]
|
||||
public async Task Read_Clock_Value()
|
||||
{
|
||||
var cs = new CommunicationSequence
|
||||
{
|
||||
ConnectionOpenTemplates.ConnectionRequestConfirm,
|
||||
ConnectionOpenTemplates.CommunicationSetup,
|
||||
{
|
||||
"""
|
||||
// TPKT
|
||||
03 00 00 1d
|
||||
|
||||
// COTP
|
||||
02 f0 80
|
||||
|
||||
// S7 read clock
|
||||
// UserData header
|
||||
32 07 00 00 PDU1 PDU2
|
||||
// Parameter length
|
||||
00 08
|
||||
// Data length
|
||||
00 04
|
||||
|
||||
// Parameter
|
||||
// Head
|
||||
00 01 12
|
||||
// Length
|
||||
04
|
||||
// Method (Request/Response): Req
|
||||
11
|
||||
// Type request (4...) Function group timers (...7)
|
||||
47
|
||||
// Subfunction: read clock
|
||||
01
|
||||
// Sequence number
|
||||
00
|
||||
|
||||
// Data
|
||||
// Return code
|
||||
0a
|
||||
// Transport size
|
||||
00
|
||||
// Payload length
|
||||
00 00
|
||||
""",
|
||||
"""
|
||||
// TPKT
|
||||
03 00 00 2b
|
||||
|
||||
// COTP
|
||||
02 f0 80
|
||||
|
||||
// S7 read clock response
|
||||
// UserData header
|
||||
32 07 00 00 PDU1 PDU2
|
||||
// Parameter length
|
||||
00 0c
|
||||
// Data length
|
||||
00 0e
|
||||
|
||||
// Parameter
|
||||
// Head
|
||||
00 01 12
|
||||
// Length
|
||||
08
|
||||
// Method (Request/Response): Res
|
||||
12
|
||||
// Type response (8...) Function group timers (...7)
|
||||
87
|
||||
// Subfunction: read clock
|
||||
01
|
||||
// Sequence number
|
||||
01
|
||||
// Data unit reference
|
||||
00
|
||||
// Last data unit? Yes
|
||||
00
|
||||
// Error code
|
||||
00 00
|
||||
|
||||
// Data
|
||||
// Error code
|
||||
ff
|
||||
// Transport size: OCTET STRING
|
||||
09
|
||||
// Length
|
||||
00 0a
|
||||
|
||||
// Timestamp
|
||||
// Reserved
|
||||
00
|
||||
// Year 1
|
||||
19
|
||||
// Year 2
|
||||
14
|
||||
// Month
|
||||
08
|
||||
// Day
|
||||
20
|
||||
// Hour
|
||||
11
|
||||
// Minute
|
||||
59
|
||||
// Seconds
|
||||
43
|
||||
// Milliseconds: 912..., Day of week: ...4
|
||||
91 24
|
||||
"""
|
||||
}
|
||||
};
|
||||
|
||||
static async Task Client(int port)
|
||||
{
|
||||
var conn = new Plc(IPAddress.Loopback.ToString(), port, new TsapPair(new Tsap(1, 2), new Tsap(3, 4)));
|
||||
await conn.OpenAsync();
|
||||
var time = await conn.ReadClockAsync();
|
||||
|
||||
Assert.AreEqual(new DateTime(2014, 8, 20, 11, 59, 43, 912), time);
|
||||
conn.Close();
|
||||
}
|
||||
|
||||
await Task.WhenAll(cs.Serve(out var port), Client(port));
|
||||
}
|
||||
|
||||
[TestMethod, Timeout(1000)]
|
||||
public async Task Write_Clock_Value()
|
||||
{
|
||||
var cs = new CommunicationSequence
|
||||
{
|
||||
ConnectionOpenTemplates.ConnectionRequestConfirm,
|
||||
ConnectionOpenTemplates.CommunicationSetup,
|
||||
{
|
||||
"""
|
||||
// TPKT
|
||||
03 00 00 27
|
||||
|
||||
// COTP
|
||||
02 f0 80
|
||||
|
||||
// S7 read clock
|
||||
// UserData header
|
||||
32 07 00 00 PDU1 PDU2
|
||||
// Parameter length
|
||||
00 08
|
||||
// Data length
|
||||
00 0e
|
||||
|
||||
// Parameter
|
||||
// Head
|
||||
00 01 12
|
||||
// Length
|
||||
04
|
||||
// Method (Request/Response): Req
|
||||
11
|
||||
// Type request (4...) Function group timers (...7)
|
||||
47
|
||||
// Subfunction: write clock
|
||||
02
|
||||
// Sequence number
|
||||
00
|
||||
|
||||
// Data
|
||||
// Return code
|
||||
ff
|
||||
// Transport size
|
||||
09
|
||||
// Payload length
|
||||
00 0a
|
||||
|
||||
// Payload
|
||||
// Timestamp
|
||||
// Reserved
|
||||
00
|
||||
// Year 1
|
||||
19
|
||||
// Year 2
|
||||
14
|
||||
// Month
|
||||
08
|
||||
// Day
|
||||
20
|
||||
// Hour
|
||||
11
|
||||
// Minute
|
||||
59
|
||||
// Seconds
|
||||
43
|
||||
// Milliseconds: 912..., Day of week: ...4
|
||||
91 24
|
||||
""",
|
||||
"""
|
||||
// TPKT
|
||||
03 00 00 21
|
||||
|
||||
// COTP
|
||||
02 f0 80
|
||||
|
||||
// S7 read clock response
|
||||
// UserData header
|
||||
32 07 00 00 PDU1 PDU2
|
||||
// Parameter length
|
||||
00 0c
|
||||
// Data length
|
||||
00 04
|
||||
|
||||
// Parameter
|
||||
// Head
|
||||
00 01 12
|
||||
// Length
|
||||
08
|
||||
// Method (Request/Response): Res
|
||||
12
|
||||
// Type response (8...) Function group timers (...7)
|
||||
87
|
||||
// Subfunction: write clock
|
||||
02
|
||||
// Sequence number
|
||||
01
|
||||
// Data unit reference
|
||||
00
|
||||
// Last data unit? Yes
|
||||
00
|
||||
// Error code
|
||||
00 00
|
||||
|
||||
// Data
|
||||
// Error code
|
||||
0a
|
||||
// Transport size: NONE
|
||||
00
|
||||
// Length
|
||||
00 00
|
||||
"""
|
||||
}
|
||||
};
|
||||
|
||||
static async Task Client(int port)
|
||||
{
|
||||
var conn = new Plc(IPAddress.Loopback.ToString(), port, new TsapPair(new Tsap(1, 2), new Tsap(3, 4)));
|
||||
await conn.OpenAsync();
|
||||
await conn.WriteClockAsync(new DateTime(2014, 08, 20, 11, 59, 43, 912));
|
||||
|
||||
conn.Close();
|
||||
}
|
||||
|
||||
await Task.WhenAll(cs.Serve(out var port), Client(port));
|
||||
}
|
||||
}
|
||||
28
S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs
Normal file
28
S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using S7.Net.Protocol;
|
||||
|
||||
namespace S7.Net.UnitTest.CommunicationTests;
|
||||
|
||||
[TestClass]
|
||||
public class ConnectionOpen
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task Does_Not_Throw()
|
||||
{
|
||||
var cs = new CommunicationSequence {
|
||||
ConnectionOpenTemplates.ConnectionRequestConfirm,
|
||||
ConnectionOpenTemplates.CommunicationSetup
|
||||
};
|
||||
|
||||
async Task Client(int port)
|
||||
{
|
||||
var conn = new Plc(IPAddress.Loopback.ToString(), port, new TsapPair(new Tsap(1, 2), new Tsap(3, 4)));
|
||||
await conn.OpenAsync();
|
||||
conn.Close();
|
||||
}
|
||||
|
||||
await Task.WhenAll(cs.Serve(out var port), Client(port));
|
||||
}
|
||||
}
|
||||
107
S7.Net.UnitTest/CommunicationTests/ConnectionOpenTemplates.cs
Normal file
107
S7.Net.UnitTest/CommunicationTests/ConnectionOpenTemplates.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
namespace S7.Net.UnitTest.CommunicationTests;
|
||||
|
||||
internal static class ConnectionOpenTemplates
|
||||
{
|
||||
public static RequestResponsePair ConnectionRequestConfirm { get; } = new RequestResponsePair(
|
||||
"""
|
||||
// TPKT
|
||||
03 // Version
|
||||
00 // Reserved
|
||||
00 16 // Length
|
||||
|
||||
// CR
|
||||
11 // Number of bytes following
|
||||
E0 // CR / Credit
|
||||
00 00 // Destination reference, unused
|
||||
__ __ // Source reference, unused
|
||||
00 // Class / Option
|
||||
|
||||
// Source TSAP
|
||||
C1 // Parameter code
|
||||
02 // Parameter length
|
||||
TSAP_SRC_CHAN // Channel
|
||||
TSAP_SRC_POS // Position
|
||||
|
||||
// Destination TSAP
|
||||
C2 // Parameter code
|
||||
02 // Parameter length
|
||||
TSAP_DEST_CHAN // Channel
|
||||
TSAP_DEST_POS // Position
|
||||
|
||||
// PDU Size parameter
|
||||
C0 // Parameter code
|
||||
01 // Parameter length
|
||||
0A // 1024 byte PDU (2 ^ 10)
|
||||
""",
|
||||
"""
|
||||
// TPKT
|
||||
03 // Version
|
||||
00 // Reserved
|
||||
00 0B // Length
|
||||
|
||||
// CC
|
||||
06 // Length
|
||||
D0 // CC / Credit
|
||||
00 00 // Destination reference
|
||||
00 00 // Source reference
|
||||
00 // Class / Option
|
||||
"""
|
||||
);
|
||||
|
||||
public static RequestResponsePair CommunicationSetup { get; } = new RequestResponsePair(
|
||||
"""
|
||||
// TPKT
|
||||
03 // Version
|
||||
00 // Reserved
|
||||
00 19 // Length
|
||||
|
||||
// Data header
|
||||
02 // Length
|
||||
F0 // Data identifier
|
||||
80 // PDU number and end of transmission
|
||||
|
||||
// S7 header
|
||||
32 // Protocol ID
|
||||
01 // Message type job request
|
||||
00 00 // Reserved
|
||||
PDU1 PDU2 // PDU reference
|
||||
00 08 // Parameter length (Communication Setup)
|
||||
00 00 // Data length
|
||||
|
||||
// Communication Setup
|
||||
F0 // Function code
|
||||
00 // Reserved
|
||||
00 03 // Max AMQ caller
|
||||
00 03 // Max AMQ callee
|
||||
03 C0 // PDU size (960)
|
||||
""",
|
||||
"""
|
||||
// TPKT
|
||||
03 // Version
|
||||
00 // Reserved
|
||||
00 1B // Length
|
||||
|
||||
// Data header
|
||||
02 // Length
|
||||
F0 // Data identifier
|
||||
80 // PDU number and end of transmission
|
||||
|
||||
// S7 header
|
||||
32 // Protocol ID
|
||||
03 // Message type ack data
|
||||
00 00 // Reserved
|
||||
PDU1 PDU2 // PDU reference
|
||||
00 08 // Parameter length (Communication Setup)
|
||||
00 00 // Data length
|
||||
00 // Error class
|
||||
00 // Error code
|
||||
|
||||
// Communication Setup
|
||||
F0 // Function code
|
||||
00 // Reserved
|
||||
00 03 // Max AMQ caller
|
||||
00 03 // Max AMQ callee
|
||||
03 C0 // PDU size (960)
|
||||
"""
|
||||
);
|
||||
}
|
||||
57
S7.Net.UnitTest/CommunicationTests/ReadPlcStatus.cs
Normal file
57
S7.Net.UnitTest/CommunicationTests/ReadPlcStatus.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using S7.Net.Protocol;
|
||||
|
||||
namespace S7.Net.UnitTest.CommunicationTests;
|
||||
|
||||
[TestClass]
|
||||
public class ReadPlcStatus
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task Read_Status_Run()
|
||||
{
|
||||
var cs = new CommunicationSequence {
|
||||
ConnectionOpenTemplates.ConnectionRequestConfirm,
|
||||
ConnectionOpenTemplates.CommunicationSetup,
|
||||
{
|
||||
"""
|
||||
// TPKT
|
||||
03 00 00 21
|
||||
|
||||
// COTP
|
||||
02 f0 80
|
||||
|
||||
// S7 SZL read
|
||||
32 07 00 00 PDU1 PDU2 00 08 00 08 00 01 12 04 11 44
|
||||
01 00 ff 09 00 04 04 24 00 00
|
||||
""",
|
||||
"""
|
||||
// TPKT
|
||||
03 00 00 3d
|
||||
|
||||
// COTP
|
||||
02 f0 80
|
||||
|
||||
// S7 SZL response
|
||||
32 07 00 00 PDU1 PDU2 00 0c 00 20 00 01 12 08 12 84
|
||||
01 02 00 00 00 00 ff 09 00 1c 04 24 00 00 00 14
|
||||
00 01 51 44 ff 08 00 00 00 00 00 00 00 00 14 08
|
||||
20 12 05 28 34 94
|
||||
"""
|
||||
}
|
||||
};
|
||||
|
||||
async Task Client(int port)
|
||||
{
|
||||
var conn = new Plc(IPAddress.Loopback.ToString(), port, new TsapPair(new Tsap(1, 2), new Tsap(3, 4)));
|
||||
await conn.OpenAsync();
|
||||
var status = await conn.ReadStatusAsync();
|
||||
|
||||
Assert.AreEqual(0x08, status);
|
||||
conn.Close();
|
||||
}
|
||||
|
||||
await Task.WhenAll(cs.Serve(out var port), Client(port));
|
||||
}
|
||||
}
|
||||
181
S7.Net.UnitTest/ConnectionCloseTest.cs
Normal file
181
S7.Net.UnitTest/ConnectionCloseTest.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace S7.Net.UnitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Test stream which only gives 1 byte per read.
|
||||
/// </summary>
|
||||
class TestStreamConnectionClose : Stream
|
||||
{
|
||||
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
public TestStreamConnectionClose(CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
_cancellationTokenSource = cancellationTokenSource;
|
||||
}
|
||||
public override bool CanRead => false;
|
||||
|
||||
public override bool CanSeek => throw new NotImplementedException();
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override long Length => throw new NotImplementedException();
|
||||
|
||||
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// These tests are intended to test <see cref="StreamExtensions"/> functions and other stream-related special cases.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class ConnectionCloseTest
|
||||
{
|
||||
const short TestServerPort = 31122;
|
||||
const string TestServerIp = "127.0.0.1";
|
||||
|
||||
[TestMethod]
|
||||
public async Task Test_CancellationDuringTransmission()
|
||||
{
|
||||
var plc = new Plc(CpuType.S7300, TestServerIp, TestServerPort, 0, 2);
|
||||
|
||||
// Set up a shared cancellation source so we can let the stream
|
||||
// initiate cancel after some data has been written to it.
|
||||
var cancellationSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationSource.Token;
|
||||
|
||||
var stream = new TestStreamConnectionClose(cancellationSource);
|
||||
var requestData = new byte[100]; // empty data, it does not matter what is in there
|
||||
|
||||
// Set up access to private method and field
|
||||
var dynMethod = plc.GetType().GetMethod("NoLockRequestTpduAsync",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (dynMethod == null)
|
||||
{
|
||||
throw new NullReferenceException("Could not find method 'NoLockRequestTpduAsync' on Plc object.");
|
||||
}
|
||||
var tcpClientField = plc.GetType().GetField("tcpClient", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (tcpClientField == null)
|
||||
{
|
||||
throw new NullReferenceException("Could not find field 'tcpClient' on Plc object.");
|
||||
}
|
||||
|
||||
// Set a value to tcpClient field so we can later ensure that it has been closed.
|
||||
tcpClientField.SetValue(plc, new TcpClient());
|
||||
var tcpClientValue = tcpClientField.GetValue(plc);
|
||||
Assert.IsNotNull(tcpClientValue);
|
||||
|
||||
try
|
||||
{
|
||||
var result = (Task<COTP.TPDU>) dynMethod.Invoke(plc, new object[] { stream, requestData, cancellationToken });
|
||||
await result;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine("Task was cancelled as expected.");
|
||||
|
||||
// Ensure that the plc connection was closed since the task was cancelled
|
||||
// after data has been sent through the network. We expect that the tcpClient
|
||||
// object was set to NULL
|
||||
var tcpClientValueAfter = tcpClientField.GetValue(plc);
|
||||
Assert.IsNull(tcpClientValueAfter);
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Assert.Fail($"Wrong exception type received. Expected {typeof(OperationCanceledException)}, received {e.GetType()}.");
|
||||
}
|
||||
|
||||
// Ensure test fails if cancellation did not occur.
|
||||
Assert.Fail("Task was not cancelled as expected.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Test_CancellationBeforeTransmission()
|
||||
{
|
||||
var plc = new Plc(CpuType.S7300, TestServerIp, TestServerPort, 0, 2);
|
||||
|
||||
// Set up a cancellation source
|
||||
var cancellationSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationSource.Token;
|
||||
|
||||
var stream = new TestStreamConnectionClose(cancellationSource);
|
||||
var requestData = new byte[100]; // empty data, it does not matter what is in there
|
||||
|
||||
// Set up access to private method and field
|
||||
var dynMethod = plc.GetType().GetMethod("NoLockRequestTpduAsync",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (dynMethod == null)
|
||||
{
|
||||
throw new NullReferenceException("Could not find method 'NoLockRequestTpduAsync' on Plc object.");
|
||||
}
|
||||
var tcpClientField = plc.GetType().GetField("tcpClient", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (tcpClientField == null)
|
||||
{
|
||||
throw new NullReferenceException("Could not find field 'tcpClient' on Plc object.");
|
||||
}
|
||||
|
||||
// Set a value to tcpClient field so we can later ensure that it has been closed.
|
||||
tcpClientField.SetValue(plc, new TcpClient());
|
||||
var tcpClientValue = tcpClientField.GetValue(plc);
|
||||
Assert.IsNotNull(tcpClientValue);
|
||||
|
||||
try
|
||||
{
|
||||
// cancel the task before we start transmitting data
|
||||
cancellationSource.Cancel();
|
||||
var result = (Task<COTP.TPDU>)dynMethod.Invoke(plc, new object[] { stream, requestData, cancellationToken });
|
||||
await result;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine("Task was cancelled as expected.");
|
||||
|
||||
// Ensure that the plc connection was not closed, since we cancelled the task before
|
||||
// sending data through the network. We expect that the tcpClient
|
||||
// object was NOT set to NULL
|
||||
var tcpClientValueAfter = tcpClientField.GetValue(plc);
|
||||
Assert.IsNotNull(tcpClientValueAfter);
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Assert.Fail($"Wrong exception type received. Expected {typeof(OperationCanceledException)}, received {e.GetType()}.");
|
||||
}
|
||||
|
||||
// Ensure test fails if cancellation did not occur.
|
||||
Assert.Fail("Task was not cancelled as expected.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,52 +9,52 @@ namespace S7.Net.UnitTest
|
||||
[TestMethod]
|
||||
public void Test_ConnectionRequest_S7_200()
|
||||
{
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(16, 0, 16, 0),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7200, 0, 0));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(16, 0, 16, 1),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7200, 0, 0)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_ConnectionRequest_S7_300()
|
||||
{
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7300, 0, 0));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 0, 0)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7300, 0, 1));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 0, 1)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7300, 1, 1));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 1, 1)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_ConnectionRequest_S7_400()
|
||||
{
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7400, 0, 0));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 0, 0)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7400, 0, 1));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 0, 1)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7400, 1, 1));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 1, 1)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_ConnectionRequest_S7_1200()
|
||||
{
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71200, 0, 0));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 0, 0)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71200, 0, 1));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 0, 1)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71200, 1, 1));
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 1, 1)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Test_ConnectionRequest_S7_1500()
|
||||
{
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 0),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 0, 0));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 1),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 0, 1));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 33),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 1, 1));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 0, 0)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 0, 1)));
|
||||
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
|
||||
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 1, 1)));
|
||||
}
|
||||
|
||||
private static byte[] MakeConnectionRequest(byte sourceTsap1, byte sourceTsap2, byte destTsap1, byte destTsap2)
|
||||
@@ -63,7 +63,7 @@ namespace S7.Net.UnitTest
|
||||
{
|
||||
3, 0, 0, 22, //TPKT
|
||||
17, //COTP Header Length
|
||||
224, //Connect Request
|
||||
224, //Connect Request
|
||||
0, 0, //Destination Reference
|
||||
0, 46, //Source Reference
|
||||
0, //Flags
|
||||
|
||||
@@ -24,5 +24,21 @@ namespace S7.Net.UnitTest
|
||||
Assert.IsFalse(dummyByte.SelectBit(7));
|
||||
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void T01_TestSetBit()
|
||||
{
|
||||
byte dummyByte = 0xAA; // 1010 1010
|
||||
dummyByte.SetBit(0, true);
|
||||
dummyByte.SetBit(1, false);
|
||||
dummyByte.SetBit(2, true);
|
||||
dummyByte.SetBit(3, false);
|
||||
Assert.AreEqual<byte>(dummyByte, 0xA5);// 1010 0101
|
||||
dummyByte.SetBit(4, true);
|
||||
dummyByte.SetBit(5, true);
|
||||
dummyByte.SetBit(6, true);
|
||||
dummyByte.SetBit(7, true);
|
||||
Assert.AreEqual<byte>(dummyByte, 0xF5);// 1111 0101
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
S7.Net.UnitTest/Framework/IsExternalInit.cs
Normal file
7
S7.Net.UnitTest/Framework/IsExternalInit.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal record IsExternalInit;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ namespace S7.Net.UnitTest.Helpers
|
||||
Console.WriteLine(Server.EventText(ref Event));
|
||||
}
|
||||
|
||||
public static void Start()
|
||||
public static void Start(short port)
|
||||
{
|
||||
Server = new S7Server();
|
||||
// Share some resources with our virtual PLC
|
||||
@@ -59,7 +59,14 @@ namespace S7.Net.UnitTest.Helpers
|
||||
// Start the server onto the default adapter.
|
||||
// To select an adapter we have to use Server->StartTo("192.168.x.y").
|
||||
// Start() is the same of StartTo("0.0.0.0");
|
||||
|
||||
Server.SetParam(S7Consts.p_u16_LocalPort, ref port);
|
||||
|
||||
int Error = Server.Start();
|
||||
if (Error != 0)
|
||||
{
|
||||
throw new Exception($"Error starting Snap7 server: {Server.ErrorText(Error)}");
|
||||
}
|
||||
//if (Error == 0)
|
||||
//{
|
||||
// // Now the server is running ... wait a key to terminate
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
using S7.Net.Types;
|
||||
|
||||
namespace S7.Net.UnitTest.Helpers
|
||||
{
|
||||
class TestClass
|
||||
@@ -35,12 +37,12 @@ namespace S7.Net.UnitTest.Helpers
|
||||
/// <summary>
|
||||
/// DB1.DBD4
|
||||
/// </summary>
|
||||
public double RealVariableDouble { get; set; }
|
||||
public double LRealVariable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DB1.DBD8
|
||||
/// </summary>
|
||||
public float RealVariableFloat { get; set; }
|
||||
public float RealVariable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DB1.DBD12
|
||||
@@ -51,5 +53,16 @@ namespace S7.Net.UnitTest.Helpers
|
||||
/// DB1.DBD16
|
||||
/// </summary>
|
||||
public ushort DWordVariable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DB1.DBX20.0
|
||||
/// </summary>
|
||||
[S7String(S7StringType.S7WString, 10)]
|
||||
public string WStringVariable { get; set; }
|
||||
/// <summary>
|
||||
/// DB1.DBX44.0
|
||||
/// </summary>
|
||||
[S7String(S7StringType.S7String, 10)]
|
||||
public string StringVariable { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
using S7.Net.Types;
|
||||
|
||||
namespace S7.Net.UnitTest.Helpers
|
||||
{
|
||||
public struct TestStruct
|
||||
@@ -7,6 +8,7 @@ namespace S7.Net.UnitTest.Helpers
|
||||
/// DB1.DBX0.0
|
||||
/// </summary>
|
||||
public bool BitVariable00;
|
||||
|
||||
public bool BitVariable01;
|
||||
public bool BitVariable02;
|
||||
public bool BitVariable03;
|
||||
@@ -19,6 +21,7 @@ namespace S7.Net.UnitTest.Helpers
|
||||
/// DB1.DBX1.0
|
||||
/// </summary>
|
||||
public bool BitVariable10;
|
||||
|
||||
public bool BitVariable11;
|
||||
public bool BitVariable12;
|
||||
public bool BitVariable13;
|
||||
@@ -35,12 +38,12 @@ namespace S7.Net.UnitTest.Helpers
|
||||
/// <summary>
|
||||
/// DB1.DBD4
|
||||
/// </summary>
|
||||
public double RealVariableDouble;
|
||||
public double LRealVariable;
|
||||
|
||||
/// <summary>
|
||||
/// DB1.DBD8
|
||||
/// </summary>
|
||||
public float RealVariableFloat;
|
||||
public float RealVariable;
|
||||
|
||||
/// <summary>
|
||||
/// DB1.DBD12
|
||||
@@ -51,5 +54,17 @@ namespace S7.Net.UnitTest.Helpers
|
||||
/// DB1.DBD16
|
||||
/// </summary>
|
||||
public ushort DWordVariable;
|
||||
|
||||
/// <summary>
|
||||
/// DB1.DBX20.0
|
||||
/// </summary>
|
||||
[S7String(S7StringType.S7WString, 10)]
|
||||
public string WStringVariable;
|
||||
|
||||
/// <summary>
|
||||
/// DB1.DBX44.0
|
||||
/// </summary>
|
||||
[S7String(S7StringType.S7String, 10)]
|
||||
public string StringVariable;
|
||||
}
|
||||
}
|
||||
|
||||
82
S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs
Normal file
82
S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace S7.Net.UnitTest;
|
||||
|
||||
internal class CommunicationSequence : IEnumerable<RequestResponsePair>
|
||||
{
|
||||
private readonly List<RequestResponsePair> _requestResponsePairs = new List<RequestResponsePair>();
|
||||
|
||||
public void Add(RequestResponsePair requestResponsePair)
|
||||
{
|
||||
_requestResponsePairs.Add(requestResponsePair);
|
||||
}
|
||||
|
||||
public void Add(string requestPattern, string responsePattern)
|
||||
{
|
||||
_requestResponsePairs.Add(new RequestResponsePair(requestPattern, responsePattern));
|
||||
}
|
||||
|
||||
public IEnumerator<RequestResponsePair> GetEnumerator()
|
||||
{
|
||||
return _requestResponsePairs.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public Task Serve(out int port)
|
||||
{
|
||||
var socket = CreateBoundListenSocket(out port);
|
||||
socket.Listen(0);
|
||||
|
||||
async Task Impl()
|
||||
{
|
||||
await Task.Yield();
|
||||
var socketIn = socket.Accept();
|
||||
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(1024);
|
||||
try
|
||||
{
|
||||
foreach (var pair in _requestResponsePairs)
|
||||
{
|
||||
var bytesReceived = socketIn.Receive(buffer, SocketFlags.None);
|
||||
|
||||
var received = buffer.Take(bytesReceived).ToArray();
|
||||
Console.WriteLine($"=> {BitConverter.ToString(received)}");
|
||||
|
||||
var response = Responder.Respond(pair, received);
|
||||
|
||||
Console.WriteLine($"<= {BitConverter.ToString(response)}");
|
||||
socketIn.Send(response);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
socketIn.Close();
|
||||
}
|
||||
|
||||
return Impl();
|
||||
}
|
||||
|
||||
private static Socket CreateBoundListenSocket(out int port)
|
||||
{
|
||||
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
var endpoint = new IPEndPoint(IPAddress.Loopback, 0);
|
||||
|
||||
socket.Bind(endpoint);
|
||||
|
||||
var localEndpoint = (IPEndPoint)socket.LocalEndPoint!;
|
||||
port = localEndpoint.Port;
|
||||
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
3
S7.Net.UnitTest/Infrastructure/RequestResponsePair.cs
Normal file
3
S7.Net.UnitTest/Infrastructure/RequestResponsePair.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace S7.Net.UnitTest;
|
||||
|
||||
internal record RequestResponsePair(string RequestPattern, string ResponsePattern);
|
||||
80
S7.Net.UnitTest/Infrastructure/Responder.cs
Normal file
80
S7.Net.UnitTest/Infrastructure/Responder.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace S7.Net.UnitTest;
|
||||
|
||||
internal static class Responder
|
||||
{
|
||||
private const string Comment = "//";
|
||||
private static char[] Space = " ".ToCharArray();
|
||||
|
||||
public static byte[] Respond(RequestResponsePair pair, byte[] request)
|
||||
{
|
||||
var offset = 0;
|
||||
var matches = new Dictionary<string, byte>();
|
||||
var res = new List<byte>();
|
||||
using var requestReader = new StringReader(pair.RequestPattern);
|
||||
|
||||
string line;
|
||||
while ((line = requestReader.ReadLine()) != null)
|
||||
{
|
||||
var tokens = line.Split(Space, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (token.StartsWith(Comment)) break;
|
||||
|
||||
if (offset >= request.Length)
|
||||
{
|
||||
throw new Exception("Request pattern has more data than request.");
|
||||
}
|
||||
|
||||
var received = request[offset];
|
||||
|
||||
if (token.Length == 2 && byte.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
// Number, exact match
|
||||
if (value != received)
|
||||
{
|
||||
throw new Exception($"Incorrect data at offset {offset}. Expected {value:X2}, received {received:X2}.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
matches[token] = received;
|
||||
}
|
||||
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
|
||||
if (offset != request.Length) throw new Exception("Request contained more data than request pattern.");
|
||||
|
||||
using var responseReader = new StringReader(pair.ResponsePattern);
|
||||
while ((line = responseReader.ReadLine()) != null)
|
||||
{
|
||||
var tokens = line.Split(Space, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (token.StartsWith(Comment)) break;
|
||||
|
||||
if (token.Length == 2 && byte.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
res.Add(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!matches.TryGetValue(token, out var match))
|
||||
{
|
||||
throw new Exception($"Unmatched token '{token}' in response.");
|
||||
}
|
||||
|
||||
res.Add(match);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("S7.Net.UnitTest")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("S7Net.UnitTest")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2014")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("6f73e1b1-301b-471e-9f38-3dcbddbcfc21")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
@@ -1,65 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using S7.Net;
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using S7.Net.Protocol;
|
||||
|
||||
namespace S7.Net.UnitTest
|
||||
{
|
||||
[TestClass]
|
||||
public class ProtocolUnitTest
|
||||
{
|
||||
public TestContext TestContext { get; set; }
|
||||
|
||||
[TestMethod]
|
||||
public void TPKT_Read()
|
||||
public async Task TPKT_Read()
|
||||
{
|
||||
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd"));
|
||||
var t = TPKT.Read(m);
|
||||
Assert.AreEqual(0x03, t.Version);
|
||||
Assert.AreEqual(0x29, t.Length);
|
||||
m.Position = 0;
|
||||
t = TPKT.ReadAsync(m).Result;
|
||||
var t = await TPKT.ReadAsync(m, TestContext.CancellationTokenSource.Token);
|
||||
Assert.AreEqual(0x03, t.Version);
|
||||
Assert.AreEqual(0x29, t.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(TPKTInvalidException))]
|
||||
public void TPKT_ReadShort()
|
||||
public async Task TPKT_ReadShort()
|
||||
{
|
||||
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff040080"));
|
||||
var t = TPKT.Read(m);
|
||||
var t = await TPKT.ReadAsync(m, CancellationToken.None);
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(TPKTInvalidException))]
|
||||
public async Task TPKT_ReadShortAsync()
|
||||
{
|
||||
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff040080"));
|
||||
var t = await TPKT.ReadAsync(m);
|
||||
}
|
||||
var t = await TPKT.ReadAsync(m, TestContext.CancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void COTP_ReadTSDU()
|
||||
public async Task COTP_ReadTSDU()
|
||||
{
|
||||
var expected = StringToByteArray("320700000400000800080001120411440100ff09000400000000");
|
||||
var m = new MemoryStream(StringToByteArray("0300000702f0000300000702f0000300002102f080320700000400000800080001120411440100ff09000400000000"));
|
||||
var t = COTP.TSDU.Read(m);
|
||||
Assert.IsTrue(expected.SequenceEqual(t));
|
||||
m.Position = 0;
|
||||
t = COTP.TSDU.ReadAsync(m).Result;
|
||||
var t = await COTP.TSDU.ReadAsync(m, TestContext.CancellationTokenSource.Token);
|
||||
Assert.IsTrue(expected.SequenceEqual(t));
|
||||
}
|
||||
|
||||
private static byte[] StringToByteArray(string hex)
|
||||
public static byte[] StringToByteArray(string hex)
|
||||
{
|
||||
return Enumerable.Range(0, hex.Length)
|
||||
.Where(x => x % 2 == 0)
|
||||
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public async Task TestResponseCode()
|
||||
{
|
||||
var expected = StringToByteArray("320700000400000800080001120411440100ff09000400000000");
|
||||
var m = new MemoryStream(StringToByteArray("0300000702f0000300000702f0000300002102f080320700000400000800080001120411440100ff09000400000000"));
|
||||
var t = await COTP.TSDU.ReadAsync(m, CancellationToken.None);
|
||||
Assert.IsTrue(expected.SequenceEqual(t));
|
||||
|
||||
// Test all possible byte values. Everything except 0xff should throw an exception.
|
||||
var testData = Enumerable.Range(0, 256).Select(i => new { StatusCode = (ReadWriteErrorCode)i, ThrowsException = i != (byte)ReadWriteErrorCode.Success });
|
||||
|
||||
foreach (var entry in testData)
|
||||
{
|
||||
if (entry.ThrowsException)
|
||||
{
|
||||
Assert.ThrowsException<Exception>(() => Plc.ValidateResponseCode(entry.StatusCode));
|
||||
}
|
||||
else
|
||||
{
|
||||
Plc.ValidateResponseCode(entry.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,129 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{303CCED6-9ABC-4899-A509-743341AAA804}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>S7.Net.UnitTest</RootNamespace>
|
||||
<AssemblyName>S7Net.UnitTest</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
|
||||
<IsCodedUITest>False</IsCodedUITest>
|
||||
<TestProjectType>UnitTest</TestProjectType>
|
||||
<TargetFrameworkProfile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup Condition=" '$(OS)' != 'Windows_NT' ">
|
||||
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT' ">
|
||||
<TargetFrameworks>net6.0;net7.0;net462</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
|
||||
<IsPackable>false</IsPackable>
|
||||
<Copyright>Copyright © 2014</Copyright>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<AssemblyOriginatorKeyFile>S7.Net.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="1.3.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<Choose>
|
||||
<When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'">
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
</ItemGroup>
|
||||
</When>
|
||||
<Otherwise />
|
||||
</Choose>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="ConnectionRequestTest.cs" />
|
||||
<Compile Include="ConvertersUnitTest.cs" />
|
||||
<Compile Include="Helpers\TestClassWithNestedClass.cs" />
|
||||
<Compile Include="PLCAddressParsingTests.cs" />
|
||||
<Compile Include="ProtocolTests.cs" />
|
||||
<Compile Include="Helpers\ConsoleManager.cs" />
|
||||
<Compile Include="Helpers\NativeMethods.cs" />
|
||||
<Compile Include="Helpers\S7TestServer.cs" />
|
||||
<Compile Include="Helpers\TestClassWithArrays.cs" />
|
||||
<Compile Include="Helpers\TestClassWithCustomType.cs" />
|
||||
<Compile Include="Helpers\TestClassWithPrivateSetters.cs" />
|
||||
<Compile Include="Helpers\TestLongClass.cs" />
|
||||
<Compile Include="S7NetTestsAsync.cs" />
|
||||
<Compile Include="Helpers\TestSmallClass.cs" />
|
||||
<Compile Include="Snap7\snap7.net.cs" />
|
||||
<Compile Include="Helpers\TestClass.cs" />
|
||||
<Compile Include="Helpers\TestStruct.cs" />
|
||||
<Compile Include="S7NetTestsSync.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Helpers\TestLongStruct.cs" />
|
||||
<Compile Include="TypeTests\ClassTests.cs" />
|
||||
<Compile Include="TypeTests\DateTimeTests.cs" />
|
||||
<Compile Include="TypeTests\StringExTests.cs" />
|
||||
<Compile Include="TypeTests\StringTests.cs" />
|
||||
<ProjectReference Include="..\S7.Net\S7.Net.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="snap7.dll">
|
||||
<None Update="runtimes\win-x64\native\snap7.dll" Link="snap7.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\S7.Net\S7.Net.csproj">
|
||||
<Project>{bfd484f9-3f04-42a2-bf2a-60a189a25dcf}</Project>
|
||||
<Name>S7.Net</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="S7.Net.snk" />
|
||||
</ItemGroup>
|
||||
<Choose>
|
||||
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.VisualStudio.QualityTools.CodedUITestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Extension, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestTools.UITesting, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</When>
|
||||
</Choose>
|
||||
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
#region Using
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using S7.Net;
|
||||
using S7.Net.UnitTest.Helpers;
|
||||
using S7.Net.UnitTest;
|
||||
using System.ServiceProcess;
|
||||
using S7.Net.Types;
|
||||
using S7.UnitTest.Helpers;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
using System.Buffers;
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -101,20 +104,13 @@ namespace S7.Net.UnitTest
|
||||
|
||||
/// <summary>
|
||||
/// Read/Write a single REAL with a single request.
|
||||
/// Test that writing a double and reading it gives the correct value.
|
||||
/// Test that writing a float and reading it gives the correct value.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Test_Async_WriteAndReadRealVariables()
|
||||
{
|
||||
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
|
||||
|
||||
// Reading and writing a double is quite complicated, because it needs to be converted to DWord before the write,
|
||||
// then reconvert to double after the read.
|
||||
double val = 35.68729;
|
||||
await plc.WriteAsync("DB1.DBD40", val.ConvertToUInt());
|
||||
double result = ((uint)await plc.ReadAsync("DB1.DBD40")).ConvertToDouble();
|
||||
Assert.AreEqual(val, Math.Round(result, 5)); // float lose precision, so i need to round it
|
||||
|
||||
// Reading and writing a float is quite complicated, because it needs to be converted to DWord before the write,
|
||||
// then reconvert to float after the read. Float values can contain only 7 digits, so no precision is lost.
|
||||
float val2 = 1234567;
|
||||
@@ -128,6 +124,53 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(val3, result3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write/Read a large amount of data to test PDU max
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Test_Async_WriteLargeByteArray()
|
||||
{
|
||||
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
|
||||
|
||||
var randomEngine = new Random();
|
||||
var data = new byte[8192];
|
||||
var db = 2;
|
||||
randomEngine.NextBytes(data);
|
||||
|
||||
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data);
|
||||
|
||||
var readData = await plc.ReadBytesAsync(DataType.DataBlock, db, 0, data.Length);
|
||||
|
||||
CollectionAssert.AreEqual(data, readData);
|
||||
}
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
|
||||
/// <summary>
|
||||
/// Write/Read a large amount of data to test PDU max
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Test_Async_WriteLargeByteArrayWithMemory()
|
||||
{
|
||||
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
|
||||
|
||||
var randomEngine = new Random();
|
||||
using var dataOwner = MemoryPool<byte>.Shared.Rent(8192);
|
||||
var data = dataOwner.Memory.Slice(0, 8192);
|
||||
var db = 2;
|
||||
randomEngine.NextBytes(data.Span);
|
||||
|
||||
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data);
|
||||
|
||||
using var readDataOwner = MemoryPool<byte>.Shared.Rent(data.Length);
|
||||
var readData = readDataOwner.Memory.Slice(0, data.Length);
|
||||
await plc.ReadBytesAsync(readData, DataType.DataBlock, db, 0);
|
||||
|
||||
CollectionAssert.AreEqual(data.ToArray(), readData.ToArray());
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Read/Write a class that has the same properties of a DB with the same field in the same order
|
||||
/// </summary>
|
||||
@@ -142,9 +185,11 @@ namespace S7.Net.UnitTest
|
||||
BitVariable10 = true,
|
||||
DIntVariable = -100000,
|
||||
IntVariable = -15000,
|
||||
RealVariableDouble = -154.789,
|
||||
RealVariableFloat = -154.789f,
|
||||
DWordVariable = 850
|
||||
LRealVariable = -154.789,
|
||||
RealVariable = -154.789f,
|
||||
DWordVariable = 850,
|
||||
WStringVariable = "ÄÜÉÊéà",
|
||||
StringVariable = "Hallo"
|
||||
};
|
||||
|
||||
await plc.WriteClassAsync(tc, DB2);
|
||||
@@ -155,9 +200,11 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
|
||||
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
|
||||
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
|
||||
Assert.AreEqual(tc.RealVariableDouble, Math.Round(tc2.RealVariableDouble, 3));
|
||||
Assert.AreEqual(tc.RealVariableFloat, tc2.RealVariableFloat);
|
||||
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable);
|
||||
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
|
||||
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
|
||||
Assert.AreEqual(tc.WStringVariable, tc2.WStringVariable);
|
||||
Assert.AreEqual(tc.StringVariable, tc2.StringVariable);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -199,9 +246,11 @@ namespace S7.Net.UnitTest
|
||||
BitVariable10 = true,
|
||||
DIntVariable = -100000,
|
||||
IntVariable = -15000,
|
||||
RealVariableDouble = -154.789,
|
||||
RealVariableFloat = -154.789f,
|
||||
DWordVariable = 850
|
||||
LRealVariable = -154.789,
|
||||
RealVariable = -154.789f,
|
||||
DWordVariable = 850,
|
||||
WStringVariable = "ÄÜÉÊéà",
|
||||
StringVariable = "Hallo"
|
||||
};
|
||||
plc.WriteStruct(tc, DB2);
|
||||
// Values that are read from a struct are stored in a new struct, returned by the funcion ReadStruct
|
||||
@@ -210,9 +259,11 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
|
||||
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
|
||||
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
|
||||
Assert.AreEqual(tc.RealVariableDouble, Math.Round(tc2.RealVariableDouble, 3));
|
||||
Assert.AreEqual(tc.RealVariableFloat, tc2.RealVariableFloat);
|
||||
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable);
|
||||
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
|
||||
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
|
||||
Assert.AreEqual(tc.WStringVariable, tc2.WStringVariable);
|
||||
Assert.AreEqual(tc.StringVariable, tc2.StringVariable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -564,9 +615,11 @@ namespace S7.Net.UnitTest
|
||||
BitVariable10 = true,
|
||||
DIntVariable = -100000,
|
||||
IntVariable = -15000,
|
||||
RealVariableDouble = -154.789,
|
||||
RealVariableFloat = -154.789f,
|
||||
DWordVariable = 850
|
||||
LRealVariable = -154.789,
|
||||
RealVariable = -154.789f,
|
||||
DWordVariable = 850,
|
||||
WStringVariable = "ÄÜÉÊéà",
|
||||
StringVariable = "Hallo"
|
||||
};
|
||||
|
||||
await plc.WriteClassAsync(tc, DB2);
|
||||
@@ -579,8 +632,8 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
|
||||
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
|
||||
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
|
||||
Assert.AreEqual(tc.RealVariableDouble, tc2.RealVariableDouble, 0.1);
|
||||
Assert.AreEqual(tc.RealVariableFloat, tc2.RealVariableFloat);
|
||||
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable, 0.1);
|
||||
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
|
||||
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
|
||||
|
||||
Assert.AreEqual(TestClassWithPrivateSetters.PRIVATE_SETTER_VALUE, tc2.PrivateSetterProperty);
|
||||
@@ -591,15 +644,13 @@ namespace S7.Net.UnitTest
|
||||
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(NullReferenceException))]
|
||||
public async Task Test_Async_ReadBytesReturnsNullIfPlcIsNotConnected()
|
||||
{
|
||||
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
|
||||
{
|
||||
Assert.IsFalse(notConnectedPlc.IsConnected);
|
||||
TestClass tc = new TestClass();
|
||||
var res = await notConnectedPlc.ReadClassAsync(tc, DB2);
|
||||
Assert.Fail();
|
||||
await Assert.ThrowsExceptionAsync<PlcException>(async () => await notConnectedPlc.ReadClassAsync(tc, DB2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,9 +665,12 @@ namespace S7.Net.UnitTest
|
||||
BitVariable10 = true,
|
||||
DIntVariable = -100000,
|
||||
IntVariable = -15000,
|
||||
RealVariableDouble = -154.789,
|
||||
RealVariableFloat = -154.789f,
|
||||
DWordVariable = 850
|
||||
LRealVariable = -154.789,
|
||||
RealVariable = -154.789f,
|
||||
DWordVariable = 850,
|
||||
WStringVariable = "ÄÜÉÊéà",
|
||||
StringVariable = "Hallo"
|
||||
|
||||
};
|
||||
|
||||
await plc.WriteClassAsync(tc, DB2);
|
||||
@@ -631,19 +685,21 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc2.BitVariable10, tc2Generic.BitVariable10);
|
||||
Assert.AreEqual(tc2.DIntVariable, tc2Generic.DIntVariable);
|
||||
Assert.AreEqual(tc2.IntVariable, tc2Generic.IntVariable);
|
||||
Assert.AreEqual(Math.Round(tc2.RealVariableDouble, 3), Math.Round(tc2Generic.RealVariableDouble, 3));
|
||||
Assert.AreEqual(tc2.RealVariableFloat, tc2Generic.RealVariableFloat);
|
||||
Assert.AreEqual(Math.Round(tc2.LRealVariable, 3), Math.Round(tc2Generic.LRealVariable, 3));
|
||||
Assert.AreEqual(tc2.RealVariable, tc2Generic.RealVariable);
|
||||
Assert.AreEqual(tc2.DWordVariable, tc2Generic.DWordVariable);
|
||||
Assert.AreEqual(tc2.WStringVariable, tc2Generic.WStringVariable);
|
||||
Assert.AreEqual(tc2.StringVariable, tc2Generic.StringVariable);
|
||||
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(NullReferenceException))]
|
||||
public async Task Test_Async_ReadClassWithGenericReturnsNullIfPlcIsNotConnected()
|
||||
{
|
||||
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
|
||||
{
|
||||
Assert.IsFalse(notConnectedPlc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
|
||||
TestClass tc = await notConnectedPlc.ReadClassAsync<TestClass>(DB2);
|
||||
await Assert.ThrowsExceptionAsync<PlcException>(async () => await notConnectedPlc.ReadClassAsync<TestClass>(DB2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,9 +714,11 @@ namespace S7.Net.UnitTest
|
||||
BitVariable10 = true,
|
||||
DIntVariable = -100000,
|
||||
IntVariable = -15000,
|
||||
RealVariableDouble = -154.789,
|
||||
RealVariableFloat = -154.789f,
|
||||
DWordVariable = 850
|
||||
LRealVariable = -154.789,
|
||||
RealVariable = -154.789f,
|
||||
DWordVariable = 850,
|
||||
WStringVariable = "ÄÜÉÊéà",
|
||||
StringVariable = "Hallo"
|
||||
};
|
||||
|
||||
await plc.WriteClassAsync(tc, DB2);
|
||||
@@ -672,19 +730,20 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc2Generic.BitVariable00, tc2GenericWithClassFactory.BitVariable00);
|
||||
Assert.AreEqual(tc2Generic.BitVariable10, tc2GenericWithClassFactory.BitVariable10);
|
||||
Assert.AreEqual(tc2Generic.DIntVariable, tc2GenericWithClassFactory.DIntVariable);
|
||||
Assert.AreEqual(Math.Round(tc2Generic.RealVariableDouble, 3), Math.Round(tc2GenericWithClassFactory.RealVariableDouble, 3));
|
||||
Assert.AreEqual(tc2Generic.RealVariableFloat, tc2GenericWithClassFactory.RealVariableFloat);
|
||||
Assert.AreEqual(Math.Round(tc2Generic.LRealVariable, 3), Math.Round(tc2GenericWithClassFactory.LRealVariable, 3));
|
||||
Assert.AreEqual(tc2Generic.RealVariable, tc2GenericWithClassFactory.RealVariable);
|
||||
Assert.AreEqual(tc2Generic.DWordVariable, tc2GenericWithClassFactory.DWordVariable);
|
||||
Assert.AreEqual(tc2Generic.WStringVariable, tc2GenericWithClassFactory.WStringVariable);
|
||||
Assert.AreEqual(tc2Generic.StringVariable, tc2GenericWithClassFactory.StringVariable);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(NullReferenceException))]
|
||||
public async Task Test_Async_ReadClassWithGenericAndClassFactoryThrowsExceptionPlcIsNotConnected()
|
||||
{
|
||||
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
|
||||
{
|
||||
Assert.IsFalse(notConnectedPlc.IsConnected);
|
||||
TestClass tc = await notConnectedPlc.ReadClassAsync(() => new TestClass(), DB2);
|
||||
await Assert.ThrowsExceptionAsync<PlcException>(async () => await notConnectedPlc.ReadClassAsync(() => new TestClass(), DB2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,13 +770,12 @@ namespace S7.Net.UnitTest
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(NullReferenceException))]
|
||||
public async Task Test_Async_ReadStructThrowsExceptionPlcIsNotConnected()
|
||||
{
|
||||
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
|
||||
{
|
||||
Assert.IsFalse(notConnectedPlc.IsConnected);
|
||||
object tsObj = await notConnectedPlc.ReadStructAsync(typeof(TestStruct), DB2);
|
||||
await Assert.ThrowsExceptionAsync<PlcException>(async () => await notConnectedPlc.ReadStructAsync(typeof(TestStruct), DB2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -732,9 +790,11 @@ namespace S7.Net.UnitTest
|
||||
BitVariable10 = true,
|
||||
DIntVariable = -100000,
|
||||
IntVariable = -15000,
|
||||
RealVariableDouble = -154.789,
|
||||
RealVariableFloat = -154.789f,
|
||||
DWordVariable = 850
|
||||
LRealVariable = -154.789,
|
||||
RealVariable = -154.789f,
|
||||
DWordVariable = 850,
|
||||
WStringVariable = "ÄÜÉÊéà",
|
||||
StringVariable = "Hallo"
|
||||
};
|
||||
|
||||
plc.WriteStruct(ts, DB2);
|
||||
@@ -748,19 +808,20 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(ts2.BitVariable10, ts2Generic.BitVariable10);
|
||||
Assert.AreEqual(ts2.DIntVariable, ts2Generic.DIntVariable);
|
||||
Assert.AreEqual(ts2.IntVariable, ts2Generic.IntVariable);
|
||||
Assert.AreEqual(Math.Round(ts2.RealVariableDouble, 3), Math.Round(ts2Generic.RealVariableDouble, 3));
|
||||
Assert.AreEqual(ts2.RealVariableFloat, ts2Generic.RealVariableFloat);
|
||||
Assert.AreEqual(ts2.LRealVariable, ts2Generic.LRealVariable);
|
||||
Assert.AreEqual(ts2.RealVariable, ts2Generic.RealVariable);
|
||||
Assert.AreEqual(ts2.DWordVariable, ts2Generic.DWordVariable);
|
||||
Assert.AreEqual(ts2.WStringVariable, ts2Generic.WStringVariable);
|
||||
Assert.AreEqual(ts2.StringVariable, ts2Generic.StringVariable);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(NullReferenceException))]
|
||||
public async Task Test_Async_ReadStructWithGenericThrowsExceptionIfPlcIsNotConnected()
|
||||
{
|
||||
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
|
||||
{
|
||||
Assert.IsFalse(notConnectedPlc.IsConnected);
|
||||
object tsObj = await notConnectedPlc.ReadStructAsync<TestStruct>(DB2);
|
||||
await Assert.ThrowsExceptionAsync<PlcException>(async () => await notConnectedPlc.ReadStructAsync<TestStruct>(DB2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,9 +839,11 @@ namespace S7.Net.UnitTest
|
||||
BitVariable10 = true,
|
||||
DIntVariable = -100000,
|
||||
IntVariable = -15000,
|
||||
RealVariableDouble = -154.789,
|
||||
RealVariableFloat = -154.789f,
|
||||
DWordVariable = 850
|
||||
LRealVariable = -154.789,
|
||||
RealVariable = -154.789f,
|
||||
DWordVariable = 850,
|
||||
WStringVariable = "ÄÜÉÊéà",
|
||||
StringVariable = "Hallo"
|
||||
};
|
||||
plc.WriteClass(tc, DB2);
|
||||
|
||||
@@ -869,17 +932,6 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc.CustomTypes[1].Bools[1], tc2.CustomTypes[1].Bools[1]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Test_Async_ReadWriteDouble()
|
||||
{
|
||||
double test_value = 55.66;
|
||||
await plc.WriteAsync("DB1.DBD0", test_value);
|
||||
var helper = await plc.ReadAsync("DB1.DBD0");
|
||||
double test_value2 = Conversion.ConvertToDouble((uint)helper);
|
||||
|
||||
Assert.AreEqual(test_value, test_value2, 0.01, "Compare Write/Read"); //Need delta here because S7 only has 32 bit reals
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Test_Async_ReadWriteSingle()
|
||||
{
|
||||
@@ -912,6 +964,142 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(x % 256, res[x], string.Format("Bit {0} failed", x));
|
||||
}
|
||||
}
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
|
||||
[TestMethod]
|
||||
public async Task Test_Async_ReadWriteBytesManyWithMemory()
|
||||
{
|
||||
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
|
||||
|
||||
using var data = MemoryPool<byte>.Shared.Rent(2000);
|
||||
for (int i = 0; i < data.Memory.Length; i++)
|
||||
data.Memory.Span[i] = (byte)(i % 256);
|
||||
|
||||
await plc.WriteBytesAsync(DataType.DataBlock, 2, 0, data.Memory);
|
||||
|
||||
using var readData = MemoryPool<byte>.Shared.Rent(data.Memory.Length);
|
||||
|
||||
await plc.ReadBytesAsync(readData.Memory.Slice(0, data.Memory.Length), DataType.DataBlock, 2, 0);
|
||||
|
||||
for (int x = 0; x < data.Memory.Length; x++)
|
||||
{
|
||||
Assert.AreEqual(x % 256, readData.Memory.Span[x], string.Format("Bit {0} failed", x));
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Write a large amount of data and test cancellation
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Test_Async_WriteLargeByteArrayWithCancellation()
|
||||
{
|
||||
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
|
||||
|
||||
var cancellationSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationSource.Token;
|
||||
|
||||
var randomEngine = new Random();
|
||||
var data = new byte[8192];
|
||||
var db = 2;
|
||||
randomEngine.NextBytes(data);
|
||||
|
||||
cancellationSource.CancelAfter(System.TimeSpan.FromMilliseconds(5));
|
||||
try
|
||||
{
|
||||
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);
|
||||
}
|
||||
catch(OperationCanceledException)
|
||||
{
|
||||
// everything is good, that is the exception we expect
|
||||
Console.WriteLine("Operation was cancelled as expected.");
|
||||
return;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Assert.Fail($"Wrong exception type received. Expected {typeof(OperationCanceledException)}, received {e.GetType()}.");
|
||||
}
|
||||
|
||||
// Depending on how tests run, this can also just succeed without getting cancelled at all. Do nothing in this case.
|
||||
Console.WriteLine("Task was not cancelled as expected.");
|
||||
}
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
|
||||
/// <summary>
|
||||
/// Write a large amount of data and test cancellation
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Test_Async_WriteLargeByteArrayWithCancellationWithMemory()
|
||||
{
|
||||
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
|
||||
|
||||
var cancellationSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationSource.Token;
|
||||
|
||||
using var dataOwner = MemoryPool<byte>.Shared.Rent(8192);
|
||||
var data = dataOwner.Memory.Slice(0, 8192);
|
||||
var randomEngine = new Random();
|
||||
var db = 2;
|
||||
randomEngine.NextBytes(data.Span);
|
||||
|
||||
cancellationSource.CancelAfter(System.TimeSpan.FromMilliseconds(5));
|
||||
try
|
||||
{
|
||||
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// everything is good, that is the exception we expect
|
||||
Console.WriteLine("Operation was cancelled as expected.");
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Assert.Fail($"Wrong exception type received. Expected {typeof(OperationCanceledException)}, received {e.GetType()}.");
|
||||
}
|
||||
|
||||
// Depending on how tests run, this can also just succeed without getting cancelled at all. Do nothing in this case.
|
||||
Console.WriteLine("Task was not cancelled as expected.");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Write a large amount of data and test cancellation
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public async Task Test_Async_ParseDataIntoDataItemsAlignment()
|
||||
{
|
||||
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
|
||||
|
||||
var db = 2;
|
||||
// First write a sensible S7 string capacity
|
||||
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, new byte[] {5, 0});
|
||||
|
||||
// Read two data items, with the first having odd number of bytes (7),
|
||||
// and the second has to be aligned on a even address
|
||||
var dataItems = new List<DataItem>
|
||||
{
|
||||
new DataItem
|
||||
{
|
||||
DataType = DataType.DataBlock,
|
||||
DB = db,
|
||||
VarType = VarType.S7String,
|
||||
Count = 5
|
||||
},
|
||||
new DataItem
|
||||
{
|
||||
DataType = DataType.DataBlock,
|
||||
DB = db,
|
||||
VarType = VarType.Word,
|
||||
}
|
||||
};
|
||||
await plc.ReadMultipleVarsAsync(dataItems, CancellationToken.None);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
#region Using
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using S7.Net;
|
||||
using S7.Net.UnitTest.Helpers;
|
||||
using S7.Net.UnitTest;
|
||||
using System.ServiceProcess;
|
||||
using S7.Net.Types;
|
||||
using S7.UnitTest.Helpers;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
using System.Buffers;
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -41,6 +42,8 @@ namespace S7.Net.UnitTest
|
||||
#region Constants
|
||||
const int DB2 = 2;
|
||||
const int DB4 = 4;
|
||||
const short TestServerPort = 31122;
|
||||
const string TestServerIp = "127.0.0.1";
|
||||
#endregion
|
||||
|
||||
#region Private fields
|
||||
@@ -53,16 +56,19 @@ namespace S7.Net.UnitTest
|
||||
/// </summary>
|
||||
public S7NetTests()
|
||||
{
|
||||
plc = new Plc(CpuType.S7300, "127.0.0.1", 0, 2);
|
||||
//ConsoleManager.Show();
|
||||
ShutDownServiceS7oiehsx64();
|
||||
plc = CreatePlc();
|
||||
|
||||
}
|
||||
|
||||
private static Plc CreatePlc()
|
||||
{
|
||||
return new Plc(CpuType.S7300, TestServerIp, TestServerPort, 0, 2);
|
||||
}
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
S7TestServer.Start();
|
||||
S7TestServer.Start(TestServerPort);
|
||||
plc.Open();
|
||||
}
|
||||
|
||||
@@ -146,20 +152,13 @@ namespace S7.Net.UnitTest
|
||||
|
||||
/// <summary>
|
||||
/// Read/Write a single REAL with a single request.
|
||||
/// Test that writing a double and reading it gives the correct value.
|
||||
/// Test that writing a float and reading it gives the correct value.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void T03_WriteAndReadRealVariables()
|
||||
{
|
||||
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
|
||||
|
||||
// Reading and writing a double is quite complicated, because it needs to be converted to DWord before the write,
|
||||
// then reconvert to double after the read.
|
||||
double val = 35.68729;
|
||||
plc.Write("DB1.DBD40", val.ConvertToUInt());
|
||||
double result = ((uint)plc.Read("DB1.DBD40")).ConvertToDouble();
|
||||
Assert.AreEqual(val, Math.Round(result, 5)); // float lose precision, so i need to round it
|
||||
|
||||
// Reading and writing a float is quite complicated, because it needs to be converted to DWord before the write,
|
||||
// then reconvert to float after the read. Float values can contain only 7 digits, so no precision is lost.
|
||||
float val2 = 1234567;
|
||||
@@ -186,9 +185,12 @@ namespace S7.Net.UnitTest
|
||||
tc.BitVariable10 = true;
|
||||
tc.DIntVariable = -100000;
|
||||
tc.IntVariable = -15000;
|
||||
tc.RealVariableDouble = -154.789;
|
||||
tc.RealVariableFloat = -154.789f;
|
||||
tc.LRealVariable = -154.789;
|
||||
tc.RealVariable = -154.789f;
|
||||
tc.DWordVariable = 850;
|
||||
tc.WStringVariable = "ÄÜÉÊéà";
|
||||
tc.StringVariable = "Hallo";
|
||||
|
||||
plc.WriteClass(tc, DB2);
|
||||
TestClass tc2 = new TestClass();
|
||||
// Values that are read from a class are stored inside the class itself, that is passed by reference
|
||||
@@ -197,9 +199,11 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
|
||||
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
|
||||
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
|
||||
Assert.AreEqual(Math.Round(tc.RealVariableDouble, 3), Math.Round(tc2.RealVariableDouble, 3));
|
||||
Assert.AreEqual(tc.RealVariableFloat, tc2.RealVariableFloat);
|
||||
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable);
|
||||
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
|
||||
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
|
||||
Assert.AreEqual(tc.WStringVariable, tc2.WStringVariable);
|
||||
Assert.AreEqual(tc.StringVariable, tc2.StringVariable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -215,9 +219,12 @@ namespace S7.Net.UnitTest
|
||||
tc.BitVariable10 = true;
|
||||
tc.DIntVariable = -100000;
|
||||
tc.IntVariable = -15000;
|
||||
tc.RealVariableDouble = -154.789;
|
||||
tc.RealVariableFloat = -154.789f;
|
||||
tc.LRealVariable = -154.789;
|
||||
tc.RealVariable = -154.789f;
|
||||
tc.DWordVariable = 850;
|
||||
tc.WStringVariable = "ÄÜÉÊéà";
|
||||
tc.StringVariable = "Hallo";
|
||||
|
||||
plc.WriteStruct(tc, DB2);
|
||||
// Values that are read from a struct are stored in a new struct, returned by the funcion ReadStruct
|
||||
TestStruct tc2 = (TestStruct)plc.ReadStruct(typeof(TestStruct), DB2);
|
||||
@@ -225,9 +232,11 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
|
||||
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
|
||||
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
|
||||
Assert.AreEqual(tc.RealVariableDouble, Math.Round(tc2.RealVariableDouble, 3));
|
||||
Assert.AreEqual(tc.RealVariableFloat, tc2.RealVariableFloat);
|
||||
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable);
|
||||
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
|
||||
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
|
||||
Assert.AreEqual(tc.WStringVariable, tc2.WStringVariable);
|
||||
Assert.AreEqual(tc.StringVariable, tc2.StringVariable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -575,9 +584,11 @@ namespace S7.Net.UnitTest
|
||||
tc.BitVariable10 = true;
|
||||
tc.DIntVariable = -100000;
|
||||
tc.IntVariable = -15000;
|
||||
tc.RealVariableDouble = -154.789;
|
||||
tc.RealVariableFloat = -154.789f;
|
||||
tc.LRealVariable = -154.789;
|
||||
tc.RealVariable = -154.789f;
|
||||
tc.DWordVariable = 850;
|
||||
tc.WStringVariable = "ÄÜÉÊéà";
|
||||
tc.StringVariable = "Hallo";
|
||||
|
||||
plc.WriteClass(tc, DB2);
|
||||
|
||||
@@ -588,8 +599,8 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
|
||||
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
|
||||
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
|
||||
Assert.AreEqual(Math.Round(tc.RealVariableDouble, 3), Math.Round(tc2.RealVariableDouble, 3));
|
||||
Assert.AreEqual(tc.RealVariableFloat, tc2.RealVariableFloat);
|
||||
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable);
|
||||
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
|
||||
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
|
||||
|
||||
Assert.AreEqual(TestClassWithPrivateSetters.PRIVATE_SETTER_VALUE, tc2.PrivateSetterProperty);
|
||||
@@ -620,9 +631,11 @@ namespace S7.Net.UnitTest
|
||||
tc.BitVariable10 = true;
|
||||
tc.DIntVariable = -100000;
|
||||
tc.IntVariable = -15000;
|
||||
tc.RealVariableDouble = -154.789;
|
||||
tc.RealVariableFloat = -154.789f;
|
||||
tc.LRealVariable = -154.789;
|
||||
tc.RealVariable = -154.789f;
|
||||
tc.DWordVariable = 850;
|
||||
tc.WStringVariable = "ÄÜÉÊéà";
|
||||
tc.StringVariable = "Hallo";
|
||||
|
||||
plc.WriteClass(tc, DB2);
|
||||
|
||||
@@ -635,9 +648,11 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc2.BitVariable10, tc2Generic.BitVariable10);
|
||||
Assert.AreEqual(tc2.DIntVariable, tc2Generic.DIntVariable);
|
||||
Assert.AreEqual(tc2.IntVariable, tc2Generic.IntVariable);
|
||||
Assert.AreEqual(Math.Round(tc2.RealVariableDouble, 3), Math.Round(tc2Generic.RealVariableDouble, 3));
|
||||
Assert.AreEqual(tc2.RealVariableFloat, tc2Generic.RealVariableFloat);
|
||||
Assert.AreEqual(Math.Round(tc2.LRealVariable, 3), Math.Round(tc2Generic.LRealVariable, 3));
|
||||
Assert.AreEqual(tc2.RealVariable, tc2Generic.RealVariable);
|
||||
Assert.AreEqual(tc2.DWordVariable, tc2Generic.DWordVariable);
|
||||
Assert.AreEqual(tc2.WStringVariable, tc2Generic.WStringVariable);
|
||||
Assert.AreEqual(tc2.StringVariable, tc2Generic.StringVariable);
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(PlcException))]
|
||||
@@ -663,9 +678,11 @@ namespace S7.Net.UnitTest
|
||||
tc.BitVariable10 = true;
|
||||
tc.DIntVariable = -100000;
|
||||
tc.IntVariable = -15000;
|
||||
tc.RealVariableDouble = -154.789;
|
||||
tc.RealVariableFloat = -154.789f;
|
||||
tc.LRealVariable = -154.789;
|
||||
tc.RealVariable = -154.789f;
|
||||
tc.DWordVariable = 850;
|
||||
tc.WStringVariable = "ÄÜÉÊéà";
|
||||
tc.StringVariable = "Hallo";
|
||||
|
||||
plc.WriteClass(tc, DB2);
|
||||
|
||||
@@ -677,9 +694,11 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc2Generic.BitVariable10, tc2GenericWithClassFactory.BitVariable10);
|
||||
Assert.AreEqual(tc2Generic.DIntVariable, tc2GenericWithClassFactory.DIntVariable);
|
||||
Assert.AreEqual(tc2Generic.IntVariable, tc2GenericWithClassFactory.IntVariable);
|
||||
Assert.AreEqual(Math.Round(tc2Generic.RealVariableDouble, 3), Math.Round(tc2GenericWithClassFactory.RealVariableDouble, 3));
|
||||
Assert.AreEqual(tc2Generic.RealVariableFloat, tc2GenericWithClassFactory.RealVariableFloat);
|
||||
Assert.AreEqual(Math.Round(tc2Generic.LRealVariable, 3), Math.Round(tc2GenericWithClassFactory.LRealVariable, 3));
|
||||
Assert.AreEqual(tc2Generic.RealVariable, tc2GenericWithClassFactory.RealVariable);
|
||||
Assert.AreEqual(tc2Generic.DWordVariable, tc2GenericWithClassFactory.DWordVariable);
|
||||
Assert.AreEqual(tc2Generic.WStringVariable, tc2GenericWithClassFactory.WStringVariable);
|
||||
Assert.AreEqual(tc2Generic.StringVariable, tc2GenericWithClassFactory.StringVariable);
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(PlcException))]
|
||||
@@ -743,6 +762,53 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(tc.ShortVariable04.ShortVarialbe00, tc2.ShortVariable04.ShortVarialbe00);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write/Read a large amount of data to test PDU max
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void T33_WriteLargeByteArray()
|
||||
{
|
||||
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
|
||||
|
||||
var randomEngine = new Random();
|
||||
var data = new byte[8192];
|
||||
var db = 2;
|
||||
randomEngine.NextBytes(data);
|
||||
|
||||
plc.WriteBytes(DataType.DataBlock, db, 0, data);
|
||||
|
||||
var readData = plc.ReadBytes(DataType.DataBlock, db, 0, data.Length);
|
||||
|
||||
CollectionAssert.AreEqual(data, readData);
|
||||
}
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
|
||||
/// <summary>
|
||||
/// Write/Read a large amount of data to test PDU max
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void T33_WriteLargeByteArrayWithSpan()
|
||||
{
|
||||
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
|
||||
|
||||
var randomEngine = new Random();
|
||||
using var dataOwner = MemoryPool<byte>.Shared.Rent(8192);
|
||||
var data = dataOwner.Memory.Span.Slice(0, 8192);
|
||||
var db = 2;
|
||||
randomEngine.NextBytes(data);
|
||||
|
||||
plc.WriteBytes(DataType.DataBlock, db, 0, data);
|
||||
|
||||
using var readDataOwner = MemoryPool<byte>.Shared.Rent(data.Length);
|
||||
var readData = readDataOwner.Memory.Span.Slice(0, data.Length);
|
||||
plc.ReadBytes(readData, DataType.DataBlock, db, 0);
|
||||
|
||||
CollectionAssert.AreEqual(data.ToArray(), readData.ToArray());
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
[TestMethod, ExpectedException(typeof(PlcException))]
|
||||
public void T18_ReadStructThrowsIfPlcIsNotConnected()
|
||||
{
|
||||
@@ -766,9 +832,11 @@ namespace S7.Net.UnitTest
|
||||
ts.BitVariable10 = true;
|
||||
ts.DIntVariable = -100000;
|
||||
ts.IntVariable = -15000;
|
||||
ts.RealVariableDouble = -154.789;
|
||||
ts.RealVariableFloat = -154.789f;
|
||||
ts.LRealVariable = -154.789;
|
||||
ts.RealVariable = -154.789f;
|
||||
ts.DWordVariable = 850;
|
||||
ts.WStringVariable = "ÄÜÉÊéà";
|
||||
ts.StringVariable = "Hallo";
|
||||
|
||||
plc.WriteStruct(ts, DB2);
|
||||
|
||||
@@ -780,9 +848,11 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(ts2.BitVariable10, ts2Generic.BitVariable10);
|
||||
Assert.AreEqual(ts2.DIntVariable, ts2Generic.DIntVariable);
|
||||
Assert.AreEqual(ts2.IntVariable, ts2Generic.IntVariable);
|
||||
Assert.AreEqual(Math.Round(ts2.RealVariableDouble, 3), Math.Round(ts2Generic.RealVariableDouble, 3));
|
||||
Assert.AreEqual(ts2.RealVariableFloat, ts2Generic.RealVariableFloat);
|
||||
Assert.AreEqual(ts2.LRealVariable, ts2Generic.LRealVariable);
|
||||
Assert.AreEqual(ts2.RealVariable, ts2Generic.RealVariable);
|
||||
Assert.AreEqual(ts2.DWordVariable, ts2Generic.DWordVariable);
|
||||
Assert.AreEqual(ts2.WStringVariable, ts2Generic.WStringVariable);
|
||||
Assert.AreEqual(ts2.StringVariable, ts2Generic.StringVariable);
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(PlcException))]
|
||||
@@ -811,9 +881,12 @@ namespace S7.Net.UnitTest
|
||||
tc.BitVariable10 = true;
|
||||
tc.DIntVariable = -100000;
|
||||
tc.IntVariable = -15000;
|
||||
tc.RealVariableDouble = -154.789;
|
||||
tc.RealVariableFloat = -154.789f;
|
||||
tc.LRealVariable = -154.789;
|
||||
tc.RealVariable = -154.789f;
|
||||
tc.DWordVariable = 850;
|
||||
tc.WStringVariable = "ÄÜÉÊéà";
|
||||
tc.StringVariable = "Hallo";
|
||||
|
||||
plc.WriteClass(tc, DB2);
|
||||
|
||||
int expectedReadBytes = (int)Types.Class.GetClassSize(tc);
|
||||
@@ -910,7 +983,14 @@ namespace S7.Net.UnitTest
|
||||
S7TestServer.Stop();
|
||||
|
||||
var unreachablePlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 2);
|
||||
Assert.IsFalse(unreachablePlc.IsAvailable);
|
||||
try
|
||||
{
|
||||
unreachablePlc.Open();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
Assert.IsFalse(unreachablePlc.IsConnected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -918,21 +998,21 @@ namespace S7.Net.UnitTest
|
||||
{
|
||||
plc.Close();
|
||||
S7TestServer.Stop();
|
||||
S7TestServer.Start();
|
||||
S7TestServer.Start(TestServerPort);
|
||||
|
||||
var reachablePlc = new Plc(CpuType.S7300, "127.0.0.1", 0, 2);
|
||||
Assert.IsTrue(reachablePlc.IsAvailable);
|
||||
var reachablePlc = CreatePlc();
|
||||
reachablePlc.Open();
|
||||
Assert.IsTrue(reachablePlc.IsConnected);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void T26_ReadWriteDouble()
|
||||
{
|
||||
double test_value = 55.66;
|
||||
plc.Write("DB1.DBD0", test_value);
|
||||
var helper = plc.Read("DB1.DBD0");
|
||||
double test_value2 = Conversion.ConvertToDouble((uint)helper);
|
||||
plc.Write(DataType.DataBlock, 1, 0, test_value);
|
||||
var result = (double)plc.Read(DataType.DataBlock, 1, 0, VarType.LReal, 1);
|
||||
|
||||
Assert.AreEqual(test_value, test_value2, 0.01, "Compare Write/Read"); //Need delta here because S7 only has 32 bit reals
|
||||
Assert.AreEqual(test_value, result, "Compare Write/Read");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -957,6 +1037,32 @@ namespace S7.Net.UnitTest
|
||||
}
|
||||
}
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
|
||||
[TestMethod]
|
||||
public void T27_ReadWriteBytesManyWithSpan()
|
||||
{
|
||||
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
|
||||
|
||||
using var dataOwner = MemoryPool<byte>.Shared.Rent(2000);
|
||||
var data = dataOwner.Memory.Span;
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
data[i] = (byte)(i % 256);
|
||||
|
||||
plc.WriteBytes(DataType.DataBlock, 2, 0, data);
|
||||
|
||||
using var readDataOwner = MemoryPool<byte>.Shared.Rent(data.Length);
|
||||
var readData = readDataOwner.Memory.Span.Slice(0, data.Length);
|
||||
plc.ReadBytes(readData, DataType.DataBlock, 2, 0);
|
||||
|
||||
for (int x = 0; x < data.Length; x++)
|
||||
{
|
||||
Assert.AreEqual(x % 256, readData[x], $"Mismatch at offset {x}, expected {x % 256}, actual {readData[x]}.");
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
[TestMethod]
|
||||
public void T28_ReadClass_DoesntCrash_When_ReadingLessThan1Byte()
|
||||
{
|
||||
@@ -997,21 +1103,23 @@ namespace S7.Net.UnitTest
|
||||
Assert.AreEqual(test_value, test_value2, "Compare Write/Read"); //No delta, datatype matches
|
||||
}
|
||||
|
||||
#endregion
|
||||
[TestMethod]
|
||||
public void T33_ReadWriteDateTimeLong()
|
||||
{
|
||||
var test_value = System.DateTime.Now;
|
||||
var db = 1;
|
||||
var offset = 0;
|
||||
|
||||
plc.WriteBytes(DataType.DataBlock, db, offset, Types.DateTimeLong.ToByteArray(test_value));
|
||||
var test_value2 = plc.Read(DataType.DataBlock, db, offset, VarType.DateTimeLong, 1);
|
||||
Assert.IsInstanceOfType(test_value2, typeof(System.DateTime));
|
||||
|
||||
Assert.AreEqual(test_value, test_value2, "Compare DateTimeLong Write/Read");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private methods
|
||||
private static void ShutDownServiceS7oiehsx64()
|
||||
{
|
||||
ServiceController[] services = ServiceController.GetServices();
|
||||
var service = services.FirstOrDefault(s => s.ServiceName == "s7oiehsx64");
|
||||
if (service != null)
|
||||
{
|
||||
if (service.Status == ServiceControllerStatus.Running)
|
||||
{
|
||||
service.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
private bool disposedValue = false; // To detect redundant calls
|
||||
|
||||
@@ -36,11 +36,8 @@ namespace Snap7
|
||||
|
||||
public class S7Consts
|
||||
{
|
||||
#if __MonoCS__ // Assuming that we are using Unix release of Mono (otherwise modify it)
|
||||
public const string Snap7LibName = "libsnap7.so";
|
||||
#else
|
||||
public const string Snap7LibName = "snap7.dll";
|
||||
#endif
|
||||
public const string Snap7LibName = "snap7";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// PARAMS LIST
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
110
S7.Net.UnitTest/StreamTests.cs
Normal file
110
S7.Net.UnitTest/StreamTests.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace S7.Net.UnitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Test stream which only gives 1 byte per read.
|
||||
/// </summary>
|
||||
class TestStream1BytePerRead : Stream
|
||||
{
|
||||
public TestStream1BytePerRead(byte[] data)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public override bool CanRead => _position < Data.Length;
|
||||
|
||||
public override bool CanSeek => throw new NotImplementedException();
|
||||
|
||||
public override bool CanWrite => throw new NotImplementedException();
|
||||
|
||||
public override long Length => throw new NotImplementedException();
|
||||
|
||||
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
||||
public byte[] Data { get; }
|
||||
|
||||
int _position = 0;
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_position >= Data.Length)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
buffer[offset] = Data[_position];
|
||||
++_position;
|
||||
|
||||
return Task.FromResult(1);
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// These tests are intended to test <see cref="StreamExtensions"/> functions and other stream-related special cases.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class StreamTests
|
||||
{
|
||||
public TestContext TestContext { get; set; }
|
||||
|
||||
[TestMethod]
|
||||
public async Task TPKT_ReadRestrictedStreamAsync()
|
||||
{
|
||||
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd");
|
||||
var m = new TestStream1BytePerRead(fullMessage);
|
||||
var t = await TPKT.ReadAsync(m, TestContext.CancellationTokenSource.Token);
|
||||
Assert.AreEqual(fullMessage.Length, t.Length);
|
||||
Assert.AreEqual(fullMessage.Last(), t.Data.Last());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task TPKT_ReadRestrictedStream()
|
||||
{
|
||||
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd");
|
||||
var m = new TestStream1BytePerRead(fullMessage);
|
||||
var t = await TPKT.ReadAsync(m, CancellationToken.None);
|
||||
Assert.AreEqual(fullMessage.Length, t.Length);
|
||||
Assert.AreEqual(fullMessage.Last(), t.Data.Last());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task TPKT_ReadStreamTooShort()
|
||||
{
|
||||
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400");
|
||||
var m = new TestStream1BytePerRead(fullMessage);
|
||||
await Assert.ThrowsExceptionAsync<TPKTInvalidException>(() => TPKT.ReadAsync(m, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
}
|
||||
38
S7.Net.UnitTest/TypeTests/BooleanTests.cs
Normal file
38
S7.Net.UnitTest/TypeTests/BooleanTests.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Boolean = S7.Net.Types.Boolean;
|
||||
|
||||
namespace S7.Net.UnitTest.TypeTests
|
||||
{
|
||||
[TestClass]
|
||||
public class BooleanTests
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(1)]
|
||||
[DataRow(2)]
|
||||
[DataRow(3)]
|
||||
[DataRow(4)]
|
||||
[DataRow(5)]
|
||||
[DataRow(6)]
|
||||
[DataRow(7)]
|
||||
public void TestValidSetBitValues(int index)
|
||||
{
|
||||
Assert.AreEqual(Math.Pow(2, index), Boolean.SetBit(0, index));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(0)]
|
||||
[DataRow(1)]
|
||||
[DataRow(2)]
|
||||
[DataRow(3)]
|
||||
[DataRow(4)]
|
||||
[DataRow(5)]
|
||||
[DataRow(6)]
|
||||
[DataRow(7)]
|
||||
public void TestValidClearBitValues(int index)
|
||||
{
|
||||
Assert.AreEqual((byte) ((uint) Math.Pow(2, index) ^ uint.MaxValue), Boolean.ClearBit(byte.MaxValue, index));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,19 @@ namespace S7.Net.UnitTest.TypeTests
|
||||
Assert.AreEqual(Class.GetClassSize(new TestClassUnevenSize(3, 17)), 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure Uint32 is correctly parsed through ReadClass functions. Adresses issue https://github.com/S7NetPlus/s7netplus/issues/414
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestUint32Read()
|
||||
{
|
||||
var result = new TestUint32();
|
||||
var data = new byte[4] { 0, 0, 0, 5 };
|
||||
var bytesRead = Class.FromBytes(result, data);
|
||||
Assert.AreEqual(bytesRead, data.Length);
|
||||
Assert.AreEqual(5u, result.Value1);
|
||||
}
|
||||
|
||||
private class TestClassUnevenSize
|
||||
{
|
||||
public bool Bool { get; set; }
|
||||
@@ -29,5 +42,10 @@ namespace S7.Net.UnitTest.TypeTests
|
||||
Bools = new bool[bitCount];
|
||||
}
|
||||
}
|
||||
|
||||
private class TestUint32
|
||||
{
|
||||
public uint Value1 { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
171
S7.Net.UnitTest/TypeTests/DateTimeLongTests.cs
Normal file
171
S7.Net.UnitTest/TypeTests/DateTimeLongTests.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace S7.Net.UnitTest.TypeTests
|
||||
{
|
||||
public static class DateTimeLongTests
|
||||
{
|
||||
private static readonly DateTime SampleDateTime = new DateTime(1993, 12, 25, 8, 12, 34, 567);
|
||||
|
||||
private static readonly byte[] SampleByteArray = {0x07, 0xC9, 0x0C, 0x19, 0x07, 0x08, 0x0C, 0x22, 0x21, 0xCB, 0xBB, 0xC0 };
|
||||
|
||||
private static readonly byte[] SpecMinByteArray =
|
||||
{
|
||||
0x07, 0xB2, 0x01, 0x01, (byte) (int) (Types.DateTimeLong.SpecMinimumDateTime.DayOfWeek + 1), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
private static readonly byte[] SpecMaxByteArray =
|
||||
{
|
||||
0x08, 0xD6, 0x04, 0x0B, (byte) (int) (Types.DateTimeLong.SpecMaximumDateTime.DayOfWeek + 1), 0x17, 0x2F, 0x10, 0x32, 0xE7, 0x01, 0x80
|
||||
};
|
||||
|
||||
[TestClass]
|
||||
public class FromByteArray
|
||||
{
|
||||
[TestMethod]
|
||||
public void Sample()
|
||||
{
|
||||
AssertFromByteArrayEquals(SampleDateTime, SampleByteArray);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SpecMinimum()
|
||||
{
|
||||
AssertFromByteArrayEquals(Types.DateTimeLong.SpecMinimumDateTime, SpecMinByteArray);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SpecMaximum()
|
||||
{
|
||||
AssertFromByteArrayEquals(Types.DateTimeLong.SpecMaximumDateTime, SpecMaxByteArray);
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnLessThan12Bytes()
|
||||
{
|
||||
Types.DateTimeLong.FromByteArray(new byte[11]);
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnMoreTHan12Bytes()
|
||||
{
|
||||
Types.DateTimeLong.FromByteArray(new byte[13]);
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnInvalidYear()
|
||||
{
|
||||
Types.DateTimeLong.FromByteArray(MutateSample(0, 0xa0));
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnZeroMonth()
|
||||
{
|
||||
Types.DateTimeLong.FromByteArray(MutateSample(2, 0x00));
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnTooLargeMonth()
|
||||
{
|
||||
Types.DateTimeLong.FromByteArray(MutateSample(2, 0x13));
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnZeroDay()
|
||||
{
|
||||
Types.DateTimeLong.FromByteArray(MutateSample(3, 0x00));
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnTooLargeDay()
|
||||
{
|
||||
Types.DateTimeLong.FromByteArray(MutateSample(3, 0x32));
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnInvalidHour()
|
||||
{
|
||||
Types.DateTimeLong.FromByteArray(MutateSample(5, 0x24));
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnInvalidMinute()
|
||||
{
|
||||
Types.DateTimeLong.FromByteArray(MutateSample(6, 0x60));
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnInvalidSecond()
|
||||
{
|
||||
Types.DateTimeLong.FromByteArray(MutateSample(7, 0x60));
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnInvalidNanosecondsFirstDigit()
|
||||
{
|
||||
Types.DateTimeLong.FromByteArray(MutateSample(8, 0x3B));
|
||||
}
|
||||
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnZeroDayOfWeek()
|
||||
{
|
||||
Types.DateTimeLong.FromByteArray(MutateSample(4, 0));
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnTooLargeDayOfWeek()
|
||||
{
|
||||
Types.DateTimeLong.FromByteArray(MutateSample(4, 8));
|
||||
}
|
||||
|
||||
private static void AssertFromByteArrayEquals(DateTime expected, params byte[] bytes)
|
||||
{
|
||||
Assert.AreEqual(expected, Types.DateTimeLong.FromByteArray(bytes));
|
||||
}
|
||||
|
||||
private static byte[] MutateSample(int index, byte value) =>
|
||||
SampleByteArray.Select((b, i) => i == index ? value : b).ToArray();
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class ToByteArray
|
||||
{
|
||||
[TestMethod]
|
||||
public void Sample()
|
||||
{
|
||||
AssertToByteArrayEquals(SampleDateTime, SampleByteArray);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SpecMinimum()
|
||||
{
|
||||
AssertToByteArrayEquals(Types.DateTimeLong.SpecMinimumDateTime, SpecMinByteArray);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SpecMaximum()
|
||||
{
|
||||
AssertToByteArrayEquals(Types.DateTimeLong.SpecMaximumDateTime, SpecMaxByteArray);
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnTimeBeforeSpecMinimum()
|
||||
{
|
||||
Types.DateTimeLong.ToByteArray(new DateTime(1950, 1, 1));
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnTimeAfterSpecMaximum()
|
||||
{
|
||||
Types.DateTimeLong.ToByteArray(new DateTime(2790, 1, 1));
|
||||
}
|
||||
|
||||
private static void AssertToByteArrayEquals(DateTime value, params byte[] expected)
|
||||
{
|
||||
CollectionAssert.AreEqual(expected, Types.DateTimeLong.ToByteArray(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
152
S7.Net.UnitTest/TypeTests/S7StringTests.cs
Normal file
152
S7.Net.UnitTest/TypeTests/S7StringTests.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using S7.Net.Types;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
|
||||
namespace S7.Net.UnitTest.TypeTests
|
||||
{
|
||||
[TestClass]
|
||||
public class S7StringTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void ReadEmptyStringWithZeroByteLength()
|
||||
{
|
||||
AssertFromByteArrayEquals("", 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadEmptyStringWithOneByteLength()
|
||||
{
|
||||
AssertFromByteArrayEquals("", 1, 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadEmptyStringWithOneByteGarbage()
|
||||
{
|
||||
AssertFromByteArrayEquals("", 1, 0, (byte) 'a');
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMalformedStringTooShort()
|
||||
{
|
||||
Assert.ThrowsException<PlcException>(() => AssertFromByteArrayEquals("", 1));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMalformedStringSizeLargerThanCapacity()
|
||||
{
|
||||
Assert.ThrowsException<PlcException>(() => S7String.FromByteArray(new byte[] { 3, 5, 0, 1, 2 }));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMalformedStringCapacityTooLarge()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentException>(() => AssertToByteArrayAndBackEquals("", 300, 0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadA()
|
||||
{
|
||||
AssertFromByteArrayEquals("A", 1, 1, (byte) 'A');
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadAbc()
|
||||
{
|
||||
AssertFromByteArrayEquals("Abc", 3, 3, (byte) 'A', (byte) 'b', (byte) 'c');
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteNullWithReservedLengthZero()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(() => AssertToByteArrayAndBackEquals(null, 0, 0, 0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteEmptyStringWithReservedLengthZero()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("", 0, 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAWithReservedLengthZero()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("", 0, 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteNullWithReservedLengthOne()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(() => AssertToByteArrayAndBackEquals(null, 1, 1, 0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteEmptyStringWithReservedLengthOne()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("", 1, 1, 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAWithReservedLengthOne()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("A", 1, 1, 1, (byte) 'A');
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAWithReservedLengthTwo()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("A", 2, 2, 1, (byte) 'A', 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAbcWithStringLargerThanReservedLength()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentException>(() => S7String.ToByteArray("Abc", 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAbcWithReservedLengthThree()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("Abc", 3, 3, 3, (byte) 'A', (byte) 'b', (byte) 'c');
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAbcWithReservedLengthFour()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("Abc", 4, 4, 3, (byte) 'A', (byte) 'b', (byte) 'c', 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OddS7StringByteLength()
|
||||
{
|
||||
AssertVarTypeToByteLength(VarType.S7String, 1, 4);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EvenS7StringByteLength()
|
||||
{
|
||||
AssertVarTypeToByteLength(VarType.S7String, 2, 4);
|
||||
}
|
||||
|
||||
private static void AssertFromByteArrayEquals(string expected, params byte[] bytes)
|
||||
{
|
||||
var convertedString = S7String.FromByteArray(bytes);
|
||||
Assert.AreEqual(expected, convertedString);
|
||||
}
|
||||
|
||||
private static void AssertToByteArrayAndBackEquals(string value, int reservedLength, params byte[] expected)
|
||||
{
|
||||
var convertedData = S7String.ToByteArray(value, reservedLength);
|
||||
CollectionAssert.AreEqual(expected, convertedData);
|
||||
var convertedBack = S7String.FromByteArray(convertedData);
|
||||
Assert.AreEqual(value, convertedBack);
|
||||
}
|
||||
|
||||
private void AssertVarTypeToByteLength(VarType varType, int count, int expectedByteLength)
|
||||
{
|
||||
var byteLength = Plc.VarTypeToByteLength(varType, count);
|
||||
Assert.AreEqual(expectedByteLength, byteLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
151
S7.Net.UnitTest/TypeTests/S7WStringTests.cs
Normal file
151
S7.Net.UnitTest/TypeTests/S7WStringTests.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using S7.Net.Types;
|
||||
using System;
|
||||
|
||||
namespace S7.Net.UnitTest.TypeTests
|
||||
{
|
||||
[TestClass]
|
||||
public class S7WStringTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void ReadEmptyStringWithZeroLength()
|
||||
{
|
||||
AssertFromByteArrayEquals("", 0, 0 , 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadEmptyStringWithOneCharLength()
|
||||
{
|
||||
AssertFromByteArrayEquals("", 0, 1, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadEmptyStringWithOneCharGarbage()
|
||||
{
|
||||
|
||||
AssertFromByteArrayEquals("", 0, 1, 0, 0, 0x00, 0x41);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMalformedStringTooShort()
|
||||
{
|
||||
Assert.ThrowsException<PlcException>(() => AssertFromByteArrayEquals("", 0, 1));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMalformedStringSizeLargerThanCapacity()
|
||||
{
|
||||
Assert.ThrowsException<PlcException>(() => S7WString.FromByteArray(new byte[] { 0, 3, 0, 5, 0, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41}));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMalformedStringCapacityTooLarge()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentException>(() => AssertToByteArrayAndBackEquals("", 20000, 0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadA()
|
||||
{
|
||||
AssertFromByteArrayEquals("A", 0, 1, 0, 1, 0x00, 0x41);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadAbc()
|
||||
{
|
||||
AssertFromByteArrayEquals("Abc", 0, 3, 0, 3, 0x00, 0x41, 0x00, 0x62, 0x00, 0x63);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteNullWithReservedLengthZero()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(() => AssertToByteArrayAndBackEquals(null, 0, 0, 0, 0, 0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteEmptyStringWithReservedLengthZero()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("", 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAWithReservedLengthZero()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("", 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteNullWithReservedLengthOne()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(() => AssertToByteArrayAndBackEquals(null, 1, 0, 1 , 0, 0));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteEmptyStringWithReservedLengthOne()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("", 1, 0, 1, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAWithReservedLengthOne()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("A", 1, 0, 1, 0, 1, 0x00, 0x41);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAWithReservedLengthTwo()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("A", 2, 0, 2, 0, 1, 0x00, 0x41, 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAbcWithStringLargerThanReservedLength()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentException>(() => S7WString.ToByteArray("Abc", 2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAbcWithReservedLengthThree()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("Abc", 3, 0, 3, 0, 3, 0x00, 0x41, 0x00, 0x62, 0x00, 0x63);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAbcWithReservedLengthFour()
|
||||
{
|
||||
AssertToByteArrayAndBackEquals("Abc", 4, 0, 4, 0, 3, 0x00, 0x41, 0x00, 0x62, 0x00, 0x63, 0 , 0);
|
||||
}
|
||||
|
||||
private static void AssertFromByteArrayEquals(string expected, params byte[] bytes)
|
||||
{
|
||||
var convertedString = S7WString.FromByteArray(bytes);
|
||||
Assert.AreEqual(expected, convertedString);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OddS7WStringByteLength()
|
||||
{
|
||||
AssertVarTypeToByteLength(VarType.S7WString, 1, 6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EvenS7WStringByteLength()
|
||||
{
|
||||
AssertVarTypeToByteLength(VarType.S7WString, 2, 8);
|
||||
}
|
||||
|
||||
private static void AssertToByteArrayAndBackEquals(string value, int reservedLength, params byte[] expected)
|
||||
{
|
||||
var convertedData = S7WString.ToByteArray(value, reservedLength);
|
||||
CollectionAssert.AreEqual(expected, convertedData);
|
||||
var convertedBack = S7WString.FromByteArray(convertedData);
|
||||
Assert.AreEqual(value, convertedBack);
|
||||
}
|
||||
|
||||
private void AssertVarTypeToByteLength(VarType varType, int count, int expectedByteLength)
|
||||
{
|
||||
var byteLength = Plc.VarTypeToByteLength(varType, count);
|
||||
Assert.AreEqual(expectedByteLength, byteLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using S7.Net.Types;
|
||||
|
||||
namespace S7.Net.UnitTest.TypeTests
|
||||
{
|
||||
[TestClass]
|
||||
public class StringExTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void ReadEmptyStringWithZeroByteLength()
|
||||
{
|
||||
AssertFromByteArrayEquals("", 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadEmptyStringWithOneByteLength()
|
||||
{
|
||||
AssertFromByteArrayEquals("", 1, 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadEmptyStringWithOneByteGarbage()
|
||||
{
|
||||
AssertFromByteArrayEquals("", 1, 0, (byte) 'a');
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadA()
|
||||
{
|
||||
AssertFromByteArrayEquals("A", 1, 1, (byte) 'A');
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadAbc()
|
||||
{
|
||||
AssertFromByteArrayEquals("Abc", 1, 3, (byte) 'A', (byte) 'b', (byte) 'c');
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteNullWithReservedLengthZero()
|
||||
{
|
||||
AssertToByteArrayEquals(null, 0, 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteEmptyStringWithReservedLengthZero()
|
||||
{
|
||||
AssertToByteArrayEquals("", 0, 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAWithReservedLengthZero()
|
||||
{
|
||||
AssertToByteArrayEquals("A", 0, 0, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteNullWithReservedLengthOne()
|
||||
{
|
||||
AssertToByteArrayEquals(null, 1, 1, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteEmptyStringWithReservedLengthOne()
|
||||
{
|
||||
AssertToByteArrayEquals("", 1, 1, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAWithReservedLengthOne()
|
||||
{
|
||||
AssertToByteArrayEquals("A", 1, 1, 1, (byte) 'A');
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAWithReservedLengthTwo()
|
||||
{
|
||||
AssertToByteArrayEquals("A", 2, 2, 1, (byte) 'A');
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAbcWithReservedLengthOne()
|
||||
{
|
||||
AssertToByteArrayEquals("Abc", 1, 1, 1, (byte) 'A');
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAbcWithReservedLengthTwo()
|
||||
{
|
||||
AssertToByteArrayEquals("Abc", 2, 2, 2, (byte) 'A', (byte) 'b');
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAbcWithReservedLengthThree()
|
||||
{
|
||||
AssertToByteArrayEquals("Abc", 3, 3, 3, (byte) 'A', (byte) 'b', (byte) 'c');
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WriteAbcWithReservedLengthFour()
|
||||
{
|
||||
AssertToByteArrayEquals("Abc", 4, 4, 3, (byte) 'A', (byte) 'b', (byte) 'c');
|
||||
}
|
||||
|
||||
private static void AssertFromByteArrayEquals(string expected, params byte[] bytes)
|
||||
{
|
||||
Assert.AreEqual(expected, StringEx.FromByteArray(bytes));
|
||||
}
|
||||
|
||||
private static void AssertToByteArrayEquals(string value, int reservedLength, params byte[] expected)
|
||||
{
|
||||
CollectionAssert.AreEqual(expected, StringEx.ToByteArray(value, reservedLength));
|
||||
}
|
||||
}
|
||||
}
|
||||
82
S7.Net.UnitTest/TypeTests/TimeSpanTests.cs
Normal file
82
S7.Net.UnitTest/TypeTests/TimeSpanTests.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace S7.Net.UnitTest.TypeTests
|
||||
{
|
||||
public static class TimeSpanTests
|
||||
{
|
||||
private static readonly TimeSpan SampleTimeSpan = new TimeSpan(12, 0, 59, 37, 856);
|
||||
|
||||
private static readonly byte[] SampleByteArray = { 0x3E, 0x02, 0xE8, 0x00 };
|
||||
|
||||
private static readonly byte[] SpecMinByteArray = { 0x80, 0x00, 0x00, 0x00 };
|
||||
|
||||
private static readonly byte[] SpecMaxByteArray = { 0x7F, 0xFF, 0xFF, 0xFF };
|
||||
|
||||
[TestClass]
|
||||
public class FromByteArray
|
||||
{
|
||||
[TestMethod]
|
||||
public void Sample()
|
||||
{
|
||||
AssertFromByteArrayEquals(SampleTimeSpan, SampleByteArray);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SpecMinimum()
|
||||
{
|
||||
AssertFromByteArrayEquals(Types.TimeSpan.SpecMinimumTimeSpan, SpecMinByteArray);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SpecMaximum()
|
||||
{
|
||||
AssertFromByteArrayEquals(Types.TimeSpan.SpecMaximumTimeSpan, SpecMaxByteArray);
|
||||
}
|
||||
|
||||
private static void AssertFromByteArrayEquals(TimeSpan expected, params byte[] bytes)
|
||||
{
|
||||
Assert.AreEqual(expected, Types.TimeSpan.FromByteArray(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public class ToByteArray
|
||||
{
|
||||
[TestMethod]
|
||||
public void Sample()
|
||||
{
|
||||
AssertToByteArrayEquals(SampleTimeSpan, SampleByteArray);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SpecMinimum()
|
||||
{
|
||||
AssertToByteArrayEquals(Types.TimeSpan.SpecMinimumTimeSpan, SpecMinByteArray);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SpecMaximum()
|
||||
{
|
||||
AssertToByteArrayEquals(Types.TimeSpan.SpecMaximumTimeSpan, SpecMaxByteArray);
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnTimeBeforeSpecMinimum()
|
||||
{
|
||||
Types.TimeSpan.ToByteArray(TimeSpan.FromDays(-25));
|
||||
}
|
||||
|
||||
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
|
||||
public void ThrowsOnTimeAfterSpecMaximum()
|
||||
{
|
||||
Types.TimeSpan.ToByteArray(new TimeSpan(30, 15, 15, 15, 15));
|
||||
}
|
||||
|
||||
private static void AssertToByteArrayEquals(TimeSpan value, params byte[] expected)
|
||||
{
|
||||
CollectionAssert.AreEqual(expected, Types.TimeSpan.ToByteArray(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
S7.Net.UnitTest/runtimes/win-x64/native/snap7.dll
Normal file
BIN
S7.Net.UnitTest/runtimes/win-x64/native/snap7.dll
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
|
||||
namespace S7.Net
|
||||
{
|
||||
@@ -11,6 +11,11 @@ namespace S7.Net
|
||||
/// </summary>
|
||||
internal class COTP
|
||||
{
|
||||
public enum PduType : byte
|
||||
{
|
||||
Data = 0xf0,
|
||||
ConnectionConfirmed = 0xd0
|
||||
}
|
||||
/// <summary>
|
||||
/// Describes a COTP TPDU (Transport protocol data unit)
|
||||
/// </summary>
|
||||
@@ -18,7 +23,7 @@ namespace S7.Net
|
||||
{
|
||||
public TPKT TPkt { get; }
|
||||
public byte HeaderLength;
|
||||
public byte PDUType;
|
||||
public PduType PDUType;
|
||||
public int TPDUNumber;
|
||||
public byte[] Data;
|
||||
public bool LastDataUnit;
|
||||
@@ -27,17 +32,17 @@ namespace S7.Net
|
||||
{
|
||||
TPkt = tPKT;
|
||||
|
||||
var br = new BinaryReader(new MemoryStream(tPKT.Data));
|
||||
HeaderLength = br.ReadByte();
|
||||
HeaderLength = tPKT.Data[0]; // Header length excluding this length byte
|
||||
if (HeaderLength >= 2)
|
||||
{
|
||||
PDUType = br.ReadByte();
|
||||
if (PDUType == 0xf0) //DT Data
|
||||
PDUType = (PduType)tPKT.Data[1];
|
||||
if (PDUType == PduType.Data) //DT Data
|
||||
{
|
||||
var flags = br.ReadByte();
|
||||
var flags = tPKT.Data[2];
|
||||
TPDUNumber = flags & 0x7F;
|
||||
LastDataUnit = (flags & 0x80) > 0;
|
||||
Data = br.ReadBytes(tPKT.Length - HeaderLength - 4); //4 = TPKT Size
|
||||
Data = new byte[tPKT.Data.Length - HeaderLength - 1]; // substract header length byte + header length.
|
||||
Array.Copy(tPKT.Data, HeaderLength + 1, Data, 0, Data.Length);
|
||||
return;
|
||||
}
|
||||
//TODO: Handle other PDUTypes
|
||||
@@ -50,25 +55,16 @@ namespace S7.Net
|
||||
/// See: https://tools.ietf.org/html/rfc905
|
||||
/// </summary>
|
||||
/// <param name="stream">The socket to read from</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||
/// <returns>COTP DPDU instance</returns>
|
||||
public static TPDU Read(Stream stream)
|
||||
public static async Task<TPDU> ReadAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var tpkt = TPKT.Read(stream);
|
||||
if (tpkt.Length > 0) return new TPDU(tpkt);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads COTP TPDU (Transport protocol data unit) from the network stream
|
||||
/// See: https://tools.ietf.org/html/rfc905
|
||||
/// </summary>
|
||||
/// <param name="stream">The socket to read from</param>
|
||||
/// <returns>COTP DPDU instance</returns>
|
||||
public static async Task<TPDU> ReadAsync(Stream stream)
|
||||
{
|
||||
var tpkt = await TPKT.ReadAsync(stream);
|
||||
if (tpkt.Length > 0) return new TPDU(tpkt);
|
||||
return null;
|
||||
var tpkt = await TPKT.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
if (tpkt.Length == 0)
|
||||
{
|
||||
throw new TPDUInvalidException("No protocol data received");
|
||||
}
|
||||
return new TPDU(tpkt);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
@@ -94,52 +90,30 @@ namespace S7.Net
|
||||
/// See: https://tools.ietf.org/html/rfc905
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||
/// <returns>Data in TSDU</returns>
|
||||
public static byte[] Read(Stream stream)
|
||||
public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var segment = TPDU.Read(stream);
|
||||
if (segment == null) return null;
|
||||
var segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (segment.LastDataUnit)
|
||||
{
|
||||
return segment.Data;
|
||||
}
|
||||
|
||||
// More segments are expected, prepare a buffer to store all data
|
||||
var buffer = new byte[segment.Data.Length];
|
||||
var output = new MemoryStream(buffer);
|
||||
output.Write(segment.Data, 0, segment.Data.Length);
|
||||
Array.Copy(segment.Data, buffer, segment.Data.Length);
|
||||
|
||||
while (!segment.LastDataUnit)
|
||||
{
|
||||
segment = TPDU.Read(stream);
|
||||
segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
var previousLength = buffer.Length;
|
||||
Array.Resize(ref buffer, buffer.Length + segment.Data.Length);
|
||||
var lastPosition = output.Position;
|
||||
output = new MemoryStream(buffer);
|
||||
output.Write(segment.Data, (int) lastPosition, segment.Data.Length);
|
||||
Array.Copy(segment.Data, 0, buffer, previousLength, segment.Data.Length);
|
||||
}
|
||||
|
||||
return buffer.Take((int)output.Position).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the full COTP TSDU (Transport service data unit)
|
||||
/// See: https://tools.ietf.org/html/rfc905
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <returns>Data in TSDU</returns>
|
||||
public static async Task<byte[]> ReadAsync(Stream stream)
|
||||
{
|
||||
var segment = await TPDU.ReadAsync(stream);
|
||||
if (segment == null) return null;
|
||||
|
||||
var buffer = new byte[segment.Data.Length];
|
||||
var output = new MemoryStream(buffer);
|
||||
output.Write(segment.Data, 0, segment.Data.Length);
|
||||
|
||||
while (!segment.LastDataUnit)
|
||||
{
|
||||
segment = await TPDU.ReadAsync(stream);
|
||||
Array.Resize(ref buffer, buffer.Length + segment.Data.Length);
|
||||
var lastPosition = output.Position;
|
||||
output = new MemoryStream(buffer);
|
||||
output.Write(segment.Data, (int) lastPosition, segment.Data.Length);
|
||||
}
|
||||
return buffer.Take((int)output.Position).ToArray();
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,19 +138,59 @@ namespace S7.Net
|
||||
|
||||
/// <summary>
|
||||
/// Helper to get a bit value given a byte and the bit index.
|
||||
/// Example: DB1.DBX0.5 -> var bytes = ReadBytes(DB1.DBW0); bool bit = bytes[0].SelectBit(5);
|
||||
/// <br/>
|
||||
/// <example>
|
||||
/// Get the bit at DB1.DBX0.5:
|
||||
/// <code>
|
||||
/// byte data = ReadByte("DB1.DBB0");
|
||||
/// bool bit = data.SelectBit(5);
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="bitPosition"></param>
|
||||
/// <returns></returns>
|
||||
public static bool SelectBit(this byte data, int bitPosition)
|
||||
/// <param name="data">The data to get from.</param>
|
||||
/// <param name="index">The zero-based index of the bit to get.</param>
|
||||
/// <returns>The Boolean value will get.</returns>
|
||||
public static bool SelectBit(this byte data, int index)
|
||||
{
|
||||
int mask = 1 << bitPosition;
|
||||
int mask = 1 << index;
|
||||
int result = data & mask;
|
||||
|
||||
return (result != 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to set a bit value to the given byte at the bit index.
|
||||
/// <br/>
|
||||
/// <example>
|
||||
/// Set the bit at index 4:
|
||||
/// <code>
|
||||
/// byte data = 0;
|
||||
/// data.SetBit(4, true);
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </summary>
|
||||
/// <param name="data">The data to be modified.</param>
|
||||
/// <param name="index">The zero-based index of the bit to set.</param>
|
||||
/// <param name="value">The Boolean value to assign to the bit.</param>
|
||||
public static void SetBit(this ref byte data, int index, bool value)
|
||||
{
|
||||
if ((uint)index > 7)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (value)
|
||||
{
|
||||
byte mask = (byte)(1 << index);
|
||||
data |= mask;
|
||||
}
|
||||
else
|
||||
{
|
||||
byte mask = (byte)~(1 << index);
|
||||
data &= mask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts from ushort value to short value; it's used to retrieve negative values from words
|
||||
/// </summary>
|
||||
@@ -199,19 +239,6 @@ namespace S7.Net
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts from double to DWord (DBD)
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[Obsolete("Double support is obsolete. Use ConvertToUInt(float) instead.")]
|
||||
public static UInt32 ConvertToUInt(this double input)
|
||||
{
|
||||
uint output;
|
||||
output = S7.Net.Types.DWord.FromByteArray(S7.Net.Types.Double.ToByteArray(input));
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts from float to DWord (DBD)
|
||||
/// </summary>
|
||||
@@ -220,20 +247,7 @@ namespace S7.Net
|
||||
public static UInt32 ConvertToUInt(this float input)
|
||||
{
|
||||
uint output;
|
||||
output = S7.Net.Types.DWord.FromByteArray(S7.Net.Types.Single.ToByteArray(input));
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts from DWord (DBD) to double
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
[Obsolete("Double support is obsolete. Use ConvertToFloat(uint) instead.")]
|
||||
public static double ConvertToDouble(this uint input)
|
||||
{
|
||||
double output;
|
||||
output = S7.Net.Types.Double.FromByteArray(S7.Net.Types.DWord.ToByteArray(input));
|
||||
output = S7.Net.Types.DWord.FromByteArray(S7.Net.Types.Real.ToByteArray(input));
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -245,7 +259,7 @@ namespace S7.Net
|
||||
public static float ConvertToFloat(this uint input)
|
||||
{
|
||||
float output;
|
||||
output = S7.Net.Types.Single.FromByteArray(S7.Net.Types.DWord.ToByteArray(input));
|
||||
output = S7.Net.Types.Real.FromByteArray(S7.Net.Types.DWord.ToByteArray(input));
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
/// </summary>
|
||||
Logo0BA8 = 1,
|
||||
|
||||
/// <summary>
|
||||
/// S7 200 Smart
|
||||
/// </summary>
|
||||
S7200Smart = 2,
|
||||
|
||||
/// <summary>
|
||||
/// S7 300 cpu type
|
||||
/// </summary>
|
||||
@@ -164,14 +169,24 @@
|
||||
Real,
|
||||
|
||||
/// <summary>
|
||||
/// String variable type (variable)
|
||||
/// LReal variable type (64 bits, 8 bytes)
|
||||
/// </summary>
|
||||
LReal,
|
||||
|
||||
/// <summary>
|
||||
/// Char Array / C-String variable type (variable)
|
||||
/// </summary>
|
||||
String,
|
||||
|
||||
/// <summary>
|
||||
/// String variable type (variable)
|
||||
/// S7 String variable type (variable)
|
||||
/// </summary>
|
||||
StringEx,
|
||||
S7String,
|
||||
|
||||
/// <summary>
|
||||
/// S7 WString variable type (variable)
|
||||
/// </summary>
|
||||
S7WString,
|
||||
|
||||
/// <summary>
|
||||
/// Timer variable type
|
||||
@@ -186,6 +201,21 @@
|
||||
/// <summary>
|
||||
/// DateTIme variable type
|
||||
/// </summary>
|
||||
DateTime
|
||||
DateTime,
|
||||
|
||||
/// <summary>
|
||||
/// IEC date (legacy) variable type
|
||||
/// </summary>
|
||||
Date,
|
||||
|
||||
/// <summary>
|
||||
/// DateTimeLong variable type
|
||||
/// </summary>
|
||||
DateTimeLong,
|
||||
|
||||
/// <summary>
|
||||
/// S7 TIME variable type - serialized as S7 DInt and deserialized as C# TimeSpan
|
||||
/// </summary>
|
||||
Time
|
||||
}
|
||||
}
|
||||
|
||||
23
S7.Net/Helper/DateTimeExtensions.cs
Normal file
23
S7.Net/Helper/DateTimeExtensions.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using S7.Net.Types;
|
||||
using DateTime = System.DateTime;
|
||||
|
||||
namespace S7.Net.Helper
|
||||
{
|
||||
public static class DateTimeExtensions
|
||||
{
|
||||
public static ushort GetDaysSinceIecDateStart(this DateTime dateTime)
|
||||
{
|
||||
if (dateTime < Date.IecMinDate)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"DateTime must be at least {Date.IecMinDate:d}");
|
||||
}
|
||||
if (dateTime > Date.IecMaxDate)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"DateTime must be lower than {Date.IecMaxDate:d}");
|
||||
}
|
||||
|
||||
return (ushort)(dateTime - Date.IecMinDate).TotalDays;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
S7.Net/Helper/MemoryStreamExtension.cs
Normal file
38
S7.Net/Helper/MemoryStreamExtension.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
|
||||
namespace S7.Net.Helper
|
||||
{
|
||||
#if !NET5_0_OR_GREATER
|
||||
internal static class MemoryStreamExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper function to write to whole content of the given byte array to a memory stream.
|
||||
///
|
||||
/// Writes all bytes in value from 0 to value.Length to the memory stream.
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void Write(this MemoryStream stream, byte[] value)
|
||||
{
|
||||
stream.Write(value, 0, value.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function to write the whole content of the given byte span to a memory stream.
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void Write(this MemoryStream stream, ReadOnlySpan<byte> value)
|
||||
{
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(value.Length);
|
||||
|
||||
value.CopyTo(buffer);
|
||||
stream.Write(buffer, 0, value.Length);
|
||||
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
28
S7.Net/Internal/TaskQueue.cs
Normal file
28
S7.Net/Internal/TaskQueue.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace S7.Net.Internal
|
||||
{
|
||||
internal class TaskQueue
|
||||
{
|
||||
private static readonly object Sentinel = new object();
|
||||
|
||||
private Task prev = Task.FromResult(Sentinel);
|
||||
|
||||
public async Task<T> Enqueue<T>(Func<Task<T>> action)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object>();
|
||||
await Interlocked.Exchange(ref prev, tcs.Task).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
return await action.Invoke().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
tcs.SetResult(Sentinel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
230
S7.Net/PLC.cs
230
S7.Net/PLC.cs
@@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using S7.Net.Internal;
|
||||
using S7.Net.Protocol;
|
||||
using S7.Net.Types;
|
||||
|
||||
|
||||
@@ -12,44 +15,59 @@ namespace S7.Net
|
||||
/// </summary>
|
||||
public partial class Plc : IDisposable
|
||||
{
|
||||
private const int CONNECTION_TIMED_OUT_ERROR_CODE = 10060;
|
||||
|
||||
//TCP connection to device
|
||||
private TcpClient tcpClient;
|
||||
private NetworkStream stream;
|
||||
/// <summary>
|
||||
/// The default port for the S7 protocol.
|
||||
/// </summary>
|
||||
public const int DefaultPort = 102;
|
||||
|
||||
private int readTimeout = System.Threading.Timeout.Infinite;
|
||||
private int writeTimeout = System.Threading.Timeout.Infinite;
|
||||
/// <summary>
|
||||
/// The default timeout (in milliseconds) used for <see cref="P:ReadTimeout"/> and <see cref="P:WriteTimeout"/>.
|
||||
/// </summary>
|
||||
public const int DefaultTimeout = 10_000;
|
||||
|
||||
private readonly TaskQueue queue = new TaskQueue();
|
||||
|
||||
//TCP connection to device
|
||||
private TcpClient? tcpClient;
|
||||
private NetworkStream? _stream;
|
||||
|
||||
private int readTimeout = DefaultTimeout; // default no timeout
|
||||
private int writeTimeout = DefaultTimeout; // default no timeout
|
||||
|
||||
/// <summary>
|
||||
/// IP address of the PLC
|
||||
/// </summary>
|
||||
public string IP { get; private set; }
|
||||
public string IP { get; }
|
||||
|
||||
/// <summary>
|
||||
/// PORT Number of the PLC, default is 102
|
||||
/// </summary>
|
||||
public int Port { get; private set; }
|
||||
public int Port { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The TSAP addresses used during the connection request.
|
||||
/// </summary>
|
||||
public TsapPair TsapPair { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// CPU type of the PLC
|
||||
/// </summary>
|
||||
public CpuType CPU { get; private set; }
|
||||
public CpuType CPU { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Rack of the PLC
|
||||
/// </summary>
|
||||
public Int16 Rack { get; private set; }
|
||||
public Int16 Rack { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Slot of the CPU of the PLC
|
||||
/// </summary>
|
||||
public Int16 Slot { get; private set; }
|
||||
public Int16 Slot { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Max PDU size this cpu supports
|
||||
/// </summary>
|
||||
public Int16 MaxPDUSize { get; set; }
|
||||
public int MaxPDUSize { get; private set; }
|
||||
|
||||
/// <summary>Gets or sets the amount of time that a read operation blocks waiting for data from PLC.</summary>
|
||||
/// <returns>A <see cref="T:System.Int32" /> that specifies the amount of time, in milliseconds, that will elapse before a read operation fails. The default value, <see cref="F:System.Threading.Timeout.Infinite" />, specifies that the read operation does not time out.</returns>
|
||||
@@ -74,75 +92,27 @@ namespace S7.Net
|
||||
if (tcpClient != null) tcpClient.SendTimeout = writeTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a connection to the PLC can be established
|
||||
/// </summary>
|
||||
public bool IsAvailable
|
||||
{
|
||||
//TODO: Fix This
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
Connect();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the socket is connected and polls the other peer (the PLC) to see if it's connected.
|
||||
/// This is the variable that you should continously check to see if the communication is working
|
||||
/// See also: http://stackoverflow.com/questions/2661764/how-to-check-if-a-socket-is-connected-disconnected-in-c
|
||||
/// Gets a value indicating whether a connection to the PLC has been established.
|
||||
/// </summary>
|
||||
public bool IsConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
if (tcpClient == null)
|
||||
return false;
|
||||
/// <remarks>
|
||||
/// The <see cref="IsConnected"/> property gets the connection state of the Client socket as
|
||||
/// of the last I/O operation. When it returns <c>false</c>, the Client socket was either
|
||||
/// never connected, or is no longer connected.
|
||||
///
|
||||
/// <para>
|
||||
/// Because the <see cref="IsConnected"/> property only reflects the state of the connection
|
||||
/// as of the most recent operation, you should attempt to send or receive a message to
|
||||
/// determine the current state. After the message send fails, this property no longer
|
||||
/// returns <c>true</c>. Note that this behavior is by design. You cannot reliably test the
|
||||
/// state of the connection because, in the time between the test and a send/receive, the
|
||||
/// connection could have been lost. Your code should assume the socket is connected, and
|
||||
/// gracefully handle failed transmissions.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool IsConnected => tcpClient?.Connected ?? false;
|
||||
|
||||
//TODO: Actually check communication by sending an empty TPDU
|
||||
return tcpClient.Connected;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PLC object with all the parameters needed for connections.
|
||||
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
|
||||
/// You need slot > 0 if you are connecting to external ethernet card (CP).
|
||||
/// For S7-300 and S7-400 the default is rack = 0 and slot = 2.
|
||||
/// </summary>
|
||||
/// <param name="cpu">CpuType of the PLC (select from the enum)</param>
|
||||
/// <param name="ip">Ip address of the PLC</param>
|
||||
/// <param name="port">Port address of the PLC, default 102</param>
|
||||
/// <param name="rack">rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal</param>
|
||||
/// <param name="slot">slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500.
|
||||
/// If you use an external ethernet card, this must be set accordingly.</param>
|
||||
public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot)
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(CpuType), cpu))
|
||||
throw new ArgumentException($"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.", nameof(cpu));
|
||||
|
||||
if (string.IsNullOrEmpty(ip))
|
||||
throw new ArgumentException("IP address must valid.", nameof(ip));
|
||||
|
||||
CPU = cpu;
|
||||
IP = ip;
|
||||
Port = port;
|
||||
Rack = rack;
|
||||
Slot = slot;
|
||||
MaxPDUSize = 240;
|
||||
}
|
||||
/// <summary>
|
||||
/// Creates a PLC object with all the parameters needed for connections.
|
||||
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
|
||||
@@ -155,19 +125,63 @@ namespace S7.Net
|
||||
/// <param name="slot">slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500.
|
||||
/// If you use an external ethernet card, this must be set accordingly.</param>
|
||||
public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot)
|
||||
: this(cpu, ip, DefaultPort, rack, slot)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PLC object with all the parameters needed for connections.
|
||||
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
|
||||
/// You need slot > 0 if you are connecting to external ethernet card (CP).
|
||||
/// For S7-300 and S7-400 the default is rack = 0 and slot = 2.
|
||||
/// </summary>
|
||||
/// <param name="cpu">CpuType of the PLC (select from the enum)</param>
|
||||
/// <param name="ip">Ip address of the PLC</param>
|
||||
/// <param name="port">Port number used for the connection, default 102.</param>
|
||||
/// <param name="rack">rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal</param>
|
||||
/// <param name="slot">slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500.
|
||||
/// If you use an external ethernet card, this must be set accordingly.</param>
|
||||
public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot)
|
||||
: this(ip, port, TsapPair.GetDefaultTsapPair(cpu, rack, slot))
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(CpuType), cpu))
|
||||
throw new ArgumentException($"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.", nameof(cpu));
|
||||
throw new ArgumentException(
|
||||
$"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.",
|
||||
nameof(cpu));
|
||||
|
||||
CPU = cpu;
|
||||
Rack = rack;
|
||||
Slot = slot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PLC object with all the parameters needed for connections.
|
||||
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
|
||||
/// You need slot > 0 if you are connecting to external ethernet card (CP).
|
||||
/// For S7-300 and S7-400 the default is rack = 0 and slot = 2.
|
||||
/// </summary>
|
||||
/// <param name="ip">Ip address of the PLC</param>
|
||||
/// <param name="tsapPair">The TSAP addresses used for the connection request.</param>
|
||||
public Plc(string ip, TsapPair tsapPair) : this(ip, DefaultPort, tsapPair)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PLC object with all the parameters needed for connections. Use this constructor
|
||||
/// if you want to manually override the TSAP addresses used during the connection request.
|
||||
/// </summary>
|
||||
/// <param name="ip">Ip address of the PLC</param>
|
||||
/// <param name="port">Port number used for the connection, default 102.</param>
|
||||
/// <param name="tsapPair">The TSAP addresses used for the connection request.</param>
|
||||
public Plc(string ip, int port, TsapPair tsapPair)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ip))
|
||||
throw new ArgumentException("IP address must valid.", nameof(ip));
|
||||
|
||||
CPU = cpu;
|
||||
IP = ip;
|
||||
Port = 102;
|
||||
Rack = rack;
|
||||
Slot = slot;
|
||||
Port = port;
|
||||
MaxPDUSize = 240;
|
||||
TsapPair = tsapPair;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -178,16 +192,19 @@ namespace S7.Net
|
||||
if (tcpClient != null)
|
||||
{
|
||||
if (tcpClient.Connected) tcpClient.Close();
|
||||
tcpClient = null; // Can not reuse TcpClient once connection gets closed.
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertPduSizeForRead(ICollection<DataItem> dataItems)
|
||||
{
|
||||
// 12 bytes of header data, 12 bytes of parameter data for each dataItem
|
||||
if ((dataItems.Count + 1) * 12 > MaxPDUSize) throw new Exception("Too many vars requested for read");
|
||||
|
||||
// 14 bytes of header data, 4 bytes of result data for each dataItem and the actual data
|
||||
if (GetDataLength(dataItems) + dataItems.Count * 4 + 14 > MaxPDUSize) throw new Exception("Too much data requested for read");
|
||||
// send request limit: 19 bytes of header data, 12 bytes of parameter data for each dataItem
|
||||
var requiredRequestSize = 19 + dataItems.Count * 12;
|
||||
if (requiredRequestSize > MaxPDUSize) throw new Exception($"Too many vars requested for read. Request size ({requiredRequestSize}) is larger than protocol limit ({MaxPDUSize}).");
|
||||
|
||||
// response limit: 14 bytes of header data, 4 bytes of result data for each dataItem and the actual data
|
||||
var requiredResponseSize = GetDataLength(dataItems) + dataItems.Count * 4 + 14;
|
||||
if (requiredResponseSize > MaxPDUSize) throw new Exception($"Too much data requested for read. Response size ({requiredResponseSize}) is larger than protocol limit ({MaxPDUSize}).");
|
||||
}
|
||||
|
||||
private void AssertPduSizeForWrite(ICollection<DataItem> dataItems)
|
||||
@@ -232,13 +249,44 @@ namespace S7.Net
|
||||
|
||||
if (s7Data.Length < 15) throw NotEnoughBytes();
|
||||
|
||||
if (s7Data[14] != 0xff)
|
||||
throw new PlcException(ErrorCode.ReadData,
|
||||
$"Invalid response from PLC: '{BitConverter.ToString(s7Data)}'.");
|
||||
ValidateResponseCode((ReadWriteErrorCode)s7Data[14]);
|
||||
|
||||
if (s7Data.Length < expectedLength) throw NotEnoughBytes();
|
||||
}
|
||||
|
||||
internal static void ValidateResponseCode(ReadWriteErrorCode statusCode)
|
||||
{
|
||||
switch (statusCode)
|
||||
{
|
||||
case ReadWriteErrorCode.ObjectDoesNotExist:
|
||||
throw new Exception("Received error from PLC: Object does not exist.");
|
||||
case ReadWriteErrorCode.DataTypeInconsistent:
|
||||
throw new Exception("Received error from PLC: Data type inconsistent.");
|
||||
case ReadWriteErrorCode.DataTypeNotSupported:
|
||||
throw new Exception("Received error from PLC: Data type not supported.");
|
||||
case ReadWriteErrorCode.AccessingObjectNotAllowed:
|
||||
throw new Exception("Received error from PLC: Accessing object not allowed.");
|
||||
case ReadWriteErrorCode.AddressOutOfRange:
|
||||
throw new Exception("Received error from PLC: Address out of range.");
|
||||
case ReadWriteErrorCode.HardwareFault:
|
||||
throw new Exception("Received error from PLC: Hardware fault.");
|
||||
case ReadWriteErrorCode.Success:
|
||||
break;
|
||||
default:
|
||||
throw new Exception( $"Invalid response from PLC: statusCode={(byte)statusCode}.");
|
||||
}
|
||||
}
|
||||
|
||||
private Stream GetStreamIfAvailable()
|
||||
{
|
||||
if (_stream == null)
|
||||
{
|
||||
throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected");
|
||||
}
|
||||
|
||||
return _stream;
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
private bool disposedValue = false; // To detect redundant calls
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
case "Z":
|
||||
case "C":
|
||||
// Counter
|
||||
dataType = DataType.Timer;
|
||||
dataType = DataType.Counter;
|
||||
dbNumber = 0;
|
||||
address = int.Parse(input.Substring(1));
|
||||
varType = VarType.Counter;
|
||||
@@ -204,4 +204,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.Runtime.Serialization;
|
||||
|
||||
namespace S7.Net
|
||||
{
|
||||
internal class WrongNumberOfBytesException : Exception
|
||||
public class WrongNumberOfBytesException : Exception
|
||||
{
|
||||
public WrongNumberOfBytesException() : base()
|
||||
{
|
||||
@@ -27,7 +27,7 @@ namespace S7.Net
|
||||
#endif
|
||||
}
|
||||
|
||||
internal class InvalidAddressException : Exception
|
||||
public class InvalidAddressException : Exception
|
||||
{
|
||||
public InvalidAddressException() : base ()
|
||||
{
|
||||
@@ -48,7 +48,7 @@ namespace S7.Net
|
||||
#endif
|
||||
}
|
||||
|
||||
internal class InvalidVariableTypeException : Exception
|
||||
public class InvalidVariableTypeException : Exception
|
||||
{
|
||||
public InvalidVariableTypeException() : base()
|
||||
{
|
||||
@@ -69,7 +69,7 @@ namespace S7.Net
|
||||
#endif
|
||||
}
|
||||
|
||||
internal class TPKTInvalidException : Exception
|
||||
public class TPKTInvalidException : Exception
|
||||
{
|
||||
public TPKTInvalidException() : base()
|
||||
{
|
||||
@@ -89,4 +89,25 @@ namespace S7.Net
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public class TPDUInvalidException : Exception
|
||||
{
|
||||
public TPDUInvalidException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public TPDUInvalidException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public TPDUInvalidException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
#if NET_FULL
|
||||
protected TPDUInvalidException(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using S7.Net.Types;
|
||||
using System;
|
||||
using S7.Net.Helper;
|
||||
using S7.Net.Protocol.S7;
|
||||
using S7.Net.Types;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DateTime = S7.Net.Types.DateTime;
|
||||
@@ -8,70 +9,139 @@ namespace S7.Net
|
||||
{
|
||||
public partial class Plc
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the header to read bytes from the PLC
|
||||
/// </summary>
|
||||
/// <param name="amount"></param>
|
||||
/// <returns></returns>
|
||||
private ByteArray ReadHeaderPackage(int amount = 1)
|
||||
private static void WriteTpktHeader(System.IO.MemoryStream stream, int length)
|
||||
{
|
||||
//header size = 19 bytes
|
||||
var package = new Types.ByteArray(19);
|
||||
package.Add(new byte[] { 0x03, 0x00 });
|
||||
//complete package size
|
||||
package.Add(Types.Int.ToByteArray((short)(19 + (12 * amount))));
|
||||
package.Add(new byte[] { 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 });
|
||||
//data part size
|
||||
package.Add(Types.Word.ToByteArray((ushort)(2 + (amount * 12))));
|
||||
package.Add(new byte[] { 0x00, 0x00, 0x04 });
|
||||
//amount of requests
|
||||
package.Add((byte)amount);
|
||||
stream.Write(new byte[] { 0x03, 0x00 });
|
||||
stream.Write(Word.ToByteArray((ushort) length));
|
||||
}
|
||||
|
||||
return package;
|
||||
private static void WriteDataHeader(System.IO.MemoryStream stream)
|
||||
{
|
||||
stream.Write(new byte[] { 0x02, 0xf0, 0x80 });
|
||||
}
|
||||
|
||||
private static void WriteS7Header(System.IO.MemoryStream stream, byte messageType, int parameterLength, int dataLength)
|
||||
{
|
||||
stream.WriteByte(0x32); // S7 protocol ID
|
||||
stream.WriteByte(messageType); // Message type
|
||||
stream.Write(new byte[] { 0x00, 0x00 }); // Reserved
|
||||
stream.Write(new byte[] { 0x00, 0x00 }); // PDU ref
|
||||
stream.Write(Word.ToByteArray((ushort) parameterLength));
|
||||
stream.Write(Word.ToByteArray((ushort) dataLength));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType),
|
||||
/// the address of the memory, the address of the byte and the bytes count.
|
||||
/// Creates the header to read bytes from the PLC.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to write to.</param>
|
||||
/// <param name="amount">The number of items to read.</param>
|
||||
private static void WriteReadHeader(System.IO.MemoryStream stream, int amount = 1)
|
||||
{
|
||||
// Header size 19, 12 bytes per item
|
||||
WriteTpktHeader(stream, 19 + 12 * amount);
|
||||
WriteDataHeader(stream);
|
||||
WriteS7Header(stream, 0x01, 2 + 12 * amount, 0);
|
||||
// Function code: read request
|
||||
stream.WriteByte(0x04);
|
||||
//amount of requests
|
||||
stream.WriteByte((byte)amount);
|
||||
}
|
||||
|
||||
private static void WriteUserDataHeader(System.IO.MemoryStream stream, int parameterLength, int dataLength)
|
||||
{
|
||||
const byte s7MessageTypeUserData = 0x07;
|
||||
|
||||
WriteTpktHeader(stream, 17 + parameterLength + dataLength);
|
||||
WriteDataHeader(stream);
|
||||
WriteS7Header(stream, s7MessageTypeUserData, parameterLength, dataLength);
|
||||
}
|
||||
|
||||
private static void WriteUserDataRequest(System.IO.MemoryStream stream, byte functionGroup, byte subFunction, int dataLength)
|
||||
{
|
||||
WriteUserDataHeader(stream, 8, dataLength);
|
||||
|
||||
// Parameter
|
||||
const byte userDataMethodRequest = 0x11;
|
||||
const byte userDataTypeRequest = 0x4;
|
||||
|
||||
// Parameter head
|
||||
stream.Write(new byte[] { 0x00, 0x01, 0x12 });
|
||||
// Parameter length
|
||||
stream.WriteByte(0x04);
|
||||
// Method
|
||||
stream.WriteByte(userDataMethodRequest);
|
||||
// Type / function group
|
||||
stream.WriteByte((byte)(userDataTypeRequest << 4 | (functionGroup & 0x0f)));
|
||||
// Subfunction
|
||||
stream.WriteByte(subFunction);
|
||||
// Sequence number
|
||||
stream.WriteByte(0);
|
||||
}
|
||||
|
||||
private static void WriteSzlReadRequest(System.IO.MemoryStream stream, ushort szlId, ushort szlIndex)
|
||||
{
|
||||
// Parameter
|
||||
const byte szlFunctionGroupCpuFunctions = 0b100;
|
||||
const byte subFunctionReadSzl = 0x01;
|
||||
|
||||
WriteUserDataRequest(stream, szlFunctionGroupCpuFunctions, subFunctionReadSzl, 8);
|
||||
|
||||
// Data
|
||||
const byte success = 0xff;
|
||||
const byte transportSizeOctetString = 0x09;
|
||||
|
||||
// Return code
|
||||
stream.WriteByte(success);
|
||||
// Transport size
|
||||
stream.WriteByte(transportSizeOctetString);
|
||||
// Length
|
||||
stream.Write(Word.ToByteArray(4));
|
||||
// SZL-ID
|
||||
stream.Write(Word.ToByteArray(szlId));
|
||||
// SZL-Index
|
||||
stream.Write(Word.ToByteArray(szlIndex));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType),
|
||||
/// the address of the memory, the address of the byte and the bytes count.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to write the read data request to.</param>
|
||||
/// <param name="dataType">MemoryType (DB, Timer, Counter, etc.)</param>
|
||||
/// <param name="db">Address of the memory to be read</param>
|
||||
/// <param name="startByteAdr">Start address of the byte</param>
|
||||
/// <param name="count">Number of bytes to be read</param>
|
||||
/// <returns></returns>
|
||||
private ByteArray CreateReadDataRequestPackage(DataType dataType, int db, int startByteAdr, int count = 1)
|
||||
private static void BuildReadDataRequestPackage(System.IO.MemoryStream stream, DataType dataType, int db, int startByteAdr, int count = 1)
|
||||
{
|
||||
//single data req = 12
|
||||
var package = new Types.ByteArray(12);
|
||||
package.Add(new byte[] { 0x12, 0x0a, 0x10 });
|
||||
stream.Write(new byte[] { 0x12, 0x0a, 0x10 });
|
||||
switch (dataType)
|
||||
{
|
||||
case DataType.Timer:
|
||||
case DataType.Counter:
|
||||
package.Add((byte)dataType);
|
||||
stream.WriteByte((byte)dataType);
|
||||
break;
|
||||
default:
|
||||
package.Add(0x02);
|
||||
stream.WriteByte(0x02);
|
||||
break;
|
||||
}
|
||||
|
||||
package.Add(Word.ToByteArray((ushort)(count)));
|
||||
package.Add(Word.ToByteArray((ushort)(db)));
|
||||
package.Add((byte)dataType);
|
||||
stream.Write(Word.ToByteArray((ushort)(count)));
|
||||
stream.Write(Word.ToByteArray((ushort)(db)));
|
||||
stream.WriteByte((byte)dataType);
|
||||
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
|
||||
package.Add((byte)overflow);
|
||||
stream.WriteByte((byte)overflow);
|
||||
switch (dataType)
|
||||
{
|
||||
case DataType.Timer:
|
||||
case DataType.Counter:
|
||||
package.Add(Types.Word.ToByteArray((ushort)(startByteAdr)));
|
||||
stream.Write(Word.ToByteArray((ushort)(startByteAdr)));
|
||||
break;
|
||||
default:
|
||||
package.Add(Types.Word.ToByteArray((ushort)((startByteAdr) * 8)));
|
||||
stream.Write(Word.ToByteArray((ushort)((startByteAdr) * 8)));
|
||||
break;
|
||||
}
|
||||
|
||||
return package;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -82,7 +152,7 @@ namespace S7.Net
|
||||
/// <param name="varCount"></param>
|
||||
/// <param name="bitAdr"></param>
|
||||
/// <returns></returns>
|
||||
private object ParseBytes(VarType varType, byte[] bytes, int varCount, byte bitAdr = 0)
|
||||
private object? ParseBytes(VarType varType, byte[] bytes, int varCount, byte bitAdr = 0)
|
||||
{
|
||||
if (bytes == null || bytes.Length == 0)
|
||||
return null;
|
||||
@@ -116,14 +186,21 @@ namespace S7.Net
|
||||
return DInt.ToArray(bytes);
|
||||
case VarType.Real:
|
||||
if (varCount == 1)
|
||||
return Types.Single.FromByteArray(bytes);
|
||||
return Types.Real.FromByteArray(bytes);
|
||||
else
|
||||
return Types.Single.ToArray(bytes);
|
||||
return Types.Real.ToArray(bytes);
|
||||
case VarType.LReal:
|
||||
if (varCount == 1)
|
||||
return Types.LReal.FromByteArray(bytes);
|
||||
else
|
||||
return Types.LReal.ToArray(bytes);
|
||||
|
||||
case VarType.String:
|
||||
return Types.String.FromByteArray(bytes);
|
||||
case VarType.StringEx:
|
||||
return StringEx.FromByteArray(bytes);
|
||||
case VarType.S7String:
|
||||
return S7String.FromByteArray(bytes);
|
||||
case VarType.S7WString:
|
||||
return S7WString.FromByteArray(bytes);
|
||||
|
||||
case VarType.Timer:
|
||||
if (varCount == 1)
|
||||
@@ -156,6 +233,33 @@ namespace S7.Net
|
||||
{
|
||||
return DateTime.ToArray(bytes);
|
||||
}
|
||||
case VarType.DateTimeLong:
|
||||
if (varCount == 1)
|
||||
{
|
||||
return DateTimeLong.FromByteArray(bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
return DateTimeLong.ToArray(bytes);
|
||||
}
|
||||
case VarType.Time:
|
||||
if (varCount == 1)
|
||||
{
|
||||
return TimeSpan.FromByteArray(bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
return TimeSpan.ToArray(bytes);
|
||||
}
|
||||
case VarType.Date:
|
||||
if (varCount == 1)
|
||||
{
|
||||
return Date.FromByteArray(bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Date.ToArray(bytes);
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -167,29 +271,36 @@ namespace S7.Net
|
||||
/// <param name="varType"></param>
|
||||
/// <param name="varCount"></param>
|
||||
/// <returns>Byte lenght of variable</returns>
|
||||
private int VarTypeToByteLength(VarType varType, int varCount = 1)
|
||||
internal static int VarTypeToByteLength(VarType varType, int varCount = 1)
|
||||
{
|
||||
switch (varType)
|
||||
{
|
||||
case VarType.Bit:
|
||||
return varCount + 7 / 8;
|
||||
return (varCount + 7) / 8;
|
||||
case VarType.Byte:
|
||||
return (varCount < 1) ? 1 : varCount;
|
||||
case VarType.String:
|
||||
return varCount;
|
||||
case VarType.StringEx:
|
||||
return varCount + 2;
|
||||
case VarType.S7String:
|
||||
return ((varCount + 2) & 1) == 1 ? (varCount + 3) : (varCount + 2);
|
||||
case VarType.S7WString:
|
||||
return (varCount * 2) + 4;
|
||||
case VarType.Word:
|
||||
case VarType.Timer:
|
||||
case VarType.Int:
|
||||
case VarType.Counter:
|
||||
case VarType.Date:
|
||||
return varCount * 2;
|
||||
case VarType.DWord:
|
||||
case VarType.DInt:
|
||||
case VarType.Real:
|
||||
case VarType.Time:
|
||||
return varCount * 4;
|
||||
case VarType.LReal:
|
||||
case VarType.DateTime:
|
||||
return varCount * 8;
|
||||
case VarType.DateTimeLong:
|
||||
return varCount * 12;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@@ -225,10 +336,35 @@ namespace S7.Net
|
||||
// next Item
|
||||
offset += byteCnt;
|
||||
|
||||
// Fill byte in response when bytecount is odd
|
||||
if (dataItem.Count % 2 != 0 && (dataItem.VarType == VarType.Byte || dataItem.VarType == VarType.Bit))
|
||||
// Always align to even offset
|
||||
if (offset % 2 != 0)
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] BuildReadRequestPackage(IList<DataItemAddress> dataItems)
|
||||
{
|
||||
int packageSize = 19 + (dataItems.Count * 12);
|
||||
var package = new System.IO.MemoryStream(packageSize);
|
||||
|
||||
WriteReadHeader(package, dataItems.Count);
|
||||
|
||||
foreach (var dataItem in dataItems)
|
||||
{
|
||||
BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAddress, dataItem.ByteLength);
|
||||
}
|
||||
|
||||
return package.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] BuildSzlReadRequestPackage(ushort szlId, ushort szlIndex)
|
||||
{
|
||||
var stream = new System.IO.MemoryStream();
|
||||
|
||||
WriteSzlReadRequest(stream, szlId, szlIndex);
|
||||
stream.SetLength(stream.Position);
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
92
S7.Net/Plc.Clock.cs
Normal file
92
S7.Net/Plc.Clock.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using S7.Net.Helper;
|
||||
using S7.Net.Types;
|
||||
using DateTime = System.DateTime;
|
||||
|
||||
namespace S7.Net;
|
||||
|
||||
partial class Plc
|
||||
{
|
||||
private const byte SzlFunctionGroupTimers = 0x07;
|
||||
private const byte SzlSubFunctionReadClock = 0x01;
|
||||
private const byte SzlSubFunctionWriteClock = 0x02;
|
||||
private const byte TransportSizeOctetString = 0x09;
|
||||
private const int PduErrOffset = 20;
|
||||
private const int UserDataResultOffset = PduErrOffset + 2;
|
||||
|
||||
/// <summary>
|
||||
/// The length in bytes of DateTime stored in the PLC.
|
||||
/// </summary>
|
||||
private const int DateTimeLength = 10;
|
||||
|
||||
private static byte[] BuildClockReadRequest()
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
|
||||
WriteUserDataRequest(stream, SzlFunctionGroupTimers, SzlSubFunctionReadClock, 4);
|
||||
stream.Write(new byte[] { 0x0a, 0x00, 0x00, 0x00 });
|
||||
|
||||
stream.SetLength(stream.Position);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static DateTime ParseClockReadResponse(byte[] message)
|
||||
{
|
||||
const int udLenOffset = UserDataResultOffset + 2;
|
||||
const int udValueOffset = udLenOffset + 2;
|
||||
const int dateTimeSkip = 2;
|
||||
|
||||
AssertPduResult(message);
|
||||
AssertUserDataResult(message, 0xff);
|
||||
|
||||
var len = Word.FromByteArray(message.Skip(udLenOffset).Take(2).ToArray());
|
||||
if (len != DateTimeLength)
|
||||
{
|
||||
throw new Exception($"Unexpected response length {len}, expected {DateTimeLength}.");
|
||||
}
|
||||
|
||||
// Skip first 2 bytes from date time value because DateTime.FromByteArray doesn't parse them.
|
||||
return Types.DateTime.FromByteArray(message.Skip(udValueOffset + dateTimeSkip)
|
||||
.Take(DateTimeLength - dateTimeSkip).ToArray());
|
||||
}
|
||||
|
||||
private static byte[] BuildClockWriteRequest(DateTime value)
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
|
||||
WriteUserDataRequest(stream, SzlFunctionGroupTimers, SzlSubFunctionWriteClock, 14);
|
||||
stream.Write(new byte[] { 0xff, TransportSizeOctetString, 0x00, DateTimeLength });
|
||||
// Start of DateTime value, DateTime.ToByteArray only serializes the final 8 bytes
|
||||
stream.Write(new byte[] { 0x00, 0x19 });
|
||||
stream.Write(Types.DateTime.ToByteArray(value));
|
||||
|
||||
stream.SetLength(stream.Position);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static void ParseClockWriteResponse(byte[] message)
|
||||
{
|
||||
AssertPduResult(message);
|
||||
AssertUserDataResult(message, 0x0a);
|
||||
}
|
||||
|
||||
private static void AssertPduResult(byte[] message)
|
||||
{
|
||||
var pduErr = Word.FromByteArray(message.Skip(PduErrOffset).Take(2).ToArray());
|
||||
if (pduErr != 0)
|
||||
{
|
||||
throw new Exception($"Response from PLC indicates error 0x{pduErr:X4}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertUserDataResult(byte[] message, byte expected)
|
||||
{
|
||||
var dtResult = message[UserDataResultOffset];
|
||||
if (dtResult != expected)
|
||||
{
|
||||
throw new Exception($"Response from PLC was 0x{dtResult:X2}, expected 0x{expected:X2}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
using S7.Net.Types;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using S7.Net.Protocol;
|
||||
using System.Threading;
|
||||
using S7.Net.Protocol.S7;
|
||||
|
||||
namespace S7.Net
|
||||
{
|
||||
@@ -16,37 +19,80 @@ namespace S7.Net
|
||||
/// <summary>
|
||||
/// Connects to the PLC and performs a COTP ConnectionRequest and S7 CommunicationSetup.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that the cancellation will not affect opening the socket in any way and only affects data transfers for configuring the connection after the socket connection is successfully established.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>A task that represents the asynchronous open operation.</returns>
|
||||
public async Task OpenAsync()
|
||||
public async Task OpenAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await ConnectAsync();
|
||||
|
||||
await stream.WriteAsync(ConnectionRequest.GetCOTPConnectionRequest(CPU, Rack, Slot), 0, 22);
|
||||
var response = await COTP.TPDU.ReadAsync(stream);
|
||||
if (response.PDUType != 0xd0) //Connect Confirm
|
||||
var stream = await ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
throw new InvalidDataException("Error reading Connection Confirm", response.TPkt.Data, 1, 0x0d);
|
||||
await queue.Enqueue(async () =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
|
||||
_stream = stream;
|
||||
|
||||
return default(object);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
stream.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
await stream.WriteAsync(GetS7ConnectionSetup(), 0, 25);
|
||||
private async Task<NetworkStream> ConnectAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
tcpClient = new TcpClient();
|
||||
ConfigureConnection();
|
||||
|
||||
var s7data = await COTP.TSDU.ReadAsync(stream);
|
||||
if (s7data == null)
|
||||
throw new WrongNumberOfBytesException("No data received in response to Communication Setup");
|
||||
#if NET5_0_OR_GREATER
|
||||
await tcpClient.ConnectAsync(IP, Port, cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
await tcpClient.ConnectAsync(IP, Port).ConfigureAwait(false);
|
||||
#endif
|
||||
return tcpClient.GetStream();
|
||||
}
|
||||
|
||||
private async Task EstablishConnection(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
await RequestConnection(stream, cancellationToken).ConfigureAwait(false);
|
||||
await SetupConnection(stream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RequestConnection(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var requestData = ConnectionRequest.GetCOTPConnectionRequest(TsapPair);
|
||||
var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.PDUType != COTP.PduType.ConnectionConfirmed)
|
||||
{
|
||||
throw new InvalidDataException("Connection request was denied", response.TPkt.Data, 1, 0x0d);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetupConnection(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var setupData = GetS7ConnectionSetup();
|
||||
|
||||
var s7data = await NoLockRequestTsduAsync(stream, setupData, 0, setupData.Length, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (s7data.Length < 2)
|
||||
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
|
||||
|
||||
//Check for S7 Ack Data
|
||||
if (s7data[1] != 0x03)
|
||||
throw new InvalidDataException("Error reading Communication Setup response", s7data, 1, 0x03);
|
||||
|
||||
MaxPDUSize = (short)(s7data[18] * 256 + s7data[19]);
|
||||
}
|
||||
if (s7data.Length < 20)
|
||||
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
|
||||
|
||||
private async Task ConnectAsync()
|
||||
{
|
||||
tcpClient = new TcpClient();
|
||||
ConfigureConnection();
|
||||
await tcpClient.ConnectAsync(IP, Port);
|
||||
stream = tcpClient.GetStream();
|
||||
// TODO: check if this should not rather be UInt16.
|
||||
MaxPDUSize = s7data[18] * 256 + s7data[19];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -57,27 +103,44 @@ namespace S7.Net
|
||||
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <param name="count">Byte count, if you want to read 120 bytes, set this to 120.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>Returns the bytes in an array</returns>
|
||||
public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count)
|
||||
public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<byte> resultBytes = new List<byte>();
|
||||
int index = startByteAdr;
|
||||
while (count > 0)
|
||||
{
|
||||
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
|
||||
var maxToRead = (int)Math.Min(count, MaxPDUSize - 18);
|
||||
byte[] bytes = await ReadBytesWithSingleRequestAsync(dataType, db, index, maxToRead);
|
||||
if (bytes == null)
|
||||
return resultBytes.ToArray();
|
||||
resultBytes.AddRange(bytes);
|
||||
count -= maxToRead;
|
||||
index += maxToRead;
|
||||
}
|
||||
return resultBytes.ToArray();
|
||||
var resultBytes = new byte[count];
|
||||
|
||||
await ReadBytesAsync(resultBytes, dataType, db, startByteAdr, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return resultBytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read and decode a certain number of bytes of the "VarType" provided.
|
||||
/// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
|
||||
/// If the read was not successful, check LastErrorCode or LastErrorString.
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to receive the read bytes. The <see cref="Memory{T}.Length"/> determines the number of bytes to read.</param>
|
||||
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
|
||||
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>Returns the bytes in an array</returns>
|
||||
public async Task ReadBytesAsync(Memory<byte> buffer, DataType dataType, int db, int startByteAdr, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int index = 0;
|
||||
while (buffer.Length > 0)
|
||||
{
|
||||
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
|
||||
var maxToRead = Math.Min(buffer.Length, MaxPDUSize - 18);
|
||||
await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, buffer.Slice(0, maxToRead), cancellationToken).ConfigureAwait(false);
|
||||
buffer = buffer.Slice(maxToRead);
|
||||
index += maxToRead;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read and decode a certain number of bytes of the "VarType" provided.
|
||||
/// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
|
||||
/// If the read was not successful, check LastErrorCode or LastErrorString.
|
||||
/// </summary>
|
||||
@@ -87,10 +150,12 @@ namespace S7.Net
|
||||
/// <param name="varType">Type of the variable/s that you are reading</param>
|
||||
/// <param name="bitAdr">Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter.</param>
|
||||
/// <param name="varCount"></param>
|
||||
public async Task<object> ReadAsync(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0)
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
public async Task<object?> ReadAsync(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int cntBytes = VarTypeToByteLength(varType, varCount);
|
||||
byte[] bytes = await ReadBytesAsync(dataType, db, startByteAdr, cntBytes);
|
||||
byte[] bytes = await ReadBytesAsync(dataType, db, startByteAdr, cntBytes, cancellationToken).ConfigureAwait(false);
|
||||
return ParseBytes(varType, bytes, varCount, bitAdr);
|
||||
}
|
||||
|
||||
@@ -99,11 +164,13 @@ namespace S7.Net
|
||||
/// If the read was not successful, check LastErrorCode or LastErrorString.
|
||||
/// </summary>
|
||||
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>Returns an object that contains the value. This object must be cast accordingly.</returns>
|
||||
public async Task<object> ReadAsync(string variable)
|
||||
public async Task<object?> ReadAsync(string variable, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var adr = new PLCAddress(variable);
|
||||
return await ReadAsync(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber);
|
||||
return await ReadAsync(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -112,12 +179,14 @@ namespace S7.Net
|
||||
/// <param name="structType">Type of the struct to be readed (es.: TypeOf(MyStruct)).</param>
|
||||
/// <param name="db">Address of the DB.</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>Returns a struct that must be cast.</returns>
|
||||
public async Task<object> ReadStructAsync(Type structType, int db, int startByteAdr = 0)
|
||||
public async Task<object?> ReadStructAsync(Type structType, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int numBytes = Types.Struct.GetStructSize(structType);
|
||||
// now read the package
|
||||
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes);
|
||||
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// and decode it
|
||||
return Types.Struct.FromBytes(structType, resultBytes);
|
||||
@@ -129,21 +198,25 @@ namespace S7.Net
|
||||
/// <typeparam name="T">The struct type</typeparam>
|
||||
/// <param name="db">Address of the DB.</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>Returns a nulable struct. If nothing was read null will be returned.</returns>
|
||||
public async Task<T?> ReadStructAsync<T>(int db, int startByteAdr = 0) where T : struct
|
||||
public async Task<T?> ReadStructAsync<T>(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : struct
|
||||
{
|
||||
return await ReadStructAsync(typeof(T), db, startByteAdr) as T?;
|
||||
return await ReadStructAsync(typeof(T), db, startByteAdr, cancellationToken).ConfigureAwait(false) as T?;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
|
||||
/// </summary>
|
||||
/// <param name="sourceClass">Instance of the class that will store the values</param>
|
||||
/// <param name="sourceClass">Instance of the class that will store the values</param>
|
||||
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>The number of read bytes</returns>
|
||||
public async Task<Tuple<int, object>> ReadClassAsync(object sourceClass, int db, int startByteAdr = 0)
|
||||
public async Task<Tuple<int, object>> ReadClassAsync(object sourceClass, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int numBytes = (int)Class.GetClassSize(sourceClass);
|
||||
if (numBytes <= 0)
|
||||
@@ -152,7 +225,7 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
// now read the package
|
||||
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes);
|
||||
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false);
|
||||
// and decode it
|
||||
Class.FromBytes(sourceClass, resultBytes);
|
||||
|
||||
@@ -160,32 +233,36 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic
|
||||
/// type, the class needs a default constructor.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class that will be instantiated. Requires a default constructor</typeparam>
|
||||
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
|
||||
public async Task<T> ReadClassAsync<T>(int db, int startByteAdr = 0) where T : class
|
||||
public async Task<T?> ReadClassAsync<T>(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class
|
||||
{
|
||||
return await ReadClassAsync(() => Activator.CreateInstance<T>(), db, startByteAdr);
|
||||
return await ReadClassAsync(() => Activator.CreateInstance<T>(), db, startByteAdr, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class that will be instantiated</typeparam>
|
||||
/// <param name="classFactory">Function to instantiate the class</param>
|
||||
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
|
||||
public async Task<T> ReadClassAsync<T>(Func<T> classFactory, int db, int startByteAdr = 0) where T : class
|
||||
public async Task<T?> ReadClassAsync<T>(Func<T> classFactory, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class
|
||||
{
|
||||
var instance = classFactory();
|
||||
var res = await ReadClassAsync(instance, db, startByteAdr);
|
||||
var res = await ReadClassAsync(instance, db, startByteAdr, cancellationToken).ConfigureAwait(false);
|
||||
int readBytes = res.Item1;
|
||||
if (readBytes <= 0)
|
||||
{
|
||||
@@ -196,36 +273,27 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads multiple vars in a single request.
|
||||
/// Reads multiple vars in a single request.
|
||||
/// You have to create and pass a list of DataItems and you obtain in response the same list with the values.
|
||||
/// Values are stored in the property "Value" of the dataItem and are already converted.
|
||||
/// If you don't want the conversion, just create a dataItem of bytes.
|
||||
/// DataItems must not be more than 20 (protocol restriction) and bytes must not be more than 200 + 22 of header (protocol restriction).
|
||||
/// If you don't want the conversion, just create a dataItem of bytes.
|
||||
/// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction).
|
||||
/// </summary>
|
||||
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read. Maximum 20 dataitems are accepted.</param>
|
||||
public async Task<List<DataItem>> ReadMultipleVarsAsync(List<DataItem> dataItems)
|
||||
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
public async Task<List<DataItem>> ReadMultipleVarsAsync(List<DataItem> dataItems, CancellationToken cancellationToken = default)
|
||||
{
|
||||
//Snap7 seems to choke on PDU sizes above 256 even if snap7
|
||||
//Snap7 seems to choke on PDU sizes above 256 even if snap7
|
||||
//replies with bigger PDU size in connection setup.
|
||||
AssertPduSizeForRead(dataItems);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
// first create the header
|
||||
int packageSize = 19 + (dataItems.Count * 12);
|
||||
ByteArray package = new ByteArray(packageSize);
|
||||
package.Add(ReadHeaderPackage(dataItems.Count));
|
||||
// package.Add(0x02); // datenart
|
||||
foreach (var dataItem in dataItems)
|
||||
{
|
||||
package.Add(CreateReadDataRequestPackage(dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count)));
|
||||
}
|
||||
var dataToSend = BuildReadRequestPackage(dataItems.Select(d => DataItem.GetDataItemAddress(d)).ToList());
|
||||
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
|
||||
|
||||
await stream.WriteAsync(package.Array, 0, package.Array.Length);
|
||||
|
||||
var s7data = await COTP.TSDU.ReadAsync(stream); //TODO use Async
|
||||
if (s7data == null || s7data[14] != 0xff)
|
||||
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
|
||||
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
|
||||
|
||||
ParseDataIntoDataItems(s7data, dataItems);
|
||||
}
|
||||
@@ -233,6 +301,10 @@ namespace S7.Net
|
||||
{
|
||||
throw new PlcException(ErrorCode.ReadData, socketException);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
throw new PlcException(ErrorCode.ReadData, exc);
|
||||
@@ -240,6 +312,49 @@ namespace S7.Net
|
||||
return dataItems;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the PLC clock value.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, with it's result set to the current PLC time on completion.</returns>
|
||||
public async Task<System.DateTime> ReadClockAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = BuildClockReadRequest();
|
||||
var response = await RequestTsduAsync(request, cancellationToken);
|
||||
|
||||
return ParseClockReadResponse(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the PLC clock value.
|
||||
/// </summary>
|
||||
/// <param name="value">The date and time to set the PLC clock to</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task WriteClockAsync(System.DateTime value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = BuildClockWriteRequest(value);
|
||||
var response = await RequestTsduAsync(request, cancellationToken);
|
||||
|
||||
ParseClockWriteResponse(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, with it's result set to the current PLC status on completion.</returns>
|
||||
public async Task<byte> ReadStatusAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var dataToSend = BuildSzlReadRequestPackage(0x0424, 0);
|
||||
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
|
||||
|
||||
return (byte) (s7data[37] & 0x0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
|
||||
/// If the write was not successful, check LastErrorCode or LastErrorString.
|
||||
@@ -248,19 +363,33 @@ namespace S7.Net
|
||||
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
|
||||
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value)
|
||||
public Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return WriteBytesAsync(dataType, db, startByteAdr, value.AsMemory(), cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
|
||||
/// If the write was not successful, check LastErrorCode or LastErrorString.
|
||||
/// </summary>
|
||||
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
|
||||
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
|
||||
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory<byte> value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int localIndex = 0;
|
||||
int count = value.Length;
|
||||
while (count > 0)
|
||||
while (value.Length > 0)
|
||||
{
|
||||
//TODO: Figure out how to use MaxPDUSize here
|
||||
//Snap7 seems to choke on PDU sizes above 256 even if snap7
|
||||
//replies with bigger PDU size in connection setup.
|
||||
var maxToWrite = (int)Math.Min(count, 200);
|
||||
await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value.Skip(localIndex).Take(maxToWrite).ToArray());
|
||||
count -= maxToWrite;
|
||||
var maxToWrite = (int)Math.Min(value.Length, MaxPDUSize - 35);
|
||||
await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value.Slice(0, maxToWrite), cancellationToken).ConfigureAwait(false);
|
||||
value = value.Slice(maxToWrite);
|
||||
localIndex += maxToWrite;
|
||||
}
|
||||
}
|
||||
@@ -273,13 +402,15 @@ namespace S7.Net
|
||||
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
|
||||
/// <param name="bitAdr">The address of the bit. (0-7)</param>
|
||||
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool value)
|
||||
public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (bitAdr < 0 || bitAdr > 7)
|
||||
throw new InvalidAddressException(string.Format("Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr));
|
||||
|
||||
await WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, value);
|
||||
await WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, value, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -290,13 +421,15 @@ namespace S7.Net
|
||||
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
|
||||
/// <param name="bitAdr">The address of the bit. (0-7)</param>
|
||||
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, int value)
|
||||
public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, int value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (value < 0 || value > 1)
|
||||
throw new ArgumentException("Value must be 0 or 1", nameof(value));
|
||||
|
||||
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, value == 1);
|
||||
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, value == 1, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -309,15 +442,17 @@ namespace S7.Net
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
|
||||
/// <param name="bitAdr">The address of the bit. (0-7)</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public async Task WriteAsync(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1)
|
||||
public async Task WriteAsync(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (bitAdr != -1)
|
||||
{
|
||||
//Must be writing a bit value as bitAdr is specified
|
||||
if (value is bool)
|
||||
if (value is bool boolean)
|
||||
{
|
||||
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, (bool) value);
|
||||
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, boolean, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (value is int intValue)
|
||||
{
|
||||
@@ -327,24 +462,25 @@ namespace S7.Net
|
||||
"Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid",
|
||||
bitAdr), nameof(bitAdr));
|
||||
|
||||
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, intValue == 1);
|
||||
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, intValue == 1, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value));
|
||||
}
|
||||
else await WriteBytesAsync(dataType, db, startByteAdr, Serialization.SerializeValue(value));
|
||||
else await WriteBytesAsync(dataType, db, startByteAdr, Serialization.SerializeValue(value), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
|
||||
/// If the write was not successful, check <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
|
||||
/// </summary>
|
||||
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
|
||||
/// <param name="value">Value to be written to the PLC</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public async Task WriteAsync(string variable, object value)
|
||||
public async Task WriteAsync(string variable, object value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var adr = new PLCAddress(variable);
|
||||
await WriteAsync(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber);
|
||||
await WriteAsync(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -353,11 +489,13 @@ namespace S7.Net
|
||||
/// <param name="structValue">The struct to be written</param>
|
||||
/// <param name="db">Db address</param>
|
||||
/// <param name="startByteAdr">Start bytes on the PLC</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public async Task WriteStructAsync(object structValue, int db, int startByteAdr = 0)
|
||||
public async Task WriteStructAsync(object structValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = Struct.ToBytes(structValue).ToList();
|
||||
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray());
|
||||
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray(), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -366,34 +504,24 @@ namespace S7.Net
|
||||
/// <param name="classValue">The class to be written</param>
|
||||
/// <param name="db">Db address</param>
|
||||
/// <param name="startByteAdr">Start bytes on the PLC</param>
|
||||
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
|
||||
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
public async Task WriteClassAsync(object classValue, int db, int startByteAdr = 0)
|
||||
public async Task WriteClassAsync(object classValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
|
||||
{
|
||||
byte[] bytes = new byte[(int)Class.GetClassSize(classValue)];
|
||||
Types.Class.ToBytes(classValue, bytes);
|
||||
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes);
|
||||
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<byte[]> ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, int count)
|
||||
private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, Memory<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] bytes = new byte[count];
|
||||
var dataToSend = BuildReadRequestPackage(new[] { new DataItemAddress(dataType, db, startByteAdr, buffer.Length) });
|
||||
|
||||
// first create the header
|
||||
int packageSize = 31;
|
||||
ByteArray package = new ByteArray(packageSize);
|
||||
package.Add(ReadHeaderPackage());
|
||||
// package.Add(0x02); // datenart
|
||||
package.Add(CreateReadDataRequestPackage(dataType, db, startByteAdr, count));
|
||||
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
|
||||
AssertReadResponse(s7data, buffer.Length);
|
||||
|
||||
await stream.WriteAsync(package.Array, 0, package.Array.Length);
|
||||
|
||||
var s7data = await COTP.TSDU.ReadAsync(stream);
|
||||
AssertReadResponse(s7data, count);
|
||||
|
||||
for (int cnt = 0; cnt < count; cnt++)
|
||||
bytes[cnt] = s7data[cnt + 18];
|
||||
|
||||
return bytes;
|
||||
s7data.AsSpan(18, buffer.Length).CopyTo(buffer.Span);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -408,9 +536,9 @@ namespace S7.Net
|
||||
|
||||
var message = new ByteArray();
|
||||
var length = S7WriteMultiple.CreateRequest(message, dataItems);
|
||||
await stream.WriteAsync(message.Array, 0, length).ConfigureAwait(false);
|
||||
|
||||
var response = await COTP.TSDU.ReadAsync(stream).ConfigureAwait(false);
|
||||
var response = await RequestTsduAsync(message.Array, 0, length).ConfigureAwait(false);
|
||||
|
||||
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
|
||||
}
|
||||
|
||||
@@ -421,45 +549,20 @@ namespace S7.Net
|
||||
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous write operation.</returns>
|
||||
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value)
|
||||
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory<byte> value, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] bReceive = new byte[513];
|
||||
int varCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
varCount = value.Length;
|
||||
// first create the header
|
||||
int packageSize = 35 + value.Length;
|
||||
ByteArray package = new ByteArray(packageSize);
|
||||
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value.Span);
|
||||
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
package.Add(new byte[] { 3, 0, 0 });
|
||||
package.Add((byte)packageSize);
|
||||
package.Add(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
|
||||
package.Add(Word.ToByteArray((ushort)(varCount - 1)));
|
||||
package.Add(new byte[] { 0, 0x0e });
|
||||
package.Add(Word.ToByteArray((ushort)(varCount + 4)));
|
||||
package.Add(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 });
|
||||
package.Add(Word.ToByteArray((ushort)varCount));
|
||||
package.Add(Word.ToByteArray((ushort)(db)));
|
||||
package.Add((byte)dataType);
|
||||
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
|
||||
package.Add((byte)overflow);
|
||||
package.Add(Word.ToByteArray((ushort)(startByteAdr * 8)));
|
||||
package.Add(new byte[] { 0, 4 });
|
||||
package.Add(Word.ToByteArray((ushort)(varCount * 8)));
|
||||
|
||||
// now join the header and the data
|
||||
package.Add(value);
|
||||
|
||||
await stream.WriteAsync(package.Array, 0, package.Array.Length);
|
||||
|
||||
var s7data = await COTP.TSDU.ReadAsync(stream);
|
||||
if (s7data == null || s7data[14] != 0xff)
|
||||
{
|
||||
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
|
||||
}
|
||||
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
@@ -467,48 +570,76 @@ namespace S7.Net
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue)
|
||||
private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] bReceive = new byte[513];
|
||||
int varCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
var value = new[] {bitValue ? (byte) 1 : (byte) 0};
|
||||
varCount = value.Length;
|
||||
// first create the header
|
||||
int packageSize = 35 + value.Length;
|
||||
ByteArray package = new Types.ByteArray(packageSize);
|
||||
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
|
||||
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
package.Add(new byte[] { 3, 0, 0 });
|
||||
package.Add((byte)packageSize);
|
||||
package.Add(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
|
||||
package.Add(Word.ToByteArray((ushort)(varCount - 1)));
|
||||
package.Add(new byte[] { 0, 0x0e });
|
||||
package.Add(Word.ToByteArray((ushort)(varCount + 4)));
|
||||
package.Add(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x01 }); //ending 0x01 is used for writing a sinlge bit
|
||||
package.Add(Word.ToByteArray((ushort)varCount));
|
||||
package.Add(Word.ToByteArray((ushort)(db)));
|
||||
package.Add((byte)dataType);
|
||||
int overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
|
||||
package.Add((byte)overflow);
|
||||
package.Add(Word.ToByteArray((ushort)(startByteAdr * 8 + bitAdr)));
|
||||
package.Add(new byte[] { 0, 0x03 }); //ending 0x03 is used for writing a sinlge bit
|
||||
package.Add(Word.ToByteArray((ushort)(varCount)));
|
||||
|
||||
// now join the header and the data
|
||||
package.Add(value);
|
||||
|
||||
await stream.WriteAsync(package.Array, 0, package.Array.Length);
|
||||
|
||||
var s7data = await COTP.TSDU.ReadAsync(stream);
|
||||
if (s7data == null || s7data[14] != 0xff)
|
||||
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
|
||||
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
throw new PlcException(ErrorCode.WriteData, exc);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<byte[]> RequestTsduAsync(byte[] requestData, CancellationToken cancellationToken = default) =>
|
||||
RequestTsduAsync(requestData, 0, requestData.Length, cancellationToken);
|
||||
|
||||
private Task<byte[]> RequestTsduAsync(byte[] requestData, int offset, int length, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var stream = GetStreamIfAvailable();
|
||||
|
||||
return queue.Enqueue(() =>
|
||||
NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken));
|
||||
}
|
||||
|
||||
private async Task<COTP.TPDU> NoLockRequestTpduAsync(Stream stream, byte[] requestData,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
using var closeOnCancellation = cancellationToken.Register(Close);
|
||||
await stream.WriteAsync(requestData, 0, requestData.Length, cancellationToken).ConfigureAwait(false);
|
||||
return await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
if (exc is TPDUInvalidException || exc is TPKTInvalidException)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<byte[]> NoLockRequestTsduAsync(Stream stream, byte[] requestData, int offset, int length,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
using var closeOnCancellation = cancellationToken.Register(Close);
|
||||
await stream.WriteAsync(requestData, offset, length, cancellationToken).ConfigureAwait(false);
|
||||
return await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
if (exc is TPDUInvalidException || exc is TPKTInvalidException)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using S7.Net.Types;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using S7.Net.Protocol;
|
||||
using S7.Net.Helper;
|
||||
|
||||
//Implement synchronous methods here
|
||||
namespace S7.Net
|
||||
@@ -15,28 +15,9 @@ namespace S7.Net
|
||||
/// </summary>
|
||||
public void Open()
|
||||
{
|
||||
Connect();
|
||||
|
||||
try
|
||||
{
|
||||
stream.Write(ConnectionRequest.GetCOTPConnectionRequest(CPU, Rack, Slot), 0, 22);
|
||||
var response = COTP.TPDU.Read(stream);
|
||||
if (response.PDUType != 0xd0) //Connect Confirm
|
||||
{
|
||||
throw new InvalidDataException("Error reading Connection Confirm", response.TPkt.Data, 1, 0x0d);
|
||||
}
|
||||
|
||||
stream.Write(GetS7ConnectionSetup(), 0, 25);
|
||||
|
||||
var s7data = COTP.TSDU.Read(stream);
|
||||
if (s7data == null)
|
||||
throw new WrongNumberOfBytesException("No data received in response to Communication Setup");
|
||||
|
||||
//Check for S7 Ack Data
|
||||
if (s7data[1] != 0x03)
|
||||
throw new InvalidDataException("Error reading Communication Setup response", s7data, 1, 0x03);
|
||||
|
||||
MaxPDUSize = (short)(s7data[18] * 256 + s7data[19]);
|
||||
OpenAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
@@ -45,28 +26,6 @@ namespace S7.Net
|
||||
}
|
||||
}
|
||||
|
||||
private void Connect()
|
||||
{
|
||||
try
|
||||
{
|
||||
tcpClient = new TcpClient();
|
||||
ConfigureConnection();
|
||||
tcpClient.Connect(IP, Port);
|
||||
stream = tcpClient.GetStream();
|
||||
}
|
||||
catch (SocketException sex)
|
||||
{
|
||||
// see https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668(v=vs.85).aspx
|
||||
throw new PlcException(
|
||||
sex.SocketErrorCode == SocketError.TimedOut
|
||||
? ErrorCode.IPAddressNotAvailable
|
||||
: ErrorCode.ConnectionError, sex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new PlcException(ErrorCode.ConnectionError, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
|
||||
@@ -79,24 +38,37 @@ namespace S7.Net
|
||||
/// <returns>Returns the bytes in an array</returns>
|
||||
public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count)
|
||||
{
|
||||
List<byte> resultBytes = new List<byte>();
|
||||
int index = startByteAdr;
|
||||
while (count > 0)
|
||||
{
|
||||
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
|
||||
var maxToRead = (int)Math.Min(count, MaxPDUSize - 18);
|
||||
byte[] bytes = ReadBytesWithSingleRequest(dataType, db, index, maxToRead);
|
||||
if (bytes == null)
|
||||
return resultBytes.ToArray();
|
||||
resultBytes.AddRange(bytes);
|
||||
count -= maxToRead;
|
||||
index += maxToRead;
|
||||
}
|
||||
return resultBytes.ToArray();
|
||||
var result = new byte[count];
|
||||
|
||||
ReadBytes(result, dataType, db, startByteAdr);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read and decode a certain number of bytes of the "VarType" provided.
|
||||
/// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
|
||||
/// If the read was not successful, check LastErrorCode or LastErrorString.
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to receive the read bytes. The <see cref="Span{T}.Length"/> determines the number of bytes to read.</param>
|
||||
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
|
||||
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <returns>Returns the bytes in an array</returns>
|
||||
public void ReadBytes(Span<byte> buffer, DataType dataType, int db, int startByteAdr)
|
||||
{
|
||||
int index = 0;
|
||||
while (buffer.Length > 0)
|
||||
{
|
||||
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
|
||||
var maxToRead = Math.Min(buffer.Length, MaxPDUSize - 18);
|
||||
ReadBytesWithSingleRequest(dataType, db, startByteAdr + index, buffer.Slice(0, maxToRead));
|
||||
buffer = buffer.Slice(maxToRead);
|
||||
index += maxToRead;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read and decode a certain number of bytes of the "VarType" provided.
|
||||
/// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
|
||||
/// If the read was not successful, check LastErrorCode or LastErrorString.
|
||||
/// </summary>
|
||||
@@ -106,7 +78,7 @@ namespace S7.Net
|
||||
/// <param name="varType">Type of the variable/s that you are reading</param>
|
||||
/// <param name="bitAdr">Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter.</param>
|
||||
/// <param name="varCount"></param>
|
||||
public object Read(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0)
|
||||
public object? Read(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0)
|
||||
{
|
||||
int cntBytes = VarTypeToByteLength(varType, varCount);
|
||||
byte[] bytes = ReadBytes(dataType, db, startByteAdr, cntBytes);
|
||||
@@ -119,8 +91,8 @@ namespace S7.Net
|
||||
/// If the read was not successful, check LastErrorCode or LastErrorString.
|
||||
/// </summary>
|
||||
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
|
||||
/// <returns>Returns an object that contains the value. This object must be cast accordingly.</returns>
|
||||
public object Read(string variable)
|
||||
/// <returns>Returns an object that contains the value. This object must be cast accordingly. If no data has been read, null will be returned</returns>
|
||||
public object? Read(string variable)
|
||||
{
|
||||
var adr = new PLCAddress(variable);
|
||||
return Read(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber);
|
||||
@@ -132,8 +104,8 @@ namespace S7.Net
|
||||
/// <param name="structType">Type of the struct to be readed (es.: TypeOf(MyStruct)).</param>
|
||||
/// <param name="db">Address of the DB.</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <returns>Returns a struct that must be cast.</returns>
|
||||
public object ReadStruct(Type structType, int db, int startByteAdr = 0)
|
||||
/// <returns>Returns a struct that must be cast. If no data has been read, null will be returned</returns>
|
||||
public object? ReadStruct(Type structType, int db, int startByteAdr = 0)
|
||||
{
|
||||
int numBytes = Struct.GetStructSize(structType);
|
||||
// now read the package
|
||||
@@ -155,12 +127,11 @@ namespace S7.Net
|
||||
return ReadStruct(typeof(T), db, startByteAdr) as T?;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
|
||||
/// </summary>
|
||||
/// <param name="sourceClass">Instance of the class that will store the values</param>
|
||||
/// <param name="sourceClass">Instance of the class that will store the values</param>
|
||||
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <returns>The number of read bytes</returns>
|
||||
@@ -180,7 +151,7 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic
|
||||
/// type, the class needs a default constructor.
|
||||
/// </summary>
|
||||
@@ -188,13 +159,13 @@ namespace S7.Net
|
||||
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
|
||||
public T ReadClass<T>(int db, int startByteAdr = 0) where T : class
|
||||
public T? ReadClass<T>(int db, int startByteAdr = 0) where T : class
|
||||
{
|
||||
return ReadClass(() => Activator.CreateInstance<T>(), db, startByteAdr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
|
||||
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class that will be instantiated</typeparam>
|
||||
@@ -202,7 +173,7 @@ namespace S7.Net
|
||||
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
|
||||
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
|
||||
public T ReadClass<T>(Func<T> classFactory, int db, int startByteAdr = 0) where T : class
|
||||
public T? ReadClass<T>(Func<T> classFactory, int db, int startByteAdr = 0) where T : class
|
||||
{
|
||||
var instance = classFactory();
|
||||
int readBytes = ReadClass(instance, db, startByteAdr);
|
||||
@@ -222,17 +193,29 @@ namespace S7.Net
|
||||
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
|
||||
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
|
||||
public void WriteBytes(DataType dataType, int db, int startByteAdr, byte[] value)
|
||||
{
|
||||
WriteBytes(dataType, db, startByteAdr, value.AsSpan());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
|
||||
/// If the write was not successful, check LastErrorCode or LastErrorString.
|
||||
/// </summary>
|
||||
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
|
||||
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
|
||||
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
|
||||
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
|
||||
public void WriteBytes(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> value)
|
||||
{
|
||||
int localIndex = 0;
|
||||
int count = value.Length;
|
||||
while (count > 0)
|
||||
while (value.Length > 0)
|
||||
{
|
||||
//TODO: Figure out how to use MaxPDUSize here
|
||||
//Snap7 seems to choke on PDU sizes above 256 even if snap7
|
||||
//Snap7 seems to choke on PDU sizes above 256 even if snap7
|
||||
//replies with bigger PDU size in connection setup.
|
||||
var maxToWrite = Math.Min(count, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480
|
||||
WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value.Skip(localIndex).Take(maxToWrite).ToArray());
|
||||
count -= maxToWrite;
|
||||
var maxToWrite = Math.Min(value.Length, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480
|
||||
WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value.Slice(0, maxToWrite));
|
||||
value = value.Slice(maxToWrite);
|
||||
localIndex += maxToWrite;
|
||||
}
|
||||
}
|
||||
@@ -284,9 +267,9 @@ namespace S7.Net
|
||||
if (bitAdr != -1)
|
||||
{
|
||||
//Must be writing a bit value as bitAdr is specified
|
||||
if (value is bool)
|
||||
if (value is bool boolean)
|
||||
{
|
||||
WriteBit(dataType, db, startByteAdr, bitAdr, (bool) value);
|
||||
WriteBit(dataType, db, startByteAdr, bitAdr, boolean);
|
||||
}
|
||||
else if (value is int intValue)
|
||||
{
|
||||
@@ -306,7 +289,6 @@ namespace S7.Net
|
||||
|
||||
/// <summary>
|
||||
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
|
||||
/// If the write was not successful, check <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
|
||||
/// </summary>
|
||||
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
|
||||
/// <param name="value">Value to be written to the PLC</param>
|
||||
@@ -338,27 +320,22 @@ namespace S7.Net
|
||||
WriteClassAsync(classValue, db, startByteAdr).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private byte[] ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, int count)
|
||||
private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, Span<byte> buffer)
|
||||
{
|
||||
byte[] bytes = new byte[count];
|
||||
try
|
||||
{
|
||||
// first create the header
|
||||
int packageSize = 31;
|
||||
ByteArray package = new ByteArray(packageSize);
|
||||
package.Add(ReadHeaderPackage());
|
||||
const int packageSize = 19 + 12; // 19 header + 12 for 1 request
|
||||
var dataToSend = new byte[packageSize];
|
||||
var package = new MemoryStream(dataToSend);
|
||||
WriteReadHeader(package);
|
||||
// package.Add(0x02); // datenart
|
||||
package.Add(CreateReadDataRequestPackage(dataType, db, startByteAdr, count));
|
||||
BuildReadDataRequestPackage(package, dataType, db, startByteAdr, buffer.Length);
|
||||
|
||||
stream.Write(package.Array, 0, package.Array.Length);
|
||||
var s7data = RequestTsdu(dataToSend);
|
||||
AssertReadResponse(s7data, buffer.Length);
|
||||
|
||||
var s7data = COTP.TSDU.Read(stream);
|
||||
AssertReadResponse(s7data, count);
|
||||
|
||||
for (int cnt = 0; cnt < count; cnt++)
|
||||
bytes[cnt] = s7data[cnt + 18];
|
||||
|
||||
return bytes;
|
||||
s7data.AsSpan(18, buffer.Length).CopyTo(buffer);
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
@@ -377,49 +354,19 @@ namespace S7.Net
|
||||
|
||||
var message = new ByteArray();
|
||||
var length = S7WriteMultiple.CreateRequest(message, dataItems);
|
||||
stream.Write(message.Array, 0, length);
|
||||
var response = RequestTsdu(message.Array, 0, length);
|
||||
|
||||
var response = COTP.TSDU.Read(stream);
|
||||
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
|
||||
}
|
||||
|
||||
private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, byte[] value)
|
||||
private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> value)
|
||||
{
|
||||
int varCount = 0;
|
||||
try
|
||||
{
|
||||
varCount = value.Length;
|
||||
// first create the header
|
||||
int packageSize = 35 + value.Length;
|
||||
ByteArray package = new ByteArray(packageSize);
|
||||
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value);
|
||||
var s7data = RequestTsdu(dataToSend);
|
||||
|
||||
package.Add(new byte[] { 3, 0 });
|
||||
//complete package size
|
||||
package.Add(Int.ToByteArray((short)packageSize));
|
||||
package.Add(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
|
||||
package.Add(Word.ToByteArray((ushort)(varCount - 1)));
|
||||
package.Add(new byte[] { 0, 0x0e });
|
||||
package.Add(Word.ToByteArray((ushort)(varCount + 4)));
|
||||
package.Add(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 });
|
||||
package.Add(Word.ToByteArray((ushort)varCount));
|
||||
package.Add(Word.ToByteArray((ushort)(db)));
|
||||
package.Add((byte)dataType);
|
||||
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
|
||||
package.Add((byte)overflow);
|
||||
package.Add(Word.ToByteArray((ushort)(startByteAdr * 8)));
|
||||
package.Add(new byte[] { 0, 4 });
|
||||
package.Add(Word.ToByteArray((ushort)(varCount * 8)));
|
||||
|
||||
// now join the header and the data
|
||||
package.Add(value);
|
||||
|
||||
stream.Write(package.Array, 0, package.Array.Length);
|
||||
|
||||
var s7data = COTP.TSDU.Read(stream);
|
||||
if (s7data == null || s7data[14] != 0xff)
|
||||
{
|
||||
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
|
||||
}
|
||||
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
@@ -427,42 +374,80 @@ namespace S7.Net
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] BuildWriteBytesPackage(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> value)
|
||||
{
|
||||
int varCount = value.Length;
|
||||
// first create the header
|
||||
int packageSize = 35 + varCount;
|
||||
var packageData = new byte[packageSize];
|
||||
var package = new MemoryStream(packageData);
|
||||
|
||||
package.WriteByte(3);
|
||||
package.WriteByte(0);
|
||||
//complete package size
|
||||
package.Write(Int.ToByteArray((short)packageSize));
|
||||
// This overload doesn't allocate the byte array, it refers to assembly's static data segment
|
||||
package.Write(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
|
||||
package.Write(Word.ToByteArray((ushort)(varCount - 1)));
|
||||
package.Write(new byte[] { 0, 0x0e });
|
||||
package.Write(Word.ToByteArray((ushort)(varCount + 4)));
|
||||
package.Write(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 });
|
||||
package.Write(Word.ToByteArray((ushort)varCount));
|
||||
package.Write(Word.ToByteArray((ushort)(db)));
|
||||
package.WriteByte((byte)dataType);
|
||||
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
|
||||
package.WriteByte((byte)overflow);
|
||||
package.Write(Word.ToByteArray((ushort)(startByteAdr * 8)));
|
||||
package.Write(new byte[] { 0, 4 });
|
||||
package.Write(Word.ToByteArray((ushort)(varCount * 8)));
|
||||
|
||||
// now join the header and the data
|
||||
package.Write(value);
|
||||
|
||||
return packageData;
|
||||
}
|
||||
|
||||
private byte[] BuildWriteBitPackage(DataType dataType, int db, int startByteAdr, bool bitValue, int bitAdr)
|
||||
{
|
||||
var value = new[] { bitValue ? (byte)1 : (byte)0 };
|
||||
int varCount = 1;
|
||||
// first create the header
|
||||
int packageSize = 35 + varCount;
|
||||
var packageData = new byte[packageSize];
|
||||
var package = new MemoryStream(packageData);
|
||||
|
||||
package.WriteByte(3);
|
||||
package.WriteByte(0);
|
||||
//complete package size
|
||||
package.Write(Int.ToByteArray((short)packageSize));
|
||||
package.Write(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
|
||||
package.Write(Word.ToByteArray((ushort)(varCount - 1)));
|
||||
package.Write(new byte[] { 0, 0x0e });
|
||||
package.Write(Word.ToByteArray((ushort)(varCount + 4)));
|
||||
package.Write(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x01 }); //ending 0x01 is used for writing a sinlge bit
|
||||
package.Write(Word.ToByteArray((ushort)varCount));
|
||||
package.Write(Word.ToByteArray((ushort)(db)));
|
||||
package.WriteByte((byte)dataType);
|
||||
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
|
||||
package.WriteByte((byte)overflow);
|
||||
package.Write(Word.ToByteArray((ushort)(startByteAdr * 8 + bitAdr)));
|
||||
package.Write(new byte[] { 0, 0x03 }); //ending 0x03 is used for writing a sinlge bit
|
||||
package.Write(Word.ToByteArray((ushort)(varCount)));
|
||||
|
||||
// now join the header and the data
|
||||
package.Write(value);
|
||||
|
||||
return packageData;
|
||||
}
|
||||
|
||||
private void WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue)
|
||||
{
|
||||
int varCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
var value = new[] {bitValue ? (byte) 1 : (byte) 0};
|
||||
varCount = value.Length;
|
||||
// first create the header
|
||||
int packageSize = 35 + value.Length;
|
||||
ByteArray package = new ByteArray(packageSize);
|
||||
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
|
||||
var s7data = RequestTsdu(dataToSend);
|
||||
|
||||
package.Add(new byte[] { 3, 0, 0 });
|
||||
package.Add((byte)packageSize);
|
||||
package.Add(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
|
||||
package.Add(Word.ToByteArray((ushort)(varCount - 1)));
|
||||
package.Add(new byte[] { 0, 0x0e });
|
||||
package.Add(Word.ToByteArray((ushort)(varCount + 4)));
|
||||
package.Add(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x01 }); //ending 0x01 is used for writing a sinlge bit
|
||||
package.Add(Word.ToByteArray((ushort)varCount));
|
||||
package.Add(Word.ToByteArray((ushort)(db)));
|
||||
package.Add((byte)dataType);
|
||||
int overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
|
||||
package.Add((byte)overflow);
|
||||
package.Add(Word.ToByteArray((ushort)(startByteAdr * 8 + bitAdr)));
|
||||
package.Add(new byte[] { 0, 0x03 }); //ending 0x03 is used for writing a sinlge bit
|
||||
package.Add(Word.ToByteArray((ushort)(varCount)));
|
||||
|
||||
// now join the header and the data
|
||||
package.Add(value);
|
||||
|
||||
stream.Write(package.Array, 0, package.Array.Length);
|
||||
|
||||
var s7data = COTP.TSDU.Read(stream);
|
||||
if (s7data == null || s7data[14] != 0xff)
|
||||
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
|
||||
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
@@ -471,36 +456,33 @@ namespace S7.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads multiple vars in a single request.
|
||||
/// Reads multiple vars in a single request.
|
||||
/// You have to create and pass a list of DataItems and you obtain in response the same list with the values.
|
||||
/// Values are stored in the property "Value" of the dataItem and are already converted.
|
||||
/// If you don't want the conversion, just create a dataItem of bytes.
|
||||
/// DataItems must not be more than 20 (protocol restriction) and bytes must not be more than 200 + 22 of header (protocol restriction).
|
||||
/// If you don't want the conversion, just create a dataItem of bytes.
|
||||
/// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction).
|
||||
/// </summary>
|
||||
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read. Maximum 20 dataitems are accepted.</param>
|
||||
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
|
||||
public void ReadMultipleVars(List<DataItem> dataItems)
|
||||
{
|
||||
//Snap7 seems to choke on PDU sizes above 256 even if snap7
|
||||
//replies with bigger PDU size in connection setup.
|
||||
AssertPduSizeForRead(dataItems);
|
||||
|
||||
try
|
||||
{
|
||||
// first create the header
|
||||
int packageSize = 19 + (dataItems.Count * 12);
|
||||
ByteArray package = new ByteArray(packageSize);
|
||||
package.Add(ReadHeaderPackage(dataItems.Count));
|
||||
var dataToSend = new byte[packageSize];
|
||||
var package = new MemoryStream(dataToSend);
|
||||
WriteReadHeader(package, dataItems.Count);
|
||||
// package.Add(0x02); // datenart
|
||||
foreach (var dataItem in dataItems)
|
||||
{
|
||||
package.Add(CreateReadDataRequestPackage(dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count)));
|
||||
BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count));
|
||||
}
|
||||
|
||||
stream.Write(package.Array, 0, package.Array.Length);
|
||||
byte[] s7data = RequestTsdu(dataToSend);
|
||||
|
||||
var s7data = COTP.TSDU.Read(stream); //TODO use Async
|
||||
if (s7data == null || s7data[14] != 0xff)
|
||||
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
|
||||
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
|
||||
|
||||
ParseDataIntoDataItems(s7data, dataItems);
|
||||
}
|
||||
@@ -509,5 +491,48 @@ namespace S7.Net
|
||||
throw new PlcException(ErrorCode.ReadData, exc);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the PLC clock value.
|
||||
/// </summary>
|
||||
/// <returns>The current PLC time.</returns>
|
||||
public System.DateTime ReadClock()
|
||||
{
|
||||
var request = BuildClockReadRequest();
|
||||
var response = RequestTsdu(request);
|
||||
|
||||
return ParseClockReadResponse(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write the PLC clock value.
|
||||
/// </summary>
|
||||
/// <param name="value">The date and time to set the PLC clock to.</param>
|
||||
public void WriteClock(System.DateTime value)
|
||||
{
|
||||
var request = BuildClockWriteRequest(value);
|
||||
var response = RequestTsdu(request);
|
||||
|
||||
ParseClockWriteResponse(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type.
|
||||
/// </summary>
|
||||
/// <returns>The current PLC status.</returns>
|
||||
public byte ReadStatus()
|
||||
{
|
||||
var dataToSend = BuildSzlReadRequestPackage(0x0424, 0);
|
||||
var s7data = RequestTsdu(dataToSend);
|
||||
|
||||
return (byte) (s7data[37] & 0x0f);
|
||||
}
|
||||
|
||||
private byte[] RequestTsdu(byte[] requestData) => RequestTsdu(requestData, 0, requestData.Length);
|
||||
|
||||
private byte[] RequestTsdu(byte[] requestData, int offset, int length)
|
||||
{
|
||||
return RequestTsduAsync(requestData, offset, length).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("S7Net.UnitTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d1032db55f60d64bf90ea1cc2247b5a8b9b6168a07bcd464a07ce2e425d027ff9409a64ba0e3f37718e14c50cf964d0d921e5ae8b8d74bd8a82431794f897cebf0ee668feb2ccd030153611b2808fcb7785c5e5136a98e0ec23de3c1ed385d2026c26e4bed5805ff9db7e0544f59b1f19d369d43403a624586795926e38c48d")]
|
||||
[assembly: InternalsVisibleTo("S7.Net.UnitTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d1032db55f60d64bf90ea1cc2247b5a8b9b6168a07bcd464a07ce2e425d027ff9409a64ba0e3f37718e14c50cf964d0d921e5ae8b8d74bd8a82431794f897cebf0ee668feb2ccd030153611b2808fcb7785c5e5136a98e0ec23de3c1ed385d2026c26e4bed5805ff9db7e0544f59b1f19d369d43403a624586795926e38c48d")]
|
||||
|
||||
@@ -1,68 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace S7.Net.Protocol
|
||||
namespace S7.Net.Protocol
|
||||
{
|
||||
internal static class ConnectionRequest
|
||||
{
|
||||
public static byte[] GetCOTPConnectionRequest(CpuType cpu, Int16 rack, Int16 slot)
|
||||
public static byte[] GetCOTPConnectionRequest(TsapPair tsapPair)
|
||||
{
|
||||
byte[] bSend1 = {
|
||||
3, 0, 0, 22, //TPKT
|
||||
17, //COTP Header Length
|
||||
224, //Connect Request
|
||||
224, //Connect Request
|
||||
0, 0, //Destination Reference
|
||||
0, 46, //Source Reference
|
||||
0, //Flags
|
||||
193, //Parameter Code (src-tasp)
|
||||
2, //Parameter Length
|
||||
1, 0, //Source TASP
|
||||
tsapPair.Local.FirstByte, tsapPair.Local.SecondByte, //Source TASP
|
||||
194, //Parameter Code (dst-tasp)
|
||||
2, //Parameter Length
|
||||
3, 0, //Destination TASP
|
||||
tsapPair.Remote.FirstByte, tsapPair.Remote.SecondByte, //Destination TASP
|
||||
192, //Parameter Code (tpdu-size)
|
||||
1, //Parameter Length
|
||||
10 //TPDU Size (2^10 = 1024)
|
||||
};
|
||||
|
||||
switch (cpu)
|
||||
{
|
||||
case CpuType.S7200:
|
||||
//S7200: Chr(193) & Chr(2) & Chr(16) & Chr(0) 'Eigener Tsap
|
||||
bSend1[13] = 0x10;
|
||||
bSend1[14] = 0x00;
|
||||
//S7200: Chr(194) & Chr(2) & Chr(16) & Chr(0) 'Fremder Tsap
|
||||
bSend1[17] = 0x10;
|
||||
bSend1[18] = 0x00;
|
||||
break;
|
||||
case CpuType.Logo0BA8:
|
||||
// These values are taken from NodeS7, it's not verified if these are
|
||||
// exact requirements to connect to the Logo0BA8.
|
||||
bSend1[13] = 0x01;
|
||||
bSend1[14] = 0x00;
|
||||
bSend1[17] = 0x01;
|
||||
bSend1[18] = 0x02;
|
||||
break;
|
||||
case CpuType.S71200:
|
||||
case CpuType.S7300:
|
||||
case CpuType.S7400:
|
||||
//S7300: Chr(193) & Chr(2) & Chr(1) & Chr(0) 'Eigener Tsap
|
||||
bSend1[13] = 0x01;
|
||||
bSend1[14] = 0x00;
|
||||
//S7300: Chr(194) & Chr(2) & Chr(3) & Chr(2) 'Fremder Tsap
|
||||
bSend1[17] = 0x03;
|
||||
bSend1[18] = (byte) ((rack << 5) | (int) slot);
|
||||
break;
|
||||
case CpuType.S71500:
|
||||
// Eigener Tsap
|
||||
bSend1[13] = 0x10;
|
||||
bSend1[14] = 0x02;
|
||||
// Fredmer Tsap
|
||||
bSend1[17] = 0x03;
|
||||
bSend1[18] = (byte) ((rack << 5) | (int) slot);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Wrong CPU Type Secified");
|
||||
}
|
||||
return bSend1;
|
||||
}
|
||||
}
|
||||
|
||||
15
S7.Net/Protocol/ReadWriteErrorCode.cs
Normal file
15
S7.Net/Protocol/ReadWriteErrorCode.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
namespace S7.Net.Protocol
|
||||
{
|
||||
internal enum ReadWriteErrorCode : byte
|
||||
{
|
||||
Reserved = 0x00,
|
||||
HardwareFault = 0x01,
|
||||
AccessingObjectNotAllowed = 0x03,
|
||||
AddressOutOfRange = 0x05,
|
||||
DataTypeNotSupported = 0x06,
|
||||
DataTypeInconsistent = 0x07,
|
||||
ObjectDoesNotExist = 0x0a,
|
||||
Success = 0xff
|
||||
}
|
||||
}
|
||||
37
S7.Net/Protocol/S7/DataItemAddress.cs
Normal file
37
S7.Net/Protocol/S7/DataItemAddress.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace S7.Net.Protocol.S7
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an area of memory in the PLC
|
||||
/// </summary>
|
||||
internal class DataItemAddress
|
||||
{
|
||||
public DataItemAddress(DataType dataType, int db, int startByteAddress, int byteLength)
|
||||
{
|
||||
DataType = dataType;
|
||||
DB = db;
|
||||
StartByteAddress = startByteAddress;
|
||||
ByteLength = byteLength;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Memory area to read
|
||||
/// </summary>
|
||||
public DataType DataType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Address of memory area to read (example: for DB1 this value is 1, for T45 this value is 45)
|
||||
/// </summary>
|
||||
public int DB { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Address of the first byte to read
|
||||
/// </summary>
|
||||
public int StartByteAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Length of data to read
|
||||
/// </summary>
|
||||
public int ByteLength { get; }
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ namespace S7.Net.Protocol
|
||||
(ushort) (2 + paramSize));
|
||||
|
||||
var paramOffset = Header.Template.Length;
|
||||
var dataOffset = paramOffset + paramSize;
|
||||
var data = new ByteArray();
|
||||
|
||||
var itemCount = 0;
|
||||
@@ -91,15 +90,20 @@ namespace S7.Net.Protocol
|
||||
|
||||
IList<byte> itemResults = new ArraySegment<byte>(message, 14, dataItems.Length);
|
||||
|
||||
List<Exception> errors = null;
|
||||
List<Exception>? errors = null;
|
||||
|
||||
for (int i = 0; i < dataItems.Length; i++)
|
||||
{
|
||||
var result = itemResults[i];
|
||||
if (result == 0xff) continue;
|
||||
try
|
||||
{
|
||||
Plc.ValidateResponseCode((ReadWriteErrorCode)itemResults[i]);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
if (errors == null) errors = new List<Exception>();
|
||||
errors.Add(new Exception($"Write of dataItem {dataItems[i]} failed: {e.Message}."));
|
||||
}
|
||||
|
||||
if (errors == null) errors = new List<Exception>();
|
||||
errors.Add(new Exception($"Write of dataItem {dataItems[i]} failed with error code {result}."));
|
||||
}
|
||||
|
||||
if (errors != null)
|
||||
|
||||
@@ -13,10 +13,23 @@ namespace S7.Net.Protocol
|
||||
|
||||
public static byte[] SerializeDataItem(DataItem dataItem)
|
||||
{
|
||||
if (dataItem.Value == null)
|
||||
{
|
||||
throw new Exception($"DataItem.Value is null, cannot serialize. StartAddr={dataItem.StartByteAdr} VarType={dataItem.VarType}");
|
||||
}
|
||||
|
||||
if (dataItem.Value is string s)
|
||||
return dataItem.VarType == VarType.StringEx
|
||||
? StringEx.ToByteArray(s, dataItem.Count)
|
||||
: Types.String.ToByteArray(s, dataItem.Count);
|
||||
return dataItem.VarType switch
|
||||
{
|
||||
VarType.S7String => S7String.ToByteArray(s, dataItem.Count),
|
||||
VarType.S7WString => S7WString.ToByteArray(s, dataItem.Count),
|
||||
_ => Types.String.ToByteArray(s, dataItem.Count)
|
||||
};
|
||||
|
||||
if (dataItem.VarType == VarType.Date)
|
||||
{
|
||||
return Date.ToByteArray((System.DateTime)dataItem.Value);
|
||||
}
|
||||
|
||||
return SerializeValue(dataItem.Value);
|
||||
}
|
||||
@@ -37,12 +50,12 @@ namespace S7.Net.Protocol
|
||||
return Types.DInt.ToByteArray((Int32)value);
|
||||
case "UInt32":
|
||||
return Types.DWord.ToByteArray((UInt32)value);
|
||||
case "Double":
|
||||
return Types.Double.ToByteArray((double)value);
|
||||
case "Single":
|
||||
return Types.Single.ToByteArray((float)value);
|
||||
return Types.Real.ToByteArray((float)value);
|
||||
case "Double":
|
||||
return Types.LReal.ToByteArray((double)value);
|
||||
case "DateTime":
|
||||
return Types.DateTime.ToByteArray((System.DateTime) value);
|
||||
return Types.DateTime.ToByteArray((System.DateTime)value);
|
||||
case "Byte[]":
|
||||
return (byte[])value;
|
||||
case "Int16[]":
|
||||
@@ -53,17 +66,19 @@ namespace S7.Net.Protocol
|
||||
return Types.DInt.ToByteArray((Int32[])value);
|
||||
case "UInt32[]":
|
||||
return Types.DWord.ToByteArray((UInt32[])value);
|
||||
case "Double[]":
|
||||
return Types.Double.ToByteArray((double[])value);
|
||||
case "Single[]":
|
||||
return Types.Single.ToByteArray((float[])value);
|
||||
return Types.Real.ToByteArray((float[])value);
|
||||
case "Double[]":
|
||||
return Types.LReal.ToByteArray((double[])value);
|
||||
case "String":
|
||||
// Hack: This is backwards compatible with the old code, but functionally it's broken
|
||||
// if the consumer does not pay attention to string length.
|
||||
var stringVal = (string) value;
|
||||
var stringVal = (string)value;
|
||||
return Types.String.ToByteArray(stringVal, stringVal.Length);
|
||||
case "DateTime[]":
|
||||
return Types.DateTime.ToByteArray((System.DateTime[]) value);
|
||||
return Types.DateTime.ToByteArray((System.DateTime[])value);
|
||||
case "DateTimeLong[]":
|
||||
return Types.DateTimeLong.ToByteArray((System.DateTime[])value);
|
||||
default:
|
||||
throw new InvalidVariableTypeException();
|
||||
}
|
||||
@@ -73,9 +88,9 @@ namespace S7.Net.Protocol
|
||||
{
|
||||
var start = startByte * 8 + bitNumber;
|
||||
buffer[index + 2] = (byte)start;
|
||||
start = start >> 8;
|
||||
start >>= 8;
|
||||
buffer[index + 1] = (byte)start;
|
||||
start = start >> 8;
|
||||
start >>= 8;
|
||||
buffer[index] = (byte)start;
|
||||
}
|
||||
|
||||
|
||||
31
S7.Net/Protocol/Tsap.cs
Normal file
31
S7.Net/Protocol/Tsap.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace S7.Net.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a representation of the Transport Service Access Point, or TSAP in short. TSAP's are used
|
||||
/// to specify a client and server address. For most PLC types a default TSAP is available that allows
|
||||
/// connection from any IP and can be calculated using the rack and slot numbers.
|
||||
/// </summary>
|
||||
public struct Tsap
|
||||
{
|
||||
/// <summary>
|
||||
/// First byte of the TSAP.
|
||||
/// </summary>
|
||||
public byte FirstByte { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Second byte of the TSAP.
|
||||
/// </summary>
|
||||
public byte SecondByte { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Tsap" /> class using the specified values.
|
||||
/// </summary>
|
||||
/// <param name="firstByte">The first byte of the TSAP.</param>
|
||||
/// <param name="secondByte">The second byte of the TSAP.</param>
|
||||
public Tsap(byte firstByte, byte secondByte)
|
||||
{
|
||||
FirstByte = firstByte;
|
||||
SecondByte = secondByte;
|
||||
}
|
||||
}
|
||||
}
|
||||
96
S7.Net/Protocol/TsapPair.cs
Normal file
96
S7.Net/Protocol/TsapPair.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
|
||||
namespace S7.Net.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a pair of TSAP addresses used to connect to a PLC.
|
||||
/// </summary>
|
||||
public class TsapPair
|
||||
{
|
||||
/// <summary>
|
||||
/// The local <see cref="Tsap" />.
|
||||
/// </summary>
|
||||
public Tsap Local { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The remote <see cref="Tsap" />
|
||||
/// </summary>
|
||||
public Tsap Remote { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TsapPair" /> class using the specified local and
|
||||
/// remote TSAP.
|
||||
/// </summary>
|
||||
/// <param name="local">The local TSAP.</param>
|
||||
/// <param name="remote">The remote TSAP.</param>
|
||||
public TsapPair(Tsap local, Tsap remote)
|
||||
{
|
||||
Local = local;
|
||||
Remote = remote;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <see cref="TsapPair" /> that can be used to connect to a PLC using the default connection
|
||||
/// addresses.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The remote TSAP is constructed using <code>new Tsap(0x03, (byte) ((rack << 5) | slot))</code>.
|
||||
/// </remarks>
|
||||
/// <param name="cpuType">The CPU type of the PLC.</param>
|
||||
/// <param name="rack">The rack of the PLC's network card.</param>
|
||||
/// <param name="slot">The slot of the PLC's network card.</param>
|
||||
/// <returns>A TSAP pair that matches the given parameters.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="cpuType"/> is invalid.
|
||||
///
|
||||
/// -or-
|
||||
///
|
||||
/// The <paramref name="rack"/> parameter is less than 0.
|
||||
///
|
||||
/// -or-
|
||||
///
|
||||
/// The <paramref name="rack"/> parameter is greater than 15.
|
||||
///
|
||||
/// -or-
|
||||
///
|
||||
/// The <paramref name="slot"/> parameter is less than 0.
|
||||
///
|
||||
/// -or-
|
||||
///
|
||||
/// The <paramref name="slot"/> parameter is greater than 15.</exception>
|
||||
public static TsapPair GetDefaultTsapPair(CpuType cpuType, int rack, int slot)
|
||||
{
|
||||
if (rack < 0) throw InvalidRackOrSlot(rack, nameof(rack), "minimum", 0);
|
||||
if (rack > 0x0F) throw InvalidRackOrSlot(rack, nameof(rack), "maximum", 0x0F);
|
||||
|
||||
if (slot < 0) throw InvalidRackOrSlot(slot, nameof(slot), "minimum", 0);
|
||||
if (slot > 0x0F) throw InvalidRackOrSlot(slot, nameof(slot), "maximum", 0x0F);
|
||||
|
||||
switch (cpuType)
|
||||
{
|
||||
case CpuType.S7200:
|
||||
return new TsapPair(new Tsap(0x10, 0x00), new Tsap(0x10, 0x01));
|
||||
case CpuType.Logo0BA8:
|
||||
// The actual values are probably on a per-project basis
|
||||
return new TsapPair(new Tsap(0x01, 0x00), new Tsap(0x01, 0x02));
|
||||
case CpuType.S7200Smart:
|
||||
case CpuType.S71200:
|
||||
case CpuType.S71500:
|
||||
case CpuType.S7300:
|
||||
case CpuType.S7400:
|
||||
// Testing with S7 1500 shows only the remote TSAP needs to match. This might differ for other
|
||||
// PLC types.
|
||||
return new TsapPair(new Tsap(0x01, 0x00), new Tsap(0x03, (byte) ((rack << 5) | slot)));
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(cpuType), "Invalid CPU Type specified");
|
||||
}
|
||||
}
|
||||
|
||||
private static ArgumentOutOfRangeException InvalidRackOrSlot(int value, string name, string extrema,
|
||||
int extremaValue)
|
||||
{
|
||||
return new ArgumentOutOfRangeException(name,
|
||||
$"Invalid {name} value specified (decimal: {value}, hexadecimal: {value:X}), {extrema} value " +
|
||||
$"is {extremaValue} (decimal) or {extremaValue:X} (hexadecimal).");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net452;netstandard2.0;netstandard1.3</TargetFrameworks>
|
||||
<TargetFrameworks>net452;net462;netstandard2.0;netstandard1.3;net5.0;net6.0;net7.0</TargetFrameworks>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
|
||||
<InternalsVisibleTo>S7.Net.UnitTest</InternalsVisibleTo>
|
||||
@@ -15,14 +15,24 @@
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>PLC Siemens Communication S7</PackageTags>
|
||||
<Copyright>Derek Heiser 2015</Copyright>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>Enable</Nullable>
|
||||
<DebugType>portable</DebugType>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);CS1591;NETSDK1138</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)' == 'net452' Or '$(TargetFramework)' == 'netstandard2.0' ">
|
||||
<PropertyGroup Condition="'$(TargetFramework)' == 'net452' Or '$(TargetFramework)' == 'net462' Or '$(TargetFramework)' == 'netstandard2.0' ">
|
||||
<DefineConstants>NET_FULL</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'net5.0' And '$(TargetFramework)' != 'net6.0' And '$(TargetFramework)' != 'net7.0'">
|
||||
<PackageReference Include="System.Memory" Version="4.5.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta-62925-02" PrivateAssets="All" />
|
||||
<PackageReference Include="SourceLink.Copy.PdbFiles" Version="2.8.3" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
58
S7.Net/StreamExtensions.cs
Normal file
58
S7.Net/StreamExtensions.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace S7.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for Streams
|
||||
/// </summary>
|
||||
public static class StreamExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads bytes from the stream into the buffer until exactly the requested number of bytes (or EOF) have been read
|
||||
/// </summary>
|
||||
/// <param name="stream">the Stream to read from</param>
|
||||
/// <param name="buffer">the buffer to read into</param>
|
||||
/// <param name="offset">the offset in the buffer to read into</param>
|
||||
/// <param name="count">the amount of bytes to read into the buffer</param>
|
||||
/// <returns>returns the amount of read bytes</returns>
|
||||
public static int ReadExact(this Stream stream, byte[] buffer, int offset, int count)
|
||||
{
|
||||
int read = 0;
|
||||
int received;
|
||||
do
|
||||
{
|
||||
received = stream.Read(buffer, offset + read, count - read);
|
||||
read += received;
|
||||
}
|
||||
while (read < count && received > 0);
|
||||
|
||||
return read;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads bytes from the stream into the buffer until exactly the requested number of bytes (or EOF) have been read
|
||||
/// </summary>
|
||||
/// <param name="stream">the Stream to read from</param>
|
||||
/// <param name="buffer">the buffer to read into</param>
|
||||
/// <param name="offset">the offset in the buffer to read into</param>
|
||||
/// <param name="count">the amount of bytes to read into the buffer</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||
/// <returns>returns the amount of read bytes</returns>
|
||||
public static async Task<int> ReadExactAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
int read = 0;
|
||||
int received;
|
||||
do
|
||||
{
|
||||
received = await stream.ReadAsync(buffer, offset + read, count - read, cancellationToken).ConfigureAwait(false);
|
||||
read += received;
|
||||
}
|
||||
while (read < count && received > 0);
|
||||
|
||||
return read;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace S7.Net
|
||||
@@ -10,60 +11,48 @@ namespace S7.Net
|
||||
/// </summary>
|
||||
internal class TPKT
|
||||
{
|
||||
|
||||
|
||||
public byte Version;
|
||||
public byte Reserved1;
|
||||
public int Length;
|
||||
public byte[] Data;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a TPKT from the socket
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <returns>TPKT Instance</returns>
|
||||
public static TPKT Read(Stream stream)
|
||||
private TPKT(byte version, byte reserved1, int length, byte[] data)
|
||||
{
|
||||
var buf = new byte[4];
|
||||
int len = stream.Read(buf, 0, 4);
|
||||
if (len < 4) throw new TPKTInvalidException("TPKT is incomplete / invalid");
|
||||
var pkt = new TPKT
|
||||
{
|
||||
Version = buf[0],
|
||||
Reserved1 = buf[1],
|
||||
Length = buf[2] * 256 + buf[3] //BigEndian
|
||||
};
|
||||
if (pkt.Length > 0)
|
||||
{
|
||||
pkt.Data = new byte[pkt.Length - 4];
|
||||
len = stream.Read(pkt.Data, 0, pkt.Length - 4);
|
||||
if (len < pkt.Length - 4)
|
||||
throw new TPKTInvalidException("TPKT is incomplete / invalid");
|
||||
}
|
||||
return pkt;
|
||||
Version = version;
|
||||
Reserved1 = reserved1;
|
||||
Length = length;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a TPKT from the socket Async
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||
/// <returns>Task TPKT Instace</returns>
|
||||
public static async Task<TPKT> ReadAsync(Stream stream)
|
||||
public static async Task<TPKT> ReadAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var buf = new byte[4];
|
||||
int len = await stream.ReadAsync(buf, 0, 4);
|
||||
int len = await stream.ReadExactAsync(buf, 0, 4, cancellationToken).ConfigureAwait(false);
|
||||
if (len < 4) throw new TPKTInvalidException("TPKT is incomplete / invalid");
|
||||
var pkt = new TPKT
|
||||
{
|
||||
Version = buf[0],
|
||||
Reserved1 = buf[1],
|
||||
Length = buf[2] * 256 + buf[3] //BigEndian
|
||||
};
|
||||
if (pkt.Length > 0)
|
||||
{
|
||||
pkt.Data = new byte[pkt.Length - 4];
|
||||
len = await stream.ReadAsync(pkt.Data, 0, pkt.Length - 4);
|
||||
if (len < pkt.Length - 4) throw new TPKTInvalidException("TPKT is incomplete / invalid");
|
||||
}
|
||||
return pkt;
|
||||
|
||||
var version = buf[0];
|
||||
var reserved1 = buf[1];
|
||||
var length = buf[2] * 256 + buf[3]; //BigEndian
|
||||
|
||||
var data = new byte[length - 4];
|
||||
len = await stream.ReadExactAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(false);
|
||||
if (len < data.Length)
|
||||
throw new TPKTInvalidException("TPKT payload incomplete / invalid");
|
||||
|
||||
return new TPKT
|
||||
(
|
||||
version: version,
|
||||
reserved1: reserved1,
|
||||
length: length,
|
||||
data: data
|
||||
);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
||||
@@ -14,20 +14,51 @@
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of a bit to 1 (true), given the address of the bit
|
||||
/// Sets the value of a bit to 1 (true), given the address of the bit. Returns
|
||||
/// a copy of the value with the bit set.
|
||||
/// </summary>
|
||||
/// <param name="value">The input value to modify.</param>
|
||||
/// <param name="bit">The index (zero based) of the bit to set.</param>
|
||||
/// <returns>The modified value with the bit at index set.</returns>
|
||||
public static byte SetBit(byte value, int bit)
|
||||
{
|
||||
return (byte)((value | (1 << bit)) & 0xFF);
|
||||
SetBit(ref value, bit);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of a bit to 1 (true), given the address of the bit.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to modify.</param>
|
||||
/// <param name="bit">The index (zero based) of the bit to set.</param>
|
||||
public static void SetBit(ref byte value, int bit)
|
||||
{
|
||||
value = (byte) ((value | (1 << bit)) & 0xFF);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the value of a bit to 0 (false), given the address of the bit. Returns
|
||||
/// a copy of the value with the bit cleared.
|
||||
/// </summary>
|
||||
/// <param name="value">The input value to modify.</param>
|
||||
/// <param name="bit">The index (zero based) of the bit to clear.</param>
|
||||
/// <returns>The modified value with the bit at index cleared.</returns>
|
||||
public static byte ClearBit(byte value, int bit)
|
||||
{
|
||||
ClearBit(ref value, bit);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the value of a bit to 0 (false), given the address of the bit
|
||||
/// </summary>
|
||||
public static byte ClearBit(byte value, int bit)
|
||||
/// <param name="value">The input value to modify.</param>
|
||||
/// <param name="bit">The index (zero based) of the bit to clear.</param>
|
||||
public static void ClearBit(ref byte value, int bit)
|
||||
{
|
||||
return (byte)((value | (~(1 << bit))) & 0xFF);
|
||||
value = (byte) (value & ~(1 << bit) & 0xFF);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,11 @@ namespace S7.Net.Types
|
||||
list.AddRange(items);
|
||||
}
|
||||
|
||||
public void Add(IEnumerable<byte> items)
|
||||
{
|
||||
list.AddRange(items);
|
||||
}
|
||||
|
||||
public void Add(ByteArray byteArray)
|
||||
{
|
||||
list.AddRange(byteArray.Array);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
@@ -25,7 +26,7 @@ namespace S7.Net.Types
|
||||
|
||||
}
|
||||
|
||||
private static double GetIncreasedNumberOfBytes(double numBytes, Type type)
|
||||
private static double GetIncreasedNumberOfBytes(double numBytes, Type type, PropertyInfo? propertyInfo)
|
||||
{
|
||||
switch (type.Name)
|
||||
{
|
||||
@@ -38,27 +39,33 @@ namespace S7.Net.Types
|
||||
break;
|
||||
case "Int16":
|
||||
case "UInt16":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
numBytes += 2;
|
||||
break;
|
||||
case "Int32":
|
||||
case "UInt32":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "Single":
|
||||
case "Double":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "Double":
|
||||
IncrementToEven(ref numBytes);
|
||||
numBytes += 8;
|
||||
break;
|
||||
case "String":
|
||||
S7StringAttribute? attribute = propertyInfo?.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
|
||||
if (attribute == default(S7StringAttribute))
|
||||
throw new ArgumentException("Please add S7StringAttribute to the string field");
|
||||
|
||||
IncrementToEven(ref numBytes);
|
||||
numBytes += attribute.ReservedLengthInBytes;
|
||||
break;
|
||||
default:
|
||||
var propertyClass = Activator.CreateInstance(type);
|
||||
var propertyClass = Activator.CreateInstance(type) ??
|
||||
throw new ArgumentException($"Failed to create instance of type {type}.", nameof(type));
|
||||
numBytes = GetClassSize(propertyClass, numBytes, true);
|
||||
break;
|
||||
}
|
||||
@@ -70,6 +77,8 @@ namespace S7.Net.Types
|
||||
/// Gets the size of the class in bytes.
|
||||
/// </summary>
|
||||
/// <param name="instance">An instance of the class</param>
|
||||
/// <param name="numBytes">The offset of the current field.</param>
|
||||
/// <param name="isInnerProperty"><see langword="true" /> if this property belongs to a class being serialized as member of the class requested for serialization; otherwise, <see langword="false" />.</param>
|
||||
/// <returns>the number of bytes</returns>
|
||||
public static double GetClassSize(object instance, double numBytes = 0.0, bool isInnerProperty = false)
|
||||
{
|
||||
@@ -78,8 +87,10 @@ namespace S7.Net.Types
|
||||
{
|
||||
if (property.PropertyType.IsArray)
|
||||
{
|
||||
Type elementType = property.PropertyType.GetElementType();
|
||||
Array array = (Array)property.GetValue(instance, null);
|
||||
Type elementType = property.PropertyType.GetElementType()!;
|
||||
Array array = (Array?) property.GetValue(instance, null) ??
|
||||
throw new ArgumentException($"Property {property.Name} on {instance} must have a non-null value to get it's size.", nameof(instance));
|
||||
|
||||
if (array.Length <= 0)
|
||||
{
|
||||
throw new Exception("Cannot determine size of class, because an array is defined which has no fixed size greater than zero.");
|
||||
@@ -88,12 +99,12 @@ namespace S7.Net.Types
|
||||
IncrementToEven(ref numBytes);
|
||||
for (int i = 0; i < array.Length; i++)
|
||||
{
|
||||
numBytes = GetIncreasedNumberOfBytes(numBytes, elementType);
|
||||
numBytes = GetIncreasedNumberOfBytes(numBytes, elementType, property);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
numBytes = GetIncreasedNumberOfBytes(numBytes, property.PropertyType);
|
||||
numBytes = GetIncreasedNumberOfBytes(numBytes, property.PropertyType, property);
|
||||
}
|
||||
}
|
||||
if (false == isInnerProperty)
|
||||
@@ -106,9 +117,9 @@ namespace S7.Net.Types
|
||||
return numBytes;
|
||||
}
|
||||
|
||||
private static object GetPropertyValue(Type propertyType, byte[] bytes, ref double numBytes)
|
||||
private static object? GetPropertyValue(Type propertyType, PropertyInfo? propertyInfo, byte[] bytes, ref double numBytes)
|
||||
{
|
||||
object value = null;
|
||||
object? value = null;
|
||||
|
||||
switch (propertyType.Name)
|
||||
{
|
||||
@@ -128,65 +139,37 @@ namespace S7.Net.Types
|
||||
numBytes++;
|
||||
break;
|
||||
case "Int16":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
// hier auswerten
|
||||
ushort source = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]);
|
||||
value = source.ConvertToShort();
|
||||
numBytes += 2;
|
||||
break;
|
||||
case "UInt16":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
// hier auswerten
|
||||
value = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]);
|
||||
numBytes += 2;
|
||||
break;
|
||||
case "Int32":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
// hier auswerten
|
||||
uint sourceUInt = DWord.FromBytes(bytes[(int)numBytes + 3],
|
||||
bytes[(int)numBytes + 2],
|
||||
bytes[(int)numBytes + 1],
|
||||
bytes[(int)numBytes + 0]);
|
||||
IncrementToEven(ref numBytes);
|
||||
var wordBuffer = new byte[4];
|
||||
Array.Copy(bytes, (int)numBytes, wordBuffer, 0, wordBuffer.Length);
|
||||
uint sourceUInt = DWord.FromByteArray(wordBuffer);
|
||||
value = sourceUInt.ConvertToInt();
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "UInt32":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
// hier auswerten
|
||||
value = DWord.FromBytes(
|
||||
bytes[(int)numBytes],
|
||||
bytes[(int)numBytes + 1],
|
||||
bytes[(int)numBytes + 2],
|
||||
bytes[(int)numBytes + 3]);
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "Double":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
// hier auswerten
|
||||
value = Double.FromByteArray(
|
||||
new byte[] {
|
||||
bytes[(int)numBytes],
|
||||
bytes[(int)numBytes + 1],
|
||||
bytes[(int)numBytes + 2],
|
||||
bytes[(int)numBytes + 3] });
|
||||
IncrementToEven(ref numBytes);
|
||||
var wordBuffer2 = new byte[4];
|
||||
Array.Copy(bytes, (int)numBytes, wordBuffer2, 0, wordBuffer2.Length);
|
||||
value = DWord.FromByteArray(wordBuffer2);
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "Single":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
IncrementToEven(ref numBytes);
|
||||
// hier auswerten
|
||||
value = Single.FromByteArray(
|
||||
value = Real.FromByteArray(
|
||||
new byte[] {
|
||||
bytes[(int)numBytes],
|
||||
bytes[(int)numBytes + 1],
|
||||
@@ -194,8 +177,36 @@ namespace S7.Net.Types
|
||||
bytes[(int)numBytes + 3] });
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "Double":
|
||||
IncrementToEven(ref numBytes);
|
||||
var buffer = new byte[8];
|
||||
Array.Copy(bytes, (int)numBytes, buffer, 0, 8);
|
||||
// hier auswerten
|
||||
value = LReal.FromByteArray(buffer);
|
||||
numBytes += 8;
|
||||
break;
|
||||
case "String":
|
||||
S7StringAttribute? attribute = propertyInfo?.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
|
||||
if (attribute == default(S7StringAttribute))
|
||||
throw new ArgumentException("Please add S7StringAttribute to the string field");
|
||||
|
||||
IncrementToEven(ref numBytes);
|
||||
|
||||
// get the value
|
||||
var sData = new byte[attribute.ReservedLengthInBytes];
|
||||
Array.Copy(bytes, (int)numBytes, sData, 0, sData.Length);
|
||||
value = attribute.Type switch
|
||||
{
|
||||
S7StringType.S7String => S7String.FromByteArray(sData),
|
||||
S7StringType.S7WString => S7WString.FromByteArray(sData),
|
||||
_ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute")
|
||||
};
|
||||
numBytes += sData.Length;
|
||||
break;
|
||||
default:
|
||||
var propClass = Activator.CreateInstance(propertyType);
|
||||
var propClass = Activator.CreateInstance(propertyType) ??
|
||||
throw new ArgumentException($"Failed to create instance of type {propertyType}.", nameof(propertyType));
|
||||
|
||||
numBytes = FromBytes(propClass, bytes, numBytes);
|
||||
value = propClass;
|
||||
break;
|
||||
@@ -209,6 +220,8 @@ namespace S7.Net.Types
|
||||
/// </summary>
|
||||
/// <param name="sourceClass">The object to fill in the given array of bytes</param>
|
||||
/// <param name="bytes">The array of bytes</param>
|
||||
/// <param name="numBytes">The offset for the current field.</param>
|
||||
/// <param name="isInnerClass"><see langword="true" /> if this class is the type of a member of the class to be serialized; otherwise, <see langword="false" />.</param>
|
||||
public static double FromBytes(object sourceClass, byte[] bytes, double numBytes = 0, bool isInnerClass = false)
|
||||
{
|
||||
if (bytes == null)
|
||||
@@ -219,13 +232,15 @@ namespace S7.Net.Types
|
||||
{
|
||||
if (property.PropertyType.IsArray)
|
||||
{
|
||||
Array array = (Array)property.GetValue(sourceClass, null);
|
||||
Array array = (Array?) property.GetValue(sourceClass, null) ??
|
||||
throw new ArgumentException($"Property {property.Name} on sourceClass must be an array instance.", nameof(sourceClass));
|
||||
|
||||
IncrementToEven(ref numBytes);
|
||||
Type elementType = property.PropertyType.GetElementType();
|
||||
Type elementType = property.PropertyType.GetElementType()!;
|
||||
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
|
||||
{
|
||||
array.SetValue(
|
||||
GetPropertyValue(elementType, bytes, ref numBytes),
|
||||
GetPropertyValue(elementType, property, bytes, ref numBytes),
|
||||
i);
|
||||
}
|
||||
}
|
||||
@@ -233,7 +248,7 @@ namespace S7.Net.Types
|
||||
{
|
||||
property.SetValue(
|
||||
sourceClass,
|
||||
GetPropertyValue(property.PropertyType, bytes, ref numBytes),
|
||||
GetPropertyValue(property.PropertyType, property, bytes, ref numBytes),
|
||||
null);
|
||||
}
|
||||
}
|
||||
@@ -241,11 +256,11 @@ namespace S7.Net.Types
|
||||
return numBytes;
|
||||
}
|
||||
|
||||
private static double SetBytesFromProperty(object propertyValue, byte[] bytes, double numBytes)
|
||||
private static double SetBytesFromProperty(object propertyValue, PropertyInfo? propertyInfo, byte[] bytes, double numBytes)
|
||||
{
|
||||
int bytePos = 0;
|
||||
int bitPos = 0;
|
||||
byte[] bytes2 = null;
|
||||
byte[]? bytes2 = null;
|
||||
|
||||
switch (propertyValue.GetType().Name)
|
||||
{
|
||||
@@ -277,11 +292,23 @@ namespace S7.Net.Types
|
||||
case "UInt32":
|
||||
bytes2 = DWord.ToByteArray((UInt32)propertyValue);
|
||||
break;
|
||||
case "Double":
|
||||
bytes2 = Double.ToByteArray((double)propertyValue);
|
||||
break;
|
||||
case "Single":
|
||||
bytes2 = Single.ToByteArray((float)propertyValue);
|
||||
bytes2 = Real.ToByteArray((float)propertyValue);
|
||||
break;
|
||||
case "Double":
|
||||
bytes2 = LReal.ToByteArray((double)propertyValue);
|
||||
break;
|
||||
case "String":
|
||||
S7StringAttribute? attribute = propertyInfo?.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
|
||||
if (attribute == default(S7StringAttribute))
|
||||
throw new ArgumentException("Please add S7StringAttribute to the string field");
|
||||
|
||||
bytes2 = attribute.Type switch
|
||||
{
|
||||
S7StringType.S7String => S7String.ToByteArray((string)propertyValue, attribute.ReservedLength),
|
||||
S7StringType.S7WString => S7WString.ToByteArray((string)propertyValue, attribute.ReservedLength),
|
||||
_ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute")
|
||||
};
|
||||
break;
|
||||
default:
|
||||
numBytes = ToBytes(propertyValue, bytes, numBytes);
|
||||
@@ -304,26 +331,30 @@ namespace S7.Net.Types
|
||||
/// <summary>
|
||||
/// Creates a byte array depending on the struct type.
|
||||
/// </summary>
|
||||
/// <param name="sourceClass">The struct object</param>
|
||||
/// <param name="sourceClass">The struct object.</param>
|
||||
/// <param name="bytes">The target byte array.</param>
|
||||
/// <param name="numBytes">The offset for the current field.</param>
|
||||
/// <returns>A byte array or null if fails.</returns>
|
||||
public static double ToBytes(object sourceClass, byte[] bytes, double numBytes = 0.0)
|
||||
{
|
||||
var properties = GetAccessableProperties(sourceClass.GetType());
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var value = property.GetValue(sourceClass, null) ??
|
||||
throw new ArgumentException($"Property {property.Name} on sourceClass can't be null.", nameof(sourceClass));
|
||||
|
||||
if (property.PropertyType.IsArray)
|
||||
{
|
||||
Array array = (Array)property.GetValue(sourceClass, null);
|
||||
Array array = (Array) value;
|
||||
IncrementToEven(ref numBytes);
|
||||
Type elementType = property.PropertyType.GetElementType();
|
||||
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
|
||||
{
|
||||
numBytes = SetBytesFromProperty(array.GetValue(i), bytes, numBytes);
|
||||
numBytes = SetBytesFromProperty(array.GetValue(i)!, property, bytes, numBytes);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
numBytes = SetBytesFromProperty(property.GetValue(sourceClass, null), bytes, numBytes);
|
||||
numBytes = SetBytesFromProperty(value, property, bytes, numBytes);
|
||||
}
|
||||
}
|
||||
return numBytes;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using S7.Net.Protocol.S7;
|
||||
using System;
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
@@ -40,7 +41,7 @@ namespace S7.Net.Types
|
||||
/// <summary>
|
||||
/// Contains the value of the memory area after the read has been executed
|
||||
/// </summary>
|
||||
public object Value { get; set; }
|
||||
public object? Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create an instance of DataItem
|
||||
@@ -83,9 +84,21 @@ namespace S7.Net.Types
|
||||
var dataItem = FromAddress(address);
|
||||
dataItem.Value = value;
|
||||
|
||||
if (typeof(T).IsArray) dataItem.Count = ((Array) dataItem.Value).Length;
|
||||
if (typeof(T).IsArray)
|
||||
{
|
||||
var array = ((Array?)dataItem.Value);
|
||||
if ( array != null)
|
||||
{
|
||||
dataItem.Count = array.Length;
|
||||
}
|
||||
}
|
||||
|
||||
return dataItem;
|
||||
}
|
||||
|
||||
internal static DataItemAddress GetDataItemAddress(DataItem dataItem)
|
||||
{
|
||||
return new DataItemAddress(dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, Plc.VarTypeToByteLength(dataItem.VarType, dataItem.Count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
82
S7.Net/Types/Date.cs
Normal file
82
S7.Net/Types/Date.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using S7.Net.Helper;
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the conversion methods to convert Words from S7 plc to C#.
|
||||
/// </summary>
|
||||
public static class Date
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum allowed date for the IEC date type
|
||||
/// </summary>
|
||||
public static System.DateTime IecMinDate { get; } = new(year: 1990, month: 01, day: 01);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed date for the IEC date type
|
||||
/// <remarks>
|
||||
/// Although the spec allows only a max date of 31-12-2168, the PLC IEC date goes up to 06-06-2169 (which is the actual
|
||||
/// WORD max value - 65535)
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public static System.DateTime IecMaxDate { get; } = new(year: 2169, month: 06, day: 06);
|
||||
|
||||
private static readonly ushort MaxNumberOfDays = (ushort)(IecMaxDate - IecMinDate).TotalDays;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a word (2 bytes) to IEC date (<see cref="System.DateTime"/>)
|
||||
/// </summary>
|
||||
public static System.DateTime FromByteArray(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length != 2)
|
||||
{
|
||||
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 2 bytes.");
|
||||
}
|
||||
|
||||
var daysSinceDateStart = Word.FromByteArray(bytes);
|
||||
if (daysSinceDateStart > MaxNumberOfDays)
|
||||
{
|
||||
throw new ArgumentException($"Read number exceeded the number of maximum days in the IEC date (read: {daysSinceDateStart}, max: {MaxNumberOfDays})",
|
||||
nameof(bytes));
|
||||
}
|
||||
|
||||
return IecMinDate.AddDays(daysSinceDateStart);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="System.DateTime"/> to word (2 bytes)
|
||||
/// </summary>
|
||||
public static byte[] ToByteArray(System.DateTime dateTime) => Word.ToByteArray(dateTime.GetDaysSinceIecDateStart());
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of <see cref="System.DateTime"/>s to an array of bytes
|
||||
/// </summary>
|
||||
public static byte[] ToByteArray(System.DateTime[] value)
|
||||
{
|
||||
var arr = new ByteArray();
|
||||
foreach (var date in value)
|
||||
arr.Add(ToByteArray(date));
|
||||
return arr.Array;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of bytes to an array of <see cref="System.DateTime"/>s
|
||||
/// </summary>
|
||||
public static System.DateTime[] ToArray(byte[] bytes)
|
||||
{
|
||||
var values = new System.DateTime[bytes.Length / sizeof(ushort)];
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = FromByteArray(
|
||||
new[]
|
||||
{
|
||||
bytes[i], bytes[i + 1]
|
||||
});
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,7 @@ namespace S7.Net.Types
|
||||
/// Converts an array of <see cref="T:System.DateTime"/> values to a byte array.
|
||||
/// </summary>
|
||||
/// <param name="dateTimes">The DateTime values to convert.</param>
|
||||
/// <returns>A byte array containing the S7 date time representations of <paramref name="dateTime"/>.</returns>
|
||||
/// <returns>A byte array containing the S7 date time representations of <paramref name="dateTimes"/>.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when any value of
|
||||
/// <paramref name="dateTimes"/> is before <see cref="P:SpecMinimumDateTime"/>
|
||||
/// or after <see cref="P:SpecMaximumDateTime"/>.</exception>
|
||||
|
||||
185
S7.Net/Types/DateTimeLong.cs
Normal file
185
S7.Net/Types/DateTimeLong.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the methods to convert between <see cref="T:System.DateTime" /> and S7 representation of DateTimeLong (DTL) values.
|
||||
/// </summary>
|
||||
public static class DateTimeLong
|
||||
{
|
||||
public const int TypeLengthInBytes = 12;
|
||||
/// <summary>
|
||||
/// The minimum <see cref="T:System.DateTime" /> value supported by the specification.
|
||||
/// </summary>
|
||||
public static readonly System.DateTime SpecMinimumDateTime = new System.DateTime(1970, 1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum <see cref="T:System.DateTime" /> value supported by the specification.
|
||||
/// </summary>
|
||||
public static readonly System.DateTime SpecMaximumDateTime = new System.DateTime(2262, 4, 11, 23, 47, 16, 854);
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="T:System.DateTime" /> value from bytes.
|
||||
/// </summary>
|
||||
/// <param name="bytes">Input bytes read from PLC.</param>
|
||||
/// <returns>A <see cref="T:System.DateTime" /> object representing the value read from PLC.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// Thrown when the length of
|
||||
/// <paramref name="bytes" /> is not 12 or any value in <paramref name="bytes" />
|
||||
/// is outside the valid range of values.
|
||||
/// </exception>
|
||||
public static System.DateTime FromByteArray(byte[] bytes)
|
||||
{
|
||||
return FromByteArrayImpl(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses an array of <see cref="T:System.DateTime" /> values from bytes.
|
||||
/// </summary>
|
||||
/// <param name="bytes">Input bytes read from PLC.</param>
|
||||
/// <returns>An array of <see cref="T:System.DateTime" /> objects representing the values read from PLC.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// Thrown when the length of
|
||||
/// <paramref name="bytes" /> is not a multiple of 12 or any value in
|
||||
/// <paramref name="bytes" /> is outside the valid range of values.
|
||||
/// </exception>
|
||||
public static System.DateTime[] ToArray(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length % TypeLengthInBytes != 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
|
||||
$"Parsing an array of DateTimeLong requires a multiple of 12 bytes of input data, input data is '{bytes.Length}' long.");
|
||||
}
|
||||
|
||||
var cnt = bytes.Length / TypeLengthInBytes;
|
||||
var result = new System.DateTime[cnt];
|
||||
|
||||
for (var i = 0; i < cnt; i++)
|
||||
{
|
||||
var slice = new byte[TypeLengthInBytes];
|
||||
Array.Copy(bytes, i * TypeLengthInBytes, slice, 0, TypeLengthInBytes);
|
||||
result[i] = FromByteArrayImpl(slice);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static System.DateTime FromByteArrayImpl(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length != TypeLengthInBytes)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
|
||||
$"Parsing a DateTimeLong requires exactly 12 bytes of input data, input data is {bytes.Length} bytes long.");
|
||||
}
|
||||
|
||||
|
||||
var year = AssertRangeInclusive(Word.FromBytes(bytes[1], bytes[0]), 1970, 2262, "year");
|
||||
var month = AssertRangeInclusive(bytes[2], 1, 12, "month");
|
||||
var day = AssertRangeInclusive(bytes[3], 1, 31, "day of month");
|
||||
var dayOfWeek = AssertRangeInclusive(bytes[4], 1, 7, "day of week");
|
||||
var hour = AssertRangeInclusive(bytes[5], 0, 23, "hour");
|
||||
var minute = AssertRangeInclusive(bytes[6], 0, 59, "minute");
|
||||
var second = AssertRangeInclusive(bytes[7], 0, 59, "second");
|
||||
;
|
||||
|
||||
var nanoseconds = AssertRangeInclusive<uint>(DWord.FromBytes(bytes[11], bytes[10], bytes[9], bytes[8]), 0,
|
||||
999999999, "nanoseconds");
|
||||
|
||||
var time = new System.DateTime(year, month, day, hour, minute, second);
|
||||
return time.AddTicks(nanoseconds / 100);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="T:System.DateTime" /> value to a byte array.
|
||||
/// </summary>
|
||||
/// <param name="dateTime">The DateTime value to convert.</param>
|
||||
/// <returns>A byte array containing the S7 DateTimeLong representation of <paramref name="dateTime" />.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// Thrown when the value of
|
||||
/// <paramref name="dateTime" /> is before <see cref="P:SpecMinimumDateTime" />
|
||||
/// or after <see cref="P:SpecMaximumDateTime" />.
|
||||
/// </exception>
|
||||
public static byte[] ToByteArray(System.DateTime dateTime)
|
||||
{
|
||||
if (dateTime < SpecMinimumDateTime)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
|
||||
$"Date time '{dateTime}' is before the minimum '{SpecMinimumDateTime}' supported in S7 DateTimeLong representation.");
|
||||
}
|
||||
|
||||
if (dateTime > SpecMaximumDateTime)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
|
||||
$"Date time '{dateTime}' is after the maximum '{SpecMaximumDateTime}' supported in S7 DateTimeLong representation.");
|
||||
}
|
||||
|
||||
var stream = new MemoryStream(TypeLengthInBytes);
|
||||
// Convert Year
|
||||
stream.Write(Word.ToByteArray(Convert.ToUInt16(dateTime.Year)), 0, 2);
|
||||
|
||||
// Convert Month
|
||||
stream.WriteByte(Convert.ToByte(dateTime.Month));
|
||||
|
||||
// Convert Day
|
||||
stream.WriteByte(Convert.ToByte(dateTime.Day));
|
||||
|
||||
// Convert WeekDay. NET DateTime starts with Sunday = 0, while S7DT has Sunday = 1.
|
||||
stream.WriteByte(Convert.ToByte(dateTime.DayOfWeek + 1));
|
||||
|
||||
// Convert Hour
|
||||
stream.WriteByte(Convert.ToByte(dateTime.Hour));
|
||||
|
||||
// Convert Minutes
|
||||
stream.WriteByte(Convert.ToByte(dateTime.Minute));
|
||||
|
||||
// Convert Seconds
|
||||
stream.WriteByte(Convert.ToByte(dateTime.Second));
|
||||
|
||||
// Convert Nanoseconds. Net DateTime has a representation of 1 Tick = 100ns.
|
||||
// Thus First take the ticks Mod 1 Second (1s = 10'000'000 ticks), and then Convert to nanoseconds.
|
||||
stream.Write(DWord.ToByteArray(Convert.ToUInt32(dateTime.Ticks % 10000000 * 100)), 0, 4);
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of <see cref="T:System.DateTime" /> values to a byte array.
|
||||
/// </summary>
|
||||
/// <param name="dateTimes">The DateTime values to convert.</param>
|
||||
/// <returns>A byte array containing the S7 DateTimeLong representations of <paramref name="dateTimes" />.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// Thrown when any value of
|
||||
/// <paramref name="dateTimes" /> is before <see cref="P:SpecMinimumDateTime" />
|
||||
/// or after <see cref="P:SpecMaximumDateTime" />.
|
||||
/// </exception>
|
||||
public static byte[] ToByteArray(System.DateTime[] dateTimes)
|
||||
{
|
||||
var bytes = new List<byte>(dateTimes.Length * TypeLengthInBytes);
|
||||
foreach (var dateTime in dateTimes)
|
||||
{
|
||||
bytes.AddRange(ToByteArray(dateTime));
|
||||
}
|
||||
|
||||
return bytes.ToArray();
|
||||
}
|
||||
|
||||
private static T AssertRangeInclusive<T>(T input, T min, T max, string field) where T : IComparable<T>
|
||||
{
|
||||
if (input.CompareTo(min) < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(input), input,
|
||||
$"Value '{input}' is lower than the minimum '{min}' allowed for {field}.");
|
||||
}
|
||||
|
||||
if (input.CompareTo(max) > 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(input), input,
|
||||
$"Value '{input}' is higher than the maximum '{max}' allowed for {field}.");
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,27 +5,13 @@ namespace S7.Net.Types
|
||||
/// <summary>
|
||||
/// Contains the conversion methods to convert Real from S7 plc to C# double.
|
||||
/// </summary>
|
||||
[Obsolete("Class Double is obsolete. Use Real instead for 32bit floating point, or LReal for 64bit floating point.")]
|
||||
public static class Double
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a S7 Real (4 bytes) to double
|
||||
/// </summary>
|
||||
public static double FromByteArray(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length != 4)
|
||||
{
|
||||
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 4 bytes.");
|
||||
}
|
||||
|
||||
// sps uses bigending so we have to reverse if platform needs
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
// create deep copy of the array and reverse
|
||||
bytes = new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
|
||||
}
|
||||
|
||||
return BitConverter.ToSingle(bytes, 0);
|
||||
}
|
||||
public static double FromByteArray(byte[] bytes) => Real.FromByteArray(bytes);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a S7 DInt to double
|
||||
@@ -51,16 +37,7 @@ namespace S7.Net.Types
|
||||
/// <summary>
|
||||
/// Converts a double to S7 Real (4 bytes)
|
||||
/// </summary>
|
||||
public static byte[] ToByteArray(double value)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes((float)(value));
|
||||
|
||||
// sps uses bigending so we have to check if platform is same
|
||||
if (!BitConverter.IsLittleEndian) return bytes;
|
||||
|
||||
// create deep copy of the array and reverse
|
||||
return new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
|
||||
}
|
||||
public static byte[] ToByteArray(double value) => Real.ToByteArray((float)value);
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of double to an array of bytes
|
||||
|
||||
57
S7.Net/Types/LReal.cs
Normal file
57
S7.Net/Types/LReal.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the conversion methods to convert Real from S7 plc to C# double.
|
||||
/// </summary>
|
||||
public static class LReal
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a S7 LReal (8 bytes) to double
|
||||
/// </summary>
|
||||
public static double FromByteArray(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length != 8)
|
||||
{
|
||||
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 8 bytes.");
|
||||
}
|
||||
var buffer = bytes;
|
||||
|
||||
// sps uses bigending so we have to reverse if platform needs
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
Array.Reverse(buffer);
|
||||
}
|
||||
|
||||
return BitConverter.ToDouble(buffer, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a double to S7 LReal (8 bytes)
|
||||
/// </summary>
|
||||
public static byte[] ToByteArray(double value)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes(value);
|
||||
|
||||
// sps uses bigending so we have to check if platform is same
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
Array.Reverse(bytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of double to an array of bytes
|
||||
/// </summary>
|
||||
public static byte[] ToByteArray(double[] value) => TypeHelper.ToByteArray(value, ToByteArray);
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of S7 LReal to an array of double
|
||||
/// </summary>
|
||||
public static double[] ToArray(byte[] bytes) => TypeHelper.ToArray(bytes, FromByteArray);
|
||||
|
||||
}
|
||||
}
|
||||
75
S7.Net/Types/Real.cs
Normal file
75
S7.Net/Types/Real.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the conversion methods to convert Real from S7 plc to C# double.
|
||||
/// </summary>
|
||||
public static class Real
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a S7 Real (4 bytes) to float
|
||||
/// </summary>
|
||||
public static float FromByteArray(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length != 4)
|
||||
{
|
||||
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 4 bytes.");
|
||||
}
|
||||
|
||||
// sps uses bigending so we have to reverse if platform needs
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
// create deep copy of the array and reverse
|
||||
bytes = new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
|
||||
}
|
||||
|
||||
return BitConverter.ToSingle(bytes, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a float to S7 Real (4 bytes)
|
||||
/// </summary>
|
||||
public static byte[] ToByteArray(float value)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
|
||||
// sps uses bigending so we have to check if platform is same
|
||||
if (!BitConverter.IsLittleEndian) return bytes;
|
||||
|
||||
// create deep copy of the array and reverse
|
||||
return new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of float to an array of bytes
|
||||
/// </summary>
|
||||
public static byte[] ToByteArray(float[] value)
|
||||
{
|
||||
var buffer = new byte[4 * value.Length];
|
||||
var stream = new MemoryStream(buffer);
|
||||
foreach (var val in value)
|
||||
{
|
||||
stream.Write(ToByteArray(val), 0, 4);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of S7 Real to an array of float
|
||||
/// </summary>
|
||||
public static float[] ToArray(byte[] bytes)
|
||||
{
|
||||
var values = new float[bytes.Length / 4];
|
||||
|
||||
int counter = 0;
|
||||
for (int cnt = 0; cnt < bytes.Length / 4; cnt++)
|
||||
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] });
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
80
S7.Net/Types/S7String.cs
Normal file
80
S7.Net/Types/S7String.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the methods to convert from S7 strings to C# strings
|
||||
/// An S7 String has a preceeding 2 byte header containing its capacity and length
|
||||
/// </summary>
|
||||
public static class S7String
|
||||
{
|
||||
private static Encoding stringEncoding = Encoding.ASCII;
|
||||
|
||||
/// <summary>
|
||||
/// The Encoding used when serializing and deserializing S7String (Encoding.ASCII by default)
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentNullException">StringEncoding must not be null</exception>
|
||||
public static Encoding StringEncoding
|
||||
{
|
||||
get => stringEncoding;
|
||||
set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts S7 bytes to a string
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <returns></returns>
|
||||
public static string FromByteArray(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length < 2)
|
||||
{
|
||||
throw new PlcException(ErrorCode.ReadData, "Malformed S7 String / too short");
|
||||
}
|
||||
|
||||
int size = bytes[0];
|
||||
int length = bytes[1];
|
||||
if (length > size)
|
||||
{
|
||||
throw new PlcException(ErrorCode.ReadData, "Malformed S7 String / length larger than capacity");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return StringEncoding.GetString(bytes, 2, length);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new PlcException(ErrorCode.ReadData,
|
||||
$"Failed to parse {VarType.S7String} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="T:string"/> to S7 string with 2-byte header.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to convert to byte array.</param>
|
||||
/// <param name="reservedLength">The length (in characters) allocated in PLC for the string.</param>
|
||||
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 2.</returns>
|
||||
public static byte[] ToByteArray(string? value, int reservedLength)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
if (reservedLength > 254) throw new ArgumentException($"The maximum string length supported is 254.");
|
||||
|
||||
var bytes = StringEncoding.GetBytes(value);
|
||||
if (bytes.Length > reservedLength) throw new ArgumentException($"The provided string length ({bytes.Length} is larger than the specified reserved length ({reservedLength}).");
|
||||
|
||||
var buffer = new byte[2 + reservedLength];
|
||||
Array.Copy(bytes, 0, buffer, 2, bytes.Length);
|
||||
buffer[0] = (byte)reservedLength;
|
||||
buffer[1] = (byte)bytes.Length;
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
S7.Net/Types/S7StringAttribute.cs
Normal file
67
S7.Net/Types/S7StringAttribute.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
|
||||
public sealed class S7StringAttribute : Attribute
|
||||
{
|
||||
private readonly S7StringType type;
|
||||
private readonly int reservedLength;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="S7StringAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The string type.</param>
|
||||
/// <param name="reservedLength">Reserved length of the string in characters.</param>
|
||||
/// <exception cref="ArgumentException">Please use a valid value for the string type</exception>
|
||||
public S7StringAttribute(S7StringType type, int reservedLength)
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(S7StringType), type))
|
||||
throw new ArgumentException("Please use a valid value for the string type");
|
||||
|
||||
this.type = type;
|
||||
this.reservedLength = reservedLength;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the string.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The string type.
|
||||
/// </value>
|
||||
public S7StringType Type => type;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reserved length of the string in characters.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The reserved length of the string in characters.
|
||||
/// </value>
|
||||
public int ReservedLength => reservedLength;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reserved length in bytes.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The reserved length in bytes.
|
||||
/// </value>
|
||||
public int ReservedLengthInBytes => type == S7StringType.S7String ? reservedLength + 2 : (reservedLength * 2) + 4;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// String type.
|
||||
/// </summary>
|
||||
public enum S7StringType
|
||||
{
|
||||
/// <summary>
|
||||
/// ASCII string.
|
||||
/// </summary>
|
||||
S7String = VarType.S7String,
|
||||
|
||||
/// <summary>
|
||||
/// Unicode string.
|
||||
/// </summary>
|
||||
S7WString = VarType.S7WString
|
||||
}
|
||||
}
|
||||
72
S7.Net/Types/S7WString.cs
Normal file
72
S7.Net/Types/S7WString.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the methods to convert from S7 wstrings to C# strings
|
||||
/// An S7 WString has a preceding 4 byte header containing its capacity and length
|
||||
/// </summary>
|
||||
public static class S7WString
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts S7 bytes to a string
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <returns></returns>
|
||||
public static string FromByteArray(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length < 4)
|
||||
{
|
||||
throw new PlcException(ErrorCode.ReadData, "Malformed S7 WString / too short");
|
||||
}
|
||||
|
||||
int size = (bytes[0] << 8) | bytes[1];
|
||||
int length = (bytes[2] << 8) | bytes[3];
|
||||
|
||||
if (length > size)
|
||||
{
|
||||
throw new PlcException(ErrorCode.ReadData, "Malformed S7 WString / length larger than capacity");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Encoding.BigEndianUnicode.GetString(bytes, 4, length * 2);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new PlcException(ErrorCode.ReadData,
|
||||
$"Failed to parse {VarType.S7WString} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.",
|
||||
e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="T:string"/> to S7 wstring with 4-byte header.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to convert to byte array.</param>
|
||||
/// <param name="reservedLength">The length (in characters) allocated in PLC for the string.</param>
|
||||
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 4.</returns>
|
||||
public static byte[] ToByteArray(string? value, int reservedLength)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
if (reservedLength > 16382) throw new ArgumentException("The maximum string length supported is 16382.");
|
||||
|
||||
var buffer = new byte[4 + reservedLength * 2];
|
||||
buffer[0] = (byte)((reservedLength >> 8) & 0xFF);
|
||||
buffer[1] = (byte)(reservedLength & 0xFF);
|
||||
buffer[2] = (byte)((value.Length >> 8) & 0xFF);
|
||||
buffer[3] = (byte)(value.Length & 0xFF);
|
||||
|
||||
var stringLength = Encoding.BigEndianUnicode.GetBytes(value, 0, value.Length, buffer, 4) / 2;
|
||||
if (stringLength > reservedLength) throw new ArgumentException($"The provided string length ({stringLength} is larger than the specified reserved length ({reservedLength}).");
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,27 +5,13 @@ namespace S7.Net.Types
|
||||
/// <summary>
|
||||
/// Contains the conversion methods to convert Real from S7 plc to C# float.
|
||||
/// </summary>
|
||||
[Obsolete("Class Single is obsolete. Use Real instead.")]
|
||||
public static class Single
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a S7 Real (4 bytes) to float
|
||||
/// </summary>
|
||||
public static float FromByteArray(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length != 4)
|
||||
{
|
||||
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 4 bytes.");
|
||||
}
|
||||
|
||||
// sps uses bigending so we have to reverse if platform needs
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
// create deep copy of the array and reverse
|
||||
bytes = new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
|
||||
}
|
||||
|
||||
return BitConverter.ToSingle(bytes, 0);
|
||||
}
|
||||
public static float FromByteArray(byte[] bytes) => Real.FromByteArray(bytes);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a S7 DInt to float
|
||||
@@ -51,16 +37,7 @@ namespace S7.Net.Types
|
||||
/// <summary>
|
||||
/// Converts a double to S7 Real (4 bytes)
|
||||
/// </summary>
|
||||
public static byte[] ToByteArray(float value)
|
||||
{
|
||||
byte[] bytes = BitConverter.GetBytes((float)(value));
|
||||
|
||||
// sps uses bigending so we have to check if platform is same
|
||||
if (!BitConverter.IsLittleEndian) return bytes;
|
||||
|
||||
// create deep copy of the array and reverse
|
||||
return new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
|
||||
}
|
||||
public static byte[] ToByteArray(float value) => Real.ToByteArray(value);
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of float to an array of bytes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the methods to convert from S7 strings to C# strings
|
||||
/// Contains the methods to convert from S7 Array of Chars (like a const char[N] C-String) to C# strings
|
||||
/// </summary>
|
||||
public class String
|
||||
{
|
||||
@@ -12,13 +12,15 @@
|
||||
/// <param name="reservedLength">The amount of bytes reserved for the <paramref name="value"/> in the PLC.</param>
|
||||
public static byte[] ToByteArray(string value, int reservedLength)
|
||||
{
|
||||
var length = value?.Length;
|
||||
if (length > reservedLength) length = reservedLength;
|
||||
var bytes = new byte[reservedLength];
|
||||
if (value == null) return bytes;
|
||||
|
||||
if (length == null || length == 0) return bytes;
|
||||
var length = value.Length;
|
||||
if (length == 0) return bytes;
|
||||
|
||||
System.Text.Encoding.ASCII.GetBytes(value, 0, length.Value, bytes, 0);
|
||||
if (length > reservedLength) length = reservedLength;
|
||||
|
||||
System.Text.Encoding.ASCII.GetBytes(value, 0, length, bytes, 0);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@@ -1,60 +1,15 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the methods to convert from S7 strings to C# strings
|
||||
/// there are two kinds how strings a send. This one is with a pre of two bytes
|
||||
/// they contain the length of the string
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="S7String"/>
|
||||
[Obsolete("Please use S7String class")]
|
||||
public static class StringEx
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts S7 bytes to a string
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <returns></returns>
|
||||
public static string FromByteArray(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length < 2) return "";
|
||||
/// <inheritdoc cref="S7String.FromByteArray(byte[])"/>
|
||||
public static string FromByteArray(byte[] bytes) => S7String.FromByteArray(bytes);
|
||||
|
||||
int size = bytes[0];
|
||||
int length = bytes[1];
|
||||
|
||||
try
|
||||
{
|
||||
return Encoding.ASCII.GetString(bytes, 2, length);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new PlcException(ErrorCode.ReadData,
|
||||
$"Failed to parse {VarType.StringEx} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.",
|
||||
e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="T:string"/> to S7 string with 2-byte header.
|
||||
/// </summary>
|
||||
/// <param name="value">The string to convert to byte array.</param>
|
||||
/// <param name="reservedLength">The length (in bytes) allocated in PLC for string excluding header.</param>
|
||||
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 2.</returns>
|
||||
public static byte[] ToByteArray(string value, int reservedLength)
|
||||
{
|
||||
if (reservedLength > byte.MaxValue) throw new ArgumentException($"The maximum string length supported is {byte.MaxValue}.");
|
||||
|
||||
var length = value?.Length;
|
||||
if (length > reservedLength) length = reservedLength;
|
||||
|
||||
var bytes = new byte[(length ?? 0) + 2];
|
||||
bytes[0] = (byte) reservedLength;
|
||||
|
||||
if (value == null) return bytes;
|
||||
|
||||
bytes[1] = (byte) Encoding.ASCII.GetBytes(value, 0, length.Value, bytes, 2);
|
||||
return bytes;
|
||||
}
|
||||
/// <inheritdoc cref="S7String.ToByteArray(string, int)"/>
|
||||
public static byte[] ToByteArray(string value, int reservedLength) => S7String.ToByteArray(value, reservedLength);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace S7.Net.Types
|
||||
@@ -18,11 +19,11 @@ namespace S7.Net.Types
|
||||
double numBytes = 0.0;
|
||||
|
||||
var infos = structType
|
||||
#if NETSTANDARD1_3
|
||||
#if NETSTANDARD1_3
|
||||
.GetTypeInfo().DeclaredFields;
|
||||
#else
|
||||
#else
|
||||
.GetFields();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
@@ -44,18 +45,34 @@ namespace S7.Net.Types
|
||||
break;
|
||||
case "Int32":
|
||||
case "UInt32":
|
||||
case "TimeSpan":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "Single":
|
||||
case "Double":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "Double":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
numBytes += 8;
|
||||
break;
|
||||
case "String":
|
||||
S7StringAttribute? attribute = info.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
|
||||
if (attribute == default(S7StringAttribute))
|
||||
throw new ArgumentException("Please add S7StringAttribute to the string field");
|
||||
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
numBytes += attribute.ReservedLengthInBytes;
|
||||
break;
|
||||
default:
|
||||
numBytes += GetStructSize(info.FieldType);
|
||||
break;
|
||||
@@ -70,7 +87,7 @@ namespace S7.Net.Types
|
||||
/// <param name="structType">The struct type</param>
|
||||
/// <param name="bytes">The array of bytes</param>
|
||||
/// <returns>The object depending on the struct type or null if fails(array-length != struct-length</returns>
|
||||
public static object FromBytes(Type structType, byte[] bytes)
|
||||
public static object? FromBytes(Type structType, byte[] bytes)
|
||||
{
|
||||
if (bytes == null)
|
||||
return null;
|
||||
@@ -82,15 +99,15 @@ namespace S7.Net.Types
|
||||
int bytePos = 0;
|
||||
int bitPos = 0;
|
||||
double numBytes = 0.0;
|
||||
object structValue = Activator.CreateInstance(structType);
|
||||
|
||||
object structValue = Activator.CreateInstance(structType) ??
|
||||
throw new ArgumentException($"Failed to create an instance of the type {structType}.", nameof(structType));
|
||||
|
||||
var infos = structValue.GetType()
|
||||
#if NETSTANDARD1_3
|
||||
#if NETSTANDARD1_3
|
||||
.GetTypeInfo().DeclaredFields;
|
||||
#else
|
||||
#else
|
||||
.GetFields();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
@@ -115,7 +132,7 @@ namespace S7.Net.Types
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
// hier auswerten
|
||||
// get the value
|
||||
ushort source = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]);
|
||||
info.SetValue(structValue, source.ConvertToShort());
|
||||
numBytes += 2;
|
||||
@@ -124,7 +141,7 @@ namespace S7.Net.Types
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
// hier auswerten
|
||||
// get the value
|
||||
info.SetValue(structValue, Word.FromBytes(bytes[(int)numBytes + 1],
|
||||
bytes[(int)numBytes]));
|
||||
numBytes += 2;
|
||||
@@ -133,7 +150,7 @@ namespace S7.Net.Types
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
// hier auswerten
|
||||
// get the value
|
||||
uint sourceUInt = DWord.FromBytes(bytes[(int)numBytes + 3],
|
||||
bytes[(int)numBytes + 2],
|
||||
bytes[(int)numBytes + 1],
|
||||
@@ -145,33 +162,73 @@ namespace S7.Net.Types
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
// hier auswerten
|
||||
// get the value
|
||||
info.SetValue(structValue, DWord.FromBytes(bytes[(int)numBytes],
|
||||
bytes[(int)numBytes + 1],
|
||||
bytes[(int)numBytes + 2],
|
||||
bytes[(int)numBytes + 3]));
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "Double":
|
||||
case "Single":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
// hier auswerten
|
||||
info.SetValue(structValue, Double.FromByteArray(new byte[] { bytes[(int)numBytes],
|
||||
// get the value
|
||||
info.SetValue(structValue, Real.FromByteArray(new byte[] { bytes[(int)numBytes],
|
||||
bytes[(int)numBytes + 1],
|
||||
bytes[(int)numBytes + 2],
|
||||
bytes[(int)numBytes + 3] }));
|
||||
numBytes += 4;
|
||||
break;
|
||||
case "Single":
|
||||
case "Double":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
// hier auswerten
|
||||
info.SetValue(structValue, Single.FromByteArray(new byte[] { bytes[(int)numBytes],
|
||||
bytes[(int)numBytes + 1],
|
||||
bytes[(int)numBytes + 2],
|
||||
bytes[(int)numBytes + 3] }));
|
||||
// get the value
|
||||
var data = new byte[8];
|
||||
Array.Copy(bytes, (int)numBytes, data, 0, 8);
|
||||
info.SetValue(structValue, LReal.FromByteArray(data));
|
||||
numBytes += 8;
|
||||
break;
|
||||
case "String":
|
||||
S7StringAttribute? attribute = info.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
|
||||
if (attribute == default(S7StringAttribute))
|
||||
throw new ArgumentException("Please add S7StringAttribute to the string field");
|
||||
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
|
||||
// get the value
|
||||
var sData = new byte[attribute.ReservedLengthInBytes];
|
||||
Array.Copy(bytes, (int)numBytes, sData, 0, sData.Length);
|
||||
switch (attribute.Type)
|
||||
{
|
||||
case S7StringType.S7String:
|
||||
info.SetValue(structValue, S7String.FromByteArray(sData));
|
||||
break;
|
||||
case S7StringType.S7WString:
|
||||
info.SetValue(structValue, S7WString.FromByteArray(sData));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Please use a valid string type for the S7StringAttribute");
|
||||
}
|
||||
|
||||
numBytes += sData.Length;
|
||||
break;
|
||||
case "TimeSpan":
|
||||
numBytes = Math.Ceiling(numBytes);
|
||||
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
|
||||
numBytes++;
|
||||
|
||||
// get the value
|
||||
info.SetValue(structValue, TimeSpan.FromByteArray(new[]
|
||||
{
|
||||
bytes[(int)numBytes + 0],
|
||||
bytes[(int)numBytes + 1],
|
||||
bytes[(int)numBytes + 2],
|
||||
bytes[(int)numBytes + 3]
|
||||
}));
|
||||
numBytes += 4;
|
||||
break;
|
||||
default:
|
||||
@@ -198,21 +255,29 @@ namespace S7.Net.Types
|
||||
|
||||
int size = Struct.GetStructSize(type);
|
||||
byte[] bytes = new byte[size];
|
||||
byte[] bytes2 = null;
|
||||
byte[]? bytes2 = null;
|
||||
|
||||
int bytePos = 0;
|
||||
int bitPos = 0;
|
||||
double numBytes = 0.0;
|
||||
|
||||
var infos = type
|
||||
#if NETSTANDARD1_3
|
||||
#if NETSTANDARD1_3
|
||||
.GetTypeInfo().DeclaredFields;
|
||||
#else
|
||||
#else
|
||||
.GetFields();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
static TValue GetValueOrThrow<TValue>(FieldInfo fi, object structValue) where TValue : struct
|
||||
{
|
||||
var value = fi.GetValue(structValue) as TValue? ??
|
||||
throw new ArgumentException($"Failed to convert value of field {fi.Name} of {structValue} to type {typeof(TValue)}");
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
bytes2 = null;
|
||||
switch (info.FieldType.Name)
|
||||
{
|
||||
@@ -220,7 +285,7 @@ namespace S7.Net.Types
|
||||
// get the value
|
||||
bytePos = (int)Math.Floor(numBytes);
|
||||
bitPos = (int)((numBytes - (double)bytePos) / 0.125);
|
||||
if ((bool)info.GetValue(structValue))
|
||||
if (GetValueOrThrow<bool>(info, structValue))
|
||||
bytes[bytePos] |= (byte)Math.Pow(2, bitPos); // is true
|
||||
else
|
||||
bytes[bytePos] &= (byte)(~(byte)Math.Pow(2, bitPos)); // is false
|
||||
@@ -229,26 +294,41 @@ namespace S7.Net.Types
|
||||
case "Byte":
|
||||
numBytes = (int)Math.Ceiling(numBytes);
|
||||
bytePos = (int)numBytes;
|
||||
bytes[bytePos] = (byte)info.GetValue(structValue);
|
||||
bytes[bytePos] = GetValueOrThrow<byte>(info, structValue);
|
||||
numBytes++;
|
||||
break;
|
||||
case "Int16":
|
||||
bytes2 = Int.ToByteArray((Int16)info.GetValue(structValue));
|
||||
bytes2 = Int.ToByteArray(GetValueOrThrow<short>(info, structValue));
|
||||
break;
|
||||
case "UInt16":
|
||||
bytes2 = Word.ToByteArray((UInt16)info.GetValue(structValue));
|
||||
bytes2 = Word.ToByteArray(GetValueOrThrow<ushort>(info, structValue));
|
||||
break;
|
||||
case "Int32":
|
||||
bytes2 = DInt.ToByteArray((Int32)info.GetValue(structValue));
|
||||
bytes2 = DInt.ToByteArray(GetValueOrThrow<int>(info, structValue));
|
||||
break;
|
||||
case "UInt32":
|
||||
bytes2 = DWord.ToByteArray((UInt32)info.GetValue(structValue));
|
||||
break;
|
||||
case "Double":
|
||||
bytes2 = Double.ToByteArray((double)info.GetValue(structValue));
|
||||
bytes2 = DWord.ToByteArray(GetValueOrThrow<uint>(info, structValue));
|
||||
break;
|
||||
case "Single":
|
||||
bytes2 = Single.ToByteArray((float)info.GetValue(structValue));
|
||||
bytes2 = Real.ToByteArray(GetValueOrThrow<float>(info, structValue));
|
||||
break;
|
||||
case "Double":
|
||||
bytes2 = LReal.ToByteArray(GetValueOrThrow<double>(info, structValue));
|
||||
break;
|
||||
case "String":
|
||||
S7StringAttribute? attribute = info.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
|
||||
if (attribute == default(S7StringAttribute))
|
||||
throw new ArgumentException("Please add S7StringAttribute to the string field");
|
||||
|
||||
bytes2 = attribute.Type switch
|
||||
{
|
||||
S7StringType.S7String => S7String.ToByteArray((string?)info.GetValue(structValue), attribute.ReservedLength),
|
||||
S7StringType.S7WString => S7WString.ToByteArray((string?)info.GetValue(structValue), attribute.ReservedLength),
|
||||
_ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute")
|
||||
};
|
||||
break;
|
||||
case "TimeSpan":
|
||||
bytes2 = TimeSpan.ToByteArray((System.TimeSpan)info.GetValue(structValue));
|
||||
break;
|
||||
}
|
||||
if (bytes2 != null)
|
||||
@@ -265,7 +345,5 @@ namespace S7.Net.Types
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
97
S7.Net/Types/TimeSpan.cs
Normal file
97
S7.Net/Types/TimeSpan.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the methods to convert between <see cref="T:System.TimeSpan"/> and S7 representation of TIME values.
|
||||
/// </summary>
|
||||
public static class TimeSpan
|
||||
{
|
||||
/// <summary>
|
||||
/// The minimum <see cref="T:System.TimeSpan"/> value supported by the specification.
|
||||
/// </summary>
|
||||
public static readonly System.TimeSpan SpecMinimumTimeSpan = System.TimeSpan.FromMilliseconds(int.MinValue);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum <see cref="T:System.TimeSpan"/> value supported by the specification.
|
||||
/// </summary>
|
||||
public static readonly System.TimeSpan SpecMaximumTimeSpan = System.TimeSpan.FromMilliseconds(int.MaxValue);
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="T:System.TimeSpan"/> value from bytes.
|
||||
/// </summary>
|
||||
/// <param name="bytes">Input bytes read from PLC.</param>
|
||||
/// <returns>A <see cref="T:System.TimeSpan"/> object representing the value read from PLC.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the length of
|
||||
/// <paramref name="bytes"/> is not 4 or any value in <paramref name="bytes"/>
|
||||
/// is outside the valid range of values.</exception>
|
||||
public static System.TimeSpan FromByteArray(byte[] bytes)
|
||||
{
|
||||
var milliseconds = DInt.FromByteArray(bytes);
|
||||
return System.TimeSpan.FromMilliseconds(milliseconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses an array of <see cref="T:System.TimeSpan"/> values from bytes.
|
||||
/// </summary>
|
||||
/// <param name="bytes">Input bytes read from PLC.</param>
|
||||
/// <returns>An array of <see cref="T:System.TimeSpan"/> objects representing the values read from PLC.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the length of
|
||||
/// <paramref name="bytes"/> is not a multiple of 4 or any value in
|
||||
/// <paramref name="bytes"/> is outside the valid range of values.</exception>
|
||||
public static System.TimeSpan[] ToArray(byte[] bytes)
|
||||
{
|
||||
const int singleTimeSpanLength = 4;
|
||||
|
||||
if (bytes.Length % singleTimeSpanLength != 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
|
||||
$"Parsing an array of {nameof(System.TimeSpan)} requires a multiple of {singleTimeSpanLength} bytes of input data, input data is '{bytes.Length}' long.");
|
||||
|
||||
var result = new System.TimeSpan[bytes.Length / singleTimeSpanLength];
|
||||
|
||||
var milliseconds = DInt.ToArray(bytes);
|
||||
for (var i = 0; i < milliseconds.Length; i++)
|
||||
result[i] = System.TimeSpan.FromMilliseconds(milliseconds[i]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="T:System.TimeSpan"/> value to a byte array.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">The TimeSpan value to convert.</param>
|
||||
/// <returns>A byte array containing the S7 date time representation of <paramref name="timeSpan"/>.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the value of
|
||||
/// <paramref name="timeSpan"/> is before <see cref="P:SpecMinimumTimeSpan"/>
|
||||
/// or after <see cref="P:SpecMaximumTimeSpan"/>.</exception>
|
||||
public static byte[] ToByteArray(System.TimeSpan timeSpan)
|
||||
{
|
||||
if (timeSpan < SpecMinimumTimeSpan)
|
||||
throw new ArgumentOutOfRangeException(nameof(timeSpan), timeSpan,
|
||||
$"Time span '{timeSpan}' is before the minimum '{SpecMinimumTimeSpan}' supported in S7 time representation.");
|
||||
|
||||
if (timeSpan > SpecMaximumTimeSpan)
|
||||
throw new ArgumentOutOfRangeException(nameof(timeSpan), timeSpan,
|
||||
$"Time span '{timeSpan}' is after the maximum '{SpecMaximumTimeSpan}' supported in S7 time representation.");
|
||||
|
||||
return DInt.ToByteArray(Convert.ToInt32(timeSpan.TotalMilliseconds));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of <see cref="T:System.TimeSpan"/> values to a byte array.
|
||||
/// </summary>
|
||||
/// <param name="timeSpans">The TimeSpan values to convert.</param>
|
||||
/// <returns>A byte array containing the S7 date time representations of <paramref name="timeSpans"/>.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when any value of
|
||||
/// <paramref name="timeSpans"/> is before <see cref="P:SpecMinimumTimeSpan"/>
|
||||
/// or after <see cref="P:SpecMaximumTimeSpan"/>.</exception>
|
||||
public static byte[] ToByteArray(System.TimeSpan[] timeSpans)
|
||||
{
|
||||
var bytes = new List<byte>(timeSpans.Length * 4);
|
||||
foreach (var timeSpan in timeSpans) bytes.AddRange(ToByteArray(timeSpan));
|
||||
|
||||
return bytes.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
43
S7.Net/Types/TypeHelper.cs
Normal file
43
S7.Net/Types/TypeHelper.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace S7.Net.Types
|
||||
{
|
||||
internal static class TypeHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an array of T to an array of bytes
|
||||
/// </summary>
|
||||
public static byte[] ToByteArray<T>(T[] value, Func<T, byte[]> converter) where T : struct
|
||||
{
|
||||
var buffer = new byte[Marshal.SizeOf(default(T)) * value.Length];
|
||||
var stream = new MemoryStream(buffer);
|
||||
foreach (var val in value)
|
||||
{
|
||||
stream.Write(converter(val), 0, 4);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an array of T repesented as S7 binary data to an array of T
|
||||
/// </summary>
|
||||
public static T[] ToArray<T>(byte[] bytes, Func<byte[], T> converter) where T : struct
|
||||
{
|
||||
var typeSize = Marshal.SizeOf(default(T));
|
||||
var entries = bytes.Length / typeSize;
|
||||
var values = new T[entries];
|
||||
|
||||
for(int i = 0; i < entries; ++i)
|
||||
{
|
||||
var buffer = new byte[typeSize];
|
||||
Array.Copy(bytes, i * typeSize, buffer, 0, typeSize);
|
||||
values[i] = converter(buffer);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
S7.sln
8
S7.sln
@@ -1,16 +1,18 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27703.2026
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29806.167
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "S7.Net", "S7.Net\S7.Net.csproj", "{BFD484F9-3F04-42A2-BF2A-60A189A25DCF}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7A8252C3-E6AE-435A-809D-4413C06E0711}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
appveyor.yml = appveyor.yml
|
||||
README.md = README.md
|
||||
.github\workflows\test.yml = .github\workflows\test.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S7.Net.UnitTest", "S7.Net.UnitTest\S7.Net.UnitTest.csproj", "{303CCED6-9ABC-4899-A509-743341AAA804}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "S7.Net.UnitTest", "S7.Net.UnitTest\S7.Net.UnitTest.csproj", "{303CCED6-9ABC-4899-A509-743341AAA804}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
||||
13
appveyor.yml
13
appveyor.yml
@@ -1,13 +0,0 @@
|
||||
image: Visual Studio 2017
|
||||
configuration: Release
|
||||
install:
|
||||
- choco install gitversion.portable -y
|
||||
before_build:
|
||||
- cmd: gitversion /l console /output buildserver
|
||||
- nuget restore
|
||||
build_script:
|
||||
msbuild /nologo /v:m /p:AssemblyVersion=%GitVersion_AssemblySemVer% /p:FileVersion=%GitVersion_MajorMinorPatch% /p:InformationalVersion=%GitVersion_InformationalVersion% /p:Configuration=%CONFIGURATION% S7.sln
|
||||
after_build:
|
||||
- dotnet pack S7.Net -c %CONFIGURATION% /p:Version=%GitVersion_NuGetVersion% --no-build -o ..\artifacts
|
||||
artifacts:
|
||||
- path: artifacts\*.*
|
||||
Reference in New Issue
Block a user