102 Commits

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
39 changed files with 1841 additions and 272 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,80 +0,0 @@
name: Test
on:
[pull_request, push]
jobs:
build_test:
name: test-${{ matrix.os }}-${{ matrix.test-framework }}
runs-on: ${{ matrix.os }}
env:
configuration: Release
artifacts: ${{ github.workspace }}/artifacts
DOTNET_NOLOGO : 1
strategy:
matrix:
os: [windows-latest, ubuntu-20.04, macos-latest]
test-framework: [netcoreapp3.1, net5.0]
include:
- os: ubuntu-20.04
test-framework: netcoreapp3.1
installSnap7: true
dotnet-sdk: '3.1.x'
- os: ubuntu-20.04
test-framework: net5.0
installSnap7: true
dotnet-sdk: '5.0.x'
- os: macos-latest
test-framework: netcoreapp3.1
installSnap7: true
dotnet-sdk: '3.1.x'
- os: macos-latest
test-framework: net5.0
installSnap7: true
dotnet-sdk: '5.0.x'
- os: windows-latest
test-framework: netcoreapp3.1
dotnet-sdk: '3.1.x'
- os: windows-latest
test-framework: net5.0
dotnet-sdk: '5.0.x'
- os: windows-latest
test-framework: net452
dotnet-sdk: '5.0.x'
fail-fast: false
steps:
- uses: actions/checkout@v2
- name: Install Snap7 Linux
if: ${{ matrix.installSnap7 && 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.installSnap7 && matrix.os == 'macos-latest' }}
run: |
brew install snap7
- name: Setup Dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: ${{ matrix.dotnet-sdk }}
- name: Nuget Cache
uses: actions/cache@v2
with:
path: ~/.nuget/packages
# Look to see if there is a cache hit for the corresponding requirements file
key: ${{ runner.os }}-${{ matrix.test-framework }}-nuget-${{ hashFiles('**/packages.lock.json') }}
restore-keys: |
${{ runner.os }}-${{ matrix.test-framework }}-nuget
- name: Restore
run: dotnet restore S7.Net.UnitTest
- name: Test
run: dotnet test --no-restore --nologo --verbosity normal --logger GitHubActions --framework ${{ matrix.test-framework }}

View File

@@ -1,21 +0,0 @@
assembly-informational-format: '{NuGetVersion}'
mode: ContinuousDeployment
branches:
master:
tag: rc
increment: Minor
feature:
regex: features?[/-]
tag: rc-{BranchName}
increment: Minor
pull-request:
regex: (pull|pull\-requests|pr)[/-]
tag: rc-pr-{BranchName}
increment: Minor
hotfix:
regex: hotfix(es)?[/-]
tag: rc
increment: Patch
develop:
regex: dev(elop)?(ment)?$
tag: b

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

@@ -24,5 +24,21 @@ namespace S7.Net.UnitTest
Assert.IsFalse(dummyByte.SelectBit(7)); 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

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

@@ -1,8 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup Condition=" '$(OS)' != 'Windows_NT' ">
<TargetFrameworks>net452;netcoreapp3.1;net5.0</TargetFrameworks> <TargetFrameworks>net6.0;net7.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT' ">
<TargetFrameworks>net6.0;net7.0;net462</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<SignAssembly>true</SignAssembly> <SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile> <AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
@@ -11,7 +17,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="GitHubActionsTestLogger" Version="1.1.0" /> <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="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" /> <PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" /> <PackageReference Include="MSTest.TestFramework" Version="2.1.2" />

View File

@@ -9,6 +9,11 @@ using System.Threading.Tasks;
using System.Threading; using System.Threading;
using System.Security.Cryptography; using System.Security.Cryptography;
#if NET5_0_OR_GREATER
using System.Buffers;
#endif
#endregion #endregion
/** /**
@@ -139,6 +144,33 @@ namespace S7.Net.UnitTest
CollectionAssert.AreEqual(data, readData); CollectionAssert.AreEqual(data, readData);
} }
#if NET5_0_OR_GREATER
/// <summary>
/// Write/Read a large amount of data to test PDU max
/// </summary>
[TestMethod]
public async Task Test_Async_WriteLargeByteArrayWithMemory()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
var randomEngine = new Random();
using var dataOwner = MemoryPool<byte>.Shared.Rent(8192);
var data = dataOwner.Memory.Slice(0, 8192);
var db = 2;
randomEngine.NextBytes(data.Span);
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data);
using var readDataOwner = MemoryPool<byte>.Shared.Rent(data.Length);
var readData = readDataOwner.Memory.Slice(0, data.Length);
await plc.ReadBytesAsync(readData, DataType.DataBlock, db, 0);
CollectionAssert.AreEqual(data.ToArray(), readData.ToArray());
}
#endif
/// <summary> /// <summary>
/// Read/Write a class that has the same properties of a DB with the same field in the same order /// Read/Write a class that has the same properties of a DB with the same field in the same order
/// </summary> /// </summary>
@@ -933,6 +965,31 @@ namespace S7.Net.UnitTest
} }
} }
#if NET5_0_OR_GREATER
[TestMethod]
public async Task Test_Async_ReadWriteBytesManyWithMemory()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
using var data = MemoryPool<byte>.Shared.Rent(2000);
for (int i = 0; i < data.Memory.Length; i++)
data.Memory.Span[i] = (byte)(i % 256);
await plc.WriteBytesAsync(DataType.DataBlock, 2, 0, data.Memory);
using var readData = MemoryPool<byte>.Shared.Rent(data.Memory.Length);
await plc.ReadBytesAsync(readData.Memory.Slice(0, data.Memory.Length), DataType.DataBlock, 2, 0);
for (int x = 0; x < data.Memory.Length; x++)
{
Assert.AreEqual(x % 256, readData.Memory.Span[x], string.Format("Bit {0} failed", x));
}
}
#endif
/// <summary> /// <summary>
/// Write a large amount of data and test cancellation /// Write a large amount of data and test cancellation
/// </summary> /// </summary>
@@ -949,7 +1006,7 @@ namespace S7.Net.UnitTest
var db = 2; var db = 2;
randomEngine.NextBytes(data); randomEngine.NextBytes(data);
cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5)); cancellationSource.CancelAfter(System.TimeSpan.FromMilliseconds(5));
try try
{ {
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken); await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);
@@ -969,6 +1026,47 @@ namespace S7.Net.UnitTest
Console.WriteLine("Task was not cancelled as expected."); Console.WriteLine("Task was not cancelled as expected.");
} }
#if NET5_0_OR_GREATER
/// <summary>
/// Write a large amount of data and test cancellation
/// </summary>
[TestMethod]
public async Task Test_Async_WriteLargeByteArrayWithCancellationWithMemory()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
var cancellationSource = new CancellationTokenSource();
var cancellationToken = cancellationSource.Token;
using var dataOwner = MemoryPool<byte>.Shared.Rent(8192);
var data = dataOwner.Memory.Slice(0, 8192);
var randomEngine = new Random();
var db = 2;
randomEngine.NextBytes(data.Span);
cancellationSource.CancelAfter(System.TimeSpan.FromMilliseconds(5));
try
{
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);
}
catch (OperationCanceledException)
{
// everything is good, that is the exception we expect
Console.WriteLine("Operation was cancelled as expected.");
return;
}
catch (Exception e)
{
Assert.Fail($"Wrong exception type received. Expected {typeof(OperationCanceledException)}, received {e.GetType()}.");
}
// Depending on how tests run, this can also just succeed without getting cancelled at all. Do nothing in this case.
Console.WriteLine("Task was not cancelled as expected.");
}
#endif
/// <summary> /// <summary>
/// Write a large amount of data and test cancellation /// Write a large amount of data and test cancellation
/// </summary> /// </summary>
@@ -1001,6 +1099,7 @@ namespace S7.Net.UnitTest
}; };
await plc.ReadMultipleVarsAsync(dataItems, CancellationToken.None); await plc.ReadMultipleVarsAsync(dataItems, CancellationToken.None);
} }
#endregion #endregion
} }
} }

View File

@@ -7,6 +7,10 @@ using S7.Net.Types;
using S7.UnitTest.Helpers; using S7.UnitTest.Helpers;
using System.Security.Cryptography; using System.Security.Cryptography;
#if NET5_0_OR_GREATER
using System.Buffers;
#endif
#endregion #endregion
/** /**
@@ -778,6 +782,33 @@ namespace S7.Net.UnitTest
CollectionAssert.AreEqual(data, readData); 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))] [TestMethod, ExpectedException(typeof(PlcException))]
public void T18_ReadStructThrowsIfPlcIsNotConnected() public void T18_ReadStructThrowsIfPlcIsNotConnected()
{ {
@@ -1006,6 +1037,32 @@ namespace S7.Net.UnitTest
} }
} }
#if NET5_0_OR_GREATER
[TestMethod]
public void T27_ReadWriteBytesManyWithSpan()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
using var dataOwner = MemoryPool<byte>.Shared.Rent(2000);
var data = dataOwner.Memory.Span;
for (int i = 0; i < data.Length; i++)
data[i] = (byte)(i % 256);
plc.WriteBytes(DataType.DataBlock, 2, 0, data);
using var readDataOwner = MemoryPool<byte>.Shared.Rent(data.Length);
var readData = readDataOwner.Memory.Span.Slice(0, data.Length);
plc.ReadBytes(readData, DataType.DataBlock, 2, 0);
for (int x = 0; x < data.Length; x++)
{
Assert.AreEqual(x % 256, readData[x], $"Mismatch at offset {x}, expected {x % 256}, actual {readData[x]}.");
}
}
#endif
[TestMethod] [TestMethod]
public void T28_ReadClass_DoesntCrash_When_ReadingLessThan1Byte() public void T28_ReadClass_DoesntCrash_When_ReadingLessThan1Byte()
{ {
@@ -1060,7 +1117,7 @@ namespace S7.Net.UnitTest
Assert.AreEqual(test_value, test_value2, "Compare DateTimeLong Write/Read"); Assert.AreEqual(test_value, test_value2, "Compare DateTimeLong Write/Read");
} }
#endregion #endregion
#region Private methods #region Private methods

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

View File

@@ -55,6 +55,7 @@ namespace S7.Net
/// See: https://tools.ietf.org/html/rfc905 /// See: https://tools.ietf.org/html/rfc905
/// </summary> /// </summary>
/// <param name="stream">The socket to read from</param> /// <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> /// <returns>COTP DPDU instance</returns>
public static async Task<TPDU> ReadAsync(Stream stream, CancellationToken cancellationToken) public static async Task<TPDU> ReadAsync(Stream stream, CancellationToken cancellationToken)
{ {
@@ -89,6 +90,7 @@ namespace S7.Net
/// See: https://tools.ietf.org/html/rfc905 /// See: https://tools.ietf.org/html/rfc905
/// </summary> /// </summary>
/// <param name="stream">The stream to read from</param> /// <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> /// <returns>Data in TSDU</returns>
public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken) public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken)
{ {

View File

@@ -138,19 +138,59 @@ namespace S7.Net
/// <summary> /// <summary>
/// Helper to get a bit value given a byte and the bit index. /// 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> /// </summary>
/// <param name="data"></param> /// <param name="data">The data to get from.</param>
/// <param name="bitPosition"></param> /// <param name="index">The zero-based index of the bit to get.</param>
/// <returns></returns> /// <returns>The Boolean value will get.</returns>
public static bool SelectBit(this byte data, int bitPosition) public static bool SelectBit(this byte data, int index)
{ {
int mask = 1 << bitPosition; int mask = 1 << index;
int result = data & mask; int result = data & mask;
return (result != 0); 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> /// <summary>
/// Converts from ushort value to short value; it's used to retrieve negative values from words /// Converts from ushort value to short value; it's used to retrieve negative values from words
/// </summary> /// </summary>

View File

@@ -202,10 +202,20 @@
/// DateTIme variable type /// DateTIme variable type
/// </summary> /// </summary>
DateTime, DateTime,
/// <summary>
/// IEC date (legacy) variable type
/// </summary>
Date,
/// <summary> /// <summary>
/// DateTimeLong variable type /// DateTimeLong variable type
/// </summary> /// </summary>
DateTimeLong 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

@@ -1,6 +1,10 @@
 using System;
using System.Buffers;
using System.IO;
namespace S7.Net.Helper namespace S7.Net.Helper
{ {
#if !NET5_0_OR_GREATER
internal static class MemoryStreamExtension internal static class MemoryStreamExtension
{ {
/// <summary> /// <summary>
@@ -10,9 +14,25 @@ namespace S7.Net.Helper
/// </summary> /// </summary>
/// <param name="stream"></param> /// <param name="stream"></param>
/// <param name="value"></param> /// <param name="value"></param>
public static void WriteByteArray(this System.IO.MemoryStream stream, byte[] value) public static void Write(this MemoryStream stream, byte[] value)
{ {
stream.Write(value, 0, value.Length); 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

@@ -6,7 +6,7 @@ using System.Runtime.Serialization;
namespace S7.Net namespace S7.Net
{ {
internal class WrongNumberOfBytesException : Exception public class WrongNumberOfBytesException : Exception
{ {
public WrongNumberOfBytesException() : base() public WrongNumberOfBytesException() : base()
{ {
@@ -27,7 +27,7 @@ namespace S7.Net
#endif #endif
} }
internal class InvalidAddressException : Exception public class InvalidAddressException : Exception
{ {
public InvalidAddressException() : base () public InvalidAddressException() : base ()
{ {
@@ -48,7 +48,7 @@ namespace S7.Net
#endif #endif
} }
internal class InvalidVariableTypeException : Exception public class InvalidVariableTypeException : Exception
{ {
public InvalidVariableTypeException() : base() public InvalidVariableTypeException() : base()
{ {
@@ -69,7 +69,7 @@ namespace S7.Net
#endif #endif
} }
internal class TPKTInvalidException : Exception public class TPKTInvalidException : Exception
{ {
public TPKTInvalidException() : base() public TPKTInvalidException() : base()
{ {
@@ -90,7 +90,7 @@ namespace S7.Net
#endif #endif
} }
internal class TPDUInvalidException : Exception public class TPDUInvalidException : Exception
{ {
public TPDUInvalidException() : base() public TPDUInvalidException() : base()
{ {

View File

@@ -1,7 +1,6 @@
using S7.Net.Helper; using S7.Net.Helper;
using S7.Net.Protocol.S7; using S7.Net.Protocol.S7;
using S7.Net.Types; using S7.Net.Types;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using DateTime = S7.Net.Types.DateTime; using DateTime = S7.Net.Types.DateTime;
@@ -10,29 +9,104 @@ namespace S7.Net
{ {
public partial class Plc public partial class Plc
{ {
/// <summary> private static void WriteTpktHeader(System.IO.MemoryStream stream, int length)
/// Creates the header to read bytes from the PLC
/// </summary>
/// <param name="amount"></param>
/// <returns></returns>
private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount = 1)
{ {
//header size = 19 bytes stream.Write(new byte[] { 0x03, 0x00 });
stream.WriteByteArray(new byte[] { 0x03, 0x00 }); stream.Write(Word.ToByteArray((ushort) length));
//complete package size }
stream.WriteByteArray(Types.Int.ToByteArray((short)(19 + (12 * amount))));
stream.WriteByteArray(new byte[] { 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 }); private static void WriteDataHeader(System.IO.MemoryStream stream)
//data part size {
stream.WriteByteArray(Types.Word.ToByteArray((ushort)(2 + (amount * 12)))); stream.Write(new byte[] { 0x02, 0xf0, 0x80 });
stream.WriteByteArray(new byte[] { 0x00, 0x00, 0x04 }); }
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 //amount of requests
stream.WriteByte((byte)amount); 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> /// <summary>
/// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType), /// 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. /// the address of the memory, the address of the byte and the bytes count.
/// </summary> /// </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="dataType">MemoryType (DB, Timer, Counter, etc.)</param>
/// <param name="db">Address of the memory to be read</param> /// <param name="db">Address of the memory to be read</param>
/// <param name="startByteAdr">Start address of the byte</param> /// <param name="startByteAdr">Start address of the byte</param>
@@ -41,7 +115,7 @@ namespace S7.Net
private static void BuildReadDataRequestPackage(System.IO.MemoryStream stream, DataType dataType, int db, int startByteAdr, int count = 1) private static void BuildReadDataRequestPackage(System.IO.MemoryStream stream, DataType dataType, int db, int startByteAdr, int count = 1)
{ {
//single data req = 12 //single data req = 12
stream.WriteByteArray(new byte[] { 0x12, 0x0a, 0x10 }); stream.Write(new byte[] { 0x12, 0x0a, 0x10 });
switch (dataType) switch (dataType)
{ {
case DataType.Timer: case DataType.Timer:
@@ -53,8 +127,8 @@ namespace S7.Net
break; break;
} }
stream.WriteByteArray(Word.ToByteArray((ushort)(count))); stream.Write(Word.ToByteArray((ushort)(count)));
stream.WriteByteArray(Word.ToByteArray((ushort)(db))); stream.Write(Word.ToByteArray((ushort)(db)));
stream.WriteByte((byte)dataType); stream.WriteByte((byte)dataType);
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191 var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
stream.WriteByte((byte)overflow); stream.WriteByte((byte)overflow);
@@ -62,10 +136,10 @@ namespace S7.Net
{ {
case DataType.Timer: case DataType.Timer:
case DataType.Counter: case DataType.Counter:
stream.WriteByteArray(Types.Word.ToByteArray((ushort)(startByteAdr))); stream.Write(Word.ToByteArray((ushort)(startByteAdr)));
break; break;
default: default:
stream.WriteByteArray(Types.Word.ToByteArray((ushort)((startByteAdr) * 8))); stream.Write(Word.ToByteArray((ushort)((startByteAdr) * 8)));
break; break;
} }
} }
@@ -168,6 +242,24 @@ namespace S7.Net
{ {
return DateTimeLong.ToArray(bytes); 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: default:
return null; return null;
} }
@@ -197,10 +289,12 @@ namespace S7.Net
case VarType.Timer: case VarType.Timer:
case VarType.Int: case VarType.Int:
case VarType.Counter: case VarType.Counter:
case VarType.Date:
return varCount * 2; return varCount * 2;
case VarType.DWord: case VarType.DWord:
case VarType.DInt: case VarType.DInt:
case VarType.Real: case VarType.Real:
case VarType.Time:
return varCount * 4; return varCount * 4;
case VarType.LReal: case VarType.LReal:
case VarType.DateTime: case VarType.DateTime:
@@ -253,7 +347,7 @@ namespace S7.Net
int packageSize = 19 + (dataItems.Count * 12); int packageSize = 19 + (dataItems.Count * 12);
var package = new System.IO.MemoryStream(packageSize); var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package, dataItems.Count); WriteReadHeader(package, dataItems.Count);
foreach (var dataItem in dataItems) foreach (var dataItem in dataItems)
{ {
@@ -262,5 +356,15 @@ namespace S7.Net
return package.ToArray(); 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}.");
}
}
}

View File

@@ -95,7 +95,6 @@ namespace S7.Net
MaxPDUSize = s7data[18] * 256 + s7data[19]; MaxPDUSize = s7data[18] * 256 + s7data[19];
} }
/// <summary> /// <summary>
/// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests. /// 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. /// If the read was not successful, check LastErrorCode or LastErrorString.
@@ -110,16 +109,34 @@ namespace S7.Net
public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken cancellationToken = default) public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken cancellationToken = default)
{ {
var resultBytes = new byte[count]; 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; int index = 0;
while (count > 0) while (buffer.Length > 0)
{ {
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0. //This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
var maxToRead = Math.Min(count, MaxPDUSize - 18); var maxToRead = Math.Min(buffer.Length, MaxPDUSize - 18);
await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, resultBytes, index, maxToRead, cancellationToken).ConfigureAwait(false); await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, buffer.Slice(0, maxToRead), cancellationToken).ConfigureAwait(false);
count -= maxToRead; buffer = buffer.Slice(maxToRead);
index += maxToRead; index += maxToRead;
} }
return resultBytes;
} }
/// <summary> /// <summary>
@@ -295,6 +312,48 @@ namespace S7.Net
return dataItems; 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> /// <summary>
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests. /// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
@@ -307,15 +366,30 @@ namespace S7.Net
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None. /// <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> /// 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> /// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken = default) public Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken = default)
{
return WriteBytesAsync(dataType, db, startByteAdr, value.AsMemory(), cancellationToken);
}
/// <summary>
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
/// If the write was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory<byte> value, CancellationToken cancellationToken = default)
{ {
int localIndex = 0; int localIndex = 0;
int count = value.Length; while (value.Length > 0)
while (count > 0)
{ {
var maxToWrite = (int)Math.Min(count, MaxPDUSize - 35); var maxToWrite = (int)Math.Min(value.Length, MaxPDUSize - 35);
await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite, cancellationToken).ConfigureAwait(false); await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value.Slice(0, maxToWrite), cancellationToken).ConfigureAwait(false);
count -= maxToWrite; value = value.Slice(maxToWrite);
localIndex += maxToWrite; localIndex += maxToWrite;
} }
} }
@@ -397,7 +471,6 @@ namespace S7.Net
/// <summary> /// <summary>
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the write was not successful, check <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
/// </summary> /// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param> /// <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="value">Value to be written to the PLC</param>
@@ -441,14 +514,14 @@ namespace S7.Net
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes, cancellationToken).ConfigureAwait(false); await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes, cancellationToken).ConfigureAwait(false);
} }
private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count, CancellationToken cancellationToken) private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, Memory<byte> buffer, CancellationToken cancellationToken)
{ {
var dataToSend = BuildReadRequestPackage(new[] { new DataItemAddress(dataType, db, startByteAdr, count) }); var dataToSend = BuildReadRequestPackage(new[] { new DataItemAddress(dataType, db, startByteAdr, buffer.Length) });
var s7data = await RequestTsduAsync(dataToSend, cancellationToken); var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
AssertReadResponse(s7data, count); AssertReadResponse(s7data, buffer.Length);
Array.Copy(s7data, 18, buffer, offset, count); s7data.AsSpan(18, buffer.Length).CopyTo(buffer.Span);
} }
/// <summary> /// <summary>
@@ -476,12 +549,13 @@ namespace S7.Net
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param> /// <param name="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="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="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> /// <returns>A task that represents the asynchronous write operation.</returns>
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count, CancellationToken cancellationToken) private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory<byte> value, CancellationToken cancellationToken)
{ {
try try
{ {
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count); var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value.Span);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false); var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]); ValidateResponseCode((ReadWriteErrorCode)s7data[14]);

View File

@@ -39,16 +39,32 @@ namespace S7.Net
public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count) public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count)
{ {
var result = new byte[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; int index = 0;
while (count > 0) while (buffer.Length > 0)
{ {
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0. //This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
var maxToRead = Math.Min(count, MaxPDUSize - 18); var maxToRead = Math.Min(buffer.Length, MaxPDUSize - 18);
ReadBytesWithSingleRequest(dataType, db, startByteAdr + index, result, index, maxToRead); ReadBytesWithSingleRequest(dataType, db, startByteAdr + index, buffer.Slice(0, maxToRead));
count -= maxToRead; buffer = buffer.Slice(maxToRead);
index += maxToRead; index += maxToRead;
} }
return result;
} }
/// <summary> /// <summary>
@@ -111,7 +127,6 @@ namespace S7.Net
return ReadStruct(typeof(T), db, startByteAdr) as T?; return ReadStruct(typeof(T), db, startByteAdr) as T?;
} }
/// <summary> /// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC. /// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. /// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
@@ -178,17 +193,29 @@ namespace S7.Net
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param> /// <param name="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="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) public void WriteBytes(DataType dataType, int db, int startByteAdr, byte[] value)
{
WriteBytes(dataType, db, startByteAdr, value.AsSpan());
}
/// <summary>
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
/// If the write was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
public void WriteBytes(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> value)
{ {
int localIndex = 0; int localIndex = 0;
int count = value.Length; while (value.Length > 0)
while (count > 0)
{ {
//TODO: Figure out how to use MaxPDUSize here //TODO: Figure out how to use MaxPDUSize here
//Snap7 seems to choke on PDU sizes above 256 even if snap7 //Snap7 seems to choke on PDU sizes above 256 even if snap7
//replies with bigger PDU size in connection setup. //replies with bigger PDU size in connection setup.
var maxToWrite = Math.Min(count, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480 var maxToWrite = Math.Min(value.Length, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480
WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite); WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value.Slice(0, maxToWrite));
count -= maxToWrite; value = value.Slice(maxToWrite);
localIndex += maxToWrite; localIndex += maxToWrite;
} }
} }
@@ -262,7 +289,6 @@ namespace S7.Net
/// <summary> /// <summary>
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the write was not successful, check <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
/// </summary> /// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param> /// <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="value">Value to be written to the PLC</param>
@@ -294,22 +320,22 @@ namespace S7.Net
WriteClassAsync(classValue, db, startByteAdr).GetAwaiter().GetResult(); WriteClassAsync(classValue, db, startByteAdr).GetAwaiter().GetResult();
} }
private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count) private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, Span<byte> buffer)
{ {
try try
{ {
// first create the header // first create the header
int packageSize = 19 + 12; // 19 header + 12 for 1 request const int packageSize = 19 + 12; // 19 header + 12 for 1 request
var package = new System.IO.MemoryStream(packageSize); var dataToSend = new byte[packageSize];
BuildHeaderPackage(package); var package = new MemoryStream(dataToSend);
WriteReadHeader(package);
// package.Add(0x02); // datenart // package.Add(0x02); // datenart
BuildReadDataRequestPackage(package, dataType, db, startByteAdr, count); BuildReadDataRequestPackage(package, dataType, db, startByteAdr, buffer.Length);
var dataToSend = package.ToArray();
var s7data = RequestTsdu(dataToSend); var s7data = RequestTsdu(dataToSend);
AssertReadResponse(s7data, count); AssertReadResponse(s7data, buffer.Length);
Array.Copy(s7data, 18, buffer, offset, count); s7data.AsSpan(18, buffer.Length).CopyTo(buffer);
} }
catch (Exception exc) catch (Exception exc)
{ {
@@ -326,7 +352,6 @@ namespace S7.Net
{ {
AssertPduSizeForWrite(dataItems); AssertPduSizeForWrite(dataItems);
var message = new ByteArray(); var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems); var length = S7WriteMultiple.CreateRequest(message, dataItems);
var response = RequestTsdu(message.Array, 0, length); var response = RequestTsdu(message.Array, 0, length);
@@ -334,11 +359,11 @@ namespace S7.Net
S7WriteMultiple.ParseResponse(response, response.Length, dataItems); S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
} }
private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count) private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> value)
{ {
try try
{ {
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count); var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value);
var s7data = RequestTsdu(dataToSend); var s7data = RequestTsdu(dataToSend);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]); ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
@@ -349,35 +374,37 @@ namespace S7.Net
} }
} }
private byte[] BuildWriteBytesPackage(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count) private byte[] BuildWriteBytesPackage(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> value)
{ {
int varCount = count; int varCount = value.Length;
// first create the header // first create the header
int packageSize = 35 + varCount; int packageSize = 35 + varCount;
var package = new MemoryStream(new byte[packageSize]); var packageData = new byte[packageSize];
var package = new MemoryStream(packageData);
package.WriteByte(3); package.WriteByte(3);
package.WriteByte(0); package.WriteByte(0);
//complete package size //complete package size
package.WriteByteArray(Int.ToByteArray((short)packageSize)); package.Write(Int.ToByteArray((short)packageSize));
package.WriteByteArray(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 }); // This overload doesn't allocate the byte array, it refers to assembly's static data segment
package.WriteByteArray(Word.ToByteArray((ushort)(varCount - 1))); package.Write(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
package.WriteByteArray(new byte[] { 0, 0x0e }); package.Write(Word.ToByteArray((ushort)(varCount - 1)));
package.WriteByteArray(Word.ToByteArray((ushort)(varCount + 4))); package.Write(new byte[] { 0, 0x0e });
package.WriteByteArray(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 }); package.Write(Word.ToByteArray((ushort)(varCount + 4)));
package.WriteByteArray(Word.ToByteArray((ushort)varCount)); package.Write(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 });
package.WriteByteArray(Word.ToByteArray((ushort)(db))); package.Write(Word.ToByteArray((ushort)varCount));
package.Write(Word.ToByteArray((ushort)(db)));
package.WriteByte((byte)dataType); package.WriteByte((byte)dataType);
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191 var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
package.WriteByte((byte)overflow); package.WriteByte((byte)overflow);
package.WriteByteArray(Word.ToByteArray((ushort)(startByteAdr * 8))); package.Write(Word.ToByteArray((ushort)(startByteAdr * 8)));
package.WriteByteArray(new byte[] { 0, 4 }); package.Write(new byte[] { 0, 4 });
package.WriteByteArray(Word.ToByteArray((ushort)(varCount * 8))); package.Write(Word.ToByteArray((ushort)(varCount * 8)));
// now join the header and the data // now join the header and the data
package.Write(value, dataOffset, count); package.Write(value);
return package.ToArray(); return packageData;
} }
private byte[] BuildWriteBitPackage(DataType dataType, int db, int startByteAdr, bool bitValue, int bitAdr) private byte[] BuildWriteBitPackage(DataType dataType, int db, int startByteAdr, bool bitValue, int bitAdr)
@@ -386,33 +413,33 @@ namespace S7.Net
int varCount = 1; int varCount = 1;
// first create the header // first create the header
int packageSize = 35 + varCount; int packageSize = 35 + varCount;
var package = new MemoryStream(new byte[packageSize]); var packageData = new byte[packageSize];
var package = new MemoryStream(packageData);
package.WriteByte(3); package.WriteByte(3);
package.WriteByte(0); package.WriteByte(0);
//complete package size //complete package size
package.WriteByteArray(Int.ToByteArray((short)packageSize)); package.Write(Int.ToByteArray((short)packageSize));
package.WriteByteArray(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 }); package.Write(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
package.WriteByteArray(Word.ToByteArray((ushort)(varCount - 1))); package.Write(Word.ToByteArray((ushort)(varCount - 1)));
package.WriteByteArray(new byte[] { 0, 0x0e }); package.Write(new byte[] { 0, 0x0e });
package.WriteByteArray(Word.ToByteArray((ushort)(varCount + 4))); package.Write(Word.ToByteArray((ushort)(varCount + 4)));
package.WriteByteArray(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x01 }); //ending 0x01 is used for writing a sinlge bit package.Write(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x01 }); //ending 0x01 is used for writing a sinlge bit
package.WriteByteArray(Word.ToByteArray((ushort)varCount)); package.Write(Word.ToByteArray((ushort)varCount));
package.WriteByteArray(Word.ToByteArray((ushort)(db))); package.Write(Word.ToByteArray((ushort)(db)));
package.WriteByte((byte)dataType); package.WriteByte((byte)dataType);
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191 var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
package.WriteByte((byte)overflow); package.WriteByte((byte)overflow);
package.WriteByteArray(Word.ToByteArray((ushort)(startByteAdr * 8 + bitAdr))); package.Write(Word.ToByteArray((ushort)(startByteAdr * 8 + bitAdr)));
package.WriteByteArray(new byte[] { 0, 0x03 }); //ending 0x03 is used for writing a sinlge bit package.Write(new byte[] { 0, 0x03 }); //ending 0x03 is used for writing a sinlge bit
package.WriteByteArray(Word.ToByteArray((ushort)(varCount))); package.Write(Word.ToByteArray((ushort)(varCount)));
// now join the header and the data // now join the header and the data
package.WriteByteArray(value); package.Write(value);
return package.ToArray(); return packageData;
} }
private void WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue) private void WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue)
{ {
try try
@@ -444,16 +471,16 @@ namespace S7.Net
{ {
// first create the header // first create the header
int packageSize = 19 + (dataItems.Count * 12); int packageSize = 19 + (dataItems.Count * 12);
var package = new System.IO.MemoryStream(packageSize); var dataToSend = new byte[packageSize];
BuildHeaderPackage(package, dataItems.Count); var package = new MemoryStream(dataToSend);
WriteReadHeader(package, dataItems.Count);
// package.Add(0x02); // datenart // package.Add(0x02); // datenart
foreach (var dataItem in dataItems) foreach (var dataItem in dataItems)
{ {
BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count)); BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count));
} }
var dataToSend = package.ToArray(); byte[] s7data = RequestTsdu(dataToSend);
var s7data = RequestTsdu(dataToSend);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]); ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
@@ -465,6 +492,42 @@ namespace S7.Net
} }
} }
/// <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) => RequestTsdu(requestData, 0, requestData.Length);
private byte[] RequestTsdu(byte[] requestData, int offset, int length) private byte[] RequestTsdu(byte[] requestData, int offset, int length)

View File

@@ -26,6 +26,11 @@ namespace S7.Net.Protocol
_ => Types.String.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); return SerializeValue(dataItem.Value);
} }

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net452;netstandard2.0;netstandard1.3;net5.0</TargetFrameworks> <TargetFrameworks>net452;net462;netstandard2.0;netstandard1.3;net5.0;net6.0;net7.0</TargetFrameworks>
<SignAssembly>true</SignAssembly> <SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile> <AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
<InternalsVisibleTo>S7.Net.UnitTest</InternalsVisibleTo> <InternalsVisibleTo>S7.Net.UnitTest</InternalsVisibleTo>
@@ -15,17 +15,22 @@
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PackageTags>PLC Siemens Communication S7</PackageTags> <PackageTags>PLC Siemens Communication S7</PackageTags>
<Copyright>Derek Heiser 2015</Copyright> <Copyright>Derek Heiser 2015</Copyright>
<LangVersion>8.0</LangVersion> <LangVersion>latest</LangVersion>
<Nullable>Enable</Nullable> <Nullable>Enable</Nullable>
<DebugType>portable</DebugType> <DebugType>portable</DebugType>
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591;NETSDK1138</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net452' Or '$(TargetFramework)' == 'netstandard2.0' "> <PropertyGroup Condition="'$(TargetFramework)' == 'net452' Or '$(TargetFramework)' == 'net462' Or '$(TargetFramework)' == 'netstandard2.0' ">
<DefineConstants>NET_FULL</DefineConstants> <DefineConstants>NET_FULL</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'net5.0' And '$(TargetFramework)' != 'net6.0' And '$(TargetFramework)' != 'net7.0'">
<PackageReference Include="System.Memory" Version="4.5.5" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />

View File

@@ -39,6 +39,7 @@ namespace S7.Net
/// <param name="buffer">the buffer to read into</param> /// <param name="buffer">the buffer to read into</param>
/// <param name="offset">the offset in 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="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> /// <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) public static async Task<int> ReadExactAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{ {

View File

@@ -29,6 +29,7 @@ namespace S7.Net
/// Reads a TPKT from the socket Async /// Reads a TPKT from the socket Async
/// </summary> /// </summary>
/// <param name="stream">The stream to read from</param> /// <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> /// <returns>Task TPKT Instace</returns>
public static async Task<TPKT> ReadAsync(Stream stream, CancellationToken cancellationToken) public static async Task<TPKT> ReadAsync(Stream stream, CancellationToken cancellationToken)
{ {

View File

@@ -64,7 +64,8 @@ namespace S7.Net.Types
numBytes += attribute.ReservedLengthInBytes; numBytes += attribute.ReservedLengthInBytes;
break; break;
default: default:
var propertyClass = Activator.CreateInstance(type); var propertyClass = Activator.CreateInstance(type) ??
throw new ArgumentException($"Failed to create instance of type {type}.", nameof(type));
numBytes = GetClassSize(propertyClass, numBytes, true); numBytes = GetClassSize(propertyClass, numBytes, true);
break; break;
} }
@@ -76,6 +77,8 @@ namespace S7.Net.Types
/// Gets the size of the class in bytes. /// Gets the size of the class in bytes.
/// </summary> /// </summary>
/// <param name="instance">An instance of the class</param> /// <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> /// <returns>the number of bytes</returns>
public static double GetClassSize(object instance, double numBytes = 0.0, bool isInnerProperty = false) public static double GetClassSize(object instance, double numBytes = 0.0, bool isInnerProperty = false)
{ {
@@ -84,8 +87,10 @@ namespace S7.Net.Types
{ {
if (property.PropertyType.IsArray) if (property.PropertyType.IsArray)
{ {
Type elementType = property.PropertyType.GetElementType(); Type elementType = property.PropertyType.GetElementType()!;
Array array = (Array)property.GetValue(instance, null); 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) 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."); throw new Exception("Cannot determine size of class, because an array is defined which has no fixed size greater than zero.");
@@ -199,7 +204,9 @@ namespace S7.Net.Types
numBytes += sData.Length; numBytes += sData.Length;
break; break;
default: default:
var propClass = Activator.CreateInstance(propertyType); var propClass = Activator.CreateInstance(propertyType) ??
throw new ArgumentException($"Failed to create instance of type {propertyType}.", nameof(propertyType));
numBytes = FromBytes(propClass, bytes, numBytes); numBytes = FromBytes(propClass, bytes, numBytes);
value = propClass; value = propClass;
break; break;
@@ -213,6 +220,8 @@ namespace S7.Net.Types
/// </summary> /// </summary>
/// <param name="sourceClass">The object to fill in the given array of bytes</param> /// <param name="sourceClass">The object to fill in the given array of bytes</param>
/// <param name="bytes">The array of bytes</param> /// <param name="bytes">The array of bytes</param>
/// <param name="numBytes">The offset for the current field.</param>
/// <param name="isInnerClass"><see langword="true" /> if this class is the type of a member of the class to be serialized; otherwise, <see langword="false" />.</param>
public static double FromBytes(object sourceClass, byte[] bytes, double numBytes = 0, bool isInnerClass = false) public static double FromBytes(object sourceClass, byte[] bytes, double numBytes = 0, bool isInnerClass = false)
{ {
if (bytes == null) if (bytes == null)
@@ -223,9 +232,11 @@ namespace S7.Net.Types
{ {
if (property.PropertyType.IsArray) if (property.PropertyType.IsArray)
{ {
Array array = (Array)property.GetValue(sourceClass, null); Array array = (Array?) property.GetValue(sourceClass, null) ??
throw new ArgumentException($"Property {property.Name} on sourceClass must be an array instance.", nameof(sourceClass));
IncrementToEven(ref numBytes); IncrementToEven(ref numBytes);
Type elementType = property.PropertyType.GetElementType(); Type elementType = property.PropertyType.GetElementType()!;
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++) for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
{ {
array.SetValue( array.SetValue(
@@ -320,26 +331,30 @@ namespace S7.Net.Types
/// <summary> /// <summary>
/// Creates a byte array depending on the struct type. /// Creates a byte array depending on the struct type.
/// </summary> /// </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> /// <returns>A byte array or null if fails.</returns>
public static double ToBytes(object sourceClass, byte[] bytes, double numBytes = 0.0) public static double ToBytes(object sourceClass, byte[] bytes, double numBytes = 0.0)
{ {
var properties = GetAccessableProperties(sourceClass.GetType()); var properties = GetAccessableProperties(sourceClass.GetType());
foreach (var property in properties) 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) if (property.PropertyType.IsArray)
{ {
Array array = (Array)property.GetValue(sourceClass, null); Array array = (Array) value;
IncrementToEven(ref numBytes); IncrementToEven(ref numBytes);
Type elementType = property.PropertyType.GetElementType();
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++) for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
{ {
numBytes = SetBytesFromProperty(array.GetValue(i), property, bytes, numBytes); numBytes = SetBytesFromProperty(array.GetValue(i)!, property, bytes, numBytes);
} }
} }
else else
{ {
numBytes = SetBytesFromProperty(property.GetValue(sourceClass, null), property, bytes, numBytes); numBytes = SetBytesFromProperty(value, property, bytes, numBytes);
} }
} }
return numBytes; return numBytes;

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

View File

@@ -141,7 +141,7 @@ namespace S7.Net.Types
/// Converts an array of <see cref="T:System.DateTime"/> values to a byte array. /// Converts an array of <see cref="T:System.DateTime"/> values to a byte array.
/// </summary> /// </summary>
/// <param name="dateTimes">The DateTime values to convert.</param> /// <param name="dateTimes">The DateTime values to convert.</param>
/// <returns>A byte array containing the S7 date time representations of <paramref name="dateTime"/>.</returns> /// <returns>A byte array containing the S7 date time representations of <paramref name="dateTimes"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any value of /// <exception cref="ArgumentOutOfRangeException">Thrown when any value of
/// <paramref name="dateTimes"/> is before <see cref="P:SpecMinimumDateTime"/> /// <paramref name="dateTimes"/> is before <see cref="P:SpecMinimumDateTime"/>
/// or after <see cref="P:SpecMaximumDateTime"/>.</exception> /// or after <see cref="P:SpecMaximumDateTime"/>.</exception>

View File

@@ -8,17 +8,17 @@ namespace S7.Net.Types
/// An S7 String has a preceeding 2 byte header containing its capacity and length /// An S7 String has a preceeding 2 byte header containing its capacity and length
/// </summary> /// </summary>
public static class S7String public static class S7String
{ {
private static Encoding stringEncoding = Encoding.ASCII; private static Encoding stringEncoding = Encoding.ASCII;
/// <summary> /// <summary>
/// The Encoding used when serializing and deserializing S7String (Encoding.ASCII by default) /// The Encoding used when serializing and deserializing S7String (Encoding.ASCII by default)
/// </summary> /// </summary>
/// <exception cref="ArgumentNullException">StringEncoding must not be null</exception> /// <exception cref="ArgumentNullException">StringEncoding must not be null</exception>
public static Encoding StringEncoding public static Encoding StringEncoding
{ {
get => stringEncoding; get => stringEncoding;
set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding)); set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding));
} }
/// <summary> /// <summary>
@@ -58,7 +58,7 @@ namespace S7.Net.Types
/// <param name="value">The string to convert to byte array.</param> /// <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> /// <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> /// <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) public static byte[] ToByteArray(string? value, int reservedLength)
{ {
if (value is null) if (value is null)
{ {

View File

@@ -48,7 +48,7 @@ namespace S7.Net.Types
/// <param name="value">The string to convert to byte array.</param> /// <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> /// <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> /// <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) public static byte[] ToByteArray(string? value, int reservedLength)
{ {
if (value is null) if (value is null)
{ {

View File

@@ -12,13 +12,15 @@
/// <param name="reservedLength">The amount of bytes reserved for the <paramref name="value"/> in 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) public static byte[] ToByteArray(string value, int reservedLength)
{ {
var length = value?.Length;
if (length > reservedLength) length = reservedLength;
var bytes = new byte[reservedLength]; var bytes = new byte[reservedLength];
if (value == null) return bytes;
if (length == null || length == 0) return bytes; var length = value.Length;
if (length == 0) return bytes;
System.Text.Encoding.ASCII.GetBytes(value, 0, length.Value, bytes, 0); if (length > reservedLength) length = reservedLength;
System.Text.Encoding.ASCII.GetBytes(value, 0, length, bytes, 0);
return bytes; return bytes;
} }

View File

@@ -45,6 +45,7 @@ namespace S7.Net.Types
break; break;
case "Int32": case "Int32":
case "UInt32": case "UInt32":
case "TimeSpan":
numBytes = Math.Ceiling(numBytes); numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0) if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++; numBytes++;
@@ -98,8 +99,8 @@ namespace S7.Net.Types
int bytePos = 0; int bytePos = 0;
int bitPos = 0; int bitPos = 0;
double numBytes = 0.0; double numBytes = 0.0;
object structValue = Activator.CreateInstance(structType); object structValue = Activator.CreateInstance(structType) ??
throw new ArgumentException($"Failed to create an instance of the type {structType}.", nameof(structType));
var infos = structValue.GetType() var infos = structValue.GetType()
#if NETSTANDARD1_3 #if NETSTANDARD1_3
@@ -215,6 +216,21 @@ namespace S7.Net.Types
numBytes += sData.Length; numBytes += sData.Length;
break; 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: default:
var buffer = new byte[GetStructSize(info.FieldType)]; var buffer = new byte[GetStructSize(info.FieldType)];
if (buffer.Length == 0) if (buffer.Length == 0)
@@ -254,6 +270,14 @@ namespace S7.Net.Types
foreach (var info in infos) 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; bytes2 = null;
switch (info.FieldType.Name) switch (info.FieldType.Name)
{ {
@@ -261,7 +285,7 @@ namespace S7.Net.Types
// get the value // get the value
bytePos = (int)Math.Floor(numBytes); bytePos = (int)Math.Floor(numBytes);
bitPos = (int)((numBytes - (double)bytePos) / 0.125); 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 bytes[bytePos] |= (byte)Math.Pow(2, bitPos); // is true
else else
bytes[bytePos] &= (byte)(~(byte)Math.Pow(2, bitPos)); // is false bytes[bytePos] &= (byte)(~(byte)Math.Pow(2, bitPos)); // is false
@@ -270,26 +294,26 @@ namespace S7.Net.Types
case "Byte": case "Byte":
numBytes = (int)Math.Ceiling(numBytes); numBytes = (int)Math.Ceiling(numBytes);
bytePos = (int)numBytes; bytePos = (int)numBytes;
bytes[bytePos] = (byte)info.GetValue(structValue); bytes[bytePos] = GetValueOrThrow<byte>(info, structValue);
numBytes++; numBytes++;
break; break;
case "Int16": case "Int16":
bytes2 = Int.ToByteArray((Int16)info.GetValue(structValue)); bytes2 = Int.ToByteArray(GetValueOrThrow<short>(info, structValue));
break; break;
case "UInt16": case "UInt16":
bytes2 = Word.ToByteArray((UInt16)info.GetValue(structValue)); bytes2 = Word.ToByteArray(GetValueOrThrow<ushort>(info, structValue));
break; break;
case "Int32": case "Int32":
bytes2 = DInt.ToByteArray((Int32)info.GetValue(structValue)); bytes2 = DInt.ToByteArray(GetValueOrThrow<int>(info, structValue));
break; break;
case "UInt32": case "UInt32":
bytes2 = DWord.ToByteArray((UInt32)info.GetValue(structValue)); bytes2 = DWord.ToByteArray(GetValueOrThrow<uint>(info, structValue));
break; break;
case "Single": case "Single":
bytes2 = Real.ToByteArray((float)info.GetValue(structValue)); bytes2 = Real.ToByteArray(GetValueOrThrow<float>(info, structValue));
break; break;
case "Double": case "Double":
bytes2 = LReal.ToByteArray((double)info.GetValue(structValue)); bytes2 = LReal.ToByteArray(GetValueOrThrow<double>(info, structValue));
break; break;
case "String": case "String":
S7StringAttribute? attribute = info.GetCustomAttributes<S7StringAttribute>().SingleOrDefault(); S7StringAttribute? attribute = info.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
@@ -298,11 +322,14 @@ namespace S7.Net.Types
bytes2 = attribute.Type switch bytes2 = attribute.Type switch
{ {
S7StringType.S7String => S7String.ToByteArray((string)info.GetValue(structValue), attribute.ReservedLength), S7StringType.S7String => S7String.ToByteArray((string?)info.GetValue(structValue), attribute.ReservedLength),
S7StringType.S7WString => S7WString.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") _ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute")
}; };
break; break;
case "TimeSpan":
bytes2 = TimeSpan.ToByteArray((System.TimeSpan)info.GetValue(structValue));
break;
} }
if (bytes2 != null) if (bytes2 != null)
{ {

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

View File

@@ -1,13 +0,0 @@
image: Visual Studio 2022
configuration: Release
install:
- choco install gitversion.portable -y
before_build:
- cmd: gitversion /l console /output buildserver
- dotnet restore
build_script:
msbuild /nologo /v:m /p:AssemblyVersion=%GitVersion_AssemblySemVer% /p:FileVersion=%GitVersion_MajorMinorPatch% /p:InformationalVersion=%GitVersion_InformationalVersion% /p:Configuration=%CONFIGURATION% S7.sln
after_build:
- dotnet pack S7.Net -c %CONFIGURATION% /p:Version=%GitVersion_NuGetVersion% --no-build -o artifacts
artifacts:
- path: artifacts\*.*