94 Commits

Author SHA1 Message Date
dependabot[bot]
197bea8a1d build(deps): Bump actions/download-artifact in /.github/workflows
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.1.7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4.1.7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-03 22:25:47 +00:00
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
35 changed files with 1517 additions and 165 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@v4.1.7
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

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

View File

@@ -1006,7 +1006,7 @@ namespace S7.Net.UnitTest
var db = 2;
randomEngine.NextBytes(data);
cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5));
cancellationSource.CancelAfter(System.TimeSpan.FromMilliseconds(5));
try
{
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);
@@ -1045,7 +1045,7 @@ namespace S7.Net.UnitTest
var db = 2;
randomEngine.NextBytes(data.Span);
cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5));
cancellationSource.CancelAfter(System.TimeSpan.FromMilliseconds(5));
try
{
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);

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

View File

@@ -138,19 +138,59 @@ namespace S7.Net
/// <summary>
/// Helper to get a bit value given a byte and the bit index.
/// Example: DB1.DBX0.5 -> var bytes = ReadBytes(DB1.DBW0); bool bit = bytes[0].SelectBit(5);
/// <br/>
/// <example>
/// Get the bit at DB1.DBX0.5:
/// <code>
/// byte data = ReadByte("DB1.DBB0");
/// bool bit = data.SelectBit(5);
/// </code>
/// </example>
/// </summary>
/// <param name="data"></param>
/// <param name="bitPosition"></param>
/// <returns></returns>
public static bool SelectBit(this byte data, int bitPosition)
/// <param name="data">The data to get from.</param>
/// <param name="index">The zero-based index of the bit to get.</param>
/// <returns>The Boolean value will get.</returns>
public static bool SelectBit(this byte data, int index)
{
int mask = 1 << bitPosition;
int mask = 1 << index;
int result = data & mask;
return (result != 0);
}
/// <summary>
/// Helper to set a bit value to the given byte at the bit index.
/// <br/>
/// <example>
/// Set the bit at index 4:
/// <code>
/// byte data = 0;
/// data.SetBit(4, true);
/// </code>
/// </example>
/// </summary>
/// <param name="data">The data to be modified.</param>
/// <param name="index">The zero-based index of the bit to set.</param>
/// <param name="value">The Boolean value to assign to the bit.</param>
public static void SetBit(this ref byte data, int index, bool value)
{
if ((uint)index > 7)
{
return;
}
if (value)
{
byte mask = (byte)(1 << index);
data |= mask;
}
else
{
byte mask = (byte)~(1 << index);
data &= mask;
}
}
/// <summary>
/// Converts from ushort value to short value; it's used to retrieve negative values from words
/// </summary>

View File

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

@@ -9,29 +9,104 @@ namespace S7.Net
{
public partial class Plc
{
/// <summary>
/// Creates the header to read bytes from the PLC
/// </summary>
/// <param name="amount"></param>
/// <returns></returns>
private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount = 1)
private static void WriteTpktHeader(System.IO.MemoryStream stream, int length)
{
//header size = 19 bytes
stream.Write(new byte[] { 0x03, 0x00 });
//complete package size
stream.Write(Int.ToByteArray((short)(19 + (12 * amount))));
stream.Write(new byte[] { 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 });
//data part size
stream.Write(Word.ToByteArray((ushort)(2 + (amount * 12))));
stream.Write(new byte[] { 0x00, 0x00, 0x04 });
stream.Write(Word.ToByteArray((ushort) length));
}
private static void WriteDataHeader(System.IO.MemoryStream stream)
{
stream.Write(new byte[] { 0x02, 0xf0, 0x80 });
}
private static void WriteS7Header(System.IO.MemoryStream stream, byte messageType, int parameterLength, int dataLength)
{
stream.WriteByte(0x32); // S7 protocol ID
stream.WriteByte(messageType); // Message type
stream.Write(new byte[] { 0x00, 0x00 }); // Reserved
stream.Write(new byte[] { 0x00, 0x00 }); // PDU ref
stream.Write(Word.ToByteArray((ushort) parameterLength));
stream.Write(Word.ToByteArray((ushort) dataLength));
}
/// <summary>
/// Creates the header to read bytes from the PLC.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="amount">The number of items to read.</param>
private static void WriteReadHeader(System.IO.MemoryStream stream, int amount = 1)
{
// Header size 19, 12 bytes per item
WriteTpktHeader(stream, 19 + 12 * amount);
WriteDataHeader(stream);
WriteS7Header(stream, 0x01, 2 + 12 * amount, 0);
// Function code: read request
stream.WriteByte(0x04);
//amount of requests
stream.WriteByte((byte)amount);
}
private static void WriteUserDataHeader(System.IO.MemoryStream stream, int parameterLength, int dataLength)
{
const byte s7MessageTypeUserData = 0x07;
WriteTpktHeader(stream, 17 + parameterLength + dataLength);
WriteDataHeader(stream);
WriteS7Header(stream, s7MessageTypeUserData, parameterLength, dataLength);
}
private static void WriteUserDataRequest(System.IO.MemoryStream stream, byte functionGroup, byte subFunction, int dataLength)
{
WriteUserDataHeader(stream, 8, dataLength);
// Parameter
const byte userDataMethodRequest = 0x11;
const byte userDataTypeRequest = 0x4;
// Parameter head
stream.Write(new byte[] { 0x00, 0x01, 0x12 });
// Parameter length
stream.WriteByte(0x04);
// Method
stream.WriteByte(userDataMethodRequest);
// Type / function group
stream.WriteByte((byte)(userDataTypeRequest << 4 | (functionGroup & 0x0f)));
// Subfunction
stream.WriteByte(subFunction);
// Sequence number
stream.WriteByte(0);
}
private static void WriteSzlReadRequest(System.IO.MemoryStream stream, ushort szlId, ushort szlIndex)
{
// Parameter
const byte szlFunctionGroupCpuFunctions = 0b100;
const byte subFunctionReadSzl = 0x01;
WriteUserDataRequest(stream, szlFunctionGroupCpuFunctions, subFunctionReadSzl, 8);
// Data
const byte success = 0xff;
const byte transportSizeOctetString = 0x09;
// Return code
stream.WriteByte(success);
// Transport size
stream.WriteByte(transportSizeOctetString);
// Length
stream.Write(Word.ToByteArray(4));
// SZL-ID
stream.Write(Word.ToByteArray(szlId));
// SZL-Index
stream.Write(Word.ToByteArray(szlIndex));
}
/// <summary>
/// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType),
/// the address of the memory, the address of the byte and the bytes count.
/// </summary>
/// <param name="stream">The stream to write the read data request to.</param>
/// <param name="dataType">MemoryType (DB, Timer, Counter, etc.)</param>
/// <param name="db">Address of the memory to be read</param>
/// <param name="startByteAdr">Start address of the byte</param>
@@ -167,6 +242,24 @@ namespace S7.Net
{
return DateTimeLong.ToArray(bytes);
}
case VarType.Time:
if (varCount == 1)
{
return TimeSpan.FromByteArray(bytes);
}
else
{
return TimeSpan.ToArray(bytes);
}
case VarType.Date:
if (varCount == 1)
{
return Date.FromByteArray(bytes);
}
else
{
return Date.ToArray(bytes);
}
default:
return null;
}
@@ -196,10 +289,12 @@ namespace S7.Net
case VarType.Timer:
case VarType.Int:
case VarType.Counter:
case VarType.Date:
return varCount * 2;
case VarType.DWord:
case VarType.DInt:
case VarType.Real:
case VarType.Time:
return varCount * 4;
case VarType.LReal:
case VarType.DateTime:
@@ -252,7 +347,7 @@ namespace S7.Net
int packageSize = 19 + (dataItems.Count * 12);
var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package, dataItems.Count);
WriteReadHeader(package, dataItems.Count);
foreach (var dataItem in dataItems)
{
@@ -261,5 +356,15 @@ namespace S7.Net
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

@@ -312,6 +312,49 @@ namespace S7.Net
return dataItems;
}
/// <summary>
/// Read the PLC clock value.
/// </summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous operation, with it's result set to the current PLC time on completion.</returns>
public async Task<System.DateTime> ReadClockAsync(CancellationToken cancellationToken = default)
{
var request = BuildClockReadRequest();
var response = await RequestTsduAsync(request, cancellationToken);
return ParseClockReadResponse(response);
}
/// <summary>
/// Write the PLC clock value.
/// </summary>
/// <param name="value">The date and time to set the PLC clock to</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task WriteClockAsync(System.DateTime value, CancellationToken cancellationToken = default)
{
var request = BuildClockWriteRequest(value);
var response = await RequestTsduAsync(request, cancellationToken);
ParseClockWriteResponse(response);
}
/// <summary>
/// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type.
/// </summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous operation, with it's result set to the current PLC status on completion.</returns>
public async Task<byte> ReadStatusAsync(CancellationToken cancellationToken = default)
{
var dataToSend = BuildSzlReadRequestPackage(0x0424, 0);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
return (byte) (s7data[37] & 0x0f);
}
/// <summary>
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
/// If the write was not successful, check LastErrorCode or LastErrorString.
@@ -428,7 +471,6 @@ namespace S7.Net
/// <summary>
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the write was not successful, check <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <param name="value">Value to be written to the PLC</param>
@@ -507,6 +549,7 @@ namespace S7.Net
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory<byte> value, CancellationToken cancellationToken)
{

View File

@@ -289,7 +289,6 @@ namespace S7.Net
/// <summary>
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the write was not successful, check <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <param name="value">Value to be written to the PLC</param>
@@ -329,7 +328,7 @@ namespace S7.Net
const int packageSize = 19 + 12; // 19 header + 12 for 1 request
var dataToSend = new byte[packageSize];
var package = new MemoryStream(dataToSend);
BuildHeaderPackage(package);
WriteReadHeader(package);
// package.Add(0x02); // datenart
BuildReadDataRequestPackage(package, dataType, db, startByteAdr, buffer.Length);
@@ -474,7 +473,7 @@ namespace S7.Net
int packageSize = 19 + (dataItems.Count * 12);
var dataToSend = new byte[packageSize];
var package = new MemoryStream(dataToSend);
BuildHeaderPackage(package, dataItems.Count);
WriteReadHeader(package, dataItems.Count);
// package.Add(0x02); // datenart
foreach (var dataItem in dataItems)
{
@@ -493,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, int offset, int length)

View File

@@ -26,6 +26,11 @@ namespace S7.Net.Protocol
_ => Types.String.ToByteArray(s, dataItem.Count)
};
if (dataItem.VarType == VarType.Date)
{
return Date.ToByteArray((System.DateTime)dataItem.Value);
}
return SerializeValue(dataItem.Value);
}

View File

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

View File

@@ -39,6 +39,7 @@ namespace S7.Net
/// <param name="buffer">the buffer to read into</param>
/// <param name="offset">the offset in the buffer to read into</param>
/// <param name="count">the amount of bytes to read into the buffer</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <returns>returns the amount of read bytes</returns>
public static async Task<int> ReadExactAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{

View File

@@ -29,6 +29,7 @@ namespace S7.Net
/// Reads a TPKT from the socket Async
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <returns>Task TPKT Instace</returns>
public static async Task<TPKT> ReadAsync(Stream stream, CancellationToken cancellationToken)
{

View File

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

View File

@@ -8,17 +8,17 @@ namespace S7.Net.Types
/// An S7 String has a preceeding 2 byte header containing its capacity and length
/// </summary>
public static class S7String
{
private static Encoding stringEncoding = Encoding.ASCII;
{
private static Encoding stringEncoding = Encoding.ASCII;
/// <summary>
/// The Encoding used when serializing and deserializing S7String (Encoding.ASCII by default)
/// </summary>
/// <exception cref="ArgumentNullException">StringEncoding must not be null</exception>
public static Encoding StringEncoding
{
get => stringEncoding;
set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding));
/// <exception cref="ArgumentNullException">StringEncoding must not be null</exception>
public static Encoding StringEncoding
{
get => stringEncoding;
set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding));
}
/// <summary>
@@ -58,7 +58,7 @@ namespace S7.Net.Types
/// <param name="value">The string to convert to byte array.</param>
/// <param name="reservedLength">The length (in characters) allocated in PLC for the string.</param>
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 2.</returns>
public static byte[] ToByteArray(string value, int reservedLength)
public static byte[] ToByteArray(string? value, int reservedLength)
{
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="reservedLength">The length (in characters) allocated in PLC for the string.</param>
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 4.</returns>
public static byte[] ToByteArray(string value, int reservedLength)
public static byte[] ToByteArray(string? value, int reservedLength)
{
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>
public static byte[] ToByteArray(string value, int reservedLength)
{
var length = value?.Length;
if (length > reservedLength) length = reservedLength;
var bytes = new byte[reservedLength];
if (value == null) return bytes;
if (length == null || length == 0) return bytes;
var length = value.Length;
if (length == 0) return bytes;
System.Text.Encoding.ASCII.GetBytes(value, 0, length.Value, bytes, 0);
if (length > reservedLength) length = reservedLength;
System.Text.Encoding.ASCII.GetBytes(value, 0, length, bytes, 0);
return bytes;
}

View File

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

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\*.*