46 Commits

Author SHA1 Message Date
Michael Croes
3b2dbd1148 Release S7NetPlus 0.7.0
Release highlights:
- Nullable reference type support
- Cleanups of TSDU and TPDU message parsing
- Better exceptions for connection Open() errors
- Support for buffered reads
2020-09-04 20:51:29 +02:00
Michael Croes
3c91aa02b0 Merge pull request #291 from scamille/fb-readFixed
Fixed size reads version 2
2020-09-03 23:46:42 +02:00
Serge Camille
edfa208c3e Simplify TPKT Read buffer length arguments and improve exception message. 2020-09-03 20:48:18 +02:00
Serge Camille
d11f46eedb Rename ReadFixed to ReadExact, separate Unit test for ReadExact. 2020-09-03 20:48:18 +02:00
Serge Camille
09c8b18d3d Adjust ReadFixed implementation somewhat. Exceeding the length of the buffer was already an error before.
Change the tests by replacing the memory buffer with a Fake stream giving 1 byte at a time.
2020-09-03 20:48:18 +02:00
Jakob Ledermann
10e5562706 Read desired amount from stream. see #273
The read method on a networkstream blocks only until the first byte is available.
It is not guaranteed to read the desired count into the buffer.

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

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

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

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

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

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

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

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

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

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

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

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

Close #175, #220.
2020-08-13 21:49:30 +02:00
Michael Croes
9ea54be524 Types/Class: Start arrays on even bytes
Addresses #175
2020-08-13 21:40:44 +02:00
Michael Croes
dcd5bb3437 Merge pull request #246 from timverwaal/PLCAdddres-Parsing
Extended PLCAddress.Parse method
2020-08-13 20:43:16 +02:00
Tim Verwaal
8dc89867e9 Extended PLCAddress.Parse method 2019-12-19 10:55:51 +01:00
32 changed files with 1210 additions and 455 deletions

View File

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

View File

@@ -1,36 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("S7.Net.UnitTest")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("S7Net.UnitTest")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("6f73e1b1-301b-471e-9f38-3dcbddbcfc21")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -34,13 +34,14 @@ namespace S7.Net.UnitTest
var t = TPKT.Read(m);
}
[TestMethod]
[ExpectedException(typeof(TPKTInvalidException))]
public async Task TPKT_ReadShortAsync()
{
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff040080"));
var t = await TPKT.ReadAsync(m);
}
}
[TestMethod]
public void COTP_ReadTSDU()
@@ -54,7 +55,7 @@ namespace S7.Net.UnitTest
Assert.IsTrue(expected.SequenceEqual(t));
}
private static byte[] StringToByteArray(string hex)
public static byte[] StringToByteArray(string hex)
{
return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0)
@@ -62,4 +63,5 @@ namespace S7.Net.UnitTest
.ToArray();
}
}
}

View File

@@ -1,127 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{303CCED6-9ABC-4899-A509-743341AAA804}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>S7.Net.UnitTest</RootNamespace>
<AssemblyName>S7Net.UnitTest</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
<IsCodedUITest>False</IsCodedUITest>
<TestProjectType>UnitTest</TestProjectType>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net452</TargetFrameworks>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
<IsPackable>false</IsPackable>
<Copyright>Copyright © 2014</Copyright>
</PropertyGroup>
<PropertyGroup>
<AssemblyOriginatorKeyFile>S7.Net.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
<PackageReference Include="coverlet.collector" Version="1.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\S7.Net\S7.Net.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<Choose>
<When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'">
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
</ItemGroup>
</When>
<Otherwise />
</Choose>
<ItemGroup>
<Compile Include="ConnectionRequestTest.cs" />
<Compile Include="ConvertersUnitTest.cs" />
<Compile Include="Helpers\TestClassWithNestedClass.cs" />
<Compile Include="ProtocolTests.cs" />
<Compile Include="Helpers\ConsoleManager.cs" />
<Compile Include="Helpers\NativeMethods.cs" />
<Compile Include="Helpers\S7TestServer.cs" />
<Compile Include="Helpers\TestClassWithArrays.cs" />
<Compile Include="Helpers\TestClassWithCustomType.cs" />
<Compile Include="Helpers\TestClassWithPrivateSetters.cs" />
<Compile Include="Helpers\TestLongClass.cs" />
<Compile Include="S7NetTestsAsync.cs" />
<Compile Include="Helpers\TestSmallClass.cs" />
<Compile Include="Snap7\snap7.net.cs" />
<Compile Include="Helpers\TestClass.cs" />
<Compile Include="Helpers\TestStruct.cs" />
<Compile Include="S7NetTestsSync.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Helpers\TestLongStruct.cs" />
<Compile Include="TypeTests\DateTimeTests.cs" />
<Compile Include="TypeTests\StringExTests.cs" />
<Compile Include="TypeTests\StringTests.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="snap7.dll">
<None Update="snap7.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\S7.Net\S7.Net.csproj">
<Project>{bfd484f9-3f04-42a2-bf2a-60a189a25dcf}</Project>
<Name>S7.Net</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="S7.Net.snk" />
</ItemGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.QualityTools.CodedUITestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Extension, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.TestTools.UITesting, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<Private>False</Private>
</Reference>
</ItemGroup>
</When>
</Choose>
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>

View File

@@ -128,6 +128,26 @@ namespace S7.Net.UnitTest
Assert.AreEqual(val3, result3);
}
/// <summary>
/// Write/Read a large amount of data to test PDU max
/// </summary>
[TestMethod]
public async Task Test_Async_WriteLargeByteArray()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
var randomEngine = new Random();
var data = new byte[8192];
var db = 2;
randomEngine.NextBytes(data);
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data);
var readData = await plc.ReadBytesAsync(DataType.DataBlock, db, 0, data.Length);
CollectionAssert.AreEqual(data, readData);
}
/// <summary>
/// Read/Write a class that has the same properties of a DB with the same field in the same order
/// </summary>
@@ -591,15 +611,13 @@ namespace S7.Net.UnitTest
[TestMethod]
[ExpectedException(typeof(NullReferenceException))]
public async Task Test_Async_ReadBytesReturnsNullIfPlcIsNotConnected()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
Assert.IsFalse(notConnectedPlc.IsConnected);
TestClass tc = new TestClass();
var res = await notConnectedPlc.ReadClassAsync(tc, DB2);
Assert.Fail();
await Assert.ThrowsExceptionAsync<PlcException>(async () => await notConnectedPlc.ReadClassAsync(tc, DB2));
}
}
@@ -637,13 +655,12 @@ namespace S7.Net.UnitTest
}
[TestMethod]
[ExpectedException(typeof(NullReferenceException))]
public async Task Test_Async_ReadClassWithGenericReturnsNullIfPlcIsNotConnected()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
Assert.IsFalse(notConnectedPlc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestClass tc = await notConnectedPlc.ReadClassAsync<TestClass>(DB2);
await Assert.ThrowsExceptionAsync<PlcException>(async () => await notConnectedPlc.ReadClassAsync<TestClass>(DB2));
}
}
@@ -678,13 +695,12 @@ namespace S7.Net.UnitTest
}
[TestMethod]
[ExpectedException(typeof(NullReferenceException))]
public async Task Test_Async_ReadClassWithGenericAndClassFactoryThrowsExceptionPlcIsNotConnected()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
Assert.IsFalse(notConnectedPlc.IsConnected);
TestClass tc = await notConnectedPlc.ReadClassAsync(() => new TestClass(), DB2);
await Assert.ThrowsExceptionAsync<PlcException>(async () => await notConnectedPlc.ReadClassAsync(() => new TestClass(), DB2));
}
}
@@ -711,13 +727,12 @@ namespace S7.Net.UnitTest
}
[TestMethod]
[ExpectedException(typeof(NullReferenceException))]
public async Task Test_Async_ReadStructThrowsExceptionPlcIsNotConnected()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
Assert.IsFalse(notConnectedPlc.IsConnected);
object tsObj = await notConnectedPlc.ReadStructAsync(typeof(TestStruct), DB2);
await Assert.ThrowsExceptionAsync<PlcException>(async () => await notConnectedPlc.ReadStructAsync(typeof(TestStruct), DB2));
}
}
@@ -754,13 +769,12 @@ namespace S7.Net.UnitTest
}
[TestMethod]
[ExpectedException(typeof(NullReferenceException))]
public async Task Test_Async_ReadStructWithGenericThrowsExceptionIfPlcIsNotConnected()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
Assert.IsFalse(notConnectedPlc.IsConnected);
object tsObj = await notConnectedPlc.ReadStructAsync<TestStruct>(DB2);
await Assert.ThrowsExceptionAsync<PlcException>(async () => await notConnectedPlc.ReadStructAsync<TestStruct>(DB2));
}
}

View File

@@ -743,6 +743,26 @@ namespace S7.Net.UnitTest
Assert.AreEqual(tc.ShortVariable04.ShortVarialbe00, tc2.ShortVariable04.ShortVarialbe00);
}
/// <summary>
/// Write/Read a large amount of data to test PDU max
/// </summary>
[TestMethod]
public void T33_WriteLargeByteArray()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
var randomEngine = new Random();
var data = new byte[8192];
var db = 2;
randomEngine.NextBytes(data);
plc.WriteBytes(DataType.DataBlock, db, 0, data);
var readData = plc.ReadBytes(DataType.DataBlock, db, 0, data.Length);
CollectionAssert.AreEqual(data, readData);
}
[TestMethod, ExpectedException(typeof(PlcException))]
public void T18_ReadStructThrowsIfPlcIsNotConnected()
{
@@ -997,6 +1017,20 @@ namespace S7.Net.UnitTest
Assert.AreEqual(test_value, test_value2, "Compare Write/Read"); //No delta, datatype matches
}
[TestMethod]
public void T33_ReadWriteDateTimeLong()
{
var test_value = System.DateTime.Now;
var db = 1;
var offset = 0;
plc.WriteBytes(DataType.DataBlock, db, offset, Types.DateTimeLong.ToByteArray(test_value));
var test_value2 = plc.Read(DataType.DataBlock, db, offset, VarType.DateTimeLong, 1);
Assert.IsInstanceOfType(test_value2, typeof(System.DateTime));
Assert.AreEqual(test_value, test_value2, "Compare DateTimeLong Write/Read");
}
#endregion
#region Private methods

View File

@@ -0,0 +1,97 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace S7.Net.UnitTest
{
/// <summary>
/// Test stream which only gives 1 byte per read.
/// </summary>
class TestStream1BytePerRead : Stream
{
public TestStream1BytePerRead(byte[] data)
{
Data = data;
}
public override bool CanRead => _position < Data.Length;
public override bool CanSeek => throw new NotImplementedException();
public override bool CanWrite => throw new NotImplementedException();
public override long Length => throw new NotImplementedException();
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public byte[] Data { get; }
public override void Flush()
{
throw new NotImplementedException();
}
int _position = 0;
public override int Read(byte[] buffer, int offset, int count)
{
if (_position >= Data.Length)
{
return 0;
}
buffer[offset] = Data[_position];
++_position;
return 1;
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
}
/// <summary>
/// These tests are intended to test <see cref="StreamExtensions"/> functions and other stream-related special cases.
/// </summary>
[TestClass]
public class StreamTests
{
[TestMethod]
public async Task TPKT_ReadRestrictedStreamAsync()
{
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd");
var m = new TestStream1BytePerRead(fullMessage);
var t = await TPKT.ReadAsync(m);
Assert.AreEqual(fullMessage.Length, t.Length);
Assert.AreEqual(fullMessage.Last(), t.Data.Last());
}
[TestMethod]
public void TPKT_ReadRestrictedStream()
{
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd");
var m = new TestStream1BytePerRead(fullMessage);
var t = TPKT.Read(m);
Assert.AreEqual(fullMessage.Length, t.Length);
Assert.AreEqual(fullMessage.Last(), t.Data.Last());
}
[TestMethod]
public void TPKT_ReadStreamTooShort()
{
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400");
var m = new TestStream1BytePerRead(fullMessage);
Assert.ThrowsException<TPKTInvalidException>(() => TPKT.Read(m));
}
}
}

View File

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

View File

@@ -0,0 +1,171 @@
using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace S7.Net.UnitTest.TypeTests
{
public static class DateTimeLongTests
{
private static readonly DateTime SampleDateTime = new DateTime(1993, 12, 25, 8, 12, 34, 567);
private static readonly byte[] SampleByteArray = {0x07, 0xC9, 0x0C, 0x19, 0x07, 0x08, 0x0C, 0x22, 0x21, 0xCB, 0xBB, 0xC0 };
private static readonly byte[] SpecMinByteArray =
{
0x07, 0xB2, 0x01, 0x01, (byte) (int) (Types.DateTimeLong.SpecMinimumDateTime.DayOfWeek + 1), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static readonly byte[] SpecMaxByteArray =
{
0x08, 0xD6, 0x04, 0x0B, (byte) (int) (Types.DateTimeLong.SpecMaximumDateTime.DayOfWeek + 1), 0x17, 0x2F, 0x10, 0x32, 0xE7, 0x01, 0x80
};
[TestClass]
public class FromByteArray
{
[TestMethod]
public void Sample()
{
AssertFromByteArrayEquals(SampleDateTime, SampleByteArray);
}
[TestMethod]
public void SpecMinimum()
{
AssertFromByteArrayEquals(Types.DateTimeLong.SpecMinimumDateTime, SpecMinByteArray);
}
[TestMethod]
public void SpecMaximum()
{
AssertFromByteArrayEquals(Types.DateTimeLong.SpecMaximumDateTime, SpecMaxByteArray);
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnLessThan12Bytes()
{
Types.DateTimeLong.FromByteArray(new byte[11]);
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnMoreTHan12Bytes()
{
Types.DateTimeLong.FromByteArray(new byte[13]);
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnInvalidYear()
{
Types.DateTimeLong.FromByteArray(MutateSample(0, 0xa0));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnZeroMonth()
{
Types.DateTimeLong.FromByteArray(MutateSample(2, 0x00));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnTooLargeMonth()
{
Types.DateTimeLong.FromByteArray(MutateSample(2, 0x13));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnZeroDay()
{
Types.DateTimeLong.FromByteArray(MutateSample(3, 0x00));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnTooLargeDay()
{
Types.DateTimeLong.FromByteArray(MutateSample(3, 0x32));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnInvalidHour()
{
Types.DateTimeLong.FromByteArray(MutateSample(5, 0x24));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnInvalidMinute()
{
Types.DateTimeLong.FromByteArray(MutateSample(6, 0x60));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnInvalidSecond()
{
Types.DateTimeLong.FromByteArray(MutateSample(7, 0x60));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnInvalidNanosecondsFirstDigit()
{
Types.DateTimeLong.FromByteArray(MutateSample(8, 0x3B));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnZeroDayOfWeek()
{
Types.DateTimeLong.FromByteArray(MutateSample(4, 0));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnTooLargeDayOfWeek()
{
Types.DateTimeLong.FromByteArray(MutateSample(4, 8));
}
private static void AssertFromByteArrayEquals(DateTime expected, params byte[] bytes)
{
Assert.AreEqual(expected, Types.DateTimeLong.FromByteArray(bytes));
}
private static byte[] MutateSample(int index, byte value) =>
SampleByteArray.Select((b, i) => i == index ? value : b).ToArray();
}
[TestClass]
public class ToByteArray
{
[TestMethod]
public void Sample()
{
AssertToByteArrayEquals(SampleDateTime, SampleByteArray);
}
[TestMethod]
public void SpecMinimum()
{
AssertToByteArrayEquals(Types.DateTimeLong.SpecMinimumDateTime, SpecMinByteArray);
}
[TestMethod]
public void SpecMaximum()
{
AssertToByteArrayEquals(Types.DateTimeLong.SpecMaximumDateTime, SpecMaxByteArray);
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnTimeBeforeSpecMinimum()
{
Types.DateTimeLong.ToByteArray(new DateTime(1950, 1, 1));
}
[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void ThrowsOnTimeAfterSpecMaximum()
{
Types.DateTimeLong.ToByteArray(new DateTime(2790, 1, 1));
}
private static void AssertToByteArrayEquals(DateTime value, params byte[] expected)
{
CollectionAssert.AreEqual(expected, Types.DateTimeLong.ToByteArray(value));
}
}
}
}

View File

@@ -1,7 +1,6 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Linq;
namespace S7.Net
{
@@ -27,17 +26,17 @@ namespace S7.Net
{
TPkt = tPKT;
var br = new BinaryReader(new MemoryStream(tPKT.Data));
HeaderLength = br.ReadByte();
HeaderLength = tPKT.Data[0]; // Header length excluding this length byte
if (HeaderLength >= 2)
{
PDUType = br.ReadByte();
PDUType = tPKT.Data[1];
if (PDUType == 0xf0) //DT Data
{
var flags = br.ReadByte();
var flags = tPKT.Data[2];
TPDUNumber = flags & 0x7F;
LastDataUnit = (flags & 0x80) > 0;
Data = br.ReadBytes(tPKT.Length - HeaderLength - 4); //4 = TPKT Size
Data = new byte[tPKT.Data.Length - HeaderLength - 1]; // substract header length byte + header length.
Array.Copy(tPKT.Data, HeaderLength + 1, Data, 0, Data.Length);
return;
}
//TODO: Handle other PDUTypes
@@ -54,8 +53,11 @@ namespace S7.Net
public static TPDU Read(Stream stream)
{
var tpkt = TPKT.Read(stream);
if (tpkt.Length > 0) return new TPDU(tpkt);
return null;
if (tpkt.Length == 0)
{
throw new TPDUInvalidException("No protocol data received");
}
return new TPDU(tpkt);
}
/// <summary>
@@ -67,8 +69,11 @@ namespace S7.Net
public static async Task<TPDU> ReadAsync(Stream stream)
{
var tpkt = await TPKT.ReadAsync(stream);
if (tpkt.Length > 0) return new TPDU(tpkt);
return null;
if (tpkt.Length == 0)
{
throw new TPDUInvalidException("No protocol data received");
}
return new TPDU(tpkt);
}
public override string ToString()
@@ -98,22 +103,25 @@ namespace S7.Net
public static byte[] Read(Stream stream)
{
var segment = TPDU.Read(stream);
if (segment == null) return null;
if (segment.LastDataUnit)
{
return segment.Data;
}
// More segments are expected, prepare a buffer to store all data
var buffer = new byte[segment.Data.Length];
var output = new MemoryStream(buffer);
output.Write(segment.Data, 0, segment.Data.Length);
Array.Copy(segment.Data, buffer, segment.Data.Length);
while (!segment.LastDataUnit)
{
segment = TPDU.Read(stream);
var previousLength = buffer.Length;
Array.Resize(ref buffer, buffer.Length + segment.Data.Length);
var lastPosition = output.Position;
output = new MemoryStream(buffer);
output.Write(segment.Data, (int) lastPosition, segment.Data.Length);
Array.Copy(segment.Data, 0, buffer, previousLength, segment.Data.Length);
}
return buffer.Take((int)output.Position).ToArray();
return buffer;
}
/// <summary>
@@ -125,21 +133,25 @@ namespace S7.Net
public static async Task<byte[]> ReadAsync(Stream stream)
{
var segment = await TPDU.ReadAsync(stream);
if (segment == null) return null;
if (segment.LastDataUnit)
{
return segment.Data;
}
// More segments are expected, prepare a buffer to store all data
var buffer = new byte[segment.Data.Length];
var output = new MemoryStream(buffer);
output.Write(segment.Data, 0, segment.Data.Length);
Array.Copy(segment.Data, buffer, segment.Data.Length);
while (!segment.LastDataUnit)
{
segment = await TPDU.ReadAsync(stream);
var previousLength = buffer.Length;
Array.Resize(ref buffer, buffer.Length + segment.Data.Length);
var lastPosition = output.Position;
output = new MemoryStream(buffer);
output.Write(segment.Data, (int) lastPosition, segment.Data.Length);
Array.Copy(segment.Data, 0, buffer, previousLength, segment.Data.Length);
}
return buffer.Take((int)output.Position).ToArray();
return buffer;
}
}
}

View File

@@ -186,6 +186,11 @@
/// <summary>
/// DateTIme variable type
/// </summary>
DateTime
DateTime,
/// <summary>
/// DateTimeLong variable type
/// </summary>
DateTimeLong
}
}

View File

@@ -0,0 +1,18 @@

namespace S7.Net.Helper
{
internal static class MemoryStreamExtension
{
/// <summary>
/// Helper function to write to whole content of the given byte array to a memory stream.
///
/// Writes all bytes in value from 0 to value.Length to the memory stream.
/// </summary>
/// <param name="stream"></param>
/// <param name="value"></param>
public static void WriteByteArray(this System.IO.MemoryStream stream, byte[] value)
{
stream.Write(value, 0, value.Length);
}
}
}

View File

@@ -15,8 +15,8 @@ namespace S7.Net
private const int CONNECTION_TIMED_OUT_ERROR_CODE = 10060;
//TCP connection to device
private TcpClient tcpClient;
private NetworkStream stream;
private TcpClient? tcpClient;
private NetworkStream? _stream;
private int readTimeout = System.Threading.Timeout.Infinite;
private int writeTimeout = System.Threading.Timeout.Infinite;
@@ -24,27 +24,27 @@ namespace S7.Net
/// <summary>
/// IP address of the PLC
/// </summary>
public string IP { get; private set; }
public string IP { get; }
/// <summary>
/// PORT Number of the PLC, default is 102
/// </summary>
public int Port { get; private set; }
public int Port { get; }
/// <summary>
/// CPU type of the PLC
/// </summary>
public CpuType CPU { get; private set; }
public CpuType CPU { get; }
/// <summary>
/// Rack of the PLC
/// </summary>
public Int16 Rack { get; private set; }
public Int16 Rack { get; }
/// <summary>
/// Slot of the CPU of the PLC
/// </summary>
public Int16 Slot { get; private set; }
public Int16 Slot { get; }
/// <summary>
/// Max PDU size this cpu supports
@@ -218,6 +218,27 @@ namespace S7.Net
.Sum(len => (len & 1) == 1 ? len + 1 : len);
}
private static void AssertReadResponse(byte[] s7Data, int dataLength)
{
var expectedLength = dataLength + 18;
PlcException NotEnoughBytes() =>
new PlcException(ErrorCode.WrongNumberReceivedBytes,
$"Received {s7Data.Length} bytes: '{BitConverter.ToString(s7Data)}', expected {expectedLength} bytes.")
;
if (s7Data == null)
throw new PlcException(ErrorCode.WrongNumberReceivedBytes, "No s7Data received.");
if (s7Data.Length < 15) throw NotEnoughBytes();
if (s7Data[14] != 0xff)
throw new PlcException(ErrorCode.ReadData,
$"Invalid response from PLC: '{BitConverter.ToString(s7Data)}'.");
if (s7Data.Length < expectedLength) throw NotEnoughBytes();
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls

View File

@@ -80,6 +80,7 @@
default:
throw new InvalidAddressException();
}
case "IB":
case "EB":
// Input byte
dataType = DataType.Input;
@@ -87,6 +88,7 @@
address = int.Parse(input.Substring(2));
varType = VarType.Byte;
return;
case "IW":
case "EW":
// Input word
dataType = DataType.Input;
@@ -94,6 +96,7 @@
address = int.Parse(input.Substring(2));
varType = VarType.Word;
return;
case "ID":
case "ED":
// Input double-word
dataType = DataType.Input;
@@ -101,21 +104,27 @@
address = int.Parse(input.Substring(2));
varType = VarType.DWord;
return;
case "QB":
case "AB":
case "OB":
// Output byte
dataType = DataType.Output;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Byte;
return;
case "QW":
case "AW":
case "OW":
// Output word
dataType = DataType.Output;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Word;
return;
case "QD":
case "AD":
case "OD":
// Output double-word
dataType = DataType.Output;
dbNumber = 0;
@@ -152,6 +161,7 @@
dataType = DataType.Input;
varType = VarType.Bit;
break;
case "Q":
case "A":
case "O":
// Output
@@ -161,7 +171,7 @@
case "M":
// Memory
dataType = DataType.Memory;
varType = VarType.Byte;
varType = VarType.Bit;
break;
case "T":
// Timer

View File

@@ -89,4 +89,25 @@ namespace S7.Net
}
#endif
}
internal class TPDUInvalidException : Exception
{
public TPDUInvalidException() : base()
{
}
public TPDUInvalidException(string message) : base(message)
{
}
public TPDUInvalidException(string message, Exception innerException) : base(message, innerException)
{
}
#if NET_FULL
protected TPDUInvalidException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
#endif
}
}

View File

@@ -1,4 +1,5 @@
using S7.Net.Types;
using S7.Net.Helper;
using S7.Net.Types;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -13,21 +14,18 @@ namespace S7.Net
/// </summary>
/// <param name="amount"></param>
/// <returns></returns>
private ByteArray ReadHeaderPackage(int amount = 1)
private void BuildHeaderPackage(System.IO.MemoryStream stream, int amount = 1)
{
//header size = 19 bytes
var package = new Types.ByteArray(19);
package.Add(new byte[] { 0x03, 0x00 });
stream.WriteByteArray(new byte[] { 0x03, 0x00 });
//complete package size
package.Add(Types.Int.ToByteArray((short)(19 + (12 * amount))));
package.Add(new byte[] { 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 });
stream.WriteByteArray(Types.Int.ToByteArray((short)(19 + (12 * amount))));
stream.WriteByteArray(new byte[] { 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 });
//data part size
package.Add(Types.Word.ToByteArray((ushort)(2 + (amount * 12))));
package.Add(new byte[] { 0x00, 0x00, 0x04 });
stream.WriteByteArray(Types.Word.ToByteArray((ushort)(2 + (amount * 12))));
stream.WriteByteArray(new byte[] { 0x00, 0x00, 0x04 });
//amount of requests
package.Add((byte)amount);
return package;
stream.WriteByte((byte)amount);
}
/// <summary>
@@ -39,39 +37,36 @@ namespace S7.Net
/// <param name="startByteAdr">Start address of the byte</param>
/// <param name="count">Number of bytes to be read</param>
/// <returns></returns>
private ByteArray CreateReadDataRequestPackage(DataType dataType, int db, int startByteAdr, int count = 1)
private void BuildReadDataRequestPackage(System.IO.MemoryStream stream, DataType dataType, int db, int startByteAdr, int count = 1)
{
//single data req = 12
var package = new Types.ByteArray(12);
package.Add(new byte[] { 0x12, 0x0a, 0x10 });
stream.WriteByteArray(new byte[] { 0x12, 0x0a, 0x10 });
switch (dataType)
{
case DataType.Timer:
case DataType.Counter:
package.Add((byte)dataType);
stream.WriteByte((byte)dataType);
break;
default:
package.Add(0x02);
stream.WriteByte(0x02);
break;
}
package.Add(Word.ToByteArray((ushort)(count)));
package.Add(Word.ToByteArray((ushort)(db)));
package.Add((byte)dataType);
stream.WriteByteArray(Word.ToByteArray((ushort)(count)));
stream.WriteByteArray(Word.ToByteArray((ushort)(db)));
stream.WriteByte((byte)dataType);
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
package.Add((byte)overflow);
stream.WriteByte((byte)overflow);
switch (dataType)
{
case DataType.Timer:
case DataType.Counter:
package.Add(Types.Word.ToByteArray((ushort)(startByteAdr)));
stream.WriteByteArray(Types.Word.ToByteArray((ushort)(startByteAdr)));
break;
default:
package.Add(Types.Word.ToByteArray((ushort)((startByteAdr) * 8)));
stream.WriteByteArray(Types.Word.ToByteArray((ushort)((startByteAdr) * 8)));
break;
}
return package;
}
/// <summary>
@@ -82,7 +77,7 @@ namespace S7.Net
/// <param name="varCount"></param>
/// <param name="bitAdr"></param>
/// <returns></returns>
private object ParseBytes(VarType varType, byte[] bytes, int varCount, byte bitAdr = 0)
private object? ParseBytes(VarType varType, byte[] bytes, int varCount, byte bitAdr = 0)
{
if (bytes == null || bytes.Length == 0)
return null;
@@ -156,6 +151,15 @@ namespace S7.Net
{
return DateTime.ToArray(bytes);
}
case VarType.DateTimeLong:
if (varCount == 1)
{
return DateTimeLong.FromByteArray(bytes);
}
else
{
return DateTimeLong.ToArray(bytes);
}
default:
return null;
}
@@ -190,6 +194,8 @@ namespace S7.Net
return varCount * 4;
case VarType.DateTime:
return varCount * 8;
case VarType.DateTimeLong:
return varCount * 12;
default:
return 0;
}

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Sockets;
using System.Threading.Tasks;
using S7.Net.Protocol;
using System.IO;
namespace S7.Net
{
@@ -20,9 +21,14 @@ namespace S7.Net
public async Task OpenAsync()
{
await ConnectAsync();
var stream = GetStreamIfAvailable();
await stream.WriteAsync(ConnectionRequest.GetCOTPConnectionRequest(CPU, Rack, Slot), 0, 22);
var response = await COTP.TPDU.ReadAsync(stream);
if (response == null)
{
throw new Exception("Error reading Connection Confirm. Malformed TPDU packet");
}
if (response.PDUType != 0xd0) //Connect Confirm
{
throw new InvalidDataException("Error reading Connection Confirm", response.TPkt.Data, 1, 0x0d);
@@ -33,11 +39,16 @@ namespace S7.Net
var s7data = await COTP.TSDU.ReadAsync(stream);
if (s7data == null)
throw new WrongNumberOfBytesException("No data received in response to Communication Setup");
if (s7data.Length < 2)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
//Check for S7 Ack Data
if (s7data[1] != 0x03)
throw new InvalidDataException("Error reading Communication Setup response", s7data, 1, 0x03);
if (s7data.Length < 20)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
MaxPDUSize = (short)(s7data[18] * 256 + s7data[19]);
}
@@ -46,9 +57,10 @@ namespace S7.Net
tcpClient = new TcpClient();
ConfigureConnection();
await tcpClient.ConnectAsync(IP, Port);
stream = tcpClient.GetStream();
_stream = tcpClient.GetStream();
}
/// <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.
@@ -60,20 +72,17 @@ namespace S7.Net
/// <returns>Returns the bytes in an array</returns>
public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count)
{
List<byte> resultBytes = new List<byte>();
int index = startByteAdr;
var resultBytes = new byte[count];
int index = 0;
while (count > 0)
{
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
var maxToRead = (int)Math.Min(count, MaxPDUSize - 18);
byte[] bytes = await ReadBytesWithSingleRequestAsync(dataType, db, index, maxToRead);
if (bytes == null)
return resultBytes.ToArray();
resultBytes.AddRange(bytes);
var maxToRead = Math.Min(count, MaxPDUSize - 18);
await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, resultBytes, index, maxToRead);
count -= maxToRead;
index += maxToRead;
}
return resultBytes.ToArray();
return resultBytes;
}
/// <summary>
@@ -87,7 +96,7 @@ namespace S7.Net
/// <param name="varType">Type of the variable/s that you are reading</param>
/// <param name="bitAdr">Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter.</param>
/// <param name="varCount"></param>
public async Task<object> ReadAsync(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0)
public async Task<object?> ReadAsync(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0)
{
int cntBytes = VarTypeToByteLength(varType, varCount);
byte[] bytes = await ReadBytesAsync(dataType, db, startByteAdr, cntBytes);
@@ -100,7 +109,7 @@ namespace S7.Net
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <returns>Returns an object that contains the value. This object must be cast accordingly.</returns>
public async Task<object> ReadAsync(string variable)
public async Task<object?> ReadAsync(string variable)
{
var adr = new PLCAddress(variable);
return await ReadAsync(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber);
@@ -113,7 +122,7 @@ namespace S7.Net
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>Returns a struct that must be cast.</returns>
public async Task<object> ReadStructAsync(Type structType, int db, int startByteAdr = 0)
public async Task<object?> ReadStructAsync(Type structType, int db, int startByteAdr = 0)
{
int numBytes = Types.Struct.GetStructSize(structType);
// now read the package
@@ -168,7 +177,7 @@ namespace S7.Net
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public async Task<T> ReadClassAsync<T>(int db, int startByteAdr = 0) where T : class
public async Task<T?> ReadClassAsync<T>(int db, int startByteAdr = 0) where T : class
{
return await ReadClassAsync(() => Activator.CreateInstance<T>(), db, startByteAdr);
}
@@ -182,7 +191,7 @@ namespace S7.Net
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public async Task<T> ReadClassAsync<T>(Func<T> classFactory, int db, int startByteAdr = 0) where T : class
public async Task<T?> ReadClassAsync<T>(Func<T> classFactory, int db, int startByteAdr = 0) where T : class
{
var instance = classFactory();
var res = await ReadClassAsync(instance, db, startByteAdr);
@@ -208,20 +217,23 @@ namespace S7.Net
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//replies with bigger PDU size in connection setup.
AssertPduSizeForRead(dataItems);
var stream = GetStreamIfAvailable();
try
{
// first create the header
int packageSize = 19 + (dataItems.Count * 12);
ByteArray package = new ByteArray(packageSize);
package.Add(ReadHeaderPackage(dataItems.Count));
var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package, dataItems.Count);
// package.Add(0x02); // datenart
foreach (var dataItem in dataItems)
{
package.Add(CreateReadDataRequestPackage(dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count)));
BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count));
}
await stream.WriteAsync(package.Array, 0, package.Array.Length);
var dataToSend = package.ToArray();
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
var s7data = await COTP.TSDU.ReadAsync(stream); //TODO use Async
if (s7data == null || s7data[14] != 0xff)
@@ -255,11 +267,8 @@ namespace S7.Net
int count = value.Length;
while (count > 0)
{
//TODO: Figure out how to use MaxPDUSize here
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//replies with bigger PDU size in connection setup.
var maxToWrite = (int)Math.Min(count, 200);
await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value.Skip(localIndex).Take(maxToWrite).ToArray());
var maxToWrite = (int)Math.Min(count, MaxPDUSize - 35);
await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite);
count -= maxToWrite;
localIndex += maxToWrite;
}
@@ -374,27 +383,24 @@ namespace S7.Net
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes);
}
private async Task<byte[]> ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, int count)
private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count)
{
byte[] bytes = new byte[count];
var stream = GetStreamIfAvailable();
// first create the header
int packageSize = 31;
ByteArray package = new ByteArray(packageSize);
package.Add(ReadHeaderPackage());
int packageSize = 31;
var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package);
// package.Add(0x02); // datenart
package.Add(CreateReadDataRequestPackage(dataType, db, startByteAdr, count));
BuildReadDataRequestPackage(package, dataType, db, startByteAdr, count);
await stream.WriteAsync(package.Array, 0, package.Array.Length);
var dataToSend = package.ToArray();
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
var s7data = await COTP.TSDU.ReadAsync(stream);
if (s7data == null || s7data[14] != 0xff)
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
AssertReadResponse(s7data, count);
for (int cnt = 0; cnt < count; cnt++)
bytes[cnt] = s7data[cnt + 18];
return bytes;
Array.Copy(s7data, 18, buffer, offset, count);
}
/// <summary>
@@ -407,6 +413,8 @@ namespace S7.Net
{
AssertPduSizeForWrite(dataItems);
var stream = GetStreamIfAvailable();
var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems);
await stream.WriteAsync(message.Array, 0, length).ConfigureAwait(false);
@@ -423,38 +431,15 @@ namespace S7.Net
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value)
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count)
{
byte[] bReceive = new byte[513];
int varCount = 0;
try
{
varCount = value.Length;
// first create the header
int packageSize = 35 + value.Length;
ByteArray package = new ByteArray(packageSize);
var stream = GetStreamIfAvailable();
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count);
package.Add(new byte[] { 3, 0, 0 });
package.Add((byte)packageSize);
package.Add(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
package.Add(Word.ToByteArray((ushort)(varCount - 1)));
package.Add(new byte[] { 0, 0x0e });
package.Add(Word.ToByteArray((ushort)(varCount + 4)));
package.Add(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 });
package.Add(Word.ToByteArray((ushort)varCount));
package.Add(Word.ToByteArray((ushort)(db)));
package.Add((byte)dataType);
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
package.Add((byte)overflow);
package.Add(Word.ToByteArray((ushort)(startByteAdr * 8)));
package.Add(new byte[] { 0, 4 });
package.Add(Word.ToByteArray((ushort)(varCount * 8)));
// now join the header and the data
package.Add(value);
await stream.WriteAsync(package.Array, 0, package.Array.Length);
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
var s7data = await COTP.TSDU.ReadAsync(stream);
if (s7data == null || s7data[14] != 0xff)
@@ -470,46 +455,33 @@ namespace S7.Net
private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue)
{
byte[] bReceive = new byte[513];
int varCount = 0;
var stream = GetStreamIfAvailable();
try
{
var value = new[] {bitValue ? (byte) 1 : (byte) 0};
varCount = value.Length;
// first create the header
int packageSize = 35 + value.Length;
ByteArray package = new Types.ByteArray(packageSize);
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
package.Add(new byte[] { 3, 0, 0 });
package.Add((byte)packageSize);
package.Add(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
package.Add(Word.ToByteArray((ushort)(varCount - 1)));
package.Add(new byte[] { 0, 0x0e });
package.Add(Word.ToByteArray((ushort)(varCount + 4)));
package.Add(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x01 }); //ending 0x01 is used for writing a sinlge bit
package.Add(Word.ToByteArray((ushort)varCount));
package.Add(Word.ToByteArray((ushort)(db)));
package.Add((byte)dataType);
int overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
package.Add((byte)overflow);
package.Add(Word.ToByteArray((ushort)(startByteAdr * 8 + bitAdr)));
package.Add(new byte[] { 0, 0x03 }); //ending 0x03 is used for writing a sinlge bit
package.Add(Word.ToByteArray((ushort)(varCount)));
// now join the header and the data
package.Add(value);
await stream.WriteAsync(package.Array, 0, package.Array.Length);
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
var s7data = await COTP.TSDU.ReadAsync(stream);
if (s7data == null || s7data[14] != 0xff)
{
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
}
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.WriteData, exc);
}
}
private Stream GetStreamIfAvailable()
{
if (_stream == null)
{
throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected");
}
return _stream;
}
}
}

View File

@@ -1,9 +1,11 @@
using S7.Net.Types;
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using S7.Net.Protocol;
using S7.Net.Helper;
//Implement synchronous methods here
namespace S7.Net
@@ -19,6 +21,7 @@ namespace S7.Net
try
{
var stream = GetStreamIfAvailable();
stream.Write(ConnectionRequest.GetCOTPConnectionRequest(CPU, Rack, Slot), 0, 22);
var response = COTP.TPDU.Read(stream);
if (response.PDUType != 0xd0) //Connect Confirm
@@ -31,11 +34,16 @@ namespace S7.Net
var s7data = COTP.TSDU.Read(stream);
if (s7data == null)
throw new WrongNumberOfBytesException("No data received in response to Communication Setup");
if (s7data.Length < 2)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
//Check for S7 Ack Data
if (s7data[1] != 0x03)
throw new InvalidDataException("Error reading Communication Setup response", s7data, 1, 0x03);
if (s7data.Length < 20)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
MaxPDUSize = (short)(s7data[18] * 256 + s7data[19]);
}
catch (Exception exc)
@@ -52,7 +60,7 @@ namespace S7.Net
tcpClient = new TcpClient();
ConfigureConnection();
tcpClient.Connect(IP, Port);
stream = tcpClient.GetStream();
_stream = tcpClient.GetStream();
}
catch (SocketException sex)
{
@@ -79,20 +87,17 @@ namespace S7.Net
/// <returns>Returns the bytes in an array</returns>
public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count)
{
List<byte> resultBytes = new List<byte>();
int index = startByteAdr;
var result = new byte[count];
int index = 0;
while (count > 0)
{
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
var maxToRead = (int)Math.Min(count, MaxPDUSize - 18);
byte[] bytes = ReadBytesWithSingleRequest(dataType, db, index, maxToRead);
if (bytes == null)
return resultBytes.ToArray();
resultBytes.AddRange(bytes);
var maxToRead = Math.Min(count, MaxPDUSize - 18);
ReadBytesWithSingleRequest(dataType, db, startByteAdr + index, result, index, maxToRead);
count -= maxToRead;
index += maxToRead;
}
return resultBytes.ToArray();
return result;
}
/// <summary>
@@ -106,7 +111,7 @@ namespace S7.Net
/// <param name="varType">Type of the variable/s that you are reading</param>
/// <param name="bitAdr">Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter.</param>
/// <param name="varCount"></param>
public object Read(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0)
public object? Read(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0)
{
int cntBytes = VarTypeToByteLength(varType, varCount);
byte[] bytes = ReadBytes(dataType, db, startByteAdr, cntBytes);
@@ -119,8 +124,8 @@ namespace S7.Net
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <returns>Returns an object that contains the value. This object must be cast accordingly.</returns>
public object Read(string variable)
/// <returns>Returns an object that contains the value. This object must be cast accordingly. If no data has been read, null will be returned</returns>
public object? Read(string variable)
{
var adr = new PLCAddress(variable);
return Read(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber);
@@ -132,8 +137,8 @@ namespace S7.Net
/// <param name="structType">Type of the struct to be readed (es.: TypeOf(MyStruct)).</param>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>Returns a struct that must be cast.</returns>
public object ReadStruct(Type structType, int db, int startByteAdr = 0)
/// <returns>Returns a struct that must be cast. If no data has been read, null will be returned</returns>
public object? ReadStruct(Type structType, int db, int startByteAdr = 0)
{
int numBytes = Struct.GetStructSize(structType);
// now read the package
@@ -188,7 +193,7 @@ namespace S7.Net
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public T ReadClass<T>(int db, int startByteAdr = 0) where T : class
public T? ReadClass<T>(int db, int startByteAdr = 0) where T : class
{
return ReadClass(() => Activator.CreateInstance<T>(), db, startByteAdr);
}
@@ -202,7 +207,7 @@ namespace S7.Net
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public T ReadClass<T>(Func<T> classFactory, int db, int startByteAdr = 0) where T : class
public T? ReadClass<T>(Func<T> classFactory, int db, int startByteAdr = 0) where T : class
{
var instance = classFactory();
int readBytes = ReadClass(instance, db, startByteAdr);
@@ -231,7 +236,7 @@ namespace S7.Net
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//replies with bigger PDU size in connection setup.
var maxToWrite = Math.Min(count, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480
WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value.Skip(localIndex).Take(maxToWrite).ToArray());
WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite);
count -= maxToWrite;
localIndex += maxToWrite;
}
@@ -338,28 +343,25 @@ namespace S7.Net
WriteClassAsync(classValue, db, startByteAdr).GetAwaiter().GetResult();
}
private byte[] ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, int count)
private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count)
{
byte[] bytes = new byte[count];
var stream = GetStreamIfAvailable();
try
{
// first create the header
int packageSize = 31;
ByteArray package = new ByteArray(packageSize);
package.Add(ReadHeaderPackage());
int packageSize = 19 + 12; // 19 header + 12 for 1 request
var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package);
// package.Add(0x02); // datenart
package.Add(CreateReadDataRequestPackage(dataType, db, startByteAdr, count));
BuildReadDataRequestPackage(package, dataType, db, startByteAdr, count);
stream.Write(package.Array, 0, package.Array.Length);
var dataToSend = package.ToArray();
stream.Write(dataToSend, 0, dataToSend.Length);
var s7data = COTP.TSDU.Read(stream);
if (s7data == null || s7data[14] != 0xff)
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
AssertReadResponse(s7data, count);
for (int cnt = 0; cnt < count; cnt++)
bytes[cnt] = s7data[cnt + 18];
return bytes;
Array.Copy(s7data, 18, buffer, offset, count);
}
catch (Exception exc)
{
@@ -376,6 +378,8 @@ namespace S7.Net
{
AssertPduSizeForWrite(dataItems);
var stream = GetStreamIfAvailable();
var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems);
stream.Write(message.Array, 0, length);
@@ -384,37 +388,14 @@ namespace S7.Net
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
}
private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, byte[] value)
private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count)
{
int varCount = 0;
try
{
varCount = value.Length;
// first create the header
int packageSize = 35 + value.Length;
ByteArray package = new ByteArray(packageSize);
var stream = GetStreamIfAvailable();
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count);
package.Add(new byte[] { 3, 0 });
//complete package size
package.Add(Int.ToByteArray((short)packageSize));
package.Add(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
package.Add(Word.ToByteArray((ushort)(varCount - 1)));
package.Add(new byte[] { 0, 0x0e });
package.Add(Word.ToByteArray((ushort)(varCount + 4)));
package.Add(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 });
package.Add(Word.ToByteArray((ushort)varCount));
package.Add(Word.ToByteArray((ushort)(db)));
package.Add((byte)dataType);
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
package.Add((byte)overflow);
package.Add(Word.ToByteArray((ushort)(startByteAdr * 8)));
package.Add(new byte[] { 0, 4 });
package.Add(Word.ToByteArray((ushort)(varCount * 8)));
// now join the header and the data
package.Add(value);
stream.Write(package.Array, 0, package.Array.Length);
stream.Write(dataToSend, 0, dataToSend.Length);
var s7data = COTP.TSDU.Read(stream);
if (s7data == null || s7data[14] != 0xff)
@@ -428,42 +409,85 @@ namespace S7.Net
}
}
private byte[] BuildWriteBytesPackage(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count)
{
int varCount = count;
// first create the header
int packageSize = 35 + varCount;
var package = new MemoryStream(new byte[packageSize]);
package.WriteByte(3);
package.WriteByte(0);
//complete package size
package.WriteByteArray(Int.ToByteArray((short)packageSize));
package.WriteByteArray(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
package.WriteByteArray(Word.ToByteArray((ushort)(varCount - 1)));
package.WriteByteArray(new byte[] { 0, 0x0e });
package.WriteByteArray(Word.ToByteArray((ushort)(varCount + 4)));
package.WriteByteArray(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 });
package.WriteByteArray(Word.ToByteArray((ushort)varCount));
package.WriteByteArray(Word.ToByteArray((ushort)(db)));
package.WriteByte((byte)dataType);
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
package.WriteByte((byte)overflow);
package.WriteByteArray(Word.ToByteArray((ushort)(startByteAdr * 8)));
package.WriteByteArray(new byte[] { 0, 4 });
package.WriteByteArray(Word.ToByteArray((ushort)(varCount * 8)));
// now join the header and the data
package.Write(value, dataOffset, count);
return package.ToArray();
}
private byte[] BuildWriteBitPackage(DataType dataType, int db, int startByteAdr, bool bitValue, int bitAdr)
{
var stream = GetStreamIfAvailable();
var value = new[] { bitValue ? (byte)1 : (byte)0 };
int varCount = 1;
// first create the header
int packageSize = 35 + varCount;
var package = new MemoryStream(new byte[packageSize]);
package.WriteByte(3);
package.WriteByte(0);
//complete package size
package.WriteByteArray(Int.ToByteArray((short)packageSize));
package.WriteByteArray(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
package.WriteByteArray(Word.ToByteArray((ushort)(varCount - 1)));
package.WriteByteArray(new byte[] { 0, 0x0e });
package.WriteByteArray(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.WriteByteArray(Word.ToByteArray((ushort)varCount));
package.WriteByteArray(Word.ToByteArray((ushort)(db)));
package.WriteByte((byte)dataType);
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
package.WriteByte((byte)overflow);
package.WriteByteArray(Word.ToByteArray((ushort)(startByteAdr * 8 + bitAdr)));
package.WriteByteArray(new byte[] { 0, 0x03 }); //ending 0x03 is used for writing a sinlge bit
package.WriteByteArray(Word.ToByteArray((ushort)(varCount)));
// now join the header and the data
package.WriteByteArray(value);
return package.ToArray();
}
private void WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue)
{
int varCount = 0;
var stream = GetStreamIfAvailable();
try
{
var value = new[] {bitValue ? (byte) 1 : (byte) 0};
varCount = value.Length;
// first create the header
int packageSize = 35 + value.Length;
ByteArray package = new ByteArray(packageSize);
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
package.Add(new byte[] { 3, 0, 0 });
package.Add((byte)packageSize);
package.Add(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
package.Add(Word.ToByteArray((ushort)(varCount - 1)));
package.Add(new byte[] { 0, 0x0e });
package.Add(Word.ToByteArray((ushort)(varCount + 4)));
package.Add(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x01 }); //ending 0x01 is used for writing a sinlge bit
package.Add(Word.ToByteArray((ushort)varCount));
package.Add(Word.ToByteArray((ushort)(db)));
package.Add((byte)dataType);
int overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
package.Add((byte)overflow);
package.Add(Word.ToByteArray((ushort)(startByteAdr * 8 + bitAdr)));
package.Add(new byte[] { 0, 0x03 }); //ending 0x03 is used for writing a sinlge bit
package.Add(Word.ToByteArray((ushort)(varCount)));
// now join the header and the data
package.Add(value);
stream.Write(package.Array, 0, package.Array.Length);
stream.Write(dataToSend, 0, dataToSend.Length);
var s7data = COTP.TSDU.Read(stream);
if (s7data == null || s7data[14] != 0xff)
{
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
}
}
catch (Exception exc)
{
@@ -485,19 +509,22 @@ namespace S7.Net
//replies with bigger PDU size in connection setup.
AssertPduSizeForRead(dataItems);
var stream = GetStreamIfAvailable();
try
{
// first create the header
int packageSize = 19 + (dataItems.Count * 12);
ByteArray package = new ByteArray(packageSize);
package.Add(ReadHeaderPackage(dataItems.Count));
var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package, dataItems.Count);
// package.Add(0x02); // datenart
foreach (var dataItem in dataItems)
{
package.Add(CreateReadDataRequestPackage(dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count)));
BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count));
}
stream.Write(package.Array, 0, package.Array.Length);
var dataToSend = package.ToArray();
stream.Write(dataToSend, 0, dataToSend.Length);
var s7data = COTP.TSDU.Read(stream); //TODO use Async
if (s7data == null || s7data[14] != 0xff)

View File

@@ -1,3 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("S7Net.UnitTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d1032db55f60d64bf90ea1cc2247b5a8b9b6168a07bcd464a07ce2e425d027ff9409a64ba0e3f37718e14c50cf964d0d921e5ae8b8d74bd8a82431794f897cebf0ee668feb2ccd030153611b2808fcb7785c5e5136a98e0ec23de3c1ed385d2026c26e4bed5805ff9db7e0544f59b1f19d369d43403a624586795926e38c48d")]
[assembly: InternalsVisibleTo("S7.Net.UnitTest, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d1032db55f60d64bf90ea1cc2247b5a8b9b6168a07bcd464a07ce2e425d027ff9409a64ba0e3f37718e14c50cf964d0d921e5ae8b8d74bd8a82431794f897cebf0ee668feb2ccd030153611b2808fcb7785c5e5136a98e0ec23de3c1ed385d2026c26e4bed5805ff9db7e0544f59b1f19d369d43403a624586795926e38c48d")]

View File

@@ -91,7 +91,7 @@ namespace S7.Net.Protocol
IList<byte> itemResults = new ArraySegment<byte>(message, 14, dataItems.Length);
List<Exception> errors = null;
List<Exception>? errors = null;
for (int i = 0; i < dataItems.Length; i++)
{

View File

@@ -64,6 +64,8 @@ namespace S7.Net.Protocol
return Types.String.ToByteArray(stringVal, stringVal.Length);
case "DateTime[]":
return Types.DateTime.ToByteArray((System.DateTime[]) value);
case "DateTimeLong[]":
return Types.DateTimeLong.ToByteArray((System.DateTime[])value);
default:
throw new InvalidVariableTypeException();
}

View File

@@ -15,6 +15,12 @@
<RepositoryType>git</RepositoryType>
<PackageTags>PLC Siemens Communication S7</PackageTags>
<Copyright>Derek Heiser 2015</Copyright>
<LangVersion>8.0</LangVersion>
<Nullable>Enable</Nullable>
<DebugType>portable</DebugType>
<EmbedAllSources>true</EmbedAllSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net452' Or '$(TargetFramework)' == 'netstandard2.0' ">
@@ -22,7 +28,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta-62925-02" PrivateAssets="All" />
<PackageReference Include="SourceLink.Copy.PdbFiles" Version="2.8.3" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,56 @@
using System;
using System.IO;
using System.Threading.Tasks;
namespace S7.Net
{
/// <summary>
/// Extensions for Streams
/// </summary>
public static class StreamExtensions
{
/// <summary>
/// Reads bytes from the stream into the buffer until exactly the requested number of bytes (or EOF) have been read
/// </summary>
/// <param name="stream">the Stream to read from</param>
/// <param name="buffer">the buffer to read into</param>
/// <param name="offset">the offset in the buffer to read into</param>
/// <param name="count">the amount of bytes to read into the buffer</param>
/// <returns>returns the amount of read bytes</returns>
public static int ReadExact(this Stream stream, byte[] buffer, int offset, int count)
{
int read = 0;
int received;
do
{
received = stream.Read(buffer, offset + read, count - read);
read += received;
}
while (read < count && received > 0);
return read;
}
/// <summary>
/// Reads bytes from the stream into the buffer until exactly the requested number of bytes (or EOF) have been read
/// </summary>
/// <param name="stream">the Stream to read from</param>
/// <param name="buffer">the buffer to read into</param>
/// <param name="offset">the offset in the buffer to read into</param>
/// <param name="count">the amount of bytes to read into the buffer</param>
/// <returns>returns the amount of read bytes</returns>
public static async Task<int> ReadExactAsync(this Stream stream, byte[] buffer, int offset, int count)
{
int read = 0;
int received;
do
{
received = await stream.ReadAsync(buffer, offset + read, count - read);
read += received;
}
while (read < count && received > 0);
return read;
}
}
}

View File

@@ -10,10 +10,19 @@ namespace S7.Net
/// </summary>
internal class TPKT
{
public byte Version;
public byte Reserved1;
public int Length;
public byte[] Data;
private TPKT(byte version, byte reserved1, int length, byte[] data)
{
Version = version;
Reserved1 = reserved1;
Length = length;
Data = data;
}
/// <summary>
/// Reads a TPKT from the socket
@@ -23,22 +32,24 @@ namespace S7.Net
public static TPKT Read(Stream stream)
{
var buf = new byte[4];
int len = stream.Read(buf, 0, 4);
if (len < 4) throw new TPKTInvalidException("TPKT is incomplete / invalid");
var pkt = new TPKT
{
Version = buf[0],
Reserved1 = buf[1],
Length = buf[2] * 256 + buf[3] //BigEndian
};
if (pkt.Length > 0)
{
pkt.Data = new byte[pkt.Length - 4];
len = stream.Read(pkt.Data, 0, pkt.Length - 4);
if (len < pkt.Length - 4)
throw new TPKTInvalidException("TPKT is incomplete / invalid");
}
return pkt;
int len = stream.ReadExact(buf, 0, 4);
if (len < 4) throw new TPKTInvalidException($"TPKT header is incomplete / invalid. Received Bytes: {len} expected: {buf.Length}");
var version = buf[0];
var reserved1 = buf[1];
var length = buf[2] * 256 + buf[3]; //BigEndian
var data = new byte[length - 4];
len = stream.ReadExact(data, 0, data.Length);
if (len < data.Length)
throw new TPKTInvalidException($"TPKT payload is incomplete / invalid. Received Bytes: {len} expected: {data.Length}");
return new TPKT
(
version: version,
reserved1: reserved1,
length: length,
data: data
);
}
/// <summary>
@@ -49,21 +60,25 @@ namespace S7.Net
public static async Task<TPKT> ReadAsync(Stream stream)
{
var buf = new byte[4];
int len = await stream.ReadAsync(buf, 0, 4);
int len = await stream.ReadExactAsync(buf, 0, 4);
if (len < 4) throw new TPKTInvalidException("TPKT is incomplete / invalid");
var pkt = new TPKT
{
Version = buf[0],
Reserved1 = buf[1],
Length = buf[2] * 256 + buf[3] //BigEndian
};
if (pkt.Length > 0)
{
pkt.Data = new byte[pkt.Length - 4];
len = await stream.ReadAsync(pkt.Data, 0, pkt.Length - 4);
if (len < pkt.Length - 4) throw new TPKTInvalidException("TPKT is incomplete / invalid");
}
return pkt;
var version = buf[0];
var reserved1 = buf[1];
var length = buf[2] * 256 + buf[3]; //BigEndian
var data = new byte[length - 4];
len = await stream.ReadExactAsync(data, 0, data.Length);
if (len < data.Length)
throw new TPKTInvalidException("TPKT payload incomplete / invalid");
return new TPKT
(
version: version,
reserved1: reserved1,
length: length,
data: data
);
}
public override string ToString()

View File

@@ -50,6 +50,11 @@ namespace S7.Net.Types
list.AddRange(items);
}
public void Add(IEnumerable<byte> items)
{
list.AddRange(items);
}
public void Add(ByteArray byteArray)
{
list.AddRange(byteArray.Array);

View File

@@ -85,6 +85,7 @@ namespace S7.Net.Types
throw new Exception("Cannot determine size of class, because an array is defined which has no fixed size greater than zero.");
}
IncrementToEven(ref numBytes);
for (int i = 0; i < array.Length; i++)
{
numBytes = GetIncreasedNumberOfBytes(numBytes, elementType);
@@ -105,9 +106,9 @@ namespace S7.Net.Types
return numBytes;
}
private static object GetPropertyValue(Type propertyType, byte[] bytes, ref double numBytes)
private static object? GetPropertyValue(Type propertyType, byte[] bytes, ref double numBytes)
{
object value = null;
object? value = null;
switch (propertyType.Name)
{
@@ -219,6 +220,7 @@ namespace S7.Net.Types
if (property.PropertyType.IsArray)
{
Array array = (Array)property.GetValue(sourceClass, null);
IncrementToEven(ref numBytes);
Type elementType = property.PropertyType.GetElementType();
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
{
@@ -243,7 +245,7 @@ namespace S7.Net.Types
{
int bytePos = 0;
int bitPos = 0;
byte[] bytes2 = null;
byte[]? bytes2 = null;
switch (propertyValue.GetType().Name)
{
@@ -288,10 +290,8 @@ namespace S7.Net.Types
if (bytes2 != null)
{
// add them
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
IncrementToEven(ref numBytes);
bytePos = (int)numBytes;
for (int bCnt = 0; bCnt < bytes2.Length; bCnt++)
bytes[bytePos + bCnt] = bytes2[bCnt];
@@ -314,6 +314,7 @@ namespace S7.Net.Types
if (property.PropertyType.IsArray)
{
Array array = (Array)property.GetValue(sourceClass, null);
IncrementToEven(ref numBytes);
Type elementType = property.PropertyType.GetElementType();
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
{
@@ -327,5 +328,11 @@ namespace S7.Net.Types
}
return numBytes;
}
private static void IncrementToEven(ref double numBytes)
{
numBytes = Math.Ceiling(numBytes);
if (numBytes % 2 > 0) numBytes++;
}
}
}

View File

@@ -40,7 +40,7 @@ namespace S7.Net.Types
/// <summary>
/// Contains the value of the memory area after the read has been executed
/// </summary>
public object Value { get; set; }
public object? Value { get; set; }
/// <summary>
/// Create an instance of DataItem
@@ -83,7 +83,14 @@ namespace S7.Net.Types
var dataItem = FromAddress(address);
dataItem.Value = value;
if (typeof(T).IsArray) dataItem.Count = ((Array) dataItem.Value).Length;
if (typeof(T).IsArray)
{
var array = ((Array?)dataItem.Value);
if ( array != null)
{
dataItem.Count = array.Length;
}
}
return dataItem;
}

View File

@@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert between <see cref="T:System.DateTime" /> and S7 representation of DateTimeLong (DTL) values.
/// </summary>
public static class DateTimeLong
{
public const int TypeLengthInBytes = 12;
/// <summary>
/// The minimum <see cref="T:System.DateTime" /> value supported by the specification.
/// </summary>
public static readonly System.DateTime SpecMinimumDateTime = new System.DateTime(1970, 1, 1);
/// <summary>
/// The maximum <see cref="T:System.DateTime" /> value supported by the specification.
/// </summary>
public static readonly System.DateTime SpecMaximumDateTime = new System.DateTime(2262, 4, 11, 23, 47, 16, 854);
/// <summary>
/// Parses a <see cref="T:System.DateTime" /> value from bytes.
/// </summary>
/// <param name="bytes">Input bytes read from PLC.</param>
/// <returns>A <see cref="T:System.DateTime" /> object representing the value read from PLC.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the length of
/// <paramref name="bytes" /> is not 12 or any value in <paramref name="bytes" />
/// is outside the valid range of values.
/// </exception>
public static System.DateTime FromByteArray(byte[] bytes)
{
return FromByteArrayImpl(bytes);
}
/// <summary>
/// Parses an array of <see cref="T:System.DateTime" /> values from bytes.
/// </summary>
/// <param name="bytes">Input bytes read from PLC.</param>
/// <returns>An array of <see cref="T:System.DateTime" /> objects representing the values read from PLC.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the length of
/// <paramref name="bytes" /> is not a multiple of 12 or any value in
/// <paramref name="bytes" /> is outside the valid range of values.
/// </exception>
public static System.DateTime[] ToArray(byte[] bytes)
{
if (bytes.Length % TypeLengthInBytes != 0)
{
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
$"Parsing an array of DateTimeLong requires a multiple of 12 bytes of input data, input data is '{bytes.Length}' long.");
}
var cnt = bytes.Length / TypeLengthInBytes;
var result = new System.DateTime[cnt];
for (var i = 0; i < cnt; i++)
{
var slice = new byte[TypeLengthInBytes];
Array.Copy(bytes, i * TypeLengthInBytes, slice, 0, TypeLengthInBytes);
result[i] = FromByteArrayImpl(slice);
}
return result;
}
private static System.DateTime FromByteArrayImpl(byte[] bytes)
{
if (bytes.Length != TypeLengthInBytes)
{
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
$"Parsing a DateTimeLong requires exactly 12 bytes of input data, input data is {bytes.Length} bytes long.");
}
var year = AssertRangeInclusive(Word.FromBytes(bytes[1], bytes[0]), 1970, 2262, "year");
var month = AssertRangeInclusive(bytes[2], 1, 12, "month");
var day = AssertRangeInclusive(bytes[3], 1, 31, "day of month");
var dayOfWeek = AssertRangeInclusive(bytes[4], 1, 7, "day of week");
var hour = AssertRangeInclusive(bytes[5], 0, 23, "hour");
var minute = AssertRangeInclusive(bytes[6], 0, 59, "minute");
var second = AssertRangeInclusive(bytes[7], 0, 59, "second");
;
var nanoseconds = AssertRangeInclusive<uint>(DWord.FromBytes(bytes[11], bytes[10], bytes[9], bytes[8]), 0,
999999999, "nanoseconds");
var time = new System.DateTime(year, month, day, hour, minute, second);
return time.AddTicks(nanoseconds / 100);
}
/// <summary>
/// Converts a <see cref="T:System.DateTime" /> value to a byte array.
/// </summary>
/// <param name="dateTime">The DateTime value to convert.</param>
/// <returns>A byte array containing the S7 DateTimeLong representation of <paramref name="dateTime" />.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the value of
/// <paramref name="dateTime" /> is before <see cref="P:SpecMinimumDateTime" />
/// or after <see cref="P:SpecMaximumDateTime" />.
/// </exception>
public static byte[] ToByteArray(System.DateTime dateTime)
{
if (dateTime < SpecMinimumDateTime)
{
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
$"Date time '{dateTime}' is before the minimum '{SpecMinimumDateTime}' supported in S7 DateTimeLong representation.");
}
if (dateTime > SpecMaximumDateTime)
{
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
$"Date time '{dateTime}' is after the maximum '{SpecMaximumDateTime}' supported in S7 DateTimeLong representation.");
}
var stream = new MemoryStream(TypeLengthInBytes);
// Convert Year
stream.Write(Word.ToByteArray(Convert.ToUInt16(dateTime.Year)), 0, 2);
// Convert Month
stream.WriteByte(Convert.ToByte(dateTime.Month));
// Convert Day
stream.WriteByte(Convert.ToByte(dateTime.Day));
// Convert WeekDay. NET DateTime starts with Sunday = 0, while S7DT has Sunday = 1.
stream.WriteByte(Convert.ToByte(dateTime.DayOfWeek + 1));
// Convert Hour
stream.WriteByte(Convert.ToByte(dateTime.Hour));
// Convert Minutes
stream.WriteByte(Convert.ToByte(dateTime.Minute));
// Convert Seconds
stream.WriteByte(Convert.ToByte(dateTime.Second));
// Convert Nanoseconds. Net DateTime has a representation of 1 Tick = 100ns.
// Thus First take the ticks Mod 1 Second (1s = 10'000'000 ticks), and then Convert to nanoseconds.
stream.Write(DWord.ToByteArray(Convert.ToUInt32(dateTime.Ticks % 10000000 * 100)), 0, 4);
return stream.ToArray();
}
/// <summary>
/// Converts an array of <see cref="T:System.DateTime" /> values to a byte array.
/// </summary>
/// <param name="dateTimes">The DateTime values to convert.</param>
/// <returns>A byte array containing the S7 DateTimeLong representations of <paramref name="dateTimes" />.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when any value of
/// <paramref name="dateTimes" /> is before <see cref="P:SpecMinimumDateTime" />
/// or after <see cref="P:SpecMaximumDateTime" />.
/// </exception>
public static byte[] ToByteArray(System.DateTime[] dateTimes)
{
var bytes = new List<byte>(dateTimes.Length * TypeLengthInBytes);
foreach (var dateTime in dateTimes)
{
bytes.AddRange(ToByteArray(dateTime));
}
return bytes.ToArray();
}
private static T AssertRangeInclusive<T>(T input, T min, T max, string field) where T : IComparable<T>
{
if (input.CompareTo(min) < 0)
{
throw new ArgumentOutOfRangeException(nameof(input), input,
$"Value '{input}' is lower than the minimum '{min}' allowed for {field}.");
}
if (input.CompareTo(max) > 0)
{
throw new ArgumentOutOfRangeException(nameof(input), input,
$"Value '{input}' is higher than the maximum '{max}' allowed for {field}.");
}
return input;
}
}
}

View File

@@ -70,7 +70,7 @@ namespace S7.Net.Types
/// <param name="structType">The struct type</param>
/// <param name="bytes">The array of bytes</param>
/// <returns>The object depending on the struct type or null if fails(array-length != struct-length</returns>
public static object FromBytes(Type structType, byte[] bytes)
public static object? FromBytes(Type structType, byte[] bytes)
{
if (bytes == null)
return null;
@@ -198,7 +198,7 @@ namespace S7.Net.Types
int size = Struct.GetStructSize(type);
byte[] bytes = new byte[size];
byte[] bytes2 = null;
byte[]? bytes2 = null;
int bytePos = 0;
int bitPos = 0;

7
S7.sln
View File

@@ -1,16 +1,17 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27703.2026
# Visual Studio Version 16
VisualStudioVersion = 16.0.29806.167
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "S7.Net", "S7.Net\S7.Net.csproj", "{BFD484F9-3F04-42A2-BF2A-60A189A25DCF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7A8252C3-E6AE-435A-809D-4413C06E0711}"
ProjectSection(SolutionItems) = preProject
appveyor.yml = appveyor.yml
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S7.Net.UnitTest", "S7.Net.UnitTest\S7.Net.UnitTest.csproj", "{303CCED6-9ABC-4899-A509-743341AAA804}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "S7.Net.UnitTest", "S7.Net.UnitTest\S7.Net.UnitTest.csproj", "{303CCED6-9ABC-4899-A509-743341AAA804}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

View File

@@ -1,13 +1,13 @@
image: Visual Studio 2017
image: Visual Studio 2019
configuration: Release
install:
- choco install gitversion.portable -y
before_build:
- cmd: gitversion /l console /output buildserver
- nuget restore
- 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
- dotnet pack S7.Net -c %CONFIGURATION% /p:Version=%GitVersion_NuGetVersion% --no-build -o artifacts
artifacts:
- path: artifacts\*.*