454 Commits
v0.1.7 ... main

Author SHA1 Message Date
Michael Croes
ab6308eacd Merge pull request #506 from bonk-dev/s5-date
Add support for reading/writing the legacy DATE (IEC date) datatype
2023-09-07 21:26:25 +02:00
Michael Croes
130eeadbd8 Merge branch 'main' into s5-date 2023-09-07 21:20:54 +02:00
Michael Croes
76a7ea04f7 Merge pull request #507 from mycroes/clock
Add support for reading and writing PLC clock
2023-09-05 21:25:25 +02:00
Michael Croes
4764c997ed refactor: Replace DateTime.Length with Plc.DateTimeLength in Plc.Clock 2023-09-04 22:20:31 +02:00
Michael Croes
cb24e9a046 feat: Implement clock write support 2023-09-04 22:15:52 +02:00
Michael Croes
10315b4b4c style: Add missing space in WriteClockAsync summary 2023-09-04 21:55:34 +02:00
Michael Croes
0774e124bf test: Fix comments in write clock messages 2023-09-04 21:55:06 +02:00
Michael Croes
1ebffe08e7 test: Add Write_Clock_Value test 2023-08-31 19:55:57 +02:00
Michael Croes
f419df4d73 feat: Stub PLC WriteClock methods 2023-08-31 19:55:41 +02:00
Michael Croes
1969aac1b2 refactor: Rename WriteSzlRequestHeader to WriteUserDataRequest 2023-08-31 19:36:00 +02:00
Michael Croes
2f2dcf7281 test: Raise ReadClock timeout to 1 second 2023-08-29 21:38:01 +02:00
Michael Croes
07325db2fa feat: Implement clock reading 2023-08-29 21:34:09 +02:00
Michael Croes
eada47cd24 refactor(PLCHelpers): Extract WriteSzlRequestHeader 2023-08-29 20:41:55 +02:00
Michael Croes
5e1ac8c7bf test(ReadClock): Use template parameters for PDU bytes 2023-08-29 19:42:59 +02:00
Michael Croes
13544a1bcf test: Add ReadClock test 2023-08-23 23:03:45 +02:00
Michael Croes
6fc526b886 Stub PLC ReadClock methods 2023-08-23 22:40:57 +02:00
bonk-dev
f227ad4b53 Expose IecMinDate and IecMaxDate as properties 2023-08-23 12:03:22 +02:00
Dawid Pągowski
e4cc42fa51 Add support for serializing IEC date 2023-08-23 01:04:23 +02:00
Dawid Pągowski
689e7ffd96 Increase the maximum date
The spec goes up to a full 2168 year, but the PLC date type goes up to 2169 June 06 which is represented by 65535 (max ushort value).
2023-08-23 00:53:29 +02:00
Dawid Pągowski
8087b8d315 Change the thrown exceptions to ArgumentOutOfRangeException 2023-08-23 00:43:17 +02:00
Dawid Pągowski
a55ceba679 Add IEC Date VarType support to PLCHelpers.cs 2023-08-23 00:38:24 +02:00
Dawid Pągowski
eb1fad9333 Add IEC date DataType 2023-08-23 00:27:55 +02:00
Michael Croes
0de9364dee Merge pull request #504 from mycroes/remove-appveyor-config
Remove leftover appveyor.yml
2023-08-22 15:49:38 +02:00
Michael Croes
9380ea85c3 chore: Remove leftover appveyor.yml 2023-08-21 21:23:46 +02:00
Michael Croes
22451bc440 Merge pull request #501 from Himmelt/add_set_bit
Add SetBit method to modify one bit of a byte
2023-08-18 20:48:32 +02:00
Himmelt
e98ce005c5 update param description 2023-08-18 16:50:53 +08:00
Himmelt
11a40cc5e3 update summary 2023-08-18 16:37:50 +08:00
Himmelt
f79286b2d0 Apply suggestions from code review
Co-authored-by: Michael Croes <mycroes@gmail.com>
2023-08-18 15:58:35 +08:00
Himmelt
fadd7d0cb3 update params descriptions and change param name from "bitPosition" to "index" 2023-08-17 13:33:07 +08:00
Himmelt
652ff3a9bb Update S7.Net/Conversion.cs
Co-authored-by: Günther Foidl <gue@korporal.at>
2023-08-16 17:19:53 +08:00
Himmelt
9c0fea721a Update S7.Net/Conversion.cs
Co-authored-by: Günther Foidl <gue@korporal.at>
2023-08-16 17:18:33 +08:00
Himmelt
2ec73224c1 Add SetBit method to modify one bit of a byte 2023-08-15 18:08:10 +08:00
Michael Croes
a8ef47b475 Merge pull request #490 from bonk-dev/timespan
Added support for serializing TimeSpan
2023-08-04 08:55:08 +02:00
Michael Croes
55aa06a1fc Merge branch 'main' into timespan 2023-08-03 21:55:20 +02:00
Michael Croes
7e631a713f Merge pull request #498 from S7NetPlus/github-actions-logger
chore: Update GitHubActionsTestLogger
2023-08-03 21:52:03 +02:00
Michael Croes
0797c5858f chore: Update GitHubActionsTestLogger 2023-08-03 21:45:46 +02:00
Michael Croes
f1ae0ea084 Merge pull request #491 from S7NetPlus/plc-status
Plc status
2023-08-02 19:36:58 +02:00
Michael Croes
addf6068bb style(ReadStatusAsync): Move opening brace to new line 2023-08-01 22:56:08 +02:00
Michael Croes
970e9d4395 feat: Add sync version of ReadStatus 2023-08-01 22:55:19 +02:00
Michael Croes
c3934c3493 fix(ReadStatusAsync): Fix index of status in response message 2023-08-01 22:52:44 +02:00
Michael Croes
e5823f2806 doc(ReadStatusAsync): Add missing cancellationToken documentation 2023-08-01 22:52:10 +02:00
Michael Croes
97e27ccc2b chore(ReadStatusAsync): Make cancellationToken optional 2023-08-01 22:51:47 +02:00
Michael Croes
9b1faa0123 test: Add test for reading PLC status 2023-08-01 22:50:50 +02:00
Michael Croes
54dadec75a test: Extract connection open templates 2023-08-01 22:50:21 +02:00
Michael Croes
8b8ad13464 test: Add ConnectionOpen communication test 2023-07-31 23:58:15 +02:00
Michael Croes
714ac62ab1 test: Add CommunicationSequence 2023-07-31 23:57:38 +02:00
Michael Croes
088cd0a4a8 Merge branch 'main' into plc-status 2023-07-29 23:12:22 +02:00
Michael Croes
361db8be9d Merge pull request #494 from S7NetPlus/warnings
Cleanup of warnings
2023-07-29 23:07:23 +02:00
Michael Croes
e26860b0c0 build: Extend NoWarn
- Amend existing NoWarn if set
- Ignore out of support target framework warning
2023-07-28 23:57:55 +02:00
Michael Croes
6e103cea63 fix: Fix warnings in Struct 2023-07-28 23:55:12 +02:00
Michael Croes
c5023c10e4 style: Cleanup line endings in S7String 2023-07-28 23:54:15 +02:00
Michael Croes
b61ac32913 fix: Permit nulls in string ToByteArray conversions 2023-07-28 23:52:57 +02:00
Michael Croes
b27e1c9083 build: Set LangVersion to latest 2023-07-28 23:51:09 +02:00
Michael Croes
71f7f8b400 fix: Fix nullability warning in String.ToByteArray 2023-07-27 00:16:40 +02:00
Michael Croes
4aca9e4e53 fix: Fix remaining nullability warnings in Class 2023-07-27 00:11:18 +02:00
Michael Croes
0bb7c5351a ci: Update actions to Node 16 compatible versions 2023-07-26 23:59:21 +02:00
Michael Croes
c3f86c32a2 fix: Fix nullability warnings in Class.FromBytes 2023-07-26 23:52:28 +02:00
Michael Croes
3d0dd693ba fix: Fix nullability warnings in Class.ToBytes 2023-07-26 23:47:32 +02:00
Michael Croes
8ad25033d5 chore: Fix xmldoc warnings 2023-07-26 23:38:32 +02:00
Michael Croes
12e180ea2d build: Don't warn on missing xmldoc
While definitely desirable, at least temporarily disabled in order to
find other warnings.
2023-07-26 23:14:38 +02:00
Michael Croes
5891a30c5d Merge branch 'main' into plc-status 2023-07-25 23:25:54 +02:00
Michael Croes
b3077b27e7 Merge pull request #493 from S7NetPlus/testing
GitHub actions test improvements
2023-07-25 23:24:43 +02:00
Michael Croes
8126018afd test: Fix target framework*s* specification 2023-07-25 23:17:25 +02:00
Michael Croes
4e4071f07f test: Only target net462 on Windows 2023-07-24 22:04:18 +02:00
Michael Croes
534d9fd69d fix: Remove leftover test-framework in runner name 2023-07-24 21:41:32 +02:00
Michael Croes
8da292ad2f ci: Run tests against all target frameworks on all OS-es 2023-07-24 21:32:49 +02:00
Michael Croes
019aeb26dc Merge branch 'main' into plc-status
# Conflicts:
#	S7.Net.UnitTest/S7.Net.UnitTest.csproj
2023-07-23 23:34:03 +02:00
Michael Croes
670fb70b78 Merge pull request #492 from S7NetPlus/testing
Fix GH actions test runs
2023-07-23 23:29:46 +02:00
Michael Croes
aa15145184 fix: Install dotnet 7.x always 2023-07-23 23:20:18 +02:00
Michael Croes
12ea402769 fix: Remove separate restore step 2023-07-23 23:10:21 +02:00
Michael Croes
18402604d1 feat: Add net462, net6.0 and net7.0 targeting to S7NetPlus
This should be the actual baseline, which is also what the test project
targets now.
2023-07-23 23:04:03 +02:00
Michael Croes
53f651a482 fix: Constrain dotnet restore to matrix runtime 2023-07-23 22:58:53 +02:00
Michael Croes
7558b9a691 fix: Retarget test project to net462, net6.0 and net7.0
These are the frameworks currently used in the GitHub workflow, when
missing the tests aren't executed and the job will succeed nonetheless.
2023-07-23 22:53:11 +02:00
Michael Croes
3185d1fccf fix: Revert Ubuntu target back to 20.04 for snap7 ppa availability 2023-07-23 22:48:52 +02:00
Michael Croes
0d9ccea11b feat: Add Plc.ReadStatusAsync 2023-07-22 22:53:45 +02:00
Michael Croes
1fc6899905 feat: Add WriteSzlReadRequest 2023-07-21 22:28:22 +02:00
Michael Croes
18c3883dc0 feat: Add WriteUserDataHeader 2023-07-21 22:27:30 +02:00
Michael Croes
1f26833244 fix: Add missing xmldoc nodes in PLCHelpers 2023-07-21 21:26:13 +02:00
Michael Croes
7d212134e3 refactor: Rename BuildHeaderPackage to WriteReadHeader 2023-07-21 21:23:00 +02:00
Michael Croes
38b26e0ce1 fix: Update test project target frameworks
Ensures tests are actually run on GitHub
2023-07-20 21:39:58 +02:00
Michael Croes
cf94f8ad11 fix(PLCHelpers): Fix errors from refactors 2023-07-20 21:25:24 +02:00
Michael Croes
8becc562a8 refactor: Cleanup inline math in BuildHeaderPackage
- Remove unnecessary parentheses
- Use constant value first in multiplication
2023-07-19 23:32:00 +02:00
Michael Croes
296ead69c7 refactor: Use Word.ToByteArray in WriteTpktHeader 2023-07-19 23:30:02 +02:00
Michael Croes
ebf3da6280 refactor: Extract WriteS7Header 2023-07-19 23:29:11 +02:00
Michael Croes
42194aa788 refactor: Extract WriteDataHeader 2023-07-19 23:19:32 +02:00
Michael Croes
9c8b453326 refactor: Extract WriteTpktHeader 2023-07-19 23:18:26 +02:00
Dawid Pągowski
49e4d3369a Add TimeSpan serialization to Struct 2023-07-19 21:12:54 +02:00
Dawid Pągowski
ee06bec0fb Fix the documentation 2023-07-19 21:04:07 +02:00
Dawid Pągowski
05ccb05f3a Added TimeSpan tests 2023-07-19 20:57:14 +02:00
Dawid Pągowski
0d2817661e Add S7 Time type (C# TimeSpan)
Adds the S7 TIME (IEC) type (32 bits long)
It is deserialized to C# TimeSpan and serialized as S7 DInt.
2023-07-19 20:30:32 +02:00
Michael Croes
e869d19587 Merge pull request #487 from mycroes/ci
Update ci workflow
2023-06-27 17:08:34 +02:00
Michael Croes
e7194bc470 Update OS, SDK and target versions 2023-06-27 17:00:25 +02:00
Michael Croes
5bc2c6c5e7 Add create_nuget and deploy jobs 2023-06-27 16:49:19 +02:00
Michael Croes
77dcb1778b Merge pull request #485 from dylandrush/484_Public_PLCExceptions
Made all exceptions public
2023-06-19 14:52:02 +02:00
DRUSH12
14053e342a Made all exceptions public 2023-06-19 06:14:14 -04:00
Michael Croes
ab3bd87701 chore: Delete GitVersion configuration
Rely on builtin defaults from now on.
2023-05-30 22:21:12 +02:00
Michael Croes
bc7c27e1d4 Release S7NetPlus 0.18.0
Release highlights:
- Add Memory/Span support from 0.17.0 to < net5 targets
2023-05-30 21:47:40 +02:00
Michael Croes
f0256fd0cb Merge pull request #483 from gfoidl/span-memory
Use System.Memory for < .NET 5 and avoid (some) unnecessary allocations
2023-05-30 21:44:33 +02:00
Günther Foidl
209148ab02 Use System.Memory for < .NET 5 and avoid (some) unnecessary allocations 2023-05-30 17:25:25 +02:00
Michael Croes
2fc9eaade3 Release S7NetPlus 0.17.0
Release highlights:
- Add Read-/WriteBytes overloads for Span<byte> and Memory<byte>
2023-05-30 12:19:39 +02:00
Michael Croes
ab70bfb041 Merge pull request #482 from ArgusMagnus/add_span_overloads
add Read/WriteBytes(Async) overloads accepting Span<byte>/Memory<byte> for .NET5 or greater
2023-05-30 12:14:19 +02:00
ArgusMagnus
e277cf6e6c add Read/WriteBytes(Async) overloads accepting Span<byte>/Memory<byte> for .NET5 or greater 2023-05-30 10:47:38 +02:00
Michael Croes
43b29825a4 Release S7NetPlus 0.16.0
Release highlights:
- Fix UInt32 reads in classes
- Add string support for classes
2022-12-10 21:22:16 +01:00
Michael Croes
6aa0133081 Merge pull request #415 from scamille/fb-fixClassUint32
Fix ReadClass for UInt32
2022-12-10 21:12:28 +01:00
Michael Croes
868e719b78 Merge remote-tracking branch 's7netplus/develop' into fb-fixClassUint32
# Conflicts:
#	S7.Net/Types/Class.cs
2022-12-10 21:09:29 +01:00
Michael Croes
3833d29c0e Merge pull request #459 from mycroes/test-workflow
ci: Run test on ubuntu-20.04 due to lack of snap7 on newer ubuntu
2022-12-10 21:01:21 +01:00
Michael Croes
144f814794 Merge branch 'develop' into test-workflow 2022-12-10 20:58:29 +01:00
Michael Croes
82aaf7e2cb Merge pull request #458 from MCPC10/StringSupportForClass
Added support for string/wstring in a class
2022-12-10 20:58:10 +01:00
Michael Croes
f47918946d ci: Run test on ubuntu-20.04 due to lack of snap7 on newer ubuntu 2022-12-10 20:51:44 +01:00
Mike Cremer
142f1ba90e Added support string/wstring for class type 2022-12-10 18:59:25 +01:00
Michael Croes
d99d0d0e6f Release S7NetPlus 0.15.0
Release highlights:
- Add flowed cancellation to ConnectAsync
2022-11-08 11:22:47 +01:00
Michael Croes
ce9f9f9e08 Merge pull request #423 from gfoidl/openasync_cancellation
Flowed cancellation token to TcpClient.ConnectAsync in .NET 5.0 target
2022-11-08 11:19:37 +01:00
Michael Croes
ea1140314b Merge branch 'develop' into openasync_cancellation 2022-11-08 11:13:25 +01:00
Michael Croes
e3fad0b94f Release S7NetPlus 0.14.0
Release highlights:
- Support setting the Encoding for S7String
2022-06-17 23:14:48 +02:00
Michael Croes
7f76d4fc5a Merge pull request #435 from ismdiego/develop
Allow changing the default Encoding used in S7String
2022-06-17 23:12:43 +02:00
diego
ec554ddb59 Better stringEncoding initialization, as per code review comments 2022-06-17 11:23:58 +02:00
diego
2ecd2c6b49 Changes following code review 2022-06-17 11:08:09 +02:00
diego
d808ea5eb6 Give the option of changing the Encoding used to serialize and deserialize S7String, instead of always using Encoding.ASCII 2022-06-16 17:21:23 +02:00
Günther Foidl
5d3f01e59e Updated AppVeyor image to VS 2022 to allow better conditional compilation 2021-12-26 19:26:44 +01:00
Günther Foidl
9c3f95ce73 Flowed cancellation token to TcpClient.ConnectAsync in .NET 5.0 target 2021-12-26 19:26:04 +01:00
Serge Camille
12281ec802 Fix ReadClass for Uint32
Use consistent DWord conversion for both Int32 and UInt32. Unfortunately there is no Span or even a FromByteArray function accepting a offset, so just use the same Array.Copy falls used for double.
2021-10-04 18:59:05 +02:00
Serge Camille
8df1a9c8cb Add unit test for ReadClass uint32 bug.
https://github.com/S7NetPlus/s7netplus/issues/414
2021-10-04 18:53:13 +02:00
Michael Croes
946536c2d6 Release S7NetPlus 0.13.0
Release highlights:
- Change default TSAP for S7 200
- Add S7 200 Smart support
- Add support for custom TSAP's
- Align data to even bytes when parsing responses from class and struct
  reads
- Close connection on IOException
- Add cancellation for Read/Write
- Set default Read-/WriteTimeout to 10 seconds
- Cleanup of sync helper methods
2021-06-21 21:52:25 +02:00
Michael Croes
b475aee2e7 Merge pull request #401 from scamille/test-ConnectionClose
Add unit test to ensure a connection has been properly closed
2021-06-10 17:04:42 +02:00
Serge Camille
82a745c972 Merge branch 'develop' into test-ConnectionClose 2021-06-08 22:53:30 +02:00
Serge Camille
74fecad48d Remove cancel timeouts. 2021-06-08 22:47:12 +02:00
Michael Croes
37c63cea8e Merge pull request #402 from mycroes/default-timeout
Default ReadTimeout and WriteTimeout to 10 seconds
2021-06-07 22:23:57 +02:00
Michael Croes
36c4564f5e Default ReadTimeout and WriteTimeout to 10 seconds 2021-06-07 22:19:23 +02:00
Michael Croes
0c0200d12d Merge pull request #400 from scamille/tests-taskCanceled
Fix Cancellation test.
2021-06-07 22:03:02 +02:00
Michael Croes
6e861d2f00 Merge branch 'develop' into tests-taskCanceled 2021-06-07 22:00:52 +02:00
Michael Croes
372f4a5dcb Merge pull request #398 from mycroes/cleanup-sync
Cleanup sync
2021-06-07 21:26:41 +02:00
Serge Camille
0d87dbf3c6 Add unit test to ensure a connection has been properly closed
If cancelled during data transmission, #349 adds code to ensure that the connection automatically gets cancelled.

Add inerted test to ensure that cancellation before sending data does not result in a cancellation.
2021-06-07 14:49:06 +02:00
Serge Camille
15f94cd7bf Fix Cancellation test. 2021-06-07 14:13:04 +02:00
Michael Croes
36b59a2926 Cleanup whitespace, usings 2021-06-06 23:20:19 +02:00
Michael Croes
8d081859da Remove unused sync methods 2021-06-06 23:19:56 +02:00
Michael Croes
2302819650 Merge pull request #349 from scamille/closeConnectionIOException
Close connection on IOExceptions.
2021-06-06 22:58:27 +02:00
Michael Croes
d65b83660f Merge branch 'develop' into closeConnectionIOException 2021-06-06 22:56:17 +02:00
Michael Croes
77b2ecfd45 Merge pull request #397 from scamille/multipleVarsAlignment
Fix ReadMultipleVars alignment bug
2021-06-06 22:51:03 +02:00
Michael Croes
b0a6a2375f Merge branch 'develop' into multipleVarsAlignment 2021-06-06 22:49:33 +02:00
Michael Croes
14823bca96 Merge pull request #395 from mycroes/s7-200-cr
Change S7 200 remote TSAP to 10.01
2021-06-06 22:45:53 +02:00
Serge Camille
53045c5952 Close connection on IOExceptions.
The idea behind this is that if we receive a response which does not conform to the underlying protocol specification (with all the TCP checks in place!), we are in a weird/corrupt state and it is better to just close the connection.
2021-06-06 22:40:04 +02:00
Michael Croes
677d2941e1 Apply S7 200 TSAP change to relocated code 2021-06-06 22:33:12 +02:00
Michael Croes
66b693676c Merge branch 'develop' into s7-200-cr 2021-06-06 22:32:41 +02:00
Michael Croes
4542bbedb2 Merge pull request #396 from mycroes/tsap
Refactor TSAP related code
2021-06-06 22:24:29 +02:00
Serge Camille
a4b6a360fe ParseDataIntoDataItems needs to always align the response address to a even value after each data item. 2021-06-06 22:16:59 +02:00
Serge Camille
0bef6bc9ff Add unit test for the problem. 2021-06-06 21:59:39 +02:00
Michael Croes
cbe04fbfb4 Merge branch 'develop' into tsap 2021-06-06 21:19:17 +02:00
Michael Croes
0b4c79cb08 Merge pull request #319 from scamille/githubActions2
GitHub actions
2021-06-06 21:14:26 +02:00
Serge Camille
5c24e801fd Add Actions Nuget Cache (#2)
* Try out action cache

* fix syntax error

* Now lets see what happens if it hits the cache
2021-06-05 18:50:28 +02:00
Serge Camille
bcde65120c Github Actions Net5.0 (#1)
* Try integrating dotnet setup version into build matrix.

* Another attempt

* change matrix.

* don't add dotnet-sdk as main matrix variable

* remove test framework as well.

* or maybe not

* Fix copy paste mixup
2021-06-05 18:37:35 +02:00
Serge Camille
4541a7ebb7 Try adjusting the dotnet setup version. 2021-06-05 18:11:06 +02:00
Serge Camille
fc9c33fdaf Add Net5.0 to test project as well. 2021-06-05 17:59:18 +02:00
Serge Camille
a23408d67e Add Net5.0 to test matrix 2021-06-05 17:58:05 +02:00
Serge Camille
adb55dc80f Merge branch 'develop' into githubActions2 2021-06-05 17:52:59 +02:00
Michael Croes
2fae2c01d5 Add TsapPair support to PLC
Add support for custom addressing by supplying a TsapPair to the PLC.
CPU, Rack and Slot properties are still present to preserve backwards
compatibility and to support alternate functionality based on the PLC
type.
2021-06-04 22:25:05 +02:00
Michael Croes
6465e3c8c7 Move GetDefaultTsapPair to TsapPair class 2021-06-04 22:01:40 +02:00
Michael Croes
fcd61c1236 Add CpuType.S7200Smart 2021-06-04 21:46:42 +02:00
Michael Croes
616dc1094c Refactor TSAP related code 2021-06-04 21:45:51 +02:00
Michael Croes
70bc1499ef ConnectionRequestTest: Cleanup trailing whitespace 2021-06-02 23:03:23 +02:00
Michael Croes
fd9aeb5b3b Fix unit test for S7 200 2021-06-02 23:03:06 +02:00
Michael Croes
0750ee006f Change S7 200 remote TSAP to 10.01 2021-06-02 22:06:00 +02:00
Michael Croes
5318f94dd7 Release S7NetPlus 0.12.0
Release highlights:
- Add synchronization to methods interacting with the PLC
2021-06-02 21:28:48 +02:00
Michael Croes
ea3beff481 Merge pull request #394 from mycroes/sync
Add synchronization
2021-06-02 21:25:51 +02:00
Michael Croes
f67b1e773f Add missing ConfigureAwait in OpenAsync 2021-06-01 20:59:20 +02:00
Michael Croes
e93a656312 Fix locking for OpenAsync 2021-06-01 20:28:34 +02:00
Michael Croes
8035f71a16 Apply synchronization to stream actions 2021-05-31 22:57:13 +02:00
Michael Croes
df4f258290 Move GetStreamIfAvailable to PLC.cs 2021-05-31 21:23:32 +02:00
Michael Croes
5636b93a53 PlcSynchronous: Remove unused usings 2021-05-31 21:17:42 +02:00
Michael Croes
8ed1b840bc PlcSynchronous: Clenaup trailing whitespace 2021-05-31 21:17:23 +02:00
Michael Croes
2afed88231 Consolidate sync stream calls 2021-05-31 21:17:03 +02:00
Michael Croes
1ded47971b Consolidate async stream calls 2021-05-31 21:07:16 +02:00
Michael Croes
ced10b4eca Release S7NetPlus 0.11.0
Release highlights:
- Fix for byte length calculation of bits
- Fix for Boolean.ClearBit
- Added Boolean.ClearBit and .SetBit by reference
2021-05-10 22:55:28 +02:00
Michael Croes
632e1c14ac Merge pull request #391 from mycroes/set-bit-clear-bit
SetBit and ClearBit tests, fixes
2021-05-10 21:24:43 +02:00
Michael Croes
aa50280233 Boolean: Add SetBit and ClearBit by reference 2021-05-10 21:14:43 +02:00
Michael Croes
3a794e8a46 Cleanup trailing whitespace in PLC and PLCHelpers 2021-05-10 21:05:02 +02:00
Michael Croes
0b8bd66bf7 Fix bitwise operator in Boolean.ClearBit
Fixes #249
2021-05-10 20:50:46 +02:00
Michael Croes
e66d21af05 Add tests for Boolean.SetBit and Boolean.ClearBit 2021-05-10 20:49:15 +02:00
Michael Croes
44ee651ac4 Merge pull request #390 from mycroes/byte-length-bits
Fix byte length calculation for VarType.Bit
2021-05-10 20:32:34 +02:00
Michael Croes
a1b4694ef6 Fix calculation of byte length for VarType.Bit
Fixes #389
2021-05-10 20:25:38 +02:00
Michael Croes
d10c15b80f Release S7NetPlus 0.10.0
Release highlights:
- Additional ConfigureAwait(false) calls used internally
- Fix for S7WString length
- Remove IsAvailable property
2021-03-29 22:01:14 +02:00
Michael Croes
5225c8bffd Merge pull request #311 from mycroes/is-available
Remove IsAvailable, document IsConnected
2021-03-29 21:51:34 +02:00
Michael Croes
aa03400350 Document IsConnected, shorten implementation 2021-03-29 21:43:24 +02:00
Michael Croes
2b4ec6d9dd Remove IsAvailable
Addresses #275, #252, #208, #154.
2021-03-29 21:43:24 +02:00
Michael Croes
54f3de6c9f Merge pull request #382 from mycroes/s7wstring-length
Fix problems with S7 string types (S7String and S7WString) and calculated byte length
2021-03-29 21:12:18 +02:00
diego
e6d14587d3 Added four new test methods to check ByteLength for S7 String types 2021-03-29 20:51:11 +02:00
diego
e63d92c61c Fix ByteLength calculation for S7Strings when reserved is odd 2021-03-29 20:51:11 +02:00
diego
b4b94e1777 Fix wrong calculation for S7WString byte length 2021-03-29 20:51:11 +02:00
Michael Croes
13c25fc20b Merge pull request #379 from scamille/fix-sync-class
Sprinkle more .ConfigureAwait(false) into async library.
2021-03-29 20:30:43 +02:00
Serge Camille
fff6f3458f Merge branch 'develop' into githubActions2 2021-03-16 20:22:49 +01:00
Serge Camille
82e29837a2 Sprinkle more .ConfigureAwait(false) into async library.
This helps to avoid deadlocks when using synchronous API which references the async one in applications with a synchronization context, like a GUI.
Should fix #344
2021-03-16 08:08:39 +01:00
Michael Croes
1afb07774b Release S7NetPlus 0.9.0
Release highlights:
- Added WString support
- Added support for strings in structs
- Set type for 'Z' and 'C' to counter instead of timer
2021-03-13 13:38:38 +01:00
Michael Croes
051091919f Merge pull request #365 from MCPC10/StructStringSupport
Added support for string types in a struct
2021-03-13 13:31:51 +01:00
Michael Croes
478c1aed52 Merge branch 'develop' into StructStringSupport 2021-03-13 13:30:31 +01:00
Mike Cremer
924eb9c48f Minor changes + removed default length in S7StringAttribute 2021-03-13 12:00:32 +01:00
Michael Croes
eb8e188c86 Merge pull request #373 from scamille/patch-1
Fix PLC parse for type "Z"/"C"
2021-03-12 20:46:17 +01:00
Serge Camille
37384d2a92 Fix PLC parse for type "Z"/"C" 2021-03-12 08:17:37 +01:00
Mike Cremer
fdd4519f64 Minor changes 2021-01-29 21:13:38 +01:00
Michael Croes
52c60f6eaf Merge pull request #363 from MCPC10/WString
Added WString support + fixed max size in S7String
2021-01-27 18:55:12 +01:00
Mike Cremer
926d74f1d2 Fixed length check and corresponding message for S7String 2021-01-27 17:55:16 +01:00
Mike Cremer
9b89acfb91 Removed unnecessary bitwise and's 2021-01-25 21:42:08 +01:00
Mike Cremer
de0a9e64dc Added support for strings in Struct type class 2021-01-24 13:56:07 +01:00
Mike Cremer
dfcc4c7408 Added WString support 2021-01-24 12:16:32 +01:00
Michael Croes
c9a98fba95 Release S7NetPlus 0.8.1
Release highlights:
- Include documentation
2021-01-05 22:18:36 +01:00
Michael Croes
40edecad43 Merge pull request #356 from mycroes/enable-documentation
Generate documentation file
2021-01-05 22:14:26 +01:00
Michael Croes
0b04c86cb9 Generate documentation file 2021-01-05 22:07:11 +01:00
Michael Croes
f03ba93a96 Release S7NetPlus 0.8.0
Release highlights:
- Replace Double type with LReal
- Fixes for PDU size assertions
- Don't embed sources
- Fixes for StringEx
- Rename StringEx to S7String
- Possible null reference fixes
- Fixes for compiler warnings
- Preparations for async method cancellation
- Extended response code validation
- Structural changes, minor improvements
2021-01-04 20:44:16 +01:00
Michael Croes
9cd63d906f Merge pull request #338 from scamille/fixOpen
Fix OpenAsync function to rethrow exception during connection establish.
2020-10-17 20:04:57 +02:00
Serge Camille
d051b93bdc Fix OpenAsync function to rethrow exception during connection establish.
This bug was introduced in 106e9912
2020-10-17 17:54:30 +02:00
Serge Camille
ae70f31af2 Remove deploy workflow. 2020-10-17 11:47:34 +02:00
Serge Camille
fb44b56c16 Skip welcome message 2020-10-17 11:47:34 +02:00
Serge Camille
ce97fcf335 Separate restore and test steps to better see execution times.
Restore could be cached, but it looks quite complicated to pull off.
2020-10-17 11:47:34 +02:00
Serge Camille
81208c0f03 Add github actions for multi-platform unit tests. 2020-10-17 11:47:34 +02:00
Michael Croes
c9bab7523a Merge pull request #317 from scamille/read-request-header
Unify async read request header building.
2020-10-14 21:14:17 +02:00
Serge Camille
a7608e3cb7 Rename and Move DataRequestItem 2020-09-26 21:28:31 +02:00
Serge Camille
4d37679c75 Move BuildReadRequestPackage to PlcHelpers, cleanup. 2020-09-26 21:21:37 +02:00
Serge Camille
e2a0ed548d Unify async read request header building.
Add concept of DataRequestItem which contains the necessary data to build the request.
2020-09-26 21:21:37 +02:00
Michael Croes
4124bae1bc Merge pull request #314 from scamille/cleanupOpen
Cleanup OpenAsync function.
2020-09-21 23:09:07 +02:00
Michael Croes
2bcc5e6b9c Merge branch 'develop' into cleanupOpen 2020-09-21 23:06:06 +02:00
Serge Camille
33981ab4f9 Use int for MaxPduSize and parse communication response for it as uint16 2020-09-16 23:09:23 +02:00
Serge Camille
de60a7b6b0 Add .ConfigureAwait(false) throughout OpenAsync callstack. 2020-09-16 22:38:05 +02:00
Michael Croes
de87409458 Merge pull request #321 from FalcoGoodbody/patch-1
Remove outdated documentation link.
2020-09-16 22:32:55 +02:00
Serge Camille
ca89736c7c Use GetAwaiter().GetResult(); 2020-09-16 22:31:51 +02:00
Serge Camille
4a72c3596b Revert MaxPDUSize parse change. 2020-09-16 22:28:42 +02:00
Michael Croes
786e012179 Merge branch 'develop' into patch-1 2020-09-16 22:07:15 +02:00
Michael Croes
f46833606f Merge pull request #316 from scamille/responseValidation
Add more extensive response code validation.
2020-09-16 20:53:46 +02:00
Serge Camille
b95b71e9aa Remove unnecessary string interpolation in ValidateResponseCode 2020-09-16 20:26:53 +02:00
Serge Camille
1069641606 Add more extensive response code validation.
Fixes #310
2020-09-16 20:25:20 +02:00
Michael Croes
36a9ecb2c8 Merge pull request #315 from scamille/stringSerialization
Rename StringEx to S7String.
2020-09-16 20:15:11 +02:00
Michael Croes
243e868488 Merge branch 'develop' into stringSerialization 2020-09-16 20:13:19 +02:00
Michael Croes
ff65f06b7d Merge pull request #318 from scamille/netcoretests
Linux timeout fix & Multi-Platform Unit Test support
2020-09-16 20:12:48 +02:00
FalcoGoodbody
065b1fbdf8 Update README.md
delete the link to PDF documentation because it is outdated. The actual docu can be found in the wiki
2020-09-15 09:19:51 +02:00
Serge Camille
8f3c701a2f Revert UnitTest IP address to 127.0.0.1.
Seems there are sometimes performance regressions when using localhost. Might be related to IPv6, who know.

Fix some TestContext not being public.
2020-09-13 10:28:06 +02:00
Serge Camille
023530322e Remove Port 102 dependencies from Unit Test.
- Adjust readme.
- It is no longer be necessary to shut down service s7oiehsx64.
2020-09-13 10:15:53 +02:00
Serge Camille
9198fc1686 Test project: Enable netcore3.1 testing, switch win64 Snap7 and use custom port.
- Add netcoreapp3.1 target framework, allowing this to run on linux and macos as well.
- Switch windows snap7 DLL to 64bit version 1.4.2. This also improves UnitTest stability (reduces false positives) on the CI (including appveyor)
- Changing the port used for S7NetTests when communicating with Snap7 to a value > 1000 allows tests to run on Linux without elevated privileges.
2020-09-13 10:01:38 +02:00
Serge Camille
12a2e3c0b1 Fix runtime error on linux with default Socket timeouts.
The default value for socket timeout API is 0 and not -1, so revert to that value to give the intended infinite timeout on all platforms.
2020-09-13 09:57:13 +02:00
Serge Camille
b088fe276b Rename StringEx to S7String.
This name is already somewhat used in code and gives this a better name.
2020-09-12 19:27:15 +02:00
Serge Camille
80ad95372b Use Stream.Dispose() for NetStandard 1.3 2020-09-12 11:19:08 +02:00
Serge Camille
106e9912ab Cleanup OpenAsync function.
- Separate into Connect and EstablishConnection step.
- Remove redundant null checks for returned data.
-  Only assing PLC stream object once we fully established a connection, and Close otherwise.
- Replace sync implementation with Async call.
2020-09-12 11:14:41 +02:00
Michael Croes
af39659944 Merge pull request #307 from scamille/fb-asyncCancellationTokens
Add CancellationToken support to async functions.
2020-09-10 23:50:41 +02:00
Serge Camille
e5bdb10ce3 Add cancellation UnitTest.
This is very fragile with time-dependent cancellation, but I don't know of a better way without messing with the code to be tested.

Seems to only work when the test is run as a single run, not when running the whole test suit.
2020-09-09 21:24:39 +02:00
Serge Camille
faea428e4c Try not to replace OperationCanceledException with PlcException. 2020-09-09 21:24:39 +02:00
Serge Camille
4da76c7f6d Add missing cancellationToken to Stream.WriteAsync call. 2020-09-09 21:24:39 +02:00
Serge Camille
6c4d4605f0 Add CancellationToken support to async functions. 2020-09-09 21:24:36 +02:00
Michael Croes
730ccbf9fc Merge pull request #308 from scamille/fb-fixWarnings
Fix some null deref warnings.
2020-09-09 21:03:25 +02:00
Michael Croes
b36c4a98ec Merge branch 'develop' into fb-fixWarnings 2020-09-09 21:01:54 +02:00
Michael Croes
70c3f8e996 Merge pull request #309 from scamille/fb-varHints
Apply some of the hints that the compiler is giving.
2020-09-09 20:42:33 +02:00
Michael Croes
cf1b71220a Merge branch 'develop' into fb-varHints 2020-09-09 20:40:09 +02:00
Michael Croes
88c45bd995 Merge pull request #292 from scamille/fb-StringEx
Adjust StringEx ToByteArray.
2020-09-08 21:59:28 +02:00
Serge Camille
cf493d47f0 Apply some of the hints that the compiler is giving. 2020-09-06 15:04:20 +02:00
Serge Camille
9e2f17fdf3 Fix some null deref warnings. 2020-09-06 15:00:08 +02:00
Serge Camille
1919b0083a StringEx more strict malformed checks when parsing byte array.
- min length of 2.
- capacity >= length
2020-09-04 22:33:54 +02:00
Serge Camille
fd4bc0fe84 Adjust StringEx ToByteArray.
- Do not allow null string to be passed, raise ArgumentNullException.
- Do not allow string whose ASCII representation is longer than the reserved length, since this currently leads to silent data loss.
- Always write the full binary data length of 2 + reservedLength, since that is what the binary representation of that string is in S7 memory, even if some tail bytes are unused by the current string.

I also suspect that S7WriteMultiple would have chocked on that last bit, but I am not sure. There aren't any tests for writing multiple Dataitems right now.

Adjust tests accordingly. Mostly add some tail bytes where necessary, and assert on exceptions where this is now required.
2020-09-04 22:33:54 +02:00
Michael Croes
9ff73ff3f7 Merge pull request #303 from mycroes/remove-embed-sources
Remove EmbedAllSources from S7.Net
2020-09-04 22:10:52 +02:00
Michael Croes
c31353bed2 Remove EmbedAllSources from S7.Net
Sources are already provided via SourceLink, no need to embed them as
well.
2020-09-04 22:06:58 +02:00
Michael Croes
b16097092b Merge pull request #290 from scamille/fb-AdjustDataItemsPduCheck
Adjust AssertPduSize checks for Reading multiple DataItems.
2020-09-04 21:39:55 +02:00
Serge Camille
eca2ed6474 Add required request & response sizes to PDU limit exception messages. 2020-09-04 21:06:06 +02:00
Serge Camille
b92242f911 Adjust AssertPduSize checks for Reading multiple DataItems.
The limit calculations did not match what the send and parsing code expected.

sending request header seems to be 19 byte in general.

Also adjust XML comments somewhat, since max PDU really differs a lot between PLC types, from 240 to 960 afaik.
2020-09-04 21:06:06 +02:00
Michael Croes
c99c3d745a Merge pull request #298 from scamille/fb-real
Add LReal support, kill old "Double" sham.
2020-09-04 21:03:00 +02:00
Michael Croes
3b2dbd1148 Release S7NetPlus 0.7.0
Release highlights:
- Nullable reference type support
- Cleanups of TSDU and TPDU message parsing
- Better exceptions for connection Open() errors
- Support for buffered reads
2020-09-04 20:51:29 +02:00
Serge Camille
64c781ec8b Remove unnecessary includes and commented-out test. 2020-09-04 20:46:16 +02:00
Serge Camille
e8a9983367 Add LReal support, kill old "Double" sham.
- Adds true support for 64bit double / LReal datatype.
- Set old Types.Single and Types.Double to obselete. Both class names use .NET types instead of S7 type names, contrary to all other types.
- Remove already obsoleted conversion from DWord to Real. Why is this even necessary?
  For users caring about converting from DWord, they can still convert to single. But unless we get LWord support, there won't be a direct conversion to double/LReal.
- Adjust unit tests by removing rounding, testing directly double read/writes.

There is quite a bit of breaking changes at least in the automated Struct and object functions which automatically translate .NET types to appropriate S7 types.
My consideration was that if we ever want to support 64bit types, there is no way against breaking those existing incorrect conversions from 64bit .NET double to 32 bit S7 Real variables.
2020-09-04 20:46:16 +02:00
Michael Croes
3c91aa02b0 Merge pull request #291 from scamille/fb-readFixed
Fixed size reads version 2
2020-09-03 23:46:42 +02:00
Serge Camille
edfa208c3e Simplify TPKT Read buffer length arguments and improve exception message. 2020-09-03 20:48:18 +02:00
Serge Camille
d11f46eedb Rename ReadFixed to ReadExact, separate Unit test for ReadExact. 2020-09-03 20:48:18 +02:00
Serge Camille
09c8b18d3d Adjust ReadFixed implementation somewhat. Exceeding the length of the buffer was already an error before.
Change the tests by replacing the memory buffer with a Fake stream giving 1 byte at a time.
2020-09-03 20:48:18 +02:00
Jakob Ledermann
10e5562706 Read desired amount from stream. see #273
The read method on a networkstream blocks only until the first byte is available.
It is not guaranteed to read the desired count into the buffer.

This solution tries to read the remaining bytes in a loop and aborts once the
full count is read or the Stream.Read method returns with 0 or less bytes read.

The synchronous read can block indefinitly if the lenght field is larger than the send package.
2020-09-03 20:48:17 +02:00
Jakob Ledermann
7d570f93c1 Unittests for issue #283
These unittests fail intentionally as the issue discussion has not reached
a consesual solution. Any solution should pass the unittests added in this commit.
2020-09-03 20:48:17 +02:00
Michael Croes
c79ae13ea1 Merge pull request #285 from scamille/fb-var
Various binary data adjustments, small code refactoring
2020-09-03 20:18:57 +02:00
Serge Camille
09851ec30b Optimize ReadBytes by copying less data around. 2020-09-02 20:17:09 +02:00
Serge Camille
592d21c3aa Add some response length checks in connection Open()
I don't know what the correct expected connection response size is, so I just added checks for the minimal index access by the current code.

This change will just change NullReferenceExceptions into WrongNumberOfBytesException when the PLC response with not enough data for a connection attempt.
2020-09-02 20:16:53 +02:00
Serge Camille
d530f1e422 COTP TPDU: change byte array copy.
This removes the binary reader, and fixes too things:

1. Properly set the data length (previous implementation requested too much, but that did not matter with BinaryReader)
2. Start reading Data after HeaderLength+1 offset, not always at 3.
2020-09-02 20:16:53 +02:00
Serge Camille
ba3dd084cb TSDU: Return data early without copying when there is only 1 segment. 2020-09-02 20:16:53 +02:00
Serge Camille
a047c5bba4 Fix chunk size for WriteBytesAsync.
The previous problem of not being able to use a larger chunk size than 200 was that the complete header building code for the async implementation was incorrect.  Specifically, it wrote the package size only as a byte instead of a int16, thus restricting the package size to something < 256.

With the sharing of the Header building code in the previous commits, this problem was resolved by accident, and thus the chunk size can be increased to the maximum value allowed by the PDUSize.
2020-09-02 20:16:53 +02:00
Serge Camille
2bb7ac7d2a Write large byte array in S7NetTests to check for MaxPDU problems on Snap7.
For me the tests work fine even when adjust the "chunk size" in WriteBytesAsync. So Snap7 seems to be fine, at least the current version.

This should probably be tested with some live PLC's as well.
2020-09-02 20:16:53 +02:00
Serge Camille
783c456dc9 Copy ReadBytes data in one go.
Instead of using a loop, use Array.Copy to
2020-09-02 20:16:52 +02:00
Serge Camille
688d4e2a28 Change implementation header package creation for reading bytes.
Use MemoryStream as well.
2020-09-02 20:16:19 +02:00
Serge Camille
bb0b57c574 Add method BuildWriteBitPackage
Merges data creation between sync and async for writing bit values.
2020-09-02 20:16:17 +02:00
Serge Camille
2f07d43062 Change BuildWriteBytesPackage to use MemoryStream. 2020-09-02 20:13:18 +02:00
Serge Camille
4ef037881c Simplify WriteBytes functiosn by merging common BuildPackage code.
Both Synchronous and Asynchronous need to build the same binary data package to write a bytes array. Move that package building out into a common function.

Also use IEnumerable to pass in data instead of converting it to array and back multiple times. Not that happy with the whole ByteArray class, we could probably just use a MemoryStream instead.
2020-09-02 20:11:35 +02:00
Serge Camille
fbd8a13c6c ReadBytesAsync: Replace list with a plain byte array. 2020-09-02 20:09:51 +02:00
Serge Camille
2a451bc049 TSDU: Use Array.Copy for Read functions. 2020-09-02 20:09:51 +02:00
Michael Croes
324ae95c42 Merge pull request #293 from scamille/fb-nullable
Add nullable support throughout the whole library.
2020-09-01 21:06:52 +02:00
Serge Camille
bd8177d39e Add nullable support throughout the whole library.
This requires reference types that can be null to be annotated by a ? operator, similar to value types.

This gives the advantage that the compiler can warn against any null dereference exceptions, of which this commits elimits a few.

To make the underlying protocol implementation not any more complicated and to eliminate existing problems, and not that precise error reporting, I replaced some return null statements with explicit Exceptions. This lead to the assumption that those core protocoll functions always return non-null objects if they do not throw, making the PLC code simpler.

Adjust some NotConnected tests to look for explicit PlcException instead of NullReferenceException.
2020-08-31 22:48:45 +02:00
Michael Croes
e68ca64596 Merge pull request #302 from mycroes/build-infra-update
AppVeyor and infra updates
2020-08-31 22:31:03 +02:00
Michael Croes
647d4c7ae2 AppVeyor and infra updates
- Change build image to VS 2019
- Update Microsoft.SourceLink.GitHub package
- Update solution file
- Build separate symbol package (.snupkg)
2020-08-31 22:28:59 +02:00
Michael Croes
c7ef055be2 Merge pull request #286 from scamille/fb-sdkTestProject
Convert UnitTest project to SDK project type.
2020-08-18 22:09:39 +02:00
Serge Camille
7035d22506 Convert UnitTest project to SDK project type. 2020-08-18 09:20:31 +02:00
Michael Croes
fc6781c37f Release S7NetPlus 0.6.0
Release highlights:
- Added support for DateTimeLong
2020-08-17 21:51:34 +02:00
Michael Croes
3555436c04 Merge pull request #284 from scamille/fb-DTL
Add DateTimeLong type
2020-08-17 21:36:05 +02:00
Serge Camille
3258c84fbc Add DateTimeLong test to S7NetTestsSync 2020-08-17 19:20:47 +02:00
Serge Camille
28257f28b3 Dtl: Add TypeLengthInBytes constant instead of always rewriting 12. 2020-08-17 19:20:19 +02:00
Serge Camille
a1d87de2d9 Rename DTL to DateTimeLong 2020-08-17 19:14:28 +02:00
Serge Camille
4be5765fc9 Run Resharper cleanup on DTL class, fix Dtl.ToByteArray list capacity. 2020-08-16 22:50:23 +02:00
Serge Camille
6614c7330a Hook up DTL to VarType enum and PLCHelper. 2020-08-16 22:36:23 +02:00
Serge Camille
5d59c8284d Add DTL type
Add new class Types.Dtl by taking the DateTime type and adjusting things.

Also add unit test with binary data calculated by hand. (Need to verify with actual S7 data)
2020-08-16 22:31:26 +02:00
Michael Croes
6554b999c0 Release S7NetPlus 0.5.0
Release highlights:
- Add support for (I|O|Q)(B|D|W) addressing
- Fix Type for Mxxxx.x addresses
- Align array offsets to even bytes in classes
- Improve exceptions on failed reads
2020-08-13 22:57:22 +02:00
Michael Croes
ff20687776 Merge pull request #282 from mycroes/develop
PLC: Improve exceptions on Read
2020-08-13 22:50:20 +02:00
Michael Croes
0b6226327b PLC: Improve exceptions on Read
Close #258.
2020-08-13 22:47:32 +02:00
Michael Croes
385240ba5e Merge pull request #281 from mycroes/develop
PLCAddress: Add OB, OW, OD types from PR #277
2020-08-13 22:08:13 +02:00
Michael Croes
0a8ee0e091 PLCAddress: Add OB, OW, OD types from PR #277
PR #246 included most types also included in #277, this adds OB, OW and
OD that were only in #277.

Close #277.
2020-08-13 22:04:53 +02:00
Michael Croes
1685270535 Merge pull request #280 from mycroes/develop
Tests/TypeTests: Add ClassTests from #178
2020-08-13 22:00:48 +02:00
Michael Croes
9a34b14e1e Tests/TypeTests: Add ClassTests from #178
Close #178.
2020-08-13 21:57:42 +02:00
Michael Croes
50f0e62573 Merge pull request #279 from mycroes/develop
Types/Class: Start arrays on even bytes.

Close #175, #220.
2020-08-13 21:49:30 +02:00
Michael Croes
9ea54be524 Types/Class: Start arrays on even bytes
Addresses #175
2020-08-13 21:40:44 +02:00
Michael Croes
dcd5bb3437 Merge pull request #246 from timverwaal/PLCAdddres-Parsing
Extended PLCAddress.Parse method
2020-08-13 20:43:16 +02:00
Tim Verwaal
8dc89867e9 Extended PLCAddress.Parse method 2019-12-19 10:55:51 +01:00
Michael Croes
798913c4c6 Release S7NetPlus 0.4.0
Release highlights:
- Adress checks for bit writes
- Support for reading/writing complex objects
- Better exceptions on Open(Async)
- Revert negotiated max PDU size to 960
- Add Logo0BA8 support
- Read/Write-Timeout support on TCP connection
- Fix for 0-padding of last dataItem in WriteMultiple
- Allow override of default port
- Improve exception message when parsing string
- Add DateTime type for reading/writing
- Verify items fit in a PDU on Read/Write-Multiple
- Fix size calculation for bit arrays
2019-07-17 22:01:55 +02:00
Michael Croes
2fbf659136 Merge pull request #167 from mycroes/bit-arrays
Fix incorrect length when reading BitArray
2019-07-17 21:50:12 +02:00
Michael Croes
6a84b6f124 Use ToBitArray overload with length in ParseBytes 2019-07-17 21:46:18 +02:00
Michael Croes
b3cb45de37 Add ToBitArray overload with length 2019-07-17 21:46:17 +02:00
Michael Croes
4fcab9a167 Fix VarTypeToByteLength for bit arrays 2019-07-17 21:46:17 +02:00
Michael Croes
94a178478b Merge pull request #227 from mycroes/calculate-pdu-constraint
Calculate PDU size constraints
2019-07-17 21:43:38 +02:00
Michael Croes
2a4485941f Calculate PDU size constraints 2019-07-17 21:39:41 +02:00
Michael Croes
47cce5887d Merge pull request #223 from thoj/develop
Fix padding of last dataitem in WriteMulitple fixes #222
2019-07-17 21:39:19 +02:00
Michael Croes
0e5651b37f Merge branch 'develop' into develop 2019-07-17 21:22:45 +02:00
Michael Croes
1d1f1e76a9 Merge pull request #189 from mycroes/add-datetime-support
Add DateTime support
2019-07-17 21:10:12 +02:00
Michael Croes
555d1e8956 Add unit test for Types.DateTime 2019-07-17 21:07:45 +02:00
Michael Croes
427d8124de Add DateTime support for read/write methods 2019-07-17 21:07:45 +02:00
Michael Croes
735f1d4533 Add support for DateTime array conversion 2019-07-17 21:07:45 +02:00
Michael Croes
2aa4f08836 Add Types.DateTime 2019-07-17 21:07:44 +02:00
Michael Croes
8d8b2ec36e Merge pull request #173 from mycroes/stringex-read-rubbish
Provide a clear message on Encoding.ASCII.GetString exceptions
2019-07-17 21:06:47 +02:00
Michael Croes
cf64c65c23 Provide a clear message on Encoding.ASCII.GetString exceptions 2019-07-17 21:04:26 +02:00
Michael Croes
93752a3485 Merge pull request #225 from tatzemax/develop
Add Property to selected Port from PLC
2019-07-11 20:10:29 +02:00
max
229df2dfcd Merge branch 'develop' of https://github.com/tatzemax/s7netplus into develop 2019-07-11 18:11:14 +02:00
max
3b23ab76e7 correction of wrong changes 2019-07-11 18:10:14 +02:00
tatzemax
3872e638aa Merge branch 'develop' into develop 2019-07-11 17:57:14 +02:00
Thomas Jager
e124c70fb1 Merge branch 'develop' into develop 2019-07-11 12:24:07 +02:00
Michael Croes
b8b4071e39 Merge pull request #226 from SevenMag/develop
Use PDU size in WriteBytes
2019-07-11 12:13:01 +02:00
Max
b2183dd760 rename PORT to Port
add a Space
2019-07-10 20:42:52 +02:00
max
f2d33855ca add Port Setting for PLC.
is required for port forwarding
2019-07-10 18:44:04 +02:00
max
b1d2d11904 add Port Setting for PLC.
is required for port forwarding
2019-07-10 18:17:02 +02:00
Evgeniy
23796de8bf Update PlcSynchronous.cs
MaxPDUSize for WriteBytes
2019-07-03 13:04:33 +05:00
Thomas Jäger
02b38326c8 Fix padding of last dataitem in WriteMulitple fixes #222 2019-06-19 13:02:53 +02:00
Michael Croes
ea96891a31 Merge pull request #218 from ChipsetSV/develop
feature: added ReadTimeout and WriteTimeout to PLC class for NetworkStream
2019-05-28 12:34:04 +02:00
Смирнов Виталий
2fbabd5517 style: updated code style. 2019-05-28 10:01:28 +03:00
Смирнов Виталий
800a790b89 feature: added changing ReadTimeout and WriteTimeout for connected tcpClient. 2019-05-28 08:56:06 +03:00
Смирнов Виталий
0dbe401ce9 feature: added ReadTimeout and WriteTimeout to PLC class for NetworkStream. 2019-05-27 13:52:38 +03:00
Michael Croes
e623b535ac Merge pull request #196 from mycroes/invalid-data-on-open
Invalid data on open
2019-03-14 23:09:50 +01:00
Michael Croes
ce359789dc Add explicit support for Logo0BA8
Unsure if the PLC actually requires the specfically provided TSAP's,
but these have been verified to work.
2019-03-14 23:04:26 +01:00
Michael Croes
4ae905ffd5 Set PDU size to 960
Allows communication with Siemens Logo 0BA8, currently no Siemens PLC
is known to support >960 PDU size.
2019-03-14 23:04:26 +01:00
Michael Croes
70506e7dba Throw InvalidDataException in Open(Async) 2019-03-14 23:02:15 +01:00
Michael Croes
20458d4b46 Add InvalidDataException 2019-03-14 23:02:14 +01:00
Michael Croes
898b870221 Expose TPKT on TPDU
Temporary hack to allow access to the received TPKT data.
2019-03-14 23:02:14 +01:00
Michael Croes
0fd193e08f Merge pull request #212 from mycroes/gitversion-v4
Update GitVersion.yml to V4
2019-03-14 22:56:36 +01:00
Michael Croes
98df02d7d4 Update GitVersion.yml to V4 2019-03-14 22:53:16 +01:00
Michael Croes
ae620f3c62 Merge pull request #191 from admo/issue190_nested_classes
Fix for Addresses are not aligned in case of custom type in class
2018-10-10 20:28:40 +02:00
Adam Oleksy
de084394a6 Fix writing nested classes 2018-10-04 18:14:44 +02:00
Adam Oleksy
370fd6b3d9 Fix reading nested classes 2018-09-26 14:43:49 +02:00
Derek Heiser
fba3ffd5db Merge pull request #169 from mycroes/dataitem-from-string-write-fix
Dataitem from string write fix
2018-07-18 18:39:21 -05:00
Michael Croes
e44cb1571c Validate BitAdr when writing a bit 2018-07-12 20:51:37 +02:00
Michael Croes
ab486e3d1f Only set bitNumber in address for Write when a bit is written 2018-07-12 20:51:20 +02:00
Michael Croes
705743e5f1 Convert bitNumber '-1' to 0 in DataItem 2018-07-12 20:49:44 +02:00
Michael Croes
fd17bfa03b Merge branch 'release/0.3' 2018-07-11 22:51:36 +02:00
Michael Croes
8dad14955e Merge pull request #166 from mycroes/fix-string-to-byte-array
Fix string to byte array
2018-07-11 22:46:44 +02:00
Michael Croes
96efb9d56a Add tests for String.ToByteArray(...) 2018-07-11 22:07:34 +02:00
Michael Croes
09d323925a Add length to String.ToByteArray(...) 2018-07-11 22:07:08 +02:00
Michael Croes
bf4550655e Add support for creating DataItem from string (#149)
* Refactor PLCAddress

- Change fields into properties
- Apply PascalCase naming to all properties
- Make Parse public static with out parameters

* Support creating DataItem from string

* Rename PlcAddress.Address to StartByte
2018-07-11 10:21:49 +02:00
Raphael Schlameuß
214a7a73c8 Read many bytes 1500 (develop branch) (#160)
* ReadBytesWithSingleRequest cannot read >491 Bytes on S7-1500
2018-07-11 10:15:08 +02:00
Michael Croes
a5d3c70373 Add SourceLink.Copy.PdbFiles to add missing .pdb files (#163) 2018-07-11 09:53:33 +02:00
Michael Croes
2204ab360c Fix write stringex (#162)
* Add StringEx.ToByteArray(...)

* Add Serialization.SerializeDataItem(DataItem)

Supports StringEx VarType or offloads to SerializeValue method.

* Use SerializeDataItem in S7WriteMultiple

* Assume string length without header in StringEx.ToByteArray

VarTypeToByteLength already assumed that StringEx declared count for
the number of characters without the header, this now matches that
behavior.

* Add unit tests for StringEx conversions

* Fix incorrect value passed to Encoding.GetBytes

The length must actually be within string limits.
2018-07-11 09:47:43 +02:00
Raphael Schlameuß
a1b69a5c5a Merge pull request #161 from mycroes/throw-on-error
Replace LastErrorCode and LastErrorString with exceptions
2018-07-11 09:26:27 +02:00
Michael Croes
1538de148b Replace LastErrorCode and LastErrorString with exceptions 2018-07-09 20:07:47 +02:00
Michael Croes
ff7e13cd49 Merge pull request #158 from mycroes/feature/ci
Add AppVeyor and GitVersion configuration
2018-07-09 19:57:06 +02:00
Michael Croes
c651380647 Add AppVeyor and GitVersion configuration 2018-07-08 21:10:05 +02:00
Michael Croes
0298371bfc Remove accidental .nuget leftover 2018-07-07 10:19:44 +02:00
Michael Croes
ff1e5fdc26 Merge pull request #155 from mycroes/release-preparations
Release preparations
2018-07-06 22:32:10 +02:00
Michael Croes
bd50709ad7 Remove .nuget/
Not sure why it was there in the first place.
2018-07-06 22:24:07 +02:00
Michael Croes
cafca8e28e Remove nuget-pack files
Obsoleted by SDK style project format.
2018-07-06 22:24:07 +02:00
Michael Croes
ab11cc8e3e Add SourceLink support 2018-07-06 22:24:07 +02:00
Michael Croes
9740224966 Add NuGet tags to S7.Net.csproj 2018-07-06 22:24:07 +02:00
Michael Croes
db50a62aad Merge pull request #152 from rapha-dev/invalid-tpkt-length
Fixes invalid TPKT length in request header
2018-07-04 23:15:58 +02:00
Raphael
2f3bbddaef Fixes invalid TPKT length in request header 2018-07-04 11:47:13 +02:00
Michael Croes
4ab73d0e3a Merge pull request #143 from rapha-dev/datatype-float
Add support for datatype float (Single) and obsolete double (Double) …
2018-07-03 21:54:22 +02:00
Raphael
932433ad69 Merge branch 'master' of https://github.com/rapha-dev/s7netplus into datatype-float 2018-07-03 08:44:51 +02:00
Raphael
d37c388d20 Fixed obsolete message 2018-07-03 08:41:54 +02:00
Michael Croes
c13fb970d0 Merge pull request #142 from mycroes/net-standard-1-3
Support for NetStandard 1.3
2018-07-02 23:00:19 +02:00
Michael Croes
28a1225265 Change S7.Net project type in S7.sln 2018-07-02 22:59:16 +02:00
Michael Croes
fc62bb79d4 Update supported frameworks and compile section 2018-07-02 20:27:29 +02:00
Raphael
b8b144d7ae Add support for datatype float (Single) and obsolete double (Double) usage 2018-07-02 10:20:02 +02:00
Michael Croes
98228924ea Simplify info declaration in Struct field loops 2018-06-30 22:19:44 +02:00
Michael Croes
a1f4e44c48 R#: Cleanup usings 2018-06-30 22:19:44 +02:00
Michael Croes
2df9d0b0bf Remove obsolete UWP / S7.Net.Core files 2018-06-30 22:19:44 +02:00
Michael Croes
7821b6b6f6 Add NetStandard 1.3 support
Supersedes UWP support since UWP 10.0 supports up to NetStandard 1.4.
2018-06-30 22:19:44 +02:00
Michael Croes
e516675a70 Replace InvalidEnumArgumentException usage
InvalidEnumArgumentException is not supported in NetStandard 1.3.
2018-06-30 22:19:44 +02:00
Michael Croes
710ab2e026 Provide a buffer to MemoryStream
NetStandard 1.3 doesn't expose .GetBuffer(), this removes the need for
having it. The support for messages that span TPKT's is quite nasty, but
probably S7 PLC's will never even use a message spanning multiple TPKT's.
2018-06-30 22:17:48 +02:00
Michael Croes
534ecb2546 Merge pull request #123 from deinok/master
Port to NetStandard
2018-06-28 22:07:09 +02:00
Raul Hidalgo Caballero
66fe6750b2 Merge branch 'master' into master 2018-06-28 10:40:11 +02:00
Michael Croes
85e1abfdb0 Merge pull request #138 from mycroes/connection-request
Connection request test and cleanup
2018-06-27 22:28:47 +02:00
Michael Croes
3409e52fef Add message for ReadWriteBytesMany assertion 2018-06-27 21:51:37 +02:00
Michael Croes
514dde365e Cleanup TSAP parameters
- Remove duplicate parameter code / length (already in initial array)
- Remove separate branch for S7-400 (same as S7-300/1200)
- Change all values to 2-character hex
2018-06-27 21:26:44 +02:00
Michael Croes
0f151e4947 Add tests for ConnectionRequest 2018-06-27 21:24:23 +02:00
Michael Croes
5f220cd31f Fix default namespace in S7.Net.UnitTest 2018-06-27 21:09:11 +02:00
Michael Croes
6569e5e169 Extract ConnectionRequest from PLCHelpers 2018-06-27 21:07:41 +02:00
Michael Croes
4bca9d8c19 Merge pull request #130 from mycroes/fix-communication-setup-pdu-size
Fix the PDU size in communication setup
2018-06-26 22:18:44 +02:00
Michael Croes
229558d586 Merge pull request #129 from mycroes/remove-obsolete-on-sync
Remove obsolete attribute and comments
2018-06-26 22:18:10 +02:00
Raul Hidalgo Caballero
daf0f8e0d6 Merge branch 'master' into master 2018-06-21 21:24:01 +02:00
Michael Croes
d9abebe550 Fix the PDU size in communication setup
[7, 80] resulted in 1872; [7, 128] (or [0x07, 0x80] in hex) results in
the 1920 as specified in comments.
2018-06-21 20:49:05 +02:00
Michael Croes
2083ab1501 Remove obsolete attribute and comments
References #124.
2018-06-21 20:28:22 +02:00
Michele Cattafesta
003d775228 Merge pull request #127 from mycroes/write-short-array-fix
Write short array fix
2018-06-21 13:32:44 +01:00
Michele Cattafesta
e1c62c899b Merge pull request #121 from mycroes/master
Add multiple write support
2018-06-21 13:31:39 +01:00
Michael Croes
5a82313eb1 Change order of byte assignment for uniformity
Order is now same as ToByteArray(Int16[] value), where the order is
important due to the index increment in the same line.
2018-06-19 21:26:03 +02:00
Michael Croes
8d64bd89fc Fix byte order when serializing short[] / Int16[]
Values should be written as big-endian, unfortunately the ordering for
short values in an array was little-endian.
2018-06-19 21:24:28 +02:00
Michael Croes
299918e293 Add Plc.Write/WriteAsync(params DataItem[] dataItems) 2018-06-15 21:50:41 +02:00
Michael Croes
740a47ab43 Add initial WriteMultiple protocol 2018-06-15 21:39:38 +02:00
Michael Croes
38d089e117 Add boolean support to GetPackage 2018-06-15 21:39:38 +02:00
Michael Croes
aaab24a4c2 Add and move serialization helpers
Added (Get|Set)WordAt, SetAddressAt and moved GetPackage method.
2018-06-15 21:39:38 +02:00
Michele Cattafesta
0721b1a84a Merge pull request #125 from rapha-dev/master
Fixes for Exception naming ReadData
2018-06-10 17:13:15 +01:00
Raphael
1b22badea1 Fixed ErrorCode.ReadData for specific methods 2018-06-08 10:26:52 +02:00
Raphael
ddfedaa17e Typo in Exception string 2018-06-08 10:22:42 +02:00
Raul Hidalgo Caballero
2d24adc874 Port to NetStandard 2018-06-05 17:33:42 +02:00
Michele Cattafesta
44bf1366e4 Merge pull request #111 from thoj/tcpclient
RFC/WIP: Async API
2018-05-21 20:23:07 +01:00
Thomas Jäger
64e485c54a Revert "Use socket instead of TcpClient and stream."
Revert back to using Stream/TcpClient. High performance stuff is moved
to highperformancesockets branch. I think this is interesting, but i also
feel that this is premature optimization. I doubth that this will be a
performance bottleneck ofr the forseeable future.

This reverts commit 1b34716a30.
2018-05-18 08:52:19 +02:00
Thomas Jäger
219c1cc71a Revert "Use high performance wrapper"
This reverts commit a3277133af.
2018-05-18 08:43:44 +02:00
Thomas Jäger
a3277133af Use high performance wrapper 2018-05-16 16:29:11 +02:00
Thomas Jäger
00e22ee214 Incorporate fixes from #117 (moved to helpers)
fix array naming.
2018-05-16 15:50:00 +02:00
Thomas Jäger
8ac96162f9 Merge remote-tracking branch 'upstream/master' into tcpclient 2018-05-16 11:33:46 +02:00
Thomas Jäger
1b34716a30 Use socket instead of TcpClient and stream.
Async implemented with wrapper.
2018-05-16 11:24:21 +02:00
Thomas Jäger
b8b890977e Fix structured comments 2018-05-14 14:19:33 +02:00
Michele Cattafesta
013ff5fd92 Merge pull request #117 from rapha-dev/master
Fixed bug in ReadMultipleVars on VarType.Bit
2018-05-12 23:03:57 +01:00
Raphael
642cf8169e Fixed bug for reading VarType.Bit and VarType.Byte on odd number of bytes in ReadMultipleVars() 2018-05-07 11:47:52 +02:00
Raphael
f6e370b162 Merge remote-tracking branch 'upstream/master' 2018-05-07 11:44:58 +02:00
Raphael
cbaa8921df import killnine master 2018-05-07 09:47:20 +02:00
Michele Cattafesta
b3458a8304 unit tests 2018-05-05 23:24:06 +01:00
Michele Cattafesta
a824344a4c Merge pull request #110 from Buchter/ImproveClassSizeCalculation
Fixed bug regarding size calculation of small S7 Structs
2018-05-05 23:00:15 +01:00
Michele Cattafesta
587e496497 Merge pull request #108 from thoj/master
Take 2: Use TPKT/COTP for reading responses from PLS
2018-05-05 22:09:13 +01:00
Raphael
0d1bc472c8 Added BitAdr to DataItem and fixed bug in ReadMultipleVars on VarType.Bit 2018-04-27 16:00:41 +02:00
Thomas Jäger
e6f1114bc1 Add COTP.ReadTSDU test 2018-04-19 15:39:41 +02:00
Thomas Jäger
84aee0671a Only return used bytes 2018-04-19 15:39:03 +02:00
Thomas Jäger
74af1c0da7 Change project to allow testing internal classes.
Add protocol tests
Simplify async tests
2018-04-19 15:08:52 +02:00
Thomas Jäger
c80b0dd55d Add signing key to S7.Net.UnitTest for testing internal classes 2018-04-19 15:07:06 +02:00
Thomas Jäger
eb0d6a3429 Better TPKT error handling 2018-04-19 15:06:10 +02:00
Thomas Jäger
f53a3bd320 Use TcpClient and implemnt async methods
Note: This keeps the old methods to be backward compatible.
Note: Unforntunatly a lot of whitespace fixes, refactoring and other trivial stuff is
included. It was to hard to split of in a seperate commit.

Note: Async methods does not use exactly the same structure/signature as the
existing methods. "Out" parameters like ReadClass and ReadStruct instead
returns the struct in tuple. Async methods also rely on exceptions
instead of ErrorCodes to communicate exception states to calling client.

* Use TcpClient and use Async methods (ReadAsync/WriteAsync)
* Implemnt async methods for all existing methods
* Implemnt existing methods using tcpclient.
* Split Plc.cs in more files. (Common, Async, Sync, Helpers)
* Mark old methods as Obsolete
* Split tests in two files
* Implement Async tests
2018-04-19 13:13:08 +02:00
Buchter
4b04ed74a1 Fixed bug regarding size calculation of small S7 Structs
There was an Error when you had Structs conaining less than 8 Bits. The size calculation in this case returned 0 and the Plc.ReadClass() method throwed an excpetion. Structs in Step7 within da DataBlock always starts with adresses that can by devided by two. The extended code ensures the correct size even if there are a couple of structs in a DataBlock containing only a few bits.
2018-04-19 12:09:30 +02:00
Thomas Jäger
50b026d7a5 Read TPKT/COTP packets / Read MaxPDU size from PLC
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
packets that SoftPLS and WinAC based PLCs send out. I use RFC names for
functions and classes.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet sizes.

Read Max PDU size from connection setup. Ref #21
Change logic to use MaxPDUSize when reading istead of hardcoded limit.

I tried using MaxPDUSize when writing data but this failed when packet size is
over 256 on snap7. So i decided to drop changes to write size.
I have done some tests against WinAC cpu and it seems to handle bigger pdu's
when writing if negotiated in the connection setup. This might just be a SNAP7 bug.

Fix MaxPDUSize for readbytes

Remove debug line

Simplify byte copy. Remove unessesarry buffer
2018-04-19 00:34:11 +02:00
Thomas Jäger
f740ba0078 Fix writing double to PLC 2018-04-19 00:34:11 +02:00
Thomas Jäger
1b4faf21d7 Code Lint: Standard dispose Pattern. /// to // comment 2018-04-19 00:34:11 +02:00
Thomas Jäger
3a18d13805 Add test for Read/Write Double and PDUSize Test 2018-04-19 00:34:10 +02:00
Michele Cattafesta
9fd515280a Revert "Merge pull request #107 from thoj/master"
This reverts commit d17fdf8efb, reversing
changes made to bfeacee08f.
2018-04-11 20:22:24 +01:00
Michele Cattafesta
d17fdf8efb Merge pull request #107 from thoj/master
Fix communication with WinAC/SoftPLC CPUs and Fix write double
2018-04-11 19:28:41 +01:00
Thomas Jäger
8a3db22629 Read TPKT/COTP packets / Read MaxPDU size from PLC
Read responses from the PLS using classes for TPKT and COPT. This
makes the communication more robust. It will now handle empty COTP
packets that SoftPLS and WinAC based PLCs send out. I use RFC names for
functions and classes.

Change logic to use COTP and S7Comm reponse codes instead of
relying on packet sizes.

Read Max PDU size from connection setup. Ref #21
Change logic to use MaxPDU from cpu limit instead of hardcoded limit.

Remove var count limit.
2018-04-10 15:26:04 +02:00
Thomas Jäger
6470f8d076 Document bSend1 data packet 2018-04-10 15:25:20 +02:00
Thomas Jäger
723b0ffd42 Fix writing double to PLC 2018-04-10 15:23:46 +02:00
Michele Cattafesta
bfeacee08f Merge pull request #102 from mbalous/master
Non breaking changes. Code styling, comments, etc...
2018-04-08 21:12:22 +01:00
Michele Cattafesta
b6b53078f9 Merge pull request #101 from tomsoftware/master
fix data type converting for num-types
2018-04-08 21:08:27 +01:00
mbalous
a99ea469ce Correct exceptions. Constructor parameter checking. 2018-03-21 22:40:25 +01:00
mbalous
dd71e1bf0b Checking constructor parameters. 2018-03-21 22:32:23 +01:00
mbalous
530072b70f Improved XML function comments. 2018-03-21 22:18:37 +01:00
Thomas Ze
117ad5cd1b fix variable length bug for StringEx 2018-03-20 22:37:44 +01:00
Thomas Ze
7cedec5909 - fix data type converting
- add new string string data type
2018-03-18 21:26:22 +01:00
Michele Cattafesta
f6a2e11045 0.1.8 2018-02-05 20:07:11 +00:00
Michele Cattafesta
8005304827 Merge pull request #97 from GS770/master
Added Bit to Types
2017-12-14 23:00:19 +00:00
shen.jz
ef5e060948 Added Bit to Types
get bool or BitArray from byte array
2017-12-14 10:38:10 +08:00
Michele Cattafesta
3178d2aa09 Merge pull request #96 from GS770/master
Get bit of read bytes
2017-12-13 08:57:52 +00:00
shen.jz
6a2bc708a9 Get bit of read bytes 2017-12-12 15:56:18 +08:00
113 changed files with 8802 additions and 5758 deletions

134
.github/workflows/dotnet.yml vendored Normal file
View 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
}

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<solution>
<add key="disableSourceControlIntegration" value="true" />
</solution>
</configuration>

Binary file not shown.

View File

@@ -1,144 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">$(MSBuildProjectDirectory)\..\</SolutionDir>
<!-- Enable the restore command to run before builds -->
<RestorePackages Condition=" '$(RestorePackages)' == '' ">false</RestorePackages>
<!-- Property that enables building a package from a project -->
<BuildPackage Condition=" '$(BuildPackage)' == '' ">false</BuildPackage>
<!-- Determines if package restore consent is required to restore packages -->
<RequireRestoreConsent Condition=" '$(RequireRestoreConsent)' != 'false' ">true</RequireRestoreConsent>
<!-- Download NuGet.exe if it does not already exist -->
<DownloadNuGetExe Condition=" '$(DownloadNuGetExe)' == '' ">false</DownloadNuGetExe>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageSources)' == '' ">
<!-- Package sources used to restore packages. By default, registered sources under %APPDATA%\NuGet\NuGet.Config will be used -->
<!-- The official NuGet package source (https://www.nuget.org/api/v2/) will be excluded if package sources are specified and it does not appear in the list -->
<!--
<PackageSource Include="https://www.nuget.org/api/v2/" />
<PackageSource Include="https://my-nuget-source/nuget/" />
-->
</ItemGroup>
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT'">
<!-- Windows specific commands -->
<NuGetToolsPath>$([System.IO.Path]::Combine($(SolutionDir), ".nuget"))</NuGetToolsPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(OS)' != 'Windows_NT'">
<!-- We need to launch nuget.exe with the mono command if we're not on windows -->
<NuGetToolsPath>$(SolutionDir).nuget</NuGetToolsPath>
</PropertyGroup>
<PropertyGroup>
<PackagesProjectConfig Condition=" '$(OS)' == 'Windows_NT'">$(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config</PackagesProjectConfig>
<PackagesProjectConfig Condition=" '$(OS)' != 'Windows_NT'">$(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config</PackagesProjectConfig>
</PropertyGroup>
<PropertyGroup>
<PackagesConfig Condition="Exists('$(MSBuildProjectDirectory)\packages.config')">$(MSBuildProjectDirectory)\packages.config</PackagesConfig>
<PackagesConfig Condition="Exists('$(PackagesProjectConfig)')">$(PackagesProjectConfig)</PackagesConfig>
</PropertyGroup>
<PropertyGroup>
<!-- NuGet command -->
<NuGetExePath Condition=" '$(NuGetExePath)' == '' ">$(NuGetToolsPath)\NuGet.exe</NuGetExePath>
<PackageSources Condition=" $(PackageSources) == '' ">@(PackageSource)</PackageSources>
<NuGetCommand Condition=" '$(OS)' == 'Windows_NT'">"$(NuGetExePath)"</NuGetCommand>
<NuGetCommand Condition=" '$(OS)' != 'Windows_NT' ">mono --runtime=v4.0.30319 "$(NuGetExePath)"</NuGetCommand>
<PackageOutputDir Condition="$(PackageOutputDir) == ''">$(TargetDir.Trim('\\'))</PackageOutputDir>
<RequireConsentSwitch Condition=" $(RequireRestoreConsent) == 'true' ">-RequireConsent</RequireConsentSwitch>
<NonInteractiveSwitch Condition=" '$(VisualStudioVersion)' != '' AND '$(OS)' == 'Windows_NT' ">-NonInteractive</NonInteractiveSwitch>
<PaddedSolutionDir Condition=" '$(OS)' == 'Windows_NT'">"$(SolutionDir) "</PaddedSolutionDir>
<PaddedSolutionDir Condition=" '$(OS)' != 'Windows_NT' ">"$(SolutionDir)"</PaddedSolutionDir>
<!-- Commands -->
<RestoreCommand>$(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir)</RestoreCommand>
<BuildCommand>$(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols</BuildCommand>
<!-- We need to ensure packages are restored prior to assembly resolve -->
<BuildDependsOn Condition="$(RestorePackages) == 'true'">
RestorePackages;
$(BuildDependsOn);
</BuildDependsOn>
<!-- Make the build depend on restore packages -->
<BuildDependsOn Condition="$(BuildPackage) == 'true'">
$(BuildDependsOn);
BuildPackage;
</BuildDependsOn>
</PropertyGroup>
<Target Name="CheckPrerequisites">
<!-- Raise an error if we're unable to locate nuget.exe -->
<Error Condition="'$(DownloadNuGetExe)' != 'true' AND !Exists('$(NuGetExePath)')" Text="Unable to locate '$(NuGetExePath)'" />
<!--
Take advantage of MsBuild's build dependency tracking to make sure that we only ever download nuget.exe once.
This effectively acts as a lock that makes sure that the download operation will only happen once and all
parallel builds will have to wait for it to complete.
-->
<MsBuild Targets="_DownloadNuGet" Projects="$(MSBuildThisFileFullPath)" Properties="Configuration=NOT_IMPORTANT;DownloadNuGetExe=$(DownloadNuGetExe)" />
</Target>
<Target Name="_DownloadNuGet">
<DownloadNuGet OutputFilename="$(NuGetExePath)" Condition=" '$(DownloadNuGetExe)' == 'true' AND !Exists('$(NuGetExePath)')" />
</Target>
<Target Name="RestorePackages" DependsOnTargets="CheckPrerequisites">
<Exec Command="$(RestoreCommand)"
Condition="'$(OS)' != 'Windows_NT' And Exists('$(PackagesConfig)')" />
<Exec Command="$(RestoreCommand)"
LogStandardErrorAsError="true"
Condition="'$(OS)' == 'Windows_NT' And Exists('$(PackagesConfig)')" />
</Target>
<Target Name="BuildPackage" DependsOnTargets="CheckPrerequisites">
<Exec Command="$(BuildCommand)"
Condition=" '$(OS)' != 'Windows_NT' " />
<Exec Command="$(BuildCommand)"
LogStandardErrorAsError="true"
Condition=" '$(OS)' == 'Windows_NT' " />
</Target>
<UsingTask TaskName="DownloadNuGet" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<OutputFilename ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Reference Include="System.Core" />
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Net" />
<Using Namespace="Microsoft.Build.Framework" />
<Using Namespace="Microsoft.Build.Utilities" />
<Code Type="Fragment" Language="cs">
<![CDATA[
try {
OutputFilename = Path.GetFullPath(OutputFilename);
Log.LogMessage("Downloading latest version of NuGet.exe...");
WebClient webClient = new WebClient();
webClient.DownloadFile("https://www.nuget.org/nuget.exe", OutputFilename);
return true;
}
catch (Exception ex) {
Log.LogErrorFromException(ex);
return false;
}
]]>
</Code>
</Task>
</UsingTask>
</Project>

View File

@@ -1,12 +0,0 @@
@echo off
set /p version="Version: "
msbuild S7.Net\S7.Net.csproj /P:Configuration=Release
rmdir /S /Q nuget-pack\lib
xcopy S7.Net\bin\Release\S7.Net.dll nuget-pack\lib\net35\ /Y
xcopy S7.Net\bin\Release\S7.Net.xml nuget-pack\lib\net35\ /Y
xcopy S7.Net\bin\Release\S7.Net.dll nuget-pack\lib\net40\ /Y
xcopy S7.Net\bin\Release\S7.Net.xml nuget-pack\lib\net40\ /Y
xcopy S7.Net\bin\Release\S7.Net.dll nuget-pack\lib\net45\ /Y
xcopy S7.Net\bin\Release\S7.Net.xml nuget-pack\lib\net45\ /Y
.nuget\nuget pack nuget-pack\S7.Net.nuspec -Version %version%
pause

View File

@@ -12,18 +12,17 @@ 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)
## Target framework
+ .NET Framework 3.5 or higher
+ Universal Windows Application (.Net Core) - see S7.UniversalWindowsApp.sln
## Supported frameworks
+ .NET Framework 4.5.2 and higher
+ .NET Standard 1.3 (.NET Core 1.0, UWP 10.0, Xamarin, ...)
+ .NET Standard 2.0 (.NET Core 2.0, .NET Framework 4.6.1)
## Compile
You need at least Visual Studio 2015 (you can download the Community Edition for free).
You need at least Visual Studio 2017 (you can download the Community Edition for free).
## Nuget
@@ -35,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.

View File

@@ -1,29 +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.Core")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Microsoft")]
[assembly: AssemblyProduct("S7.Net.Core")]
[assembly: AssemblyCopyright("Copyright © Microsoft 2016")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 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")]
[assembly: ComVisible(false)]

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file contains Runtime Directives, specifications about types your application accesses
through reflection and other dynamic code patterns. Runtime Directives are used to control the
.NET Native optimizer and ensure that it does not remove code accessed by your library. If your
library does not do any reflection, then you generally do not need to edit this file. However,
if your library reflects over types, especially types passed to it or derived from its types,
then you should write Runtime Directives.
The most common use of reflection in libraries is to discover information about types passed
to the library. Runtime Directives have three ways to express requirements on types passed to
your library.
1. Parameter, GenericParameter, TypeParameter, TypeEnumerableParameter
Use these directives to reflect over types passed as a parameter.
2. SubTypes
Use a SubTypes directive to reflect over types derived from another type.
3. AttributeImplies
Use an AttributeImplies directive to indicate that your library needs to reflect over
types or methods decorated with an attribute.
For more information on writing Runtime Directives for libraries, please visit
http://go.microsoft.com/fwlink/?LinkID=391919
-->
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
<Library Name="S7.Net.Core">
<!-- add directives for your library here -->
</Library>
</Directives>

View File

@@ -1,176 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{CBFF80E8-3D3D-4656-A27C-A65EA5774536}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>S7.Net.Core</RootNamespace>
<AssemblyName>S7.Net.Core</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion>10.0.10586.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.10240.0</TargetPlatformMinVersion>
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>ARM</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>ARM</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<PlatformTarget>x86</PlatformTarget>
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x86</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM'">
<PlatformTarget>ARM</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\ARM\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<PlatformTarget>ARM</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM'">
<PlatformTarget>ARM</PlatformTarget>
<OutputPath>bin\ARM\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<PlatformTarget>ARM</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<PlatformTarget>x64</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<PlatformTarget>x64</PlatformTarget>
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<!-- A reference to the entire .Net Framework and Windows SDK are automatically included -->
<None Include="project.json" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\S7.Net\Conversion.cs">
<Link>Conversion.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Enums.cs">
<Link>Enums.cs</Link>
</Compile>
<Compile Include="..\S7.Net\PLC.cs">
<Link>PLC.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\Boolean.cs">
<Link>Types\Boolean.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\Byte.cs">
<Link>Types\Byte.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\ByteArray.cs">
<Link>Types\ByteArray.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\Class.cs">
<Link>Types\Class.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\Counter.cs">
<Link>Types\Counter.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\DataItem.cs">
<Link>Types\DataItem.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\DInt.cs">
<Link>Types\DInt.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\Double.cs">
<Link>Types\Double.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\DWord.cs">
<Link>Types\DWord.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\Int.cs">
<Link>Types\Int.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\String.cs">
<Link>Types\String.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\Struct.cs">
<Link>Types\Struct.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\Timer.cs">
<Link>Types\Timer.cs</Link>
</Compile>
<Compile Include="..\S7.Net\Types\Word.cs">
<Link>Types\Word.cs</Link>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SocketClient.cs" />
<EmbeddedResource Include="Properties\S7.Net.Core.rd.xml" />
</ItemGroup>
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' ">
<VisualStudioVersion>14.0</VisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.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>

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">C:\Users\shade\Documents\GitHub\s7netplus\S7.Net.Core\project.lock.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\shade\.nuget\packages\</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">ProjectJson</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">4.3.1</NuGetToolVersion>
</PropertyGroup>
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)microsoft.net.native.sharedlibrary-x86\1.7.0\build\Microsoft.Net.Native.SharedLibrary-x86.props" Condition="Exists('$(NuGetPackageRoot)microsoft.net.native.sharedlibrary-x86\1.7.0\build\Microsoft.Net.Native.SharedLibrary-x86.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.net.native.sharedlibrary-x64\1.7.0\build\Microsoft.Net.Native.SharedLibrary-x64.props" Condition="Exists('$(NuGetPackageRoot)microsoft.net.native.sharedlibrary-x64\1.7.0\build\Microsoft.Net.Native.SharedLibrary-x64.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.net.native.sharedlibrary-arm\1.7.0\build\Microsoft.Net.Native.SharedLibrary-arm.props" Condition="Exists('$(NuGetPackageRoot)microsoft.net.native.sharedlibrary-arm\1.7.0\build\Microsoft.Net.Native.SharedLibrary-arm.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.net.native.compiler\1.7.0\build\Microsoft.Net.Native.Compiler.props" Condition="Exists('$(NuGetPackageRoot)microsoft.net.native.compiler\1.7.0\build\Microsoft.Net.Native.Compiler.props')" />
</ImportGroup>
</Project>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)microsoft.net.native.sharedlibrary-x86\1.7.0\build\Microsoft.Net.Native.SharedLibrary-x86.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.net.native.sharedlibrary-x86\1.7.0\build\Microsoft.Net.Native.SharedLibrary-x86.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.net.native.sharedlibrary-x64\1.7.0\build\Microsoft.Net.Native.SharedLibrary-x64.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.net.native.sharedlibrary-x64\1.7.0\build\Microsoft.Net.Native.SharedLibrary-x64.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.net.native.sharedlibrary-arm\1.7.0\build\Microsoft.Net.Native.SharedLibrary-arm.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.net.native.sharedlibrary-arm\1.7.0\build\Microsoft.Net.Native.SharedLibrary-arm.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.net.native.compiler\1.7.0\build\Microsoft.Net.Native.Compiler.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.net.native.compiler\1.7.0\build\Microsoft.Net.Native.Compiler.targets')" />
</ImportGroup>
</Project>

View File

@@ -1,481 +0,0 @@
using System;
using System.Net.Sockets;
using System.Threading;
using System.Net;
namespace S7.Net
{
/// <summary>
/// This class encapsulate System.Net.Sockets.Socket class of .Net core, so we can use the same methods of the standard Socket class inside the S7.Net sources.
/// </summary>
internal class Socket
{
public bool Connected
{
get
{
if (_socket == null)
return false;
return _socket.Connected;
}
}
public SocketError LastSocketError { get; private set; }
public Socket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType)
{
_socket = new System.Net.Sockets.Socket(addressFamily, socketType, protocolType);
}
public void Connect(IPEndPoint server)
{
if (Connected)
return;
LastSocketError = SocketError.NotConnected;
var socketEventArg = new SocketAsyncEventArgs();
socketEventArg.RemoteEndPoint = server;
var completedEvent = new EventHandler<SocketAsyncEventArgs>(delegate (object s, SocketAsyncEventArgs e)
{
LastSocketError = e.SocketError;
_clientDone.Set();
});
socketEventArg.Completed += completedEvent;
_clientDone.Reset();
LastSocketError = SocketError.TimedOut;
_socket.ConnectAsync(socketEventArg);
_clientDone.WaitOne(TIMEOUT_MILLISECONDS);
socketEventArg.Completed -= completedEvent;
}
public int Send(byte[] buffer, int size, SocketFlags socketFlag)
{
var response = 0;
if (_socket != null)
{
var socketEventArg = new SocketAsyncEventArgs();
socketEventArg.RemoteEndPoint = _socket.RemoteEndPoint;
socketEventArg.UserToken = null;
var completedEvent = new EventHandler<SocketAsyncEventArgs>(delegate (object s, SocketAsyncEventArgs e)
{
LastSocketError = e.SocketError;
if (e.SocketError == SocketError.Success)
response = e.BytesTransferred;
_clientDone.Set();
});
socketEventArg.Completed += completedEvent;
socketEventArg.SetBuffer(buffer, 0, size);
_clientDone.Reset();
LastSocketError = SocketError.TimedOut;
_socket.SendAsync(socketEventArg);
_clientDone.WaitOne(_sendTimeout);
socketEventArg.Completed -= completedEvent;
}
else
{
LastSocketError = SocketError.NotInitialized;
}
return response;
}
public int Receive(byte[] buffer, int size, SocketFlags socketFlag)
{
var response = 0;
if (_socket != null)
{
var socketEventArg = new SocketAsyncEventArgs();
socketEventArg.RemoteEndPoint = _socket.RemoteEndPoint;
socketEventArg.SetBuffer(buffer, 0, size);
var completedEvent = new EventHandler<SocketAsyncEventArgs>(delegate (object s, SocketAsyncEventArgs e)
{
LastSocketError = e.SocketError;
if (e.SocketError == SocketError.Success)
response = e.BytesTransferred;
_clientDone.Set();
});
socketEventArg.Completed += completedEvent;
_clientDone.Reset();
LastSocketError = SocketError.TimedOut;
_socket.ReceiveAsync(socketEventArg);
_clientDone.WaitOne(_receiveTimeout);
socketEventArg.Completed -= completedEvent;
}
else
{
LastSocketError = SocketError.NotInitialized;
}
return response;
}
public void Shutdown(SocketShutdown how)
{
_socket.Shutdown(how);
}
public void Close()
{
if (_socket != null)
{
_socket.Dispose();
_socket = null;
}
}
//
// Summary:
// Sets the specified System.Net.Sockets.Socket option to the specified integer
// value.
//
// Parameters:
// optionLevel:
// One of the System.Net.Sockets.SocketOptionLevel values.
//
// optionName:
// One of the System.Net.Sockets.SocketOptionName values.
//
// optionValue:
// A value of the option.
//
// Exceptions:
// T:System.Net.Sockets.SocketException:
// An error occurred when attempting to access the socket. See the Remarks section
// for more information.
//
// T:System.ObjectDisposedException:
// The System.Net.Sockets.Socket has been closed.
public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue)
{
switch (optionName)
{
case SocketOptionName.ReceiveTimeout:
_receiveTimeout = optionValue;
break;
case SocketOptionName.SendTimeout:
_sendTimeout = optionValue;
break;
default:
throw new NotImplementedException("SetSocketOption option not implemented");
}
}
private System.Net.Sockets.Socket _socket = null;
private int _receiveTimeout = TIMEOUT_MILLISECONDS;
private int _sendTimeout = TIMEOUT_MILLISECONDS;
private readonly static ManualResetEvent _clientDone =
new ManualResetEvent(false);
private const int TIMEOUT_MILLISECONDS = 1000;
}
//
// Summary:
// Specifies socket send and receive behaviors.
[Flags]
public enum SocketFlags
{
//
// Summary:
// Use no flags for this call.
None = 0,
////
//// Summary:
//// Process out-of-band data.
//OutOfBand = 1,
////
//// Summary:
//// Peek at the incoming message.
//Peek = 2,
////
//// Summary:
//// Send without using routing tables.
//DontRoute = 4,
////
//// Summary:
//// Provides a standard value for the number of WSABUF structures that are used to
//// send and receive data.
//MaxIOVectorLength = 16,
////
//// Summary:
//// The message was too large to fit into the specified buffer and was truncated.
//Truncated = 256,
////
//// Summary:
//// Indicates that the control data did not fit into an internal 64-KB buffer and
//// was truncated.
//ControlDataTruncated = 512,
////
//// Summary:
//// Indicates a broadcast packet.
//Broadcast = 1024,
////
//// Summary:
//// Indicates a multicast packet.
//Multicast = 2048,
////
//// Summary:
//// Partial send or receive for message.
//Partial = 32768
}
//
// Summary:
// Defines socket option levels for the System.Net.Sockets.Socket.SetSocketOption(System.Net.Sockets.SocketOptionLevel,System.Net.Sockets.SocketOptionName,System.Int32)
// and System.Net.Sockets.Socket.GetSocketOption(System.Net.Sockets.SocketOptionLevel,System.Net.Sockets.SocketOptionName)
// methods.
public enum SocketOptionLevel
{
//
// Summary:
// System.Net.Sockets.Socket options apply only to IP sockets.
IP = 0,
//
// Summary:
// System.Net.Sockets.Socket options apply only to TCP sockets.
Tcp = 6,
//
// Summary:
// System.Net.Sockets.Socket options apply only to UDP sockets.
Udp = 17,
//
// Summary:
// System.Net.Sockets.Socket options apply only to IPv6 sockets.
IPv6 = 41,
//
// Summary:
// System.Net.Sockets.Socket options apply to all sockets.
Socket = 65535
}
//
// Summary:
// Defines configuration option names.
public enum SocketOptionName
{
//
// Summary:
// Close the socket gracefully without lingering.
DontLinger = -129,
//
// Summary:
// Enables a socket to be bound for exclusive access.
ExclusiveAddressUse = -5,
//
// Summary:
// Record debugging information.
Debug = 1,
//
// Summary:
// Specifies the IP options to be inserted into outgoing datagrams.
IPOptions = 1,
//
// Summary:
// Disables the Nagle algorithm for send coalescing.
NoDelay = 1,
//
// Summary:
// Send UDP datagrams with checksum set to zero.
NoChecksum = 1,
//
// Summary:
// The socket is listening.
AcceptConnection = 2,
//
// Summary:
// Indicates that the application provides the IP header for outgoing datagrams.
HeaderIncluded = 2,
//
// Summary:
// Use urgent data as defined in RFC-1222. This option can be set only once; after
// it is set, it cannot be turned off.
BsdUrgent = 2,
//
// Summary:
// Use expedited data as defined in RFC-1222. This option can be set only once;
// after it is set, it cannot be turned off.
Expedited = 2,
//
// Summary:
// Change the IP header type of the service field.
TypeOfService = 3,
//
// Summary:
// Allows the socket to be bound to an address that is already in use.
ReuseAddress = 4,
//
// Summary:
// Set the IP header Time-to-Live field.
IpTimeToLive = 4,
//
// Summary:
// Use keep-alives.
KeepAlive = 8,
//
// Summary:
// Set the interface for outgoing multicast packets.
MulticastInterface = 9,
//
// Summary:
// An IP multicast Time to Live.
MulticastTimeToLive = 10,
//
// Summary:
// An IP multicast loopback.
MulticastLoopback = 11,
//
// Summary:
// Add an IP group membership.
AddMembership = 12,
//
// Summary:
// Drop an IP group membership.
DropMembership = 13,
//
// Summary:
// Do not fragment IP datagrams.
DontFragment = 14,
//
// Summary:
// Join a source group.
AddSourceMembership = 15,
//
// Summary:
// Do not route; send the packet directly to the interface addresses.
DontRoute = 16,
//
// Summary:
// Drop a source group.
DropSourceMembership = 16,
//
// Summary:
// Block data from a source.
BlockSource = 17,
//
// Summary:
// Unblock a previously blocked source.
UnblockSource = 18,
//
// Summary:
// Return information about received packets.
PacketInformation = 19,
//
// Summary:
// Set or get the UDP checksum coverage.
ChecksumCoverage = 20,
//
// Summary:
// Specifies the maximum number of router hops for an Internet Protocol version
// 6 (IPv6) packet. This is similar to Time to Live (TTL) for Internet Protocol
// version 4.
HopLimit = 21,
//
// Summary:
// Permit sending broadcast messages on the socket.
Broadcast = 32,
//
// Summary:
// Bypass hardware when possible.
UseLoopback = 64,
//
// Summary:
// Linger on close if unsent data is present.
Linger = 128,
//
// Summary:
// Receives out-of-band data in the normal data stream.
OutOfBandInline = 256,
//
// Summary:
// Specifies the total per-socket buffer space reserved for sends. This is unrelated
// to the maximum message size or the size of a TCP window.
SendBuffer = 4097,
//
// Summary:
// Specifies the total per-socket buffer space reserved for receives. This is unrelated
// to the maximum message size or the size of a TCP window.
ReceiveBuffer = 4098,
//
// Summary:
// Specifies the low water mark for Overload:System.Net.Sockets.Socket.Send operations.
SendLowWater = 4099,
//
// Summary:
// Specifies the low water mark for Overload:System.Net.Sockets.Socket.Receive operations.
ReceiveLowWater = 4100,
//
// Summary:
// Send a time-out. This option applies only to synchronous methods; it has no effect
// on asynchronous methods such as the System.Net.Sockets.Socket.BeginSend(System.Byte[],System.Int32,System.Int32,System.Net.Sockets.SocketFlags,System.AsyncCallback,System.Object)
// method.
SendTimeout = 4101,
//
// Summary:
// Receive a time-out. This option applies only to synchronous methods; it has no
// effect on asynchronous methods such as the System.Net.Sockets.Socket.BeginSend(System.Byte[],System.Int32,System.Int32,System.Net.Sockets.SocketFlags,System.AsyncCallback,System.Object)
// method.
ReceiveTimeout = 4102,
//
// Summary:
// Get the error status and clear.
Error = 4103,
//
// Summary:
// Get the socket type.
Type = 4104,
//
// Summary:
// Updates an accepted socket's properties by using those of an existing socket.
// This is equivalent to using the Winsock2 SO_UPDATE_ACCEPT_CONTEXT socket option
// and is supported only on connection-oriented sockets.
UpdateAcceptContext = 28683,
//
// Summary:
// Updates a connected socket's properties by using those of an existing socket.
// This is equivalent to using the Winsock2 SO_UPDATE_CONNECT_CONTEXT socket option
// and is supported only on connection-oriented sockets.
UpdateConnectContext = 28688,
//
// Summary:
// Not supported; will throw a System.Net.Sockets.SocketException if used.
MaxConnections = int.MaxValue
}
}

View File

@@ -1,16 +0,0 @@
{
"dependencies": {
"Microsoft.NETCore.UniversalWindowsPlatform": "5.4.0"
},
"frameworks": {
"uap10.0.10240": {}
},
"runtimes": {
"win10-arm": {},
"win10-arm-aot": {},
"win10-x86": {},
"win10-x86-aot": {},
"win10-x64": {},
"win10-x64-aot": {}
}
}

View 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));
}
}

View 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));
}
}

View 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)
"""
);
}

View 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));
}
}

View 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.");
}
}
}

View File

@@ -0,0 +1,82 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S7.Net.Protocol;
namespace S7.Net.UnitTest
{
[TestClass]
public class ConnectionRequestTest
{
[TestMethod]
public void Test_ConnectionRequest_S7_200()
{
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(TsapPair.GetDefaultTsapPair(CpuType.S7300, 0, 0)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 0, 1)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 1, 1)));
}
[TestMethod]
public void Test_ConnectionRequest_S7_400()
{
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 0, 0)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 0, 1)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 1, 1)));
}
[TestMethod]
public void Test_ConnectionRequest_S7_1200()
{
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0),
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 0, 0)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1),
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 0, 1)));
CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33),
ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 1, 1)));
}
[TestMethod]
public void Test_ConnectionRequest_S7_1500()
{
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)
{
return new byte[]
{
3, 0, 0, 22, //TPKT
17, //COTP Header Length
224, //Connect Request
0, 0, //Destination Reference
0, 46, //Source Reference
0, //Flags
193, //Parameter Code (src-tasp)
2, //Parameter Length
sourceTsap1, sourceTsap2, //Source TASP
194, //Parameter Code (dst-tasp)
2, //Parameter Length
destTsap1, destTsap2, //Destination TASP
192, //Parameter Code (tpdu-size)
1, //Parameter Length
10 //TPDU Size (2^11 = 2048)
};
}
}
}

View File

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

View File

@@ -0,0 +1,7 @@
using System.ComponentModel;
namespace System.Runtime.CompilerServices
{
[EditorBrowsable(EditorBrowsableState.Never)]
internal record IsExternalInit;
}

View File

@@ -9,6 +9,7 @@ namespace S7.Net.UnitTest.Helpers
static private byte[] DB1 = new byte[1024]; // Our DB1
static private byte[] DB2 = new byte[64000]; // Our DB2
static private byte[] DB3 = new byte[1024]; // Our DB3
static private byte[] DB4 = new byte[6] { 3, 128, 1, 0, 197, 104 }; // Our DB4
private static S7Server.TSrvCallback TheEventCallBack; // <== Static var containig the callback
private static S7Server.TSrvCallback TheReadCallBack; // <== Static var containig the callback
@@ -28,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
@@ -36,9 +37,10 @@ namespace S7.Net.UnitTest.Helpers
1, // Its number is 1 (DB1)
DB1, // Our buffer for DB1
DB1.Length); // Its size
// Do the same for DB2 and DB3
// Do the same for DB2, DB3, and DB4
Server.RegisterArea(S7Server.srvAreaDB, 2, DB2, DB2.Length);
Server.RegisterArea(S7Server.srvAreaDB, 3, DB3, DB3.Length);
Server.RegisterArea(S7Server.srvAreaDB, 4, DB4, DB4.Length);
// Exclude read event to avoid the double report
// Set the callbacks (using the static var to avoid the garbage collect)
@@ -57,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

View File

@@ -1,4 +1,6 @@

using S7.Net.Types;
namespace S7.Net.UnitTest.Helpers
{
class TestClass
@@ -35,16 +37,32 @@ namespace S7.Net.UnitTest.Helpers
/// <summary>
/// DB1.DBD4
/// </summary>
public double RealVariable { get; set; }
public double LRealVariable { get; set; }
/// <summary>
/// DB1.DBD8
/// </summary>
public int DIntVariable { get; set; }
public float RealVariable { get; set; }
/// <summary>
/// DB1.DBD12
/// </summary>
public int DIntVariable { get; set; }
/// <summary>
/// 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; }
}
}

View File

@@ -15,10 +15,12 @@ namespace S7.UnitTest.Helpers
public ushort[] UShorts { get; set; } = new ushort[2];
public int[] Ints { get; set; } = new int[2];
public double[] Doubles { get; set; } = new double[2];
public float[] Singles { get; set; } = new float[2];
public short Short { get; set; }
public ushort UShort { get; set; }
public int Int { get; set; }
public double Double { get; set; }
public float Single { get; set; }
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace S7.Net.UnitTest.Helpers
{
class TestClassInnerWithBool
{
public bool BitVariable00 { get; set; }
}
class TestClassInnerWithByte
{
public byte ByteVariable00 { get; set; }
}
class TestClassInnerWithShort
{
public short ShortVarialbe00 { get; set; }
}
class TestClassWithNestedClass
{
/// <summary>
/// DB1.DBX0.0
/// </summary>
public bool BitVariable00 { get; set; }
/// <summary>
/// DB1.DBX0.1
/// </summary>
public TestClassInnerWithBool BitVariable01 { get; set; } = new TestClassInnerWithBool();
/// <summary>
/// DB1.DBB1.0
/// </summary>
public TestClassInnerWithByte ByteVariable02 { get; set; } = new TestClassInnerWithByte();
/// <summary>
/// DB1.DBX2.0
/// </summary>
public bool BitVariable03 { get; set; }
/// <summary>
/// DB1.DBW4
/// </summary>
public TestClassInnerWithShort ShortVariable04 { get; set; } = new TestClassInnerWithShort();
}
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace S7.UnitTest.Helpers
{
class TestSmallClass
{
public bool Bool1 { get; set; }
}
}

View File

@@ -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,16 +38,33 @@ namespace S7.Net.UnitTest.Helpers
/// <summary>
/// DB1.DBD4
/// </summary>
public double RealVariable;
public double LRealVariable;
/// <summary>
/// DB1.DBD8
/// </summary>
public int DIntVariable;
public float RealVariable;
/// <summary>
/// DB1.DBD12
/// </summary>
public int DIntVariable;
/// <summary>
/// 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,3 @@
namespace S7.Net.UnitTest;
internal record RequestResponsePair(string RequestPattern, string ResponsePattern);

View 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();
}
}

View File

@@ -0,0 +1,156 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S7.Net.Types;
using System;
namespace S7.Net.UnitTest
{
[TestClass]
public class PLCAddressParsingTests
{
[TestMethod]
public void T01_ParseM2000_1()
{
DataItem dataItem = DataItem.FromAddress("M2000.1");
Assert.AreEqual(DataType.Memory, dataItem.DataType, "Wrong datatype for M2000.1");
Assert.AreEqual(0, dataItem.DB, "Wrong dbnumber for M2000.1");
Assert.AreEqual(VarType.Bit, dataItem.VarType, "Wrong vartype for M2000.1");
Assert.AreEqual(2000, dataItem.StartByteAdr, "Wrong startbyte for M2000.1");
Assert.AreEqual(1, dataItem.BitAdr, "Wrong bit for M2000.1");
}
[TestMethod]
public void T02_ParseMB200()
{
DataItem dataItem = DataItem.FromAddress("MB200");
Assert.AreEqual(DataType.Memory, dataItem.DataType, "Wrong datatype for MB200");
Assert.AreEqual(0, dataItem.DB, "Wrong dbnumber for MB200");
Assert.AreEqual(VarType.Byte, dataItem.VarType, "Wrong vartype for MB200");
Assert.AreEqual(200, dataItem.StartByteAdr, "Wrong startbyte for MB200");
Assert.AreEqual(0, dataItem.BitAdr, "Wrong bit for MB200");
}
[TestMethod]
public void T03_ParseMW200()
{
DataItem dataItem = DataItem.FromAddress("MW200");
Assert.AreEqual(DataType.Memory, dataItem.DataType, "Wrong datatype for MW200");
Assert.AreEqual(0, dataItem.DB, "Wrong dbnumber for MW200");
Assert.AreEqual(VarType.Word, dataItem.VarType, "Wrong vartype for MW200");
Assert.AreEqual(200, dataItem.StartByteAdr, "Wrong startbyte for MW200");
Assert.AreEqual(0, dataItem.BitAdr, "Wrong bit for MW200");
}
[TestMethod]
public void T04_ParseMD200()
{
DataItem dataItem = DataItem.FromAddress("MD200");
Assert.AreEqual(DataType.Memory, dataItem.DataType, "Wrong datatype for MD200");
Assert.AreEqual(0, dataItem.DB, "Wrong dbnumber for MD200");
Assert.AreEqual(VarType.DWord, dataItem.VarType, "Wrong vartype for MD200");
Assert.AreEqual(200, dataItem.StartByteAdr, "Wrong startbyte for MD200");
Assert.AreEqual(0, dataItem.BitAdr, "Wrong bit for MD200");
}
[TestMethod]
public void T05_ParseI2000_1()
{
DataItem dataItem = DataItem.FromAddress("I2000.1");
Assert.AreEqual(DataType.Input, dataItem.DataType, "Wrong datatype for I2000.1");
Assert.AreEqual(0, dataItem.DB, "Wrong dbnumber for I2000.1");
Assert.AreEqual(VarType.Bit, dataItem.VarType, "Wrong vartype for I2000.1");
Assert.AreEqual(2000, dataItem.StartByteAdr, "Wrong startbyte for I2000.1");
Assert.AreEqual(1, dataItem.BitAdr, "Wrong bit for I2000.1");
}
[TestMethod]
public void T06_ParseIB200()
{
DataItem dataItem = DataItem.FromAddress("IB200");
Assert.AreEqual(DataType.Input, dataItem.DataType, "Wrong datatype for IB200");
Assert.AreEqual(0, dataItem.DB, "Wrong dbnumber for IB200");
Assert.AreEqual(VarType.Byte, dataItem.VarType, "Wrong vartype for IB200");
Assert.AreEqual(200, dataItem.StartByteAdr, "Wrong startbyte for IB200");
Assert.AreEqual(0, dataItem.BitAdr, "Wrong bit for IB200");
}
[TestMethod]
public void T07_ParseIW200()
{
DataItem dataItem = DataItem.FromAddress("IW200");
Assert.AreEqual(DataType.Input, dataItem.DataType, "Wrong datatype for IW200");
Assert.AreEqual(0, dataItem.DB, "Wrong dbnumber for IW200");
Assert.AreEqual(VarType.Word, dataItem.VarType, "Wrong vartype for IW200");
Assert.AreEqual(200, dataItem.StartByteAdr, "Wrong startbyte for IW200");
Assert.AreEqual(0, dataItem.BitAdr, "Wrong bit for IW200");
}
[TestMethod]
public void T08_ParseID200()
{
DataItem dataItem = DataItem.FromAddress("ID200");
Assert.AreEqual(DataType.Input, dataItem.DataType, "Wrong datatype for ID200");
Assert.AreEqual(0, dataItem.DB, "Wrong dbnumber for ID200");
Assert.AreEqual(VarType.DWord, dataItem.VarType, "Wrong vartype for ID200");
Assert.AreEqual(200, dataItem.StartByteAdr, "Wrong startbyte for ID200");
Assert.AreEqual(0, dataItem.BitAdr, "Wrong bit for ID200");
}
[TestMethod]
public void T09_ParseQ2000_1()
{
DataItem dataItem = DataItem.FromAddress("Q2000.1");
Assert.AreEqual(DataType.Output, dataItem.DataType, "Wrong datatype for Q2000.1");
Assert.AreEqual(0, dataItem.DB, "Wrong dbnumber for Q2000.1");
Assert.AreEqual(VarType.Bit, dataItem.VarType, "Wrong vartype for Q2000.1");
Assert.AreEqual(2000, dataItem.StartByteAdr, "Wrong startbyte for Q2000.1");
Assert.AreEqual(1, dataItem.BitAdr, "Wrong bit for Q2000.1");
}
[TestMethod]
public void T10_ParseQB200()
{
DataItem dataItem = DataItem.FromAddress("QB200");
Assert.AreEqual(DataType.Output, dataItem.DataType, "Wrong datatype for QB200");
Assert.AreEqual(0, dataItem.DB, "Wrong dbnumber for QB200");
Assert.AreEqual(VarType.Byte, dataItem.VarType, "Wrong vartype for QB200");
Assert.AreEqual(200, dataItem.StartByteAdr, "Wrong startbyte for QB200");
Assert.AreEqual(0, dataItem.BitAdr, "Wrong bit for QB200");
}
[TestMethod]
public void T11_ParseQW200()
{
DataItem dataItem = DataItem.FromAddress("QW200");
Assert.AreEqual(DataType.Output, dataItem.DataType, "Wrong datatype for QW200");
Assert.AreEqual(0, dataItem.DB, "Wrong dbnumber for QW200");
Assert.AreEqual(VarType.Word, dataItem.VarType, "Wrong vartype for QW200");
Assert.AreEqual(200, dataItem.StartByteAdr, "Wrong startbyte for QW200");
Assert.AreEqual(0, dataItem.BitAdr, "Wrong bit for QW200");
}
[TestMethod]
public void T12_ParseQD200()
{
DataItem dataItem = DataItem.FromAddress("QD200");
Assert.AreEqual(DataType.Output, dataItem.DataType, "Wrong datatype for QD200");
Assert.AreEqual(0, dataItem.DB, "Wrong dbnumber for QD200");
Assert.AreEqual(VarType.DWord, dataItem.VarType, "Wrong vartype for QD200");
Assert.AreEqual(200, dataItem.StartByteAdr, "Wrong startbyte for QD200");
Assert.AreEqual(0, dataItem.BitAdr, "Wrong bit for QD200");
}
}
}

View File

@@ -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")]

Binary file not shown.

View File

@@ -0,0 +1,85 @@
using System;
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 async Task TPKT_Read()
{
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd"));
var t = await TPKT.ReadAsync(m, TestContext.CancellationTokenSource.Token);
Assert.AreEqual(0x03, t.Version);
Assert.AreEqual(0x29, t.Length);
}
[TestMethod]
[ExpectedException(typeof(TPKTInvalidException))]
public async Task TPKT_ReadShort()
{
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff040080"));
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, TestContext.CancellationTokenSource.Token);
}
[TestMethod]
public async Task COTP_ReadTSDU()
{
var expected = StringToByteArray("320700000400000800080001120411440100ff09000400000000");
var m = new MemoryStream(StringToByteArray("0300000702f0000300000702f0000300002102f080320700000400000800080001120411440100ff09000400000000"));
var t = await COTP.TSDU.ReadAsync(m, TestContext.CancellationTokenSource.Token);
Assert.IsTrue(expected.SequenceEqual(t));
}
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);
}
}
}
}
}

View File

@@ -1,108 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<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>
<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.UnitTest</RootNamespace>
<AssemblyName>S7Net.UnitTest</AssemblyName>
<TargetFrameworkVersion>v3.5</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>
</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>
</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>
<LangVersion>latest</LangVersion>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
<IsPackable>false</IsPackable>
<Copyright>Copyright © 2014</Copyright>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<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>
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework" />
</ItemGroup>
</Otherwise>
</Choose>
<ItemGroup>
<Compile Include="ConvertersUnitTest.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="Snap7\snap7.net.cs" />
<Compile Include="Helpers\TestClass.cs" />
<Compile Include="Helpers\TestStruct.cs" />
<Compile Include="S7NetTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Helpers\TestLongStruct.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>
<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>

File diff suppressed because it is too large Load Diff

View File

@@ -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
@@ -30,13 +31,19 @@ using S7.UnitTest.Helpers;
* server has problems.
*
*/
//This file contains tests for the synchronous methods
#pragma warning disable CS0618
namespace S7.Net.UnitTest
{
[TestClass]
public class S7NetTests
public partial class S7NetTests : IDisposable
{
#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
@@ -49,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();
}
@@ -78,8 +88,14 @@ namespace S7.Net.UnitTest
{
if (plc.IsConnected == false)
{
var error = plc.Open();
Assert.AreEqual(ErrorCode.NoError, error, "If you have s7 installed you must close s7oiehsx64 service.");
try
{
plc.Open();
}
catch (Exception e)
{
throw new Exception("If you have s7 installed you must close s7oiehsx64 service.", e);
}
}
}
@@ -136,19 +152,24 @@ 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.687;
plc.Write("DB1.DBD40", val.ConvertToUInt());
double result = ((uint)plc.Read("DB1.DBD40")).ConvertToDouble();
Assert.AreEqual(val, Math.Round(result, 3)); // 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;
plc.Write("DB1.DBD40", val2.ConvertToUInt());
float result2 = ((uint)plc.Read("DB1.DBD40")).ConvertToFloat();
Assert.AreEqual(val2, result2);
float val3 = 12.34567f;
plc.Write("DB1.DBD40", val3.ConvertToUInt());
float result3 = ((uint)plc.Read("DB1.DBD40")).ConvertToFloat();
Assert.AreEqual(val3, result3);
}
/// <summary>
@@ -164,8 +185,12 @@ namespace S7.Net.UnitTest
tc.BitVariable10 = true;
tc.DIntVariable = -100000;
tc.IntVariable = -15000;
tc.RealVariable = -154.789;
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
@@ -174,8 +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(tc.RealVariable, Math.Round(tc2.RealVariable, 3));
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>
@@ -191,8 +219,12 @@ namespace S7.Net.UnitTest
tc.BitVariable10 = true;
tc.DIntVariable = -100000;
tc.IntVariable = -15000;
tc.RealVariable = -154.789;
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);
@@ -200,8 +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.RealVariable, Math.Round(tc2.RealVariable, 3));
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>
@@ -238,30 +273,28 @@ namespace S7.Net.UnitTest
tc.IntVariable110 = 200;
tc.IntVariable111 = 201;
plc.WriteStruct(tc, DB2);
Assert.AreEqual(ErrorCode.NoError, plc.LastErrorCode);
// Values that are read from a struct are stored in a new struct, returned by the funcion ReadStruct
TestLongStruct tc2 = (TestLongStruct)plc.ReadStruct(typeof(TestLongStruct), DB2);
Assert.AreEqual(ErrorCode.NoError, plc.LastErrorCode);
Assert.AreEqual( tc.IntVariable0, tc2.IntVariable0 );
Assert.AreEqual( tc.IntVariable1, tc2.IntVariable1 );
Assert.AreEqual( tc.IntVariable10, tc2.IntVariable10);
Assert.AreEqual( tc.IntVariable11, tc2.IntVariable11);
Assert.AreEqual( tc.IntVariable20, tc2.IntVariable20);
Assert.AreEqual( tc.IntVariable21, tc2.IntVariable21);
Assert.AreEqual( tc.IntVariable30, tc2.IntVariable30);
Assert.AreEqual( tc.IntVariable31, tc2.IntVariable31);
Assert.AreEqual( tc.IntVariable40, tc2.IntVariable40);
Assert.AreEqual( tc.IntVariable41, tc2.IntVariable41);
Assert.AreEqual( tc.IntVariable50, tc2.IntVariable50);
Assert.AreEqual( tc.IntVariable51, tc2.IntVariable51);
Assert.AreEqual( tc.IntVariable60, tc2.IntVariable60);
Assert.AreEqual( tc.IntVariable61, tc2.IntVariable61);
Assert.AreEqual( tc.IntVariable70, tc2.IntVariable70);
Assert.AreEqual( tc.IntVariable71, tc2.IntVariable71);
Assert.AreEqual( tc.IntVariable80, tc2.IntVariable80);
Assert.AreEqual( tc.IntVariable81, tc2.IntVariable81);
Assert.AreEqual( tc.IntVariable90, tc2.IntVariable90);
Assert.AreEqual(tc.IntVariable91, tc2.IntVariable91);
Assert.AreEqual(tc.IntVariable0, tc2.IntVariable0);
Assert.AreEqual(tc.IntVariable1, tc2.IntVariable1);
Assert.AreEqual(tc.IntVariable10, tc2.IntVariable10);
Assert.AreEqual(tc.IntVariable11, tc2.IntVariable11);
Assert.AreEqual(tc.IntVariable20, tc2.IntVariable20);
Assert.AreEqual(tc.IntVariable21, tc2.IntVariable21);
Assert.AreEqual(tc.IntVariable30, tc2.IntVariable30);
Assert.AreEqual(tc.IntVariable31, tc2.IntVariable31);
Assert.AreEqual(tc.IntVariable40, tc2.IntVariable40);
Assert.AreEqual(tc.IntVariable41, tc2.IntVariable41);
Assert.AreEqual(tc.IntVariable50, tc2.IntVariable50);
Assert.AreEqual(tc.IntVariable51, tc2.IntVariable51);
Assert.AreEqual(tc.IntVariable60, tc2.IntVariable60);
Assert.AreEqual(tc.IntVariable61, tc2.IntVariable61);
Assert.AreEqual(tc.IntVariable70, tc2.IntVariable70);
Assert.AreEqual(tc.IntVariable71, tc2.IntVariable71);
Assert.AreEqual(tc.IntVariable80, tc2.IntVariable80);
Assert.AreEqual(tc.IntVariable81, tc2.IntVariable81);
Assert.AreEqual(tc.IntVariable90, tc2.IntVariable90);
Assert.AreEqual(tc.IntVariable91, tc2.IntVariable91);
Assert.AreEqual(tc.IntVariable100, tc2.IntVariable100);
Assert.AreEqual(tc.IntVariable101, tc2.IntVariable101);
Assert.AreEqual(tc.IntVariable110, tc2.IntVariable110);
@@ -302,11 +335,9 @@ namespace S7.Net.UnitTest
tc.IntVariable110 = 200;
tc.IntVariable111 = 201;
plc.WriteClass(tc, DB2);
Assert.AreEqual(ErrorCode.NoError, plc.LastErrorCode);
// Values that are read from a struct are stored in a new struct, returned by the funcion ReadStruct
TestLongClass tc2 = new TestLongClass();
plc.ReadClass(tc2, DB2);
Assert.AreEqual(ErrorCode.NoError, plc.LastErrorCode);
Assert.AreEqual(tc.IntVariable0, tc2.IntVariable0);
Assert.AreEqual(tc.IntVariable1, tc2.IntVariable1);
Assert.AreEqual(tc.IntVariable10, tc2.IntVariable10);
@@ -386,19 +417,51 @@ namespace S7.Net.UnitTest
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
ushort val = 16384;
plc.Write("DB2.DBW16384", val);
ushort result = (ushort)plc.Read("DB2.DBW16384");
Assert.AreEqual(val, result, "A ushort goes from 0 to 64512");
bool val = true;
plc.Write("DB2.DBX0.5", val);
bool result = (bool)plc.Read("DB2.DBX0.5");
Assert.AreEqual(val, result);
ushort val2 = 129;
plc.Write("DB2.DBW16", val2);
ushort result2 = (ushort)plc.Read("DB2.DBW16");
Assert.AreEqual(val2, result2, "A ushort goes from 0 to 64512");
ushort val1 = 16384;
plc.Write("DB2.DBW16384", val1);
ushort result1 = (ushort)plc.Read("DB2.DBW16384");
Assert.AreEqual(val1, result1, "A ushort goes from 0 to 64512");
bool val2 = true;
plc.Write("DB2.DBX8192.7", val2);
bool result2 = (bool)plc.Read("DB2.DBX8192.7");
Assert.AreEqual(val2, result2);
ushort val3 = 129;
plc.Write("DB2.DBW16", val3);
ushort result3 = (ushort)plc.Read("DB2.DBW16");
Assert.AreEqual(val3, result3, "A ushort goes from 0 to 64512");
byte[] val4 = new byte[] { 0x12, 0x34 };
plc.Write("DB2.DBB2048", val4[0]);
plc.Write("DB2.DBB2049", val4[1]);
byte result4b0 = (byte)plc.Read("DB2.DBB2048");
byte result4b1 = (byte)plc.Read("DB2.DBB2049");
Assert.AreEqual(val4[0], result4b0);
Assert.AreEqual(val4[1], result4b1);
bool val6 = true;
plc.Write("DB2.DBX16384.6", val6);
bool result6 = (bool)plc.Read("DB2.DBX16384.6");
Assert.AreEqual(val6, result6);
var dataItems = new List<DataItem>()
{
new DataItem
{
Count = 1,
DataType = DataType.DataBlock,
DB = 2,
StartByteAdr = 0,
BitAdr = 5,
VarType = VarType.Bit
}
,new DataItem
{
Count = 1,
DataType = DataType.DataBlock,
@@ -407,19 +470,61 @@ namespace S7.Net.UnitTest
VarType = VarType.Word
},
new DataItem
{
Count = 1,
DataType = DataType.DataBlock,
DB = 2,
StartByteAdr = 8192,
BitAdr = 7,
VarType = VarType.Bit
},
new DataItem
{
Count = 1,
DataType = DataType.DataBlock,
DB = 2,
StartByteAdr = 16,
VarType = VarType.Word
}
},
// single byte
new DataItem
{
Count = 1,
DataType = DataType.DataBlock,
DB = 2,
StartByteAdr = 2048,
VarType = VarType.Byte
},
// multiple bytes
new DataItem
{
Count = 2,
DataType = DataType.DataBlock,
DB = 2,
StartByteAdr = 2048,
VarType = VarType.Byte
},
new DataItem
{
Count = 1,
DataType = DataType.DataBlock,
DB = 2,
StartByteAdr = 16384,
BitAdr = 6,
VarType = VarType.Bit
},
};
plc.ReadMultipleVars(dataItems);
Assert.AreEqual(dataItems[0].Value, val);
Assert.AreEqual(dataItems[1].Value, val2);
Assert.AreEqual(dataItems[1].Value, val1);
Assert.AreEqual(dataItems[2].Value, val2);
Assert.AreEqual(dataItems[3].Value, val3);
Assert.AreEqual(dataItems[4].Value, val4[0]);
Assert.AreEqual(((byte[])dataItems[5].Value)[0], val4[0]); //dataItem[5].Value should be byte[2]
Assert.AreEqual(((byte[])dataItems[5].Value)[1], val4[1]);
Assert.AreEqual(dataItems[6].Value, val6);
}
/// <summary>
@@ -479,8 +584,11 @@ namespace S7.Net.UnitTest
tc.BitVariable10 = true;
tc.DIntVariable = -100000;
tc.IntVariable = -15000;
tc.RealVariable = -154.789;
tc.LRealVariable = -154.789;
tc.RealVariable = -154.789f;
tc.DWordVariable = 850;
tc.WStringVariable = "ÄÜÉÊéà";
tc.StringVariable = "Hallo";
plc.WriteClass(tc, DB2);
@@ -491,7 +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(tc.RealVariable, Math.Round(tc2.RealVariable, 3));
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);
@@ -501,19 +610,14 @@ namespace S7.Net.UnitTest
}
[TestMethod]
public void T13_ReadBytesReturnsEmptyArrayIfPlcIsNotConnected()
[TestMethod, ExpectedException(typeof(PlcException))]
public void T13_ReadBytesThrowsIfPlcIsNotConnected()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
Assert.IsFalse(notConnectedPlc.IsConnected);
int expectedReadBytes = 0; // 0 bytes, because no connection was established
TestClass tc = new TestClass();
int actualReadBytes = notConnectedPlc.ReadClass(tc, DB2);
Assert.AreEqual(expectedReadBytes, actualReadBytes);
}
}
@@ -527,8 +631,11 @@ namespace S7.Net.UnitTest
tc.BitVariable10 = true;
tc.DIntVariable = -100000;
tc.IntVariable = -15000;
tc.RealVariable = -154.789;
tc.LRealVariable = -154.789;
tc.RealVariable = -154.789f;
tc.DWordVariable = 850;
tc.WStringVariable = "ÄÜÉÊéà";
tc.StringVariable = "Hallo";
plc.WriteClass(tc, DB2);
@@ -541,12 +648,15 @@ 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.RealVariable, 3), Math.Round(tc2Generic.RealVariable, 3));
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]
public void T15_ReadClassWithGenericReturnsNullIfPlcIsNotConnected()
[TestMethod, ExpectedException(typeof(PlcException))]
public void T15_ReadClassWithGenericThrowsIfPlcIsNotConnected()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
@@ -568,8 +678,11 @@ namespace S7.Net.UnitTest
tc.BitVariable10 = true;
tc.DIntVariable = -100000;
tc.IntVariable = -15000;
tc.RealVariable = -154.789;
tc.LRealVariable = -154.789;
tc.RealVariable = -154.789f;
tc.DWordVariable = 850;
tc.WStringVariable = "ÄÜÉÊéà";
tc.StringVariable = "Hallo";
plc.WriteClass(tc, DB2);
@@ -581,12 +694,15 @@ 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.RealVariable, 3), Math.Round(tc2GenericWithClassFactory.RealVariable, 3));
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]
public void T17_ReadClassWithGenericAndClassFactoryReturnsNullIfPlcIsNotConnected()
[TestMethod, ExpectedException(typeof(PlcException))]
public void T17_ReadClassWithGenericAndClassFactoryThrowsIfPlcIsNotConnected()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
@@ -599,7 +715,102 @@ namespace S7.Net.UnitTest
}
[TestMethod]
public void T18_ReadStructReturnsNullIfPlcIsNotConnected()
public void T31_ReadClassWithNestedClassAfterBit()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
Assert.AreEqual(6, Types.Class.GetClassSize(new TestClassWithNestedClass()));
TestClassWithNestedClass tc = new TestClassWithNestedClass();
tc.BitVariable00 = true;
tc.BitVariable01.BitVariable00 = true;
tc.ByteVariable02.ByteVariable00 = 128;
tc.BitVariable03 = true;
tc.ShortVariable04.ShortVarialbe00 = -15000;
TestClassWithNestedClass tc2 = new TestClassWithNestedClass();
plc.ReadClass(tc2, DB4);
Assert.AreEqual(tc.BitVariable00, tc2.BitVariable00);
Assert.AreEqual(tc.BitVariable01.BitVariable00, tc2.BitVariable01.BitVariable00);
Assert.AreEqual(tc.ByteVariable02.ByteVariable00, tc2.ByteVariable02.ByteVariable00);
Assert.AreEqual(tc.BitVariable03, tc2.BitVariable03);
Assert.AreEqual(tc.ShortVariable04.ShortVarialbe00, tc2.ShortVariable04.ShortVarialbe00);
}
[TestMethod]
public void T32_ReadAndWriteNestedClass()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestClassWithNestedClass tc = new TestClassWithNestedClass
{
BitVariable00 = true,
BitVariable01 = new TestClassInnerWithBool { BitVariable00 = true },
ByteVariable02 = new TestClassInnerWithByte { ByteVariable00 = 128 },
BitVariable03 = true,
ShortVariable04 = new TestClassInnerWithShort { ShortVarialbe00 = -15000 }
};
plc.WriteClass(tc, DB4);
TestClassWithNestedClass tc2 = new TestClassWithNestedClass();
// Values that are read from a class are stored inside the class itself, that is passed by reference
plc.ReadClass(tc2, DB4);
Assert.AreEqual(tc.BitVariable00, tc2.BitVariable00);
Assert.AreEqual(tc.BitVariable01.BitVariable00, tc2.BitVariable01.BitVariable00);
Assert.AreEqual(tc.ByteVariable02.ByteVariable00, tc2.ByteVariable02.ByteVariable00);
Assert.AreEqual(tc.BitVariable03, tc2.BitVariable03);
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()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
@@ -621,8 +832,11 @@ namespace S7.Net.UnitTest
ts.BitVariable10 = true;
ts.DIntVariable = -100000;
ts.IntVariable = -15000;
ts.RealVariable = -154.789;
ts.LRealVariable = -154.789;
ts.RealVariable = -154.789f;
ts.DWordVariable = 850;
ts.WStringVariable = "ÄÜÉÊéà";
ts.StringVariable = "Hallo";
plc.WriteStruct(ts, DB2);
@@ -634,12 +848,15 @@ 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.RealVariable, 3), Math.Round(ts2Generic.RealVariable, 3));
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]
public void T20_ReadStructWithGenericReturnsNullIfPlcIsNotConnected()
[TestMethod, ExpectedException(typeof(PlcException))]
public void T20_ReadStructThrowsIfPlcIsNotConnected()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
@@ -664,11 +881,15 @@ namespace S7.Net.UnitTest
tc.BitVariable10 = true;
tc.DIntVariable = -100000;
tc.IntVariable = -15000;
tc.RealVariable = -154.789;
tc.LRealVariable = -154.789;
tc.RealVariable = -154.789f;
tc.DWordVariable = 850;
tc.WStringVariable = "ÄÜÉÊéà";
tc.StringVariable = "Hallo";
plc.WriteClass(tc, DB2);
int expectedReadBytes = Types.Class.GetClassSize(tc);
int expectedReadBytes = (int)Types.Class.GetClassSize(tc);
TestClass tc2 = new TestClass();
// Values that are read from a class are stored inside the class itself, that is passed by reference
@@ -676,7 +897,7 @@ namespace S7.Net.UnitTest
Assert.AreEqual(expectedReadBytes, actualReadBytes);
}
[TestMethod]
public void T22_ReadClassWithArray()
{
@@ -694,6 +915,9 @@ namespace S7.Net.UnitTest
tc.Double = float.MinValue;
tc.Doubles[0] = float.MinValue + 1;
tc.Doubles[1] = float.MaxValue;
tc.Single = float.MinValue;
tc.Singles[0] = float.MinValue + 1;
tc.Singles[1] = float.MaxValue;
tc.UShort = ushort.MinValue + 1;
tc.UShorts[0] = ushort.MinValue + 1;
tc.UShorts[1] = ushort.MaxValue;
@@ -717,6 +941,10 @@ namespace S7.Net.UnitTest
Assert.AreEqual(tc.Doubles[0], tc2.Doubles[0]);
Assert.AreEqual(tc.Doubles[1], tc2.Doubles[1]);
Assert.AreEqual(tc.Single, tc2.Single);
Assert.AreEqual(tc.Singles[0], tc2.Singles[0]);
Assert.AreEqual(tc.Singles[1], tc2.Singles[1]);
Assert.AreEqual(tc.UShort, tc2.UShort);
Assert.AreEqual(tc.UShorts[0], tc2.UShorts[0]);
Assert.AreEqual(tc.UShorts[1], tc2.UShorts[1]);
@@ -755,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]
@@ -763,31 +998,163 @@ 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);
}
#endregion
[TestMethod]
public void T26_ReadWriteDouble()
{
double test_value = 55.66;
plc.Write(DataType.DataBlock, 1, 0, test_value);
var result = (double)plc.Read(DataType.DataBlock, 1, 0, VarType.LReal, 1);
Assert.AreEqual(test_value, result, "Compare Write/Read");
}
[TestMethod]
public void T27_ReadWriteBytesMany()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
var count = 2000;
var dataItems = new List<byte>();
for (int i = 0; i < count; i++)
{
dataItems.Add((byte)(i % 256));
}
plc.WriteBytes(DataType.DataBlock, 2, 0, dataItems.ToArray());
var res = plc.ReadBytes(DataType.DataBlock, 2, 0, count);
for (int x = 0; x < count; x++)
{
Assert.AreEqual(x % 256, res[x], $"Mismatch at offset {x}, expected {x % 256}, actual {res[x]}.");
}
}
#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()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
var tc = new TestSmallClass
{
Bool1 = true
};
plc.WriteClass(tc, DB2);
var tc2 = plc.ReadClass<TestSmallClass>(DB2);
Assert.AreEqual(tc.Bool1, tc2.Bool1);
}
[TestMethod, ExpectedException(typeof(PlcException))]
public void T29_Read_Write_ThrowsWhenPlcIsNotReachable()
{
// leave plc Open
S7TestServer.Stop();
double test_value = 55.66;
plc.Write("DB1.DBD0", test_value);
var helper = plc.Read("DB1.DBD0");
Assert.AreEqual(helper, null, "Value in Read.");
}
[TestMethod]
public void T30_ReadWriteSingle()
{
float test_value = 55.6632f;
plc.Write("DB1.DBD0", test_value);
var helper = plc.Read("DB1.DBD0");
float test_value2 = Conversion.ConvertToFloat((uint)helper);
Assert.AreEqual(test_value, test_value2, "Compare Write/Read"); //No delta, datatype matches
}
[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()
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
try
if (!disposedValue)
{
ServiceController sc = new ServiceController("s7oiehsx64");
switch (sc.Status)
if (disposing)
{
case ServiceControllerStatus.Running:
sc.Stop();
break;
plc.Close();
}
// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
disposedValue = true;
}
catch { } // service not found
}
// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~S7NetTests() {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// TODO: uncomment the following line if the finalizer is overridden above.
// GC.SuppressFinalize(this);
}
#endregion
#endregion
}
}

View File

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

View 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));
}
}
}

View 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));
}
}
}

View File

@@ -0,0 +1,51 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S7.Net.Types;
namespace S7.Net.UnitTest.TypeTests
{
[TestClass]
public class ClassTests
{
[TestMethod]
public void GetClassSizeTest()
{
Assert.AreEqual(Class.GetClassSize(new TestClassUnevenSize(1, 1)), 6);
Assert.AreEqual(Class.GetClassSize(new TestClassUnevenSize(2, 15)), 6);
Assert.AreEqual(Class.GetClassSize(new TestClassUnevenSize(2, 16)), 6);
Assert.AreEqual(Class.GetClassSize(new TestClassUnevenSize(2, 17)), 8);
Assert.AreEqual(Class.GetClassSize(new TestClassUnevenSize(3, 15)), 8);
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; }
public byte[] Bytes { get; set; }
public bool[] Bools { get; set; }
public TestClassUnevenSize(int byteCount, int bitCount)
{
Bytes = new byte[byteCount];
Bools = new bool[bitCount];
}
}
private class TestUint32
{
public uint Value1 { get; set; }
}
}
}

View 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));
}
}
}
}

View File

@@ -0,0 +1,176 @@
using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace S7.Net.UnitTest.TypeTests
{
public static class DateTimeTests
{
private static readonly DateTime SampleDateTime = new DateTime(1993, 12, 25, 8, 12, 34, 567);
private static readonly byte[] SampleByteArray = {0x93, 0x12, 0x25, 0x08, 0x12, 0x34, 0x56, 7 << 4 | 7};
private static readonly byte[] SpecMinByteArray =
{
0x90, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, (byte) (int) (Types.DateTime.SpecMinimumDateTime.DayOfWeek + 1)
};
private static readonly byte[] SpecMaxByteArray =
{
0x89, 0x12, 0x31, 0x23, 0x59, 0x59, 0x99, (byte) (9 << 4 | (int) (Types.DateTime.SpecMaximumDateTime.DayOfWeek + 1))
};
[TestClass]
public class FromByteArray
{
[TestMethod]
public void Sample()
{
AssertFromByteArrayEquals(SampleDateTime, SampleByteArray);
}
[TestMethod]
public void SpecMinimum()
{
AssertFromByteArrayEquals(Types.DateTime.SpecMinimumDateTime, SpecMinByteArray);
}
[TestMethod]
public void SpecMaximum()
{
AssertFromByteArrayEquals(Types.DateTime.SpecMaximumDateTime, SpecMaxByteArray);
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnLessThan8Bytes()
{
Types.DateTime.FromByteArray(new byte[7]);
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnMoreTHan8Bytes()
{
Types.DateTime.FromByteArray(new byte[9]);
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnInvalidYear()
{
Types.DateTime.FromByteArray(MutateSample(0, 0xa0));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnZeroMonth()
{
Types.DateTime.FromByteArray(MutateSample(1, 0x00));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnTooLargeMonth()
{
Types.DateTime.FromByteArray(MutateSample(1, 0x13));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnZeroDay()
{
Types.DateTime.FromByteArray(MutateSample(2, 0x00));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnTooLargeDay()
{
Types.DateTime.FromByteArray(MutateSample(2, 0x32));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnInvalidHour()
{
Types.DateTime.FromByteArray(MutateSample(3, 0x24));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnInvalidMinute()
{
Types.DateTime.FromByteArray(MutateSample(4, 0x60));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnInvalidSecond()
{
Types.DateTime.FromByteArray(MutateSample(5, 0x60));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnInvalidFirstTwoMillisecondDigits()
{
Types.DateTime.FromByteArray(MutateSample(6, 0xa0));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnInvalidThirdMillisecondDigit()
{
Types.DateTime.FromByteArray(MutateSample(7, 10 << 4));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnZeroDayOfWeek()
{
Types.DateTime.FromByteArray(MutateSample(7, 0));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnTooLargeDayOfWeek()
{
Types.DateTime.FromByteArray(MutateSample(7, 8));
}
private static void AssertFromByteArrayEquals(DateTime expected, params byte[] bytes)
{
Assert.AreEqual(expected, Types.DateTime.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.DateTime.SpecMinimumDateTime, SpecMinByteArray);
}
[TestMethod]
public void SpecMaximum()
{
AssertToByteArrayEquals(Types.DateTime.SpecMaximumDateTime, SpecMaxByteArray);
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnTimeBeforeSpecMinimum()
{
Types.DateTime.ToByteArray(new DateTime(1970, 1, 1));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnTimeAfterSpecMaximum()
{
Types.DateTime.ToByteArray(new DateTime(2090, 1, 1));
}
private static void AssertToByteArrayEquals(DateTime value, params byte[] expected)
{
CollectionAssert.AreEqual(expected, Types.DateTime.ToByteArray(value));
}
}
}
}

View 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);
}
}
}

View 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);
}
}
}

View File

@@ -0,0 +1,85 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S7.Net.Types;
namespace S7.Net.UnitTest.TypeTests
{
[TestClass]
public class StringTests
{
[TestMethod]
public void WriteNullWIthReservedLengthZero()
{
AssertToByteArrayEquals(null, 0);
}
[TestMethod]
public void WriteEmptyStringWithReservedLengthZero()
{
AssertToByteArrayEquals("", 0);
}
[TestMethod]
public void WriteAWithReservedLengthZero()
{
AssertToByteArrayEquals("A", 0);
}
[TestMethod]
public void WriteNullWithReservedLengthOne()
{
AssertToByteArrayEquals(null, 1, 0);
}
[TestMethod]
public void WriteEmptyStringWithReservedLengthOne()
{
AssertToByteArrayEquals("", 1, 0);
}
[TestMethod]
public void WriteAWithReservedLengthOne()
{
AssertToByteArrayEquals("A", 1, (byte) 'A');
}
[TestMethod]
public void WriteAWithReservedLengthTwo()
{
AssertToByteArrayEquals("A", 2, (byte) 'A', 0);
}
[TestMethod]
public void WriteAbcWithReservedLengthOne()
{
AssertToByteArrayEquals("Abc", 1, (byte) 'A');
}
[TestMethod]
public void WriteAbcWithReservedLengthTwo()
{
AssertToByteArrayEquals("Abc", 2, (byte) 'A', (byte) 'b');
}
[TestMethod]
public void WriteAbcWithReservedLengthThree()
{
AssertToByteArrayEquals("Abc", 3, (byte) 'A', (byte) 'b', (byte) 'c');
}
[TestMethod]
public void WriteAbcWithReservedLengthFour()
{
AssertToByteArrayEquals("Abc", 4, (byte) 'A', (byte) 'b', (byte) 'c', 0);
}
private static void AssertFromByteArrayEquals(string expected, params byte[] bytes)
{
Assert.AreEqual(expected, String.FromByteArray(bytes));
}
private static void AssertToByteArrayEquals(string value, int reservedLength, params byte[] expected)
{
CollectionAssert.AreEqual(expected, String.ToByteArray(value, reservedLength));
}
}
}

View 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));
}
}
}
}

Binary file not shown.

Binary file not shown.

120
S7.Net/COTP.cs Normal file
View File

@@ -0,0 +1,120 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net
{
/// <summary>
/// COTP Protocol functions and types
/// </summary>
internal class COTP
{
public enum PduType : byte
{
Data = 0xf0,
ConnectionConfirmed = 0xd0
}
/// <summary>
/// Describes a COTP TPDU (Transport protocol data unit)
/// </summary>
public class TPDU
{
public TPKT TPkt { get; }
public byte HeaderLength;
public PduType PDUType;
public int TPDUNumber;
public byte[] Data;
public bool LastDataUnit;
public TPDU(TPKT tPKT)
{
TPkt = tPKT;
HeaderLength = tPKT.Data[0]; // Header length excluding this length byte
if (HeaderLength >= 2)
{
PDUType = (PduType)tPKT.Data[1];
if (PDUType == PduType.Data) //DT Data
{
var flags = tPKT.Data[2];
TPDUNumber = flags & 0x7F;
LastDataUnit = (flags & 0x80) > 0;
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
}
Data = new byte[0];
}
/// <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>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <returns>COTP DPDU instance</returns>
public static async Task<TPDU> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
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()
{
return string.Format("Length: {0} PDUType: {1} TPDUNumber: {2} Last: {3} Segment Data: {4}",
HeaderLength,
PDUType,
TPDUNumber,
LastDataUnit,
BitConverter.ToString(Data)
);
}
}
/// <summary>
/// Describes a COTP TSDU (Transport service data unit). One TSDU consist of 1 ore more TPDUs
/// </summary>
public class TSDU
{
/// <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>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <returns>Data in TSDU</returns>
public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
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];
Array.Copy(segment.Data, buffer, segment.Data.Length);
while (!segment.LastDataUnit)
{
segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
var previousLength = buffer.Length;
Array.Resize(ref buffer, buffer.Length + segment.Data.Length);
Array.Copy(segment.Data, 0, buffer, previousLength, segment.Data.Length);
}
return buffer;
}
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Net.Sockets;
namespace S7.Net
{
public static class TcpClientMixins
{
#if NETSTANDARD1_3
public static void Close(this TcpClient tcpClient)
{
tcpClient.Dispose();
}
public static void Connect(this TcpClient tcpClient, string host, int port)
{
tcpClient.ConnectAsync(host, port).GetAwaiter().GetResult();
}
#endif
}
}

View File

@@ -15,15 +15,11 @@ namespace S7.Net
/// <returns></returns>
public static int BinStringToInt32(this string txt)
{
int cnt = 0;
int ret = 0;
for (cnt = txt.Length - 1; cnt >= 0; cnt += -1)
for (int i = 0; i < txt.Length; i++)
{
if (int.Parse(txt.Substring(cnt, 1)) == 1)
{
ret += (int)(Math.Pow(2, (txt.Length - 1 - cnt)));
}
ret = (ret << 1) | ((txt[i] == '1') ? 1 : 0);
}
return ret;
}
@@ -35,20 +31,7 @@ namespace S7.Net
/// <returns></returns>
public static byte? BinStringToByte(this string txt)
{
int cnt = 0;
int ret = 0;
if (txt.Length == 8)
{
for (cnt = 7; cnt >= 0; cnt += -1)
{
if (int.Parse(txt.Substring(cnt, 1)) == 1)
{
ret += (int)(Math.Pow(2, (txt.Length - 1 - cnt)));
}
}
return (byte)ret;
}
if (txt.Length == 8) return (byte)BinStringToInt32(txt);
return null;
}
@@ -147,7 +130,7 @@ namespace S7.Net
}
return txt;
}
catch
catch
{
return "";
}
@@ -155,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>
@@ -217,26 +240,26 @@ namespace S7.Net
}
/// <summary>
/// Converts from double to DWord (DBD)
/// Converts from float to DWord (DBD)
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static UInt32 ConvertToUInt(this double input)
public static UInt32 ConvertToUInt(this float input)
{
uint output;
output = S7.Net.Types.DWord.FromByteArray(S7.Net.Types.Double.ToByteArray(input));
output = S7.Net.Types.DWord.FromByteArray(S7.Net.Types.Real.ToByteArray(input));
return output;
}
/// <summary>
/// Converts from DWord (DBD) to double
/// Converts from DWord (DBD) to float
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static double ConvertToDouble(this uint input)
public static float ConvertToFloat(this uint input)
{
double output;
output = S7.Net.Types.Double.FromByteArray(S7.Net.Types.DWord.ToByteArray(input));
float output;
output = S7.Net.Types.Real.FromByteArray(S7.Net.Types.DWord.ToByteArray(input));
return output;
}
}

View File

@@ -10,6 +10,16 @@
/// </summary>
S7200 = 0,
/// <summary>
/// Siemens Logo 0BA8
/// </summary>
Logo0BA8 = 1,
/// <summary>
/// S7 200 Smart
/// </summary>
S7200Smart = 2,
/// <summary>
/// S7 300 cpu type
/// </summary>
@@ -159,10 +169,25 @@
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>
/// S7 String variable type (variable)
/// </summary>
S7String,
/// <summary>
/// S7 WString variable type (variable)
/// </summary>
S7WString,
/// <summary>
/// Timer variable type
/// </summary>
@@ -171,6 +196,26 @@
/// <summary>
/// Counter variable type
/// </summary>
Counter
Counter,
/// <summary>
/// DateTIme variable type
/// </summary>
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
}
}

View 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;
}
}
}

View 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
}

View 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);
}
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
namespace S7.Net
{
#if NET_FULL
[Serializable]
#endif
public class InvalidDataException : Exception
{
public byte[] ReceivedData { get; }
public int ErrorIndex { get; }
public byte ExpectedValue { get; }
public InvalidDataException(string message, byte[] receivedData, int errorIndex, byte expectedValue)
: base(FormatMessage(message, receivedData, errorIndex, expectedValue))
{
ReceivedData = receivedData;
ErrorIndex = errorIndex;
ExpectedValue = expectedValue;
}
#if NET_FULL
protected InvalidDataException(System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
ReceivedData = (byte[]) info.GetValue(nameof(ReceivedData), typeof(byte[]));
ErrorIndex = info.GetInt32(nameof(ErrorIndex));
ExpectedValue = info.GetByte(nameof(ExpectedValue));
}
#endif
private static string FormatMessage(string message, byte[] receivedData, int errorIndex, byte expectedValue)
{
if (errorIndex >= receivedData.Length)
throw new ArgumentOutOfRangeException(nameof(errorIndex),
$"{nameof(errorIndex)} {errorIndex} is outside the bounds of {nameof(receivedData)} with length {receivedData.Length}.");
return $"{message} Invalid data received. Expected '{expectedValue}' at index {errorIndex}, " +
$"but received {receivedData[errorIndex]}. See the {nameof(ReceivedData)} property " +
"for the full message received.";
}
}
}

File diff suppressed because it is too large Load Diff

207
S7.Net/PLCAddress.cs Normal file
View File

@@ -0,0 +1,207 @@
namespace S7.Net
{
internal class PLCAddress
{
private DataType dataType;
private int dbNumber;
private int startByte;
private int bitNumber;
private VarType varType;
public DataType DataType
{
get => dataType;
set => dataType = value;
}
public int DbNumber
{
get => dbNumber;
set => dbNumber = value;
}
public int StartByte
{
get => startByte;
set => startByte = value;
}
public int BitNumber
{
get => bitNumber;
set => bitNumber = value;
}
public VarType VarType
{
get => varType;
set => varType = value;
}
public PLCAddress(string address)
{
Parse(address, out dataType, out dbNumber, out varType, out startByte, out bitNumber);
}
public static void Parse(string input, out DataType dataType, out int dbNumber, out VarType varType, out int address, out int bitNumber)
{
bitNumber = -1;
dbNumber = 0;
switch (input.Substring(0, 2))
{
case "DB":
string[] strings = input.Split(new char[] { '.' });
if (strings.Length < 2)
throw new InvalidAddressException("To few periods for DB address");
dataType = DataType.DataBlock;
dbNumber = int.Parse(strings[0].Substring(2));
address = int.Parse(strings[1].Substring(3));
string dbType = strings[1].Substring(0, 3);
switch (dbType)
{
case "DBB":
varType = VarType.Byte;
return;
case "DBW":
varType = VarType.Word;
return;
case "DBD":
varType = VarType.DWord;
return;
case "DBX":
bitNumber = int.Parse(strings[2]);
if (bitNumber > 7)
throw new InvalidAddressException("Bit can only be 0-7");
varType = VarType.Bit;
return;
default:
throw new InvalidAddressException();
}
case "IB":
case "EB":
// Input byte
dataType = DataType.Input;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Byte;
return;
case "IW":
case "EW":
// Input word
dataType = DataType.Input;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Word;
return;
case "ID":
case "ED":
// Input double-word
dataType = DataType.Input;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.DWord;
return;
case "QB":
case "AB":
case "OB":
// Output byte
dataType = DataType.Output;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Byte;
return;
case "QW":
case "AW":
case "OW":
// Output word
dataType = DataType.Output;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Word;
return;
case "QD":
case "AD":
case "OD":
// Output double-word
dataType = DataType.Output;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.DWord;
return;
case "MB":
// Memory byte
dataType = DataType.Memory;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Byte;
return;
case "MW":
// Memory word
dataType = DataType.Memory;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Word;
return;
case "MD":
// Memory double-word
dataType = DataType.Memory;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.DWord;
return;
default:
switch (input.Substring(0, 1))
{
case "E":
case "I":
// Input
dataType = DataType.Input;
varType = VarType.Bit;
break;
case "Q":
case "A":
case "O":
// Output
dataType = DataType.Output;
varType = VarType.Bit;
break;
case "M":
// Memory
dataType = DataType.Memory;
varType = VarType.Bit;
break;
case "T":
// Timer
dataType = DataType.Timer;
dbNumber = 0;
address = int.Parse(input.Substring(1));
varType = VarType.Timer;
return;
case "Z":
case "C":
// Counter
dataType = DataType.Counter;
dbNumber = 0;
address = int.Parse(input.Substring(1));
varType = VarType.Counter;
return;
default:
throw new InvalidAddressException(string.Format("{0} is not a valid address", input.Substring(0, 1)));
}
string txt2 = input.Substring(1);
if (txt2.IndexOf(".") == -1)
throw new InvalidAddressException("To few periods for DB address");
address = int.Parse(txt2.Substring(0, txt2.IndexOf(".")));
bitNumber = int.Parse(txt2.Substring(txt2.IndexOf(".") + 1));
if (bitNumber > 7)
throw new InvalidAddressException("Bit can only be 0-7");
return;
}
}
}
}

113
S7.Net/PLCExceptions.cs Normal file
View File

@@ -0,0 +1,113 @@
using System;
#if NET_FULL
using System.Runtime.Serialization;
#endif
namespace S7.Net
{
public class WrongNumberOfBytesException : Exception
{
public WrongNumberOfBytesException() : base()
{
}
public WrongNumberOfBytesException(string message) : base(message)
{
}
public WrongNumberOfBytesException(string message, Exception innerException) : base(message, innerException)
{
}
#if NET_FULL
protected WrongNumberOfBytesException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
#endif
}
public class InvalidAddressException : Exception
{
public InvalidAddressException() : base ()
{
}
public InvalidAddressException(string message) : base(message)
{
}
public InvalidAddressException(string message, Exception innerException) : base(message, innerException)
{
}
#if NET_FULL
protected InvalidAddressException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
#endif
}
public class InvalidVariableTypeException : Exception
{
public InvalidVariableTypeException() : base()
{
}
public InvalidVariableTypeException(string message) : base(message)
{
}
public InvalidVariableTypeException(string message, Exception innerException) : base(message, innerException)
{
}
#if NET_FULL
protected InvalidVariableTypeException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
#endif
}
public class TPKTInvalidException : Exception
{
public TPKTInvalidException() : base()
{
}
public TPKTInvalidException(string message) : base(message)
{
}
public TPKTInvalidException(string message, Exception innerException) : base(message, innerException)
{
}
#if NET_FULL
protected TPKTInvalidException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
#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
}
}

370
S7.Net/PLCHelpers.cs Normal file
View File

@@ -0,0 +1,370 @@
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;
namespace S7.Net
{
public partial class Plc
{
private static void WriteTpktHeader(System.IO.MemoryStream stream, int length)
{
stream.Write(new byte[] { 0x03, 0x00 });
stream.Write(Word.ToByteArray((ushort) length));
}
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>
/// 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 static void BuildReadDataRequestPackage(System.IO.MemoryStream stream, DataType dataType, int db, int startByteAdr, int count = 1)
{
//single data req = 12
stream.Write(new byte[] { 0x12, 0x0a, 0x10 });
switch (dataType)
{
case DataType.Timer:
case DataType.Counter:
stream.WriteByte((byte)dataType);
break;
default:
stream.WriteByte(0x02);
break;
}
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
stream.WriteByte((byte)overflow);
switch (dataType)
{
case DataType.Timer:
case DataType.Counter:
stream.Write(Word.ToByteArray((ushort)(startByteAdr)));
break;
default:
stream.Write(Word.ToByteArray((ushort)((startByteAdr) * 8)));
break;
}
}
/// <summary>
/// Given a S7 variable type (Bool, Word, DWord, etc.), it converts the bytes in the appropriate C# format.
/// </summary>
/// <param name="varType"></param>
/// <param name="bytes"></param>
/// <param name="varCount"></param>
/// <param name="bitAdr"></param>
/// <returns></returns>
private object? ParseBytes(VarType varType, byte[] bytes, int varCount, byte bitAdr = 0)
{
if (bytes == null || bytes.Length == 0)
return null;
switch (varType)
{
case VarType.Byte:
if (varCount == 1)
return bytes[0];
else
return bytes;
case VarType.Word:
if (varCount == 1)
return Word.FromByteArray(bytes);
else
return Word.ToArray(bytes);
case VarType.Int:
if (varCount == 1)
return Int.FromByteArray(bytes);
else
return Int.ToArray(bytes);
case VarType.DWord:
if (varCount == 1)
return DWord.FromByteArray(bytes);
else
return DWord.ToArray(bytes);
case VarType.DInt:
if (varCount == 1)
return DInt.FromByteArray(bytes);
else
return DInt.ToArray(bytes);
case VarType.Real:
if (varCount == 1)
return Types.Real.FromByteArray(bytes);
else
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.S7String:
return S7String.FromByteArray(bytes);
case VarType.S7WString:
return S7WString.FromByteArray(bytes);
case VarType.Timer:
if (varCount == 1)
return Timer.FromByteArray(bytes);
else
return Timer.ToArray(bytes);
case VarType.Counter:
if (varCount == 1)
return Counter.FromByteArray(bytes);
else
return Counter.ToArray(bytes);
case VarType.Bit:
if (varCount == 1)
{
if (bitAdr > 7)
return null;
else
return Bit.FromByte(bytes[0], bitAdr);
}
else
{
return Bit.ToBitArray(bytes, varCount);
}
case VarType.DateTime:
if (varCount == 1)
{
return DateTime.FromByteArray(bytes);
}
else
{
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;
}
}
/// <summary>
/// Given a S7 <see cref="VarType"/> (Bool, Word, DWord, etc.), it returns how many bytes to read.
/// </summary>
/// <param name="varType"></param>
/// <param name="varCount"></param>
/// <returns>Byte lenght of variable</returns>
internal static int VarTypeToByteLength(VarType varType, int varCount = 1)
{
switch (varType)
{
case VarType.Bit:
return (varCount + 7) / 8;
case VarType.Byte:
return (varCount < 1) ? 1 : varCount;
case VarType.String:
return varCount;
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;
}
}
private byte[] GetS7ConnectionSetup()
{
return new byte[] { 3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 255, 255, 0, 8, 0, 0, 240, 0, 0, 3, 0, 3,
3, 192 // Use 960 PDU size
};
}
private void ParseDataIntoDataItems(byte[] s7data, List<DataItem> dataItems)
{
int offset = 14;
foreach (var dataItem in dataItems)
{
// check for Return Code = Success
if (s7data[offset] != 0xff)
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
// to Data bytes
offset += 4;
int byteCnt = VarTypeToByteLength(dataItem.VarType, dataItem.Count);
dataItem.Value = ParseBytes(
dataItem.VarType,
s7data.Skip(offset).Take(byteCnt).ToArray(),
dataItem.Count,
dataItem.BitAdr
);
// next Item
offset += byteCnt;
// 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
View 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}.");
}
}
}

645
S7.Net/PlcAsynchronous.cs Normal file
View File

@@ -0,0 +1,645 @@
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
{
/// <summary>
/// Creates an instance of S7.Net driver
/// </summary>
public partial class Plc
{
/// <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(CancellationToken cancellationToken = default)
{
var stream = await ConnectAsync(cancellationToken).ConfigureAwait(false);
try
{
await queue.Enqueue(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
_stream = stream;
return default(object);
}).ConfigureAwait(false);
}
catch (Exception)
{
stream.Dispose();
throw;
}
}
private async Task<NetworkStream> ConnectAsync(CancellationToken cancellationToken)
{
tcpClient = new TcpClient();
ConfigureConnection();
#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);
if (s7data.Length < 20)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
// TODO: check if this should not rather be UInt16.
MaxPDUSize = s7data[18] * 256 + s7data[19];
}
/// <summary>
/// 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="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="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, CancellationToken cancellationToken = default)
{
var resultBytes = new byte[count];
await ReadBytesAsync(resultBytes, dataType, db, startByteAdr, cancellationToken).ConfigureAwait(false);
return resultBytes;
}
/// <summary>
/// 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>
/// <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="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>
/// <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, cancellationToken).ConfigureAwait(false);
return ParseBytes(varType, bytes, varCount, bitAdr);
}
/// <summary>
/// Reads a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// 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, CancellationToken cancellationToken = default)
{
var adr = new PLCAddress(variable);
return await ReadAsync(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and return an object that can be casted to the struct.
/// </summary>
/// <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, CancellationToken cancellationToken = default)
{
int numBytes = Types.Struct.GetStructSize(structType);
// now read the package
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false);
// and decode it
return Types.Struct.FromBytes(structType, resultBytes);
}
/// <summary>
/// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and returns the struct or null if nothing was read.
/// </summary>
/// <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, CancellationToken cancellationToken = default) where T : struct
{
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.
/// 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="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, CancellationToken cancellationToken = default)
{
int numBytes = (int)Class.GetClassSize(sourceClass);
if (numBytes <= 0)
{
throw new Exception("The size of the class is less than 1 byte and therefore cannot be read");
}
// now read the package
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false);
// and decode it
Class.FromBytes(sourceClass, resultBytes);
return new Tuple<int, object>(resultBytes.Length, sourceClass);
}
/// <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.
/// 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, CancellationToken cancellationToken = default) where T : class
{
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.
/// 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, CancellationToken cancellationToken = default) where T : class
{
var instance = classFactory();
var res = await ReadClassAsync(instance, db, startByteAdr, cancellationToken).ConfigureAwait(false);
int readBytes = res.Item1;
if (readBytes <= 0)
{
return null;
}
return (T)res.Item2;
}
/// <summary>
/// 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.
/// 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.</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
//replies with bigger PDU size in connection setup.
AssertPduSizeForRead(dataItems);
try
{
var dataToSend = BuildReadRequestPackage(dataItems.Select(d => DataItem.GetDataItemAddress(d)).ToList());
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
ParseDataIntoDataItems(s7data, dataItems);
}
catch (SocketException socketException)
{
throw new PlcException(ErrorCode.ReadData, socketException);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.ReadData, exc);
}
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.
/// </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 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;
while (value.Length > 0)
{
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;
}
}
/// <summary>
/// Write a single bit from a DB with the specified index.
/// </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="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, 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, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Write a single bit from a DB with the specified index.
/// </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="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, 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, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Takes in input an object and tries to parse it to an array of values. This can be used to write many data, all of the same type.
/// You must specify the memory area type, memory are address, byte start address and bytes count.
/// If the read 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 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, CancellationToken cancellationToken = default)
{
if (bitAdr != -1)
{
//Must be writing a bit value as bitAdr is specified
if (value is bool boolean)
{
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, boolean, cancellationToken).ConfigureAwait(false);
}
else if (value is int intValue)
{
if (intValue < 0 || intValue > 7)
throw new ArgumentOutOfRangeException(
string.Format(
"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, 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), cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// </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, CancellationToken cancellationToken = default)
{
var adr = new PLCAddress(variable);
await WriteAsync(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Writes a C# struct to a DB in the PLC
/// </summary>
/// <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, CancellationToken cancellationToken = default)
{
var bytes = Struct.ToBytes(structValue).ToList();
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray(), cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Writes a C# class to a DB in the PLC
/// </summary>
/// <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, CancellationToken cancellationToken = default)
{
byte[] bytes = new byte[(int)Class.GetClassSize(classValue)];
Types.Class.ToBytes(classValue, bytes);
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes, cancellationToken).ConfigureAwait(false);
}
private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, Memory<byte> buffer, CancellationToken cancellationToken)
{
var dataToSend = BuildReadRequestPackage(new[] { new DataItemAddress(dataType, db, startByteAdr, buffer.Length) });
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
AssertReadResponse(s7data, buffer.Length);
s7data.AsSpan(18, buffer.Length).CopyTo(buffer.Span);
}
/// <summary>
/// Write DataItem(s) to the PLC. Throws an exception if the response is invalid
/// or when the PLC reports errors for item(s) written.
/// </summary>
/// <param name="dataItems">The DataItem(s) to write to the PLC.</param>
/// <returns>Task that completes when response from PLC is parsed.</returns>
public async Task WriteAsync(params DataItem[] dataItems)
{
AssertPduSizeForWrite(dataItems);
var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems);
var response = await RequestTsduAsync(message.Array, 0, length).ConfigureAwait(false);
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
}
/// <summary>
/// Writes up to 200 bytes to the PLC. You must specify the memory area type, memory are address, byte start address and bytes count.
/// </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 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, ReadOnlyMemory<byte> value, CancellationToken cancellationToken)
{
try
{
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value.Span);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.WriteData, exc);
}
}
private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken)
{
try
{
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
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;
}
}
}
}

39
S7.Net/PlcException.cs Normal file
View File

@@ -0,0 +1,39 @@
using System;
namespace S7.Net
{
#if NET_FULL
[Serializable]
#endif
public class PlcException : Exception
{
public ErrorCode ErrorCode { get; }
public PlcException(ErrorCode errorCode) : this(errorCode, $"PLC communication failed with error '{errorCode}'.")
{
}
public PlcException(ErrorCode errorCode, Exception innerException) : this(errorCode, innerException.Message,
innerException)
{
}
public PlcException(ErrorCode errorCode, string message) : base(message)
{
ErrorCode = errorCode;
}
public PlcException(ErrorCode errorCode, string message, Exception inner) : base(message, inner)
{
ErrorCode = errorCode;
}
#if NET_FULL
protected PlcException(System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
ErrorCode = (ErrorCode) info.GetInt32(nameof(ErrorCode));
}
#endif
}
}

538
S7.Net/PlcSynchronous.cs Normal file
View File

@@ -0,0 +1,538 @@
using S7.Net.Types;
using System;
using System.IO;
using System.Collections.Generic;
using S7.Net.Protocol;
using S7.Net.Helper;
//Implement synchronous methods here
namespace S7.Net
{
public partial class Plc
{
/// <summary>
/// Connects to the PLC and performs a COTP ConnectionRequest and S7 CommunicationSetup.
/// </summary>
public void Open()
{
try
{
OpenAsync().GetAwaiter().GetResult();
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.ConnectionError,
$"Couldn't establish the connection to {IP}.\nMessage: {exc.Message}", exc);
}
}
/// <summary>
/// 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="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="count">Byte count, if you want to read 120 bytes, set this to 120.</param>
/// <returns>Returns the bytes in an array</returns>
public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count)
{
var result = new byte[count];
ReadBytes(result, dataType, db, startByteAdr);
return result;
}
/// <summary>
/// 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>
/// <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="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)
{
int cntBytes = VarTypeToByteLength(varType, varCount);
byte[] bytes = ReadBytes(dataType, db, startByteAdr, cntBytes);
return ParseBytes(varType, bytes, varCount, bitAdr);
}
/// <summary>
/// Reads a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// 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. 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);
}
/// <summary>
/// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and return an object that can be casted to the struct.
/// </summary>
/// <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. 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
var resultBytes = ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes);
// and decode it
return Struct.FromBytes(structType, resultBytes);
}
/// <summary>
/// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and returns the struct or null if nothing was read.
/// </summary>
/// <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>
/// <returns>Returns a nullable struct. If nothing was read null will be returned.</returns>
public T? ReadStruct<T>(int db, int startByteAdr = 0) where T : struct
{
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.
/// 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="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>
public int ReadClass(object sourceClass, int db, int startByteAdr = 0)
{
int numBytes = (int)Class.GetClassSize(sourceClass);
if (numBytes <= 0)
{
throw new Exception("The size of the class is less than 1 byte and therefore cannot be read");
}
// now read the package
var resultBytes = ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes);
// and decode it
Class.FromBytes(sourceClass, resultBytes);
return resultBytes.Length;
}
/// <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.
/// 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>
/// <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
{
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.
/// 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>
/// <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
{
var instance = classFactory();
int readBytes = ReadClass(instance, db, startByteAdr);
if (readBytes <= 0)
{
return null;
}
return instance;
}
/// <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, 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;
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 = 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;
}
}
/// <summary>
/// Write a single bit from a DB with the specified index.
/// </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="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>
public void WriteBit(DataType dataType, int db, int startByteAdr, int bitAdr, bool value)
{
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));
WriteBitWithASingleRequest(dataType, db, startByteAdr, bitAdr, value);
}
/// <summary>
/// Write a single bit to a DB with the specified index.
/// </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 write 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="bitAdr">The address of the bit. (0-7)</param>
/// <param name="value">Value to write (0 or 1).</param>
public void WriteBit(DataType dataType, int db, int startByteAdr, int bitAdr, int value)
{
if (value < 0 || value > 1)
throw new ArgumentException("Value must be 0 or 1", nameof(value));
WriteBit(dataType, db, startByteAdr, bitAdr, value == 1);
}
/// <summary>
/// Takes in input an object and tries to parse it to an array of values. This can be used to write many data, all of the same type.
/// You must specify the memory area type, memory are address, byte start address and bytes count.
/// If the read 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 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>
public void Write(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1)
{
if (bitAdr != -1)
{
//Must be writing a bit value as bitAdr is specified
if (value is bool boolean)
{
WriteBit(dataType, db, startByteAdr, bitAdr, boolean);
}
else if (value is int intValue)
{
if (intValue < 0 || intValue > 7)
throw new ArgumentOutOfRangeException(
string.Format(
"Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid",
bitAdr), nameof(bitAdr));
WriteBit(dataType, db, startByteAdr, bitAdr, intValue == 1);
}
else
throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value));
}
else WriteBytes(dataType, db, startByteAdr, Serialization.SerializeValue(value));
}
/// <summary>
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// </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>
public void Write(string variable, object value)
{
var adr = new PLCAddress(variable);
Write(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber);
}
/// <summary>
/// Writes a C# struct to a DB in the PLC
/// </summary>
/// <param name="structValue">The struct to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
public void WriteStruct(object structValue, int db, int startByteAdr = 0)
{
WriteStructAsync(structValue, db, startByteAdr).GetAwaiter().GetResult();
}
/// <summary>
/// Writes a C# class to a DB in the PLC
/// </summary>
/// <param name="classValue">The class to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
public void WriteClass(object classValue, int db, int startByteAdr = 0)
{
WriteClassAsync(classValue, db, startByteAdr).GetAwaiter().GetResult();
}
private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, Span<byte> buffer)
{
try
{
// first create the header
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
BuildReadDataRequestPackage(package, dataType, db, startByteAdr, buffer.Length);
var s7data = RequestTsdu(dataToSend);
AssertReadResponse(s7data, buffer.Length);
s7data.AsSpan(18, buffer.Length).CopyTo(buffer);
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.ReadData, exc);
}
}
/// <summary>
/// Write DataItem(s) to the PLC. Throws an exception if the response is invalid
/// or when the PLC reports errors for item(s) written.
/// </summary>
/// <param name="dataItems">The DataItem(s) to write to the PLC.</param>
public void Write(params DataItem[] dataItems)
{
AssertPduSizeForWrite(dataItems);
var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems);
var response = RequestTsdu(message.Array, 0, length);
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
}
private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> value)
{
try
{
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value);
var s7data = RequestTsdu(dataToSend);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.WriteData, exc);
}
}
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)
{
try
{
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
var s7data = RequestTsdu(dataToSend);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.WriteData, exc);
}
}
/// <summary>
/// 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.
/// 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.</param>
public void ReadMultipleVars(List<DataItem> dataItems)
{
AssertPduSizeForRead(dataItems);
try
{
// first create the header
int packageSize = 19 + (dataItems.Count * 12);
var dataToSend = new byte[packageSize];
var package = new MemoryStream(dataToSend);
WriteReadHeader(package, dataItems.Count);
// package.Add(0x02); // datenart
foreach (var dataItem in dataItems)
{
BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count));
}
byte[] s7data = RequestTsdu(dataToSend);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
ParseDataIntoDataItems(s7data, dataItems);
}
catch (Exception exc)
{
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();
}
}
}

View File

@@ -1,36 +1,3 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
// Allgemeine Informationen über eine Assembly werden über die folgenden
// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern,
// die mit einer Assembly verknüpft sind.
[assembly: AssemblyTitle("S7.Net")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Microsoft")]
[assembly: AssemblyProduct("S7.Net")]
[assembly: AssemblyCopyright("Copyright © Microsoft 2009")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Durch Festlegen von ComVisible auf "false" werden die Typen in dieser Assembly unsichtbar
// für COM-Komponenten. Wenn Sie auf einen Typ in dieser Assembly von
// COM zugreifen müssen, legen Sie das ComVisible-Attribut für diesen Typ auf "true" fest.
[assembly: ComVisible(false)]
// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird
[assembly: Guid("1c01e753-a660-4c35-a681-c6f6a7deee83")]
// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten:
//
// Hauptversion
// Nebenversion
// Buildnummer
// Revision
//
// Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern
// übernehmen, indem Sie "*" eingeben:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: InternalsVisibleTo("S7.Net.UnitTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d1032db55f60d64bf90ea1cc2247b5a8b9b6168a07bcd464a07ce2e425d027ff9409a64ba0e3f37718e14c50cf964d0d921e5ae8b8d74bd8a82431794f897cebf0ee668feb2ccd030153611b2808fcb7785c5e5136a98e0ec23de3c1ed385d2026c26e4bed5805ff9db7e0544f59b1f19d369d43403a624586795926e38c48d")]

View File

@@ -0,0 +1,28 @@
namespace S7.Net.Protocol
{
internal static class ConnectionRequest
{
public static byte[] GetCOTPConnectionRequest(TsapPair tsapPair)
{
byte[] bSend1 = {
3, 0, 0, 22, //TPKT
17, //COTP Header Length
224, //Connect Request
0, 0, //Destination Reference
0, 46, //Source Reference
0, //Flags
193, //Parameter Code (src-tasp)
2, //Parameter Length
tsapPair.Local.FirstByte, tsapPair.Local.SecondByte, //Source TASP
194, //Parameter Code (dst-tasp)
2, //Parameter Length
tsapPair.Remote.FirstByte, tsapPair.Remote.SecondByte, //Destination TASP
192, //Parameter Code (tpdu-size)
1, //Parameter Length
10 //TPDU Size (2^10 = 1024)
};
return bSend1;
}
}
}

View 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
}
}

View 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; }
}
}

View File

@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using S7.Net.Types;
namespace S7.Net.Protocol
{
internal static class S7WriteMultiple
{
public static int CreateRequest(ByteArray message, DataItem[] dataItems)
{
message.Add(Header.Template);
message[Header.Offsets.ParameterCount] = (byte) dataItems.Length;
var paramSize = dataItems.Length * Parameter.Template.Length;
Serialization.SetWordAt(message, Header.Offsets.ParameterSize,
(ushort) (2 + paramSize));
var paramOffset = Header.Template.Length;
var data = new ByteArray();
var itemCount = 0;
foreach (var item in dataItems)
{
itemCount++;
message.Add(Parameter.Template);
var value = Serialization.SerializeDataItem(item);
var wordLen = item.Value is bool ? 1 : 2;
message[paramOffset + Parameter.Offsets.WordLength] = (byte) wordLen;
Serialization.SetWordAt(message, paramOffset + Parameter.Offsets.Amount, (ushort) value.Length);
Serialization.SetWordAt(message, paramOffset + Parameter.Offsets.DbNumber, (ushort) item.DB);
message[paramOffset + Parameter.Offsets.Area] = (byte) item.DataType;
data.Add(0x00);
if (item.Value is bool b)
{
if (item.BitAdr > 7)
throw new ArgumentException(
$"Cannot read bit with invalid {nameof(item.BitAdr)} '{item.BitAdr}'.", nameof(dataItems));
Serialization.SetAddressAt(message, paramOffset + Parameter.Offsets.Address, item.StartByteAdr,
item.BitAdr);
data.Add(0x03);
data.AddWord(1);
data.Add(b ? (byte)1 : (byte)0);
if (itemCount != dataItems.Length) {
data.Add(0);
}
}
else
{
Serialization.SetAddressAt(message, paramOffset + Parameter.Offsets.Address, item.StartByteAdr, 0);
var len = value.Length;
data.Add(0x04);
data.AddWord((ushort) (len << 3));
data.Add(value);
if ((len & 0b1) == 1 && itemCount != dataItems.Length)
{
data.Add(0);
}
}
paramOffset += Parameter.Template.Length;
}
message.Add(data.Array);
Serialization.SetWordAt(message, Header.Offsets.MessageLength, (ushort) message.Length);
Serialization.SetWordAt(message, Header.Offsets.DataLength, (ushort) (message.Length - paramOffset));
return message.Length;
}
public static void ParseResponse(byte[] message, int length, DataItem[] dataItems)
{
if (length < 12) throw new Exception("Not enough data received to parse write response.");
var messageError = Serialization.GetWordAt(message, 10);
if (messageError != 0)
throw new Exception($"Write failed with error {messageError}.");
if (length < 14 + dataItems.Length)
throw new Exception("Not enough data received to parse individual item responses.");
IList<byte> itemResults = new ArraySegment<byte>(message, 14, dataItems.Length);
List<Exception>? errors = null;
for (int i = 0; i < dataItems.Length; i++)
{
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)
throw new AggregateException(
$"Write failed for {errors.Count} items. See the innerExceptions for details.", errors);
}
private static class Header
{
public static byte[] Template { get; } =
{
0x03, 0x00, 0x00, 0x00, // TPKT
0x02, 0xf0, 0x80, // ISO DT
0x32, // S7 protocol ID
0x01, // JobRequest
0x00, 0x00, // Reserved
0x05, 0x00, // PDU reference
0x00, 0x0e, // Parameters length
0x00, 0x00, // Data length
0x05, // Function: Write var
0x00, // Number of items to write
};
public static class Offsets
{
public const int MessageLength = 2;
public const int ParameterSize = 13;
public const int DataLength = 15;
public const int ParameterCount = 18;
}
}
private static class Parameter
{
public static byte[] Template { get; } =
{
0x12, // Spec
0x0a, // Length of remaining bytes
0x10, // Addressing mode
0x02, // Transport size
0x00, 0x00, // Number of elements
0x00, 0x00, // DB number
0x84, // Area type
0x00, 0x00, 0x00 // Area offset
};
public static class Offsets
{
public const int WordLength = 3;
public const int Amount = 4;
public const int DbNumber = 6;
public const int Area = 8;
public const int Address = 9;
}
}
}
}

View File

@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using S7.Net.Types;
namespace S7.Net.Protocol
{
internal static class Serialization
{
public static ushort GetWordAt(IList<byte> buf, int index)
{
return (ushort)((buf[index] << 8) + buf[index]);
}
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 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);
}
public static byte[] SerializeValue(object value)
{
switch (value.GetType().Name)
{
case "Boolean":
return new[] { (byte)((bool)value ? 1 : 0) };
case "Byte":
return Types.Byte.ToByteArray((byte)value);
case "Int16":
return Types.Int.ToByteArray((Int16)value);
case "UInt16":
return Types.Word.ToByteArray((UInt16)value);
case "Int32":
return Types.DInt.ToByteArray((Int32)value);
case "UInt32":
return Types.DWord.ToByteArray((UInt32)value);
case "Single":
return Types.Real.ToByteArray((float)value);
case "Double":
return Types.LReal.ToByteArray((double)value);
case "DateTime":
return Types.DateTime.ToByteArray((System.DateTime)value);
case "Byte[]":
return (byte[])value;
case "Int16[]":
return Types.Int.ToByteArray((Int16[])value);
case "UInt16[]":
return Types.Word.ToByteArray((UInt16[])value);
case "Int32[]":
return Types.DInt.ToByteArray((Int32[])value);
case "UInt32[]":
return Types.DWord.ToByteArray((UInt32[])value);
case "Single[]":
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;
return Types.String.ToByteArray(stringVal, stringVal.Length);
case "DateTime[]":
return Types.DateTime.ToByteArray((System.DateTime[])value);
case "DateTimeLong[]":
return Types.DateTimeLong.ToByteArray((System.DateTime[])value);
default:
throw new InvalidVariableTypeException();
}
}
public static void SetAddressAt(ByteArray buffer, int index, int startByte, byte bitNumber)
{
var start = startByte * 8 + bitNumber;
buffer[index + 2] = (byte)start;
start >>= 8;
buffer[index + 1] = (byte)start;
start >>= 8;
buffer[index] = (byte)start;
}
public static void SetWordAt(ByteArray buffer, int index, ushort value)
{
buffer[index] = (byte)(value >> 8);
buffer[index + 1] = (byte)value;
}
}
}

31
S7.Net/Protocol/Tsap.cs Normal file
View 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;
}
}
}

View 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 &lt;&lt; 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).");
}
}
}

View File

@@ -1,122 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{BFD484F9-3F04-42A2-BF2A-60A189A25DCF}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>S7.Net</RootNamespace>
<AssemblyName>S7.Net</AssemblyName>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<FileUpgradeFlags>
</FileUpgradeFlags>
<UpgradeBackupLocation>
</UpgradeBackupLocation>
<OldToolsVersion>3.5</OldToolsVersion>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
<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>
<DocumentationFile>bin\Debug\S7.Net.xml</DocumentationFile>
</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>
<DocumentationFile>bin\Release\S7.Net.xml</DocumentationFile>
</PropertyGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net452;net462;netstandard2.0;netstandard1.3;net5.0;net6.0;net7.0</TargetFrameworks>
<SignAssembly>true</SignAssembly>
</PropertyGroup>
<PropertyGroup>
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
<InternalsVisibleTo>S7.Net.UnitTest</InternalsVisibleTo>
<PackageId>S7netplus</PackageId>
<Title>S7.Net Plus</Title>
<Description>A continuation of Juergen1969's Siemens communication library.</Description>
<Authors>Derek Heiser;Michele Cattafesta;Michael Croes;Raphael Schlameuß</Authors>
<PackageProjectUrl>https://github.com/killnine/s7netplus</PackageProjectUrl>
<RepositoryUrl>https://github.com/killnine/s7netplus</RepositoryUrl>
<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>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Xml.Linq">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Data.DataSetExtensions">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<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>
<Compile Include="Conversion.cs" />
<Compile Include="Enums.cs" />
<Compile Include="PLC.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Types\Boolean.cs" />
<Compile Include="Types\Byte.cs" />
<Compile Include="Types\ByteArray.cs" />
<Compile Include="Types\Class.cs" />
<Compile Include="Types\Counter.cs" />
<Compile Include="Types\DataItem.cs" />
<Compile Include="Types\DInt.cs" />
<Compile Include="Types\Double.cs" />
<Compile Include="Types\DWord.cs" />
<Compile Include="Types\Int.cs" />
<Compile Include="Types\String.cs" />
<Compile Include="Types\Struct.cs" />
<Compile Include="Types\Timer.cs" />
<Compile Include="Types\Word.cs" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.Net.Client.3.5">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>true</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<None Include="Properties\S7.Net.snk" />
</ItemGroup>
<ItemGroup />
<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>

View 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;
}
}
}

67
S7.Net/TPKT.cs Normal file
View File

@@ -0,0 +1,67 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net
{
/// <summary>
/// Describes a TPKT Packet
/// </summary>
internal class TPKT
{
public byte Version;
public byte Reserved1;
public int Length;
public byte[] Data;
private TPKT(byte version, byte reserved1, int length, byte[] data)
{
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, CancellationToken cancellationToken)
{
var buf = new byte[4];
int len = await stream.ReadExactAsync(buf, 0, 4, cancellationToken).ConfigureAwait(false);
if (len < 4) throw new TPKTInvalidException("TPKT is incomplete / invalid");
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()
{
return string.Format("Version: {0} Length: {1} Data: {2}",
Version,
Length,
BitConverter.ToString(Data)
);
}
}
}

43
S7.Net/Types/Bit.cs Normal file
View File

@@ -0,0 +1,43 @@
using System;
using System.Collections;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Bit from S7 plc to C#.
/// </summary>
public static class Bit
{
/// <summary>
/// Converts a Bit to bool
/// </summary>
public static bool FromByte(byte v, byte bitAdr)
{
return (((int)v & (1 << bitAdr)) != 0);
}
/// <summary>
/// Converts an array of bytes to a BitArray.
/// </summary>
/// <param name="bytes">The bytes to convert.</param>
/// <returns>A BitArray with the same number of bits and equal values as <paramref name="bytes"/>.</returns>
public static BitArray ToBitArray(byte[] bytes) => ToBitArray(bytes, bytes.Length * 8);
/// <summary>
/// Converts an array of bytes to a BitArray.
/// </summary>
/// <param name="bytes">The bytes to convert.</param>
/// <param name="length">The number of bits to return.</param>
/// <returns>A BitArray with <paramref name="length"/> bits.</returns>
public static BitArray ToBitArray(byte[] bytes, int length)
{
if (length > bytes.Length * 8) throw new ArgumentException($"Not enough data in bytes to return {length} bits.", nameof(bytes));
var bitArr = new BitArray(bytes);
var bools = new bool[length];
for (var i = 0; i < length; i++) bools[i] = bitArr[i];
return new BitArray(bools);
}
}
}

View File

@@ -1,6 +1,4 @@
using System;
namespace S7.Net.Types
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to read, set and reset bits inside bytes
@@ -12,27 +10,55 @@ namespace S7.Net.Types
/// </summary>
public static bool GetValue(byte value, int bit)
{
if ((value & (int)Math.Pow(2, bit)) != 0)
return true;
else
return false;
return (((int)value & (1 << bit)) != 0);
}
/// <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 | (byte)Math.Pow(2, bit));
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 & (byte)(~(byte)Math.Pow(2, bit)));
value = (byte) (value & ~(1 << bit) & 0xFF);
}
}
}

View File

@@ -12,8 +12,7 @@ namespace S7.Net.Types
/// </summary>
public static byte[] ToByteArray(byte value)
{
byte[] bytes = new byte[] { value};
return bytes;
return new byte[] { value }; ;
}
/// <summary>

View File

@@ -6,11 +6,19 @@ namespace S7.Net.Types
{
List<byte> list = new List<byte>();
public byte[] array
public byte this[int index]
{
get => list[index];
set => list[index] = value;
}
public byte[] Array
{
get { return list.ToArray(); }
}
public int Length => list.Count;
public ByteArray()
{
list = new List<byte>();
@@ -31,14 +39,25 @@ namespace S7.Net.Types
list.Add(item);
}
public void AddWord(ushort value)
{
list.Add((byte) (value >> 8));
list.Add((byte) value);
}
public void Add(byte[] items)
{
list.AddRange(items);
}
public void Add(IEnumerable<byte> items)
{
list.AddRange(items);
}
public void Add(ByteArray byteArray)
{
list.AddRange(byteArray.array);
list.AddRange(byteArray.Array);
}
}
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
namespace S7.Net.Types
{
@@ -13,8 +14,8 @@ namespace S7.Net.Types
private static IEnumerable<PropertyInfo> GetAccessableProperties(Type classType)
{
return classType
#if NETFX_CORE
.GetProperties().Where(p => p.GetSetMethod() != null);
#if NETSTANDARD1_3
.GetTypeInfo().DeclaredProperties.Where(p => p.SetMethod != null);
#else
.GetProperties(
BindingFlags.SetProperty |
@@ -25,10 +26,8 @@ namespace S7.Net.Types
}
private static double GetIncreasedNumberOfBytes(double startingNumberOfBytes, Type type)
private static double GetIncreasedNumberOfBytes(double numBytes, Type type, PropertyInfo? propertyInfo)
{
double numBytes = startingNumberOfBytes;
switch (type.Name)
{
case "Boolean":
@@ -40,28 +39,34 @@ 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 "Float":
case "Double":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
case "Single":
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);
numBytes += GetClassSize(propertyClass);
var propertyClass = Activator.CreateInstance(type) ??
throw new ArgumentException($"Failed to create instance of type {type}.", nameof(type));
numBytes = GetClassSize(propertyClass, numBytes, true);
break;
}
@@ -72,39 +77,49 @@ 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 int GetClassSize(object instance)
public static double GetClassSize(object instance, double numBytes = 0.0, bool isInnerProperty = false)
{
double numBytes = 0.0;
var properties = GetAccessableProperties(instance.GetType());
foreach (var property in properties)
{
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.");
}
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);
}
}
return (int)numBytes;
if (false == isInnerProperty)
{
// enlarge numBytes to next even number because S7-Structs in a DB always will be resized to an even byte count
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
}
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)
{
@@ -124,52 +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]);
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 "Double":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
case "Single":
IncrementToEven(ref numBytes);
// hier auswerten
value = Double.FromByteArray(
value = Real.FromByteArray(
new byte[] {
bytes[(int)numBytes],
bytes[(int)numBytes + 1],
@@ -177,16 +177,38 @@ namespace S7.Net.Types
bytes[(int)numBytes + 3] });
numBytes += 4;
break;
default:
var propClass = Activator.CreateInstance(propertyType);
var buffer = new byte[GetClassSize(propClass)];
if (buffer.Length > 0)
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
{
Buffer.BlockCopy(bytes, (int)Math.Ceiling(numBytes), buffer, 0, buffer.Length);
FromBytes(propClass, buffer);
value = propClass;
numBytes += buffer.Length;
}
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) ??
throw new ArgumentException($"Failed to create instance of type {propertyType}.", nameof(propertyType));
numBytes = FromBytes(propClass, bytes, numBytes);
value = propClass;
break;
}
@@ -198,28 +220,27 @@ 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>
public static void FromBytes(object sourceClass, byte[] bytes)
/// <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)
return;
if (bytes.Length != GetClassSize(sourceClass))
return;
// and decode it
double numBytes = 0.0;
return numBytes;
var properties = GetAccessableProperties(sourceClass.GetType());
foreach (var property in properties)
{
if (property.PropertyType.IsArray)
{
Array array = (Array)property.GetValue(sourceClass, null);
Type elementType = property.PropertyType.GetElementType();
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()!;
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);
}
}
@@ -227,17 +248,19 @@ namespace S7.Net.Types
{
property.SetValue(
sourceClass,
GetPropertyValue(property.PropertyType, bytes, ref numBytes),
GetPropertyValue(property.PropertyType, property, bytes, ref numBytes),
null);
}
}
return numBytes;
}
private static void ToBytes(object propertyValue, byte[] bytes, ref 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)
{
@@ -269,56 +292,78 @@ namespace S7.Net.Types
case "UInt32":
bytes2 = DWord.ToByteArray((UInt32)propertyValue);
break;
case "Single":
bytes2 = Real.ToByteArray((float)propertyValue);
break;
case "Double":
bytes2 = Double.ToByteArray((double)propertyValue);
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:
bytes2 = ToBytes(propertyValue);
numBytes = ToBytes(propertyValue, bytes, numBytes);
break;
}
if (bytes2 != null)
{
// add them
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
IncrementToEven(ref numBytes);
bytePos = (int)numBytes;
for (int bCnt = 0; bCnt < bytes2.Length; bCnt++)
bytes[bytePos + bCnt] = bytes2[bCnt];
numBytes += bytes2.Length;
}
return numBytes;
}
/// <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 byte[] ToBytes(object sourceClass)
public static double ToBytes(object sourceClass, byte[] bytes, double numBytes = 0.0)
{
int size = GetClassSize(sourceClass);
byte[] bytes = new byte[size];
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);
Type elementType = property.PropertyType.GetElementType();
Array array = (Array) value;
IncrementToEven(ref numBytes);
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
{
ToBytes(array.GetValue(i), bytes, ref numBytes);
numBytes = SetBytesFromProperty(array.GetValue(i)!, property, bytes, numBytes);
}
}
else
{
ToBytes(property.GetValue(sourceClass, null), bytes, ref numBytes);
numBytes = SetBytesFromProperty(value, property, bytes, numBytes);
}
}
return bytes;
return numBytes;
}
private static void IncrementToEven(ref double numBytes)
{
numBytes = Math.Ceiling(numBytes);
if (numBytes % 2 > 0) numBytes++;
}
}
}

View File

@@ -18,16 +18,9 @@ namespace S7.Net.Types
}
// bytes[0] -> HighByte
// bytes[1] -> LowByte
return FromBytes(bytes[1], bytes[0]);
return (UInt16)((bytes[0] << 8) | bytes[1]);
}
/// <summary>
/// Converts a Counter (2 bytes) to ushort (UInt16)
/// </summary>
public static UInt16 FromBytes(byte LoVal, byte HiVal)
{
return (UInt16)(HiVal * 256 + LoVal);
}
/// <summary>
/// Converts a ushort (UInt16) to word (2 bytes)
@@ -35,16 +28,10 @@ namespace S7.Net.Types
public static byte[] ToByteArray(UInt16 value)
{
byte[] bytes = new byte[2];
int x = 2;
long valLong = (long)((UInt16)value);
for (int cnt = 0; cnt < x; cnt++)
{
Int64 x1 = (Int64)Math.Pow(256, (cnt));
Int64 x3 = (Int64)(valLong / x1);
bytes[x - cnt - 1] = (byte)(x3 & 255);
valLong -= bytes[x - cnt - 1] * x1;
}
bytes[0] = (byte)((value << 8) & 0xFF);
bytes[1] = (byte)((value) & 0xFF);
return bytes;
}
@@ -56,7 +43,7 @@ namespace S7.Net.Types
ByteArray arr = new ByteArray();
foreach (UInt16 val in value)
arr.Add(ToByteArray(val));
return arr.array;
return arr.Array;
}
/// <summary>

View File

@@ -16,16 +16,9 @@ namespace S7.Net.Types
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 4 bytes.");
}
return FromBytes(bytes[3], bytes[2], bytes[1], bytes[0]);
return bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3];
}
/// <summary>
/// Converts a S7 DInt (4 bytes) to int (Int32)
/// </summary>
public static Int32 FromBytes(byte v1, byte v2, byte v3, byte v4)
{
return (Int32)(v1 + v2 * Math.Pow(2, 8) + v3 * Math.Pow(2, 16) + v4 * Math.Pow(2, 24));
}
/// <summary>
/// Converts a int (Int32) to S7 DInt (4 bytes)
@@ -33,16 +26,12 @@ namespace S7.Net.Types
public static byte[] ToByteArray(Int32 value)
{
byte[] bytes = new byte[4];
int x = 4;
long valLong = (long)((Int32)value);
for (int cnt = 0; cnt < x; cnt++)
{
Int64 x1 = (Int64)Math.Pow(256, (cnt));
Int64 x3 = (Int64)(valLong / x1);
bytes[x - cnt - 1] = (byte)(x3 & 255);
valLong -= bytes[x - cnt - 1] * x1;
}
bytes[0] = (byte)((value >> 24) & 0xFF);
bytes[1] = (byte)((value >> 16) & 0xFF);
bytes[2] = (byte)((value >> 8) & 0xFF);
bytes[3] = (byte)((value) & 0xFF);
return bytes;
}
@@ -54,7 +43,7 @@ namespace S7.Net.Types
ByteArray arr = new ByteArray();
foreach (Int32 val in value)
arr.Add(ToByteArray(val));
return arr.array;
return arr.Array;
}
/// <summary>
@@ -71,18 +60,6 @@ namespace S7.Net.Types
return values;
}
/// <summary>
/// Converts from C# long (Int64) to C# int (Int32)
/// </summary>
public static Int32 CDWord(Int64 value)
{
if (value > Int32.MaxValue)
{
value -= (long)Int32.MaxValue + 1;
value = (long)Int32.MaxValue + 1 - value;
value *= -1;
}
return (int)value;
}
}
}

View File

@@ -12,36 +12,39 @@ namespace S7.Net.Types
/// </summary>
public static UInt32 FromByteArray(byte[] bytes)
{
return FromBytes(bytes[3], bytes[2], bytes[1], bytes[0]);
return (UInt32)(bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]);
}
/// <summary>
/// Converts a S7 DWord (4 bytes) to uint (UInt32)
/// Converts 4 bytes to DWord (UInt32)
/// </summary>
public static UInt32 FromBytes(byte v1, byte v2, byte v3, byte v4)
public static UInt32 FromBytes(byte b1, byte b2, byte b3, byte b4)
{
return (UInt32)(v1 + v2 * Math.Pow(2, 8) + v3 * Math.Pow(2, 16) + v4 * Math.Pow(2, 24));
return (UInt32)((b4 << 24) | (b3 << 16) | (b2 << 8) | b1);
}
/// <summary>
/// Converts a uint (UInt32) to S7 DWord (4 bytes)
/// </summary>
public static byte[] ToByteArray(UInt32 value)
{
byte[] bytes = new byte[4];
int x = 4;
long valLong = (long)((UInt32)value);
for (int cnt = 0; cnt < x; cnt++)
{
Int64 x1 = (Int64)Math.Pow(256, (cnt));
Int64 x3 = (Int64)(valLong / x1);
bytes[x - cnt - 1] = (byte)(x3 & 255);
valLong -= bytes[x - cnt - 1] * x1;
}
bytes[0] = (byte)((value >> 24) & 0xFF);
bytes[1] = (byte)((value >> 16) & 0xFF);
bytes[2] = (byte)((value >> 8) & 0xFF);
bytes[3] = (byte)((value) & 0xFF);
return bytes;
}
/// <summary>
/// Converts an array of uint (UInt32) to an array of S7 DWord (4 bytes)
/// </summary>
@@ -50,7 +53,7 @@ namespace S7.Net.Types
ByteArray arr = new ByteArray();
foreach (UInt32 val in value)
arr.Add(ToByteArray(val));
return arr.array;
return arr.Array;
}
/// <summary>

View File

@@ -1,4 +1,7 @@
namespace S7.Net.Types
using S7.Net.Protocol.S7;
using System;
namespace S7.Net.Types
{
/// <summary>
/// Create an instance of a memory block that can be read by using ReadMultipleVars
@@ -25,6 +28,11 @@
/// </summary>
public int StartByteAdr { get; set; }
/// <summary>
/// Addess of bit to read from StartByteAdr
/// </summary>
public byte BitAdr { get; set; }
/// <summary>
/// Number of variables to read
/// </summary>
@@ -33,7 +41,7 @@
/// <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
@@ -43,5 +51,54 @@
VarType = VarType.Byte;
Count = 1;
}
/// <summary>
/// Create an instance of <see cref="DataItem"/> from the supplied address.
/// </summary>
/// <param name="address">The address to create the DataItem for.</param>
/// <returns>A new <see cref="DataItem"/> instance with properties parsed from <paramref name="address"/>.</returns>
/// <remarks>The <see cref="Count" /> property is not parsed from the address.</remarks>
public static DataItem FromAddress(string address)
{
PLCAddress.Parse(address, out var dataType, out var dbNumber, out var varType, out var startByte,
out var bitNumber);
return new DataItem
{
DataType = dataType,
DB = dbNumber,
VarType = varType,
StartByteAdr = startByte,
BitAdr = (byte) (bitNumber == -1 ? 0 : bitNumber)
};
}
/// <summary>
/// Create an instance of <see cref="DataItem"/> from the supplied address and value.
/// </summary>
/// <param name="address">The address to create the DataItem for.</param>
/// <param name="value">The value to be applied to the DataItem.</param>
/// <returns>A new <see cref="DataItem"/> instance with properties parsed from <paramref name="address"/> and the supplied value set.</returns>
public static DataItem FromAddressAndValue<T>(string address, T value)
{
var dataItem = FromAddress(address);
dataItem.Value = value;
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
View 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;
}
}
}

156
S7.Net/Types/DateTime.cs Normal file
View File

@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert between <see cref="T:System.DateTime"/> and S7 representation of datetime values.
/// </summary>
public static class DateTime
{
/// <summary>
/// The minimum <see cref="T:System.DateTime"/> value supported by the specification.
/// </summary>
public static readonly System.DateTime SpecMinimumDateTime = new System.DateTime(1990, 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(2089, 12, 31, 23, 59, 59, 999);
/// <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 8 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 8 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 % 8 != 0)
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
$"Parsing an array of DateTime requires a multiple of 8 bytes of input data, input data is '{bytes.Length}' long.");
var cnt = bytes.Length / 8;
var result = new System.DateTime[bytes.Length / 8];
for (var i = 0; i < cnt; i++)
result[i] = FromByteArrayImpl(new ArraySegment<byte>(bytes, i * 8, 8));
return result;
}
private static System.DateTime FromByteArrayImpl(IList<byte> bytes)
{
if (bytes.Count != 8)
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Count,
$"Parsing a DateTime requires exactly 8 bytes of input data, input data is {bytes.Count} bytes long.");
int DecodeBcd(byte input) => 10 * (input >> 4) + (input & 0b00001111);
int ByteToYear(byte bcdYear)
{
var input = DecodeBcd(bcdYear);
if (input < 90) return input + 2000;
if (input < 100) return input + 1900;
throw new ArgumentOutOfRangeException(nameof(bcdYear), bcdYear,
$"Value '{input}' is higher than the maximum '99' of S7 date and time representation.");
}
int AssertRangeInclusive(int input, byte min, byte max, string field)
{
if (input < min)
throw new ArgumentOutOfRangeException(nameof(input), input,
$"Value '{input}' is lower than the minimum '{min}' allowed for {field}.");
if (input > max)
throw new ArgumentOutOfRangeException(nameof(input), input,
$"Value '{input}' is higher than the maximum '{max}' allowed for {field}.");
return input;
}
var year = ByteToYear(bytes[0]);
var month = AssertRangeInclusive(DecodeBcd(bytes[1]), 1, 12, "month");
var day = AssertRangeInclusive(DecodeBcd(bytes[2]), 1, 31, "day of month");
var hour = AssertRangeInclusive(DecodeBcd(bytes[3]), 0, 23, "hour");
var minute = AssertRangeInclusive(DecodeBcd(bytes[4]), 0, 59, "minute");
var second = AssertRangeInclusive(DecodeBcd(bytes[5]), 0, 59, "second");
var hsec = AssertRangeInclusive(DecodeBcd(bytes[6]), 0, 99, "first two millisecond digits");
var msec = AssertRangeInclusive(bytes[7] >> 4, 0, 9, "third millisecond digit");
var dayOfWeek = AssertRangeInclusive(bytes[7] & 0b00001111, 1, 7, "day of week");
return new System.DateTime(year, month, day, hour, minute, second, hsec * 10 + msec);
}
/// <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 date time 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)
{
byte EncodeBcd(int value)
{
return (byte) ((value / 10 << 4) | value % 10);
}
if (dateTime < SpecMinimumDateTime)
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
$"Date time '{dateTime}' is before the minimum '{SpecMinimumDateTime}' supported in S7 date time representation.");
if (dateTime > SpecMaximumDateTime)
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
$"Date time '{dateTime}' is after the maximum '{SpecMaximumDateTime}' supported in S7 date time representation.");
byte MapYear(int year) => (byte) (year < 2000 ? year - 1900 : year - 2000);
int DayOfWeekToInt(DayOfWeek dayOfWeek) => (int) dayOfWeek + 1;
return new[]
{
EncodeBcd(MapYear(dateTime.Year)),
EncodeBcd(dateTime.Month),
EncodeBcd(dateTime.Day),
EncodeBcd(dateTime.Hour),
EncodeBcd(dateTime.Minute),
EncodeBcd(dateTime.Second),
EncodeBcd(dateTime.Millisecond / 10),
(byte) (dateTime.Millisecond % 10 << 4 | DayOfWeekToInt(dateTime.DayOfWeek))
};
}
/// <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 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>
public static byte[] ToByteArray(System.DateTime[] dateTimes)
{
var bytes = new List<byte>(dateTimes.Length * 8);
foreach (var dateTime in dateTimes) bytes.AddRange(ToByteArray(dateTime));
return bytes.ToArray();
}
}
}

View 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;
}
}
}

View File

@@ -5,50 +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.");
}
byte v1 = bytes[0];
byte v2 = bytes[1];
byte v3 = bytes[2];
byte v4 = bytes[3];
if ((int)v1 + v2 + v3 + v4 == 0)
{
return 0.0;
}
else
{
// nun String bilden
string txt = ValToBinString(v1) + ValToBinString(v2) + ValToBinString(v3) + ValToBinString(v4);
// erstmal das Vorzeichen
int vz = int.Parse(txt.Substring(0, 1));
int exd = Conversion.BinStringToInt32(txt.Substring(1, 8));
string ma = txt.Substring(9, 23);
double mantisse = 1;
double faktor = 1.0;
//das ist die Anzahl der restlichen bit's
for (int cnt = 0; cnt <= 22; cnt++)
{
faktor = faktor / 2.0;
//entspricht 2^-y
if (ma.Substring(cnt, 1) == "1")
{
mantisse = mantisse + faktor;
}
}
return Math.Pow((-1), vz) * Math.Pow(2, (exd - 127)) * mantisse;
}
}
public static double FromByteArray(byte[] bytes) => Real.FromByteArray(bytes);
/// <summary>
/// Converts a S7 DInt to double
@@ -74,51 +37,7 @@ namespace S7.Net.Types
/// <summary>
/// Converts a double to S7 Real (4 bytes)
/// </summary>
public static byte[] ToByteArray(double value)
{
double wert = (double)value;
string binString = "";
byte[] bytes = new byte[4];
if (wert != 0f)
{
if (wert < 0)
{
wert *= -1;
binString = "1";
}
else
{
binString = "0";
}
int exponent = (int)Math.Floor((double)Math.Log(wert) / Math.Log(2.0));
wert = wert / (Math.Pow(2, exponent)) - 1;
binString += ValToBinString((byte)(exponent + 127));
for (int cnt = 1; cnt <= 23; cnt++)
{
if (!(wert - System.Math.Pow(2, -cnt) < 0))
{
wert = wert - System.Math.Pow(2, -cnt);
binString += "1";
}
else
binString += "0";
}
bytes[0] = (byte)BinStringToByte(binString.Substring(0, 8));
bytes[1] = (byte)BinStringToByte(binString.Substring(8, 8));
bytes[2] = (byte)BinStringToByte(binString.Substring(16, 8));
bytes[3] = (byte)BinStringToByte(binString.Substring(24, 8));
}
else
{
bytes[0] = 0;
bytes[1] = 0;
bytes[2] = 0;
bytes[3] = 0;
}
return bytes;
}
public static byte[] ToByteArray(double value) => Real.ToByteArray((float)value);
/// <summary>
/// Converts an array of double to an array of bytes
@@ -128,7 +47,7 @@ namespace S7.Net.Types
ByteArray arr = new ByteArray();
foreach (double val in value)
arr.Add(ToByteArray(val));
return arr.array;
return arr.Array;
}
/// <summary>
@@ -145,38 +64,5 @@ namespace S7.Net.Types
return values;
}
private static string ValToBinString(byte value)
{
string txt = "";
for (int cnt = 7; cnt >= 0; cnt += -1)
{
if ((value & (byte)Math.Pow(2, cnt)) > 0)
txt += "1";
else
txt += "0";
}
return txt;
}
private static byte? BinStringToByte(string txt)
{
int cnt = 0;
int ret = 0;
if (txt.Length == 8)
{
for (cnt = 7; cnt >= 0; cnt += -1)
{
if (int.Parse(txt.Substring(cnt, 1)) == 1)
{
ret += (int)(Math.Pow(2, (txt.Length - 1 - cnt)));
}
}
return (byte)ret;
}
return null;
}
}
}

View File

@@ -10,7 +10,7 @@ namespace S7.Net.Types
/// <summary>
/// Converts a S7 Int (2 bytes) to short (Int16)
/// </summary>
public static Int16 FromByteArray(byte[] bytes)
public static short FromByteArray(byte[] bytes)
{
if (bytes.Length != 2)
{
@@ -18,16 +18,9 @@ namespace S7.Net.Types
}
// bytes[0] -> HighByte
// bytes[1] -> LowByte
return FromBytes(bytes[1], bytes[0]);
return (short)((int)(bytes[1]) | ((int)(bytes[0]) << 8));
}
/// <summary>
/// Converts a S7 Int (2 bytes) to short (Int16)
/// </summary>
public static Int16 FromBytes(byte LoVal, byte HiVal)
{
return (Int16)(HiVal * 256 + LoVal);
}
/// <summary>
/// Converts a short (Int16) to a S7 Int byte array (2 bytes)
@@ -35,16 +28,10 @@ namespace S7.Net.Types
public static byte[] ToByteArray(Int16 value)
{
byte[] bytes = new byte[2];
int x = 2;
long valLong = (long)((Int16)value);
for (int cnt = 0; cnt < x; cnt++)
{
Int64 x1 = (Int64)Math.Pow(256, (cnt));
Int64 x3 = (Int64)(valLong / x1);
bytes[x - cnt - 1] = (byte)(x3 & 255);
valLong -= bytes[x - cnt - 1] * x1;
}
bytes[0] = (byte) (value >> 8 & 0xFF);
bytes[1] = (byte)(value & 0xFF);
return bytes;
}
@@ -53,10 +40,15 @@ namespace S7.Net.Types
/// </summary>
public static byte[] ToByteArray(Int16[] value)
{
ByteArray arr = new ByteArray();
foreach (Int16 val in value)
arr.Add(ToByteArray(val));
return arr.array;
byte[] bytes = new byte[value.Length * 2];
int bytesPos = 0;
for(int i=0; i< value.Length; i++)
{
bytes[bytesPos++] = (byte)((value[i] >> 8) & 0xFF);
bytes[bytesPos++] = (byte) (value[i] & 0xFF);
}
return bytes;
}
/// <summary>
@@ -64,10 +56,12 @@ namespace S7.Net.Types
/// </summary>
public static Int16[] ToArray(byte[] bytes)
{
Int16[] values = new Int16[bytes.Length / 2];
int shortsCount = bytes.Length / 2;
Int16[] values = new Int16[shortsCount];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 2; cnt++)
for (int cnt = 0; cnt < shortsCount; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++] });
return values;

57
S7.Net/Types/LReal.cs Normal file
View 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
View 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
View 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;
}
}
}

View 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
View 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;
}
}
}

68
S7.Net/Types/Single.cs Normal file
View File

@@ -0,0 +1,68 @@
using System;
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) => Real.FromByteArray(bytes);
/// <summary>
/// Converts a S7 DInt to float
/// </summary>
public static float FromDWord(Int32 value)
{
byte[] b = DInt.ToByteArray(value);
float d = FromByteArray(b);
return d;
}
/// <summary>
/// Converts a S7 DWord to float
/// </summary>
public static float FromDWord(UInt32 value)
{
byte[] b = DWord.ToByteArray(value);
float d = FromByteArray(b);
return d;
}
/// <summary>
/// Converts a double to S7 Real (4 bytes)
/// </summary>
public static byte[] ToByteArray(float value) => Real.ToByteArray(value);
/// <summary>
/// Converts an array of float to an array of bytes
/// </summary>
public static byte[] ToByteArray(float[] value)
{
ByteArray arr = new ByteArray();
foreach (float val in value)
arr.Add(ToByteArray(val));
return arr.Array;
}
/// <summary>
/// Converts an array of S7 Real to an array of float
/// </summary>
public static float[] ToArray(byte[] bytes)
{
float[] 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;
}
}
}

View File

@@ -1,20 +1,27 @@
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 static class String
public class String
{
/// <summary>
/// Converts a string to S7 bytes
/// Converts a string to <paramref name="reservedLength"/> of bytes, padded with 0-bytes if required.
/// </summary>
public static byte[] ToByteArray(string value)
/// <param name="value">The string to write to the PLC.</param>
/// <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)
{
string txt = (string)value;
char[] ca = txt.ToCharArray();
byte[] bytes = new byte[txt.Length];
for (int cnt = 0; cnt <= ca.Length - 1; cnt++)
bytes[cnt] = (byte)Asc(ca[cnt].ToString());
var bytes = new byte[reservedLength];
if (value == null) return bytes;
var length = value.Length;
if (length == 0) return bytes;
if (length > reservedLength) length = reservedLength;
System.Text.Encoding.ASCII.GetBytes(value, 0, length, bytes, 0);
return bytes;
}
@@ -27,13 +34,6 @@
{
return System.Text.Encoding.ASCII.GetString(bytes);
}
private static int Asc(string s)
{
byte[] b = System.Text.Encoding.ASCII.GetBytes(s);
if (b.Length > 0)
return b[0];
return 0;
}
}
}

15
S7.Net/Types/StringEx.cs Normal file
View File

@@ -0,0 +1,15 @@
using System;
namespace S7.Net.Types
{
/// <inheritdoc cref="S7String"/>
[Obsolete("Please use S7String class")]
public static class StringEx
{
/// <inheritdoc cref="S7String.FromByteArray(byte[])"/>
public static string FromByteArray(byte[] bytes) => S7String.FromByteArray(bytes);
/// <inheritdoc cref="S7String.ToByteArray(string, int)"/>
public static byte[] ToByteArray(string value, int reservedLength) => S7String.ToByteArray(value, reservedLength);
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Linq;
using System.Globalization;
using System.Reflection;
namespace S7.Net.Types
@@ -19,8 +18,14 @@ namespace S7.Net.Types
{
double numBytes = 0.0;
System.Reflection.FieldInfo[] infos = structType.GetFields();
foreach (System.Reflection.FieldInfo info in infos)
var infos = structType
#if NETSTANDARD1_3
.GetTypeInfo().DeclaredFields;
#else
.GetFields();
#endif
foreach (var info in infos)
{
switch (info.FieldType.Name)
{
@@ -40,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 "Float":
case "Double":
case "Single":
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;
@@ -66,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;
@@ -78,10 +99,17 @@ 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));
System.Reflection.FieldInfo[] infos = structValue.GetType().GetFields();
foreach (System.Reflection.FieldInfo info in infos)
var infos = structValue.GetType()
#if NETSTANDARD1_3
.GetTypeInfo().DeclaredFields;
#else
.GetFields();
#endif
foreach (var info in infos)
{
switch (info.FieldType.Name)
{
@@ -104,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;
@@ -113,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;
@@ -122,34 +150,85 @@ 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],
bytes[(int)numBytes + 0]);
info.SetValue(structValue, sourceUInt.ConvertToInt());
bytes[(int)numBytes + 0]);
info.SetValue(structValue, sourceUInt.ConvertToInt());
numBytes += 4;
break;
case "UInt32":
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 "Single":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
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 "Double":
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],
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:
@@ -176,15 +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;
System.Reflection.FieldInfo[] infos = type.GetFields();
foreach (System.Reflection.FieldInfo info in infos)
var infos = type
#if NETSTANDARD1_3
.GetTypeInfo().DeclaredFields;
#else
.GetFields();
#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)
{
@@ -192,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
@@ -201,23 +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));
bytes2 = DWord.ToByteArray(GetValueOrThrow<uint>(info, structValue));
break;
case "Single":
bytes2 = Real.ToByteArray(GetValueOrThrow<float>(info, structValue));
break;
case "Double":
bytes2 = Double.ToByteArray((double)info.GetValue(structValue));
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)
@@ -227,14 +338,12 @@ namespace S7.Net.Types
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
bytePos = (int)numBytes;
for (int bCnt=0; bCnt<bytes2.Length; bCnt++)
for (int bCnt = 0; bCnt < bytes2.Length; bCnt++)
bytes[bytePos + bCnt] = bytes2[bCnt];
numBytes += bytes2.Length;
}
}
return bytes;
}
}
}

97
S7.Net/Types/TimeSpan.cs Normal file
View 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();
}
}
}

Some files were not shown because too many files have changed in this diff Show More