65 Commits

Author SHA1 Message Date
Michael Croes
fc6781c37f Release S7NetPlus 0.6.0
Release highlights:
- Added support for DateTimeLong
2020-08-17 21:51:34 +02:00
Michael Croes
3555436c04 Merge pull request #284 from scamille/fb-DTL
Add DateTimeLong type
2020-08-17 21:36:05 +02:00
Serge Camille
3258c84fbc Add DateTimeLong test to S7NetTestsSync 2020-08-17 19:20:47 +02:00
Serge Camille
28257f28b3 Dtl: Add TypeLengthInBytes constant instead of always rewriting 12. 2020-08-17 19:20:19 +02:00
Serge Camille
a1d87de2d9 Rename DTL to DateTimeLong 2020-08-17 19:14:28 +02:00
Serge Camille
4be5765fc9 Run Resharper cleanup on DTL class, fix Dtl.ToByteArray list capacity. 2020-08-16 22:50:23 +02:00
Serge Camille
6614c7330a Hook up DTL to VarType enum and PLCHelper. 2020-08-16 22:36:23 +02:00
Serge Camille
5d59c8284d Add DTL type
Add new class Types.Dtl by taking the DateTime type and adjusting things.

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

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

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

View File

@@ -4,14 +4,18 @@ branches:
master:
tag: rc
increment: Minor
features?[/-]:
feature:
regex: features?[/-]
tag: rc-{BranchName}
increment: Minor
(pull|pull\-requests|pr)[/-]:
pull-request:
regex: (pull|pull\-requests|pr)[/-]
tag: rc-pr-{BranchName}
increment: Minor
hotfix(es)?[/-]:
hotfix:
regex: hotfix(es)?[/-]
tag: rc
increment: Patch
dev(elop)?(ment)?$:
develop:
regex: dev(elop)?(ment)?$
tag: b

View File

@@ -75,7 +75,7 @@ namespace S7.Net.UnitTest
destTsap1, destTsap2, //Destination TASP
192, //Parameter Code (tpdu-size)
1, //Parameter Length
11 //TPDU Size (2^11 = 2048)
10 //TPDU Size (2^11 = 2048)
};
}
}

View File

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

View File

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

View File

@@ -0,0 +1,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

@@ -62,6 +62,8 @@
<ItemGroup>
<Compile Include="ConnectionRequestTest.cs" />
<Compile Include="ConvertersUnitTest.cs" />
<Compile Include="Helpers\TestClassWithNestedClass.cs" />
<Compile Include="PLCAddressParsingTests.cs" />
<Compile Include="ProtocolTests.cs" />
<Compile Include="Helpers\ConsoleManager.cs" />
<Compile Include="Helpers\NativeMethods.cs" />
@@ -78,6 +80,9 @@
<Compile Include="S7NetTestsSync.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Helpers\TestLongStruct.cs" />
<Compile Include="TypeTests\ClassTests.cs" />
<Compile Include="TypeTests\DateTimeLongTests.cs" />
<Compile Include="TypeTests\DateTimeTests.cs" />
<Compile Include="TypeTests\StringExTests.cs" />
<Compile Include="TypeTests\StringTests.cs" />
</ItemGroup>

View File

@@ -160,6 +160,31 @@ namespace S7.Net.UnitTest
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
}
[TestMethod]
public async Task Test_Async_ReadAndWriteNestedClass()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestClassWithNestedClass tc = new TestClassWithNestedClass
{
BitVariable00 = true,
BitVariable01 = new TestClassInnerWithBool { BitVariable00 = true },
ByteVariable02 = new TestClassInnerWithByte { ByteVariable00 = 128 },
BitVariable03 = true,
ShortVariable04 = new TestClassInnerWithShort { ShortVarialbe00 = -15000 }
};
await plc.WriteClassAsync(tc, DB4);
TestClassWithNestedClass tc2 = new TestClassWithNestedClass();
// Values that are read from a class are stored inside the class itself, that is passed by reference
await plc.ReadClassAsync(tc2, DB4);
Assert.AreEqual(tc.BitVariable00, tc2.BitVariable00);
Assert.AreEqual(tc.BitVariable01.BitVariable00, tc2.BitVariable01.BitVariable00);
Assert.AreEqual(tc.ByteVariable02.ByteVariable00, tc2.ByteVariable02.ByteVariable00);
Assert.AreEqual(tc.BitVariable03, tc2.BitVariable03);
Assert.AreEqual(tc.ShortVariable04.ShortVarialbe00, tc2.ShortVariable04.ShortVarialbe00);
}
/// <summary>
/// Read/Write a struct that has the same properties of a DB with the same field in the same order
/// </summary>
@@ -663,6 +688,28 @@ namespace S7.Net.UnitTest
}
}
[TestMethod]
public async Task Test_Async_ReadClassWithNestedClassAfterBit()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
Assert.AreEqual(6, Types.Class.GetClassSize(new TestClassWithNestedClass()));
TestClassWithNestedClass tc = new TestClassWithNestedClass();
tc.BitVariable00 = true;
tc.BitVariable01.BitVariable00 = true;
tc.ByteVariable02.ByteVariable00 = 128;
tc.BitVariable03 = true;
tc.ShortVariable04.ShortVarialbe00 = -15000;
TestClassWithNestedClass tc2 = await plc.ReadClassAsync<TestClassWithNestedClass>(DB4);
Assert.AreEqual(tc.BitVariable00, tc2.BitVariable00);
Assert.AreEqual(tc.BitVariable01.BitVariable00, tc2.BitVariable01.BitVariable00);
Assert.AreEqual(tc.ByteVariable02.ByteVariable00, tc2.ByteVariable02.ByteVariable00);
Assert.AreEqual(tc.BitVariable03, tc2.BitVariable03);
Assert.AreEqual(tc.ShortVariable04.ShortVarialbe00, tc2.ShortVariable04.ShortVarialbe00);
}
[TestMethod]
[ExpectedException(typeof(NullReferenceException))]
public async Task Test_Async_ReadStructThrowsExceptionPlcIsNotConnected()
@@ -737,7 +784,7 @@ namespace S7.Net.UnitTest
};
plc.WriteClass(tc, DB2);
int expectedReadBytes = Types.Class.GetClassSize(tc);
int expectedReadBytes = (int)Types.Class.GetClassSize(tc);
TestClass tc2 = new TestClass();
// Values that are read from a class are stored inside the class itself, that is passed by reference

View File

@@ -40,6 +40,7 @@ namespace S7.Net.UnitTest
{
#region Constants
const int DB2 = 2;
const int DB4 = 4;
#endregion
#region Private fields
@@ -694,6 +695,54 @@ namespace S7.Net.UnitTest
}
}
[TestMethod]
public void T31_ReadClassWithNestedClassAfterBit()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
Assert.AreEqual(6, Types.Class.GetClassSize(new TestClassWithNestedClass()));
TestClassWithNestedClass tc = new TestClassWithNestedClass();
tc.BitVariable00 = true;
tc.BitVariable01.BitVariable00 = true;
tc.ByteVariable02.ByteVariable00 = 128;
tc.BitVariable03 = true;
tc.ShortVariable04.ShortVarialbe00 = -15000;
TestClassWithNestedClass tc2 = new TestClassWithNestedClass();
plc.ReadClass(tc2, DB4);
Assert.AreEqual(tc.BitVariable00, tc2.BitVariable00);
Assert.AreEqual(tc.BitVariable01.BitVariable00, tc2.BitVariable01.BitVariable00);
Assert.AreEqual(tc.ByteVariable02.ByteVariable00, tc2.ByteVariable02.ByteVariable00);
Assert.AreEqual(tc.BitVariable03, tc2.BitVariable03);
Assert.AreEqual(tc.ShortVariable04.ShortVarialbe00, tc2.ShortVariable04.ShortVarialbe00);
}
[TestMethod]
public void T32_ReadAndWriteNestedClass()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestClassWithNestedClass tc = new TestClassWithNestedClass
{
BitVariable00 = true,
BitVariable01 = new TestClassInnerWithBool { BitVariable00 = true },
ByteVariable02 = new TestClassInnerWithByte { ByteVariable00 = 128 },
BitVariable03 = true,
ShortVariable04 = new TestClassInnerWithShort { ShortVarialbe00 = -15000 }
};
plc.WriteClass(tc, DB4);
TestClassWithNestedClass tc2 = new TestClassWithNestedClass();
// Values that are read from a class are stored inside the class itself, that is passed by reference
plc.ReadClass(tc2, DB4);
Assert.AreEqual(tc.BitVariable00, tc2.BitVariable00);
Assert.AreEqual(tc.BitVariable01.BitVariable00, tc2.BitVariable01.BitVariable00);
Assert.AreEqual(tc.ByteVariable02.ByteVariable00, tc2.ByteVariable02.ByteVariable00);
Assert.AreEqual(tc.BitVariable03, tc2.BitVariable03);
Assert.AreEqual(tc.ShortVariable04.ShortVarialbe00, tc2.ShortVariable04.ShortVarialbe00);
}
[TestMethod, ExpectedException(typeof(PlcException))]
public void T18_ReadStructThrowsIfPlcIsNotConnected()
{
@@ -767,7 +816,7 @@ namespace S7.Net.UnitTest
tc.DWordVariable = 850;
plc.WriteClass(tc, DB2);
int expectedReadBytes = Types.Class.GetClassSize(tc);
int expectedReadBytes = (int)Types.Class.GetClassSize(tc);
TestClass tc2 = new TestClass();
// Values that are read from a class are stored inside the class itself, that is passed by reference
@@ -948,6 +997,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,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

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

View File

@@ -16,6 +16,7 @@ namespace S7.Net
/// </summary>
public class TPDU
{
public TPKT TPkt { get; }
public byte HeaderLength;
public byte PDUType;
public int TPDUNumber;
@@ -24,6 +25,8 @@ namespace S7.Net
public TPDU(TPKT tPKT)
{
TPkt = tPKT;
var br = new BinaryReader(new MemoryStream(tPKT.Data));
HeaderLength = br.ReadByte();
if (HeaderLength >= 2)

View File

@@ -10,6 +10,11 @@
/// </summary>
S7200 = 0,
/// <summary>
/// Siemens Logo 0BA8
/// </summary>
Logo0BA8 = 1,
/// <summary>
/// S7 300 cpu type
/// </summary>
@@ -176,6 +181,16 @@
/// <summary>
/// Counter variable type
/// </summary>
Counter
Counter,
/// <summary>
/// DateTIme variable type
/// </summary>
DateTime,
/// <summary>
/// DateTimeLong variable type
/// </summary>
DateTimeLong
}
}

View File

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

View File

@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using S7.Net.Types;
namespace S7.Net
@@ -15,11 +18,19 @@ namespace S7.Net
private TcpClient tcpClient;
private NetworkStream stream;
private int readTimeout = System.Threading.Timeout.Infinite;
private int writeTimeout = System.Threading.Timeout.Infinite;
/// <summary>
/// IP address of the PLC
/// </summary>
public string IP { get; private set; }
/// <summary>
/// PORT Number of the PLC, default is 102
/// </summary>
public int Port { get; private set; }
/// <summary>
/// CPU type of the PLC
/// </summary>
@@ -39,6 +50,30 @@ namespace S7.Net
/// Max PDU size this cpu supports
/// </summary>
public Int16 MaxPDUSize { get; set; }
/// <summary>Gets or sets the amount of time that a read operation blocks waiting for data from PLC.</summary>
/// <returns>A <see cref="T:System.Int32" /> that specifies the amount of time, in milliseconds, that will elapse before a read operation fails. The default value, <see cref="F:System.Threading.Timeout.Infinite" />, specifies that the read operation does not time out.</returns>
public int ReadTimeout
{
get => readTimeout;
set
{
readTimeout = value;
if (tcpClient != null) tcpClient.ReceiveTimeout = readTimeout;
}
}
/// <summary>Gets or sets the amount of time that a write operation blocks waiting for data to PLC. </summary>
/// <returns>A <see cref="T:System.Int32" /> that specifies the amount of time, in milliseconds, that will elapse before a write operation fails. The default value, <see cref="F:System.Threading.Timeout.Infinite" />, specifies that the write operation does not time out.</returns>
public int WriteTimeout
{
get => writeTimeout;
set
{
writeTimeout = value;
if (tcpClient != null) tcpClient.SendTimeout = writeTimeout;
}
}
/// <summary>
/// Returns true if a connection to the PLC can be established
@@ -80,7 +115,34 @@ namespace S7.Net
catch { return false; }
}
}
/// <summary>
/// Creates a PLC object with all the parameters needed for connections.
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
/// You need slot > 0 if you are connecting to external ethernet card (CP).
/// For S7-300 and S7-400 the default is rack = 0 and slot = 2.
/// </summary>
/// <param name="cpu">CpuType of the PLC (select from the enum)</param>
/// <param name="ip">Ip address of the PLC</param>
/// <param name="port">Port address of the PLC, default 102</param>
/// <param name="rack">rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal</param>
/// <param name="slot">slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500.
/// If you use an external ethernet card, this must be set accordingly.</param>
public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot)
{
if (!Enum.IsDefined(typeof(CpuType), cpu))
throw new ArgumentException($"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.", nameof(cpu));
if (string.IsNullOrEmpty(ip))
throw new ArgumentException("IP address must valid.", nameof(ip));
CPU = cpu;
IP = ip;
Port = port;
Rack = rack;
Slot = slot;
MaxPDUSize = 240;
}
/// <summary>
/// Creates a PLC object with all the parameters needed for connections.
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
@@ -102,10 +164,12 @@ namespace S7.Net
CPU = cpu;
IP = ip;
Port = 102;
Rack = rack;
Slot = slot;
MaxPDUSize = 240;
}
/// <summary>
/// Close connection to PLC
/// </summary>
@@ -117,6 +181,64 @@ namespace S7.Net
}
}
private void AssertPduSizeForRead(ICollection<DataItem> dataItems)
{
// 12 bytes of header data, 12 bytes of parameter data for each dataItem
if ((dataItems.Count + 1) * 12 > MaxPDUSize) throw new Exception("Too many vars requested for read");
// 14 bytes of header data, 4 bytes of result data for each dataItem and the actual data
if (GetDataLength(dataItems) + dataItems.Count * 4 + 14 > MaxPDUSize) throw new Exception("Too much data requested for read");
}
private void AssertPduSizeForWrite(ICollection<DataItem> dataItems)
{
// 12 bytes of header data, 18 bytes of parameter data for each dataItem
if (dataItems.Count * 18 + 12 > MaxPDUSize) throw new Exception("Too many vars supplied for write");
// 12 bytes of header data, 16 bytes of data for each dataItem and the actual data
if (GetDataLength(dataItems) + dataItems.Count * 16 + 12 > MaxPDUSize)
throw new Exception("Too much data supplied for write");
}
private void ConfigureConnection()
{
if (tcpClient == null)
{
return;
}
tcpClient.ReceiveTimeout = ReadTimeout;
tcpClient.SendTimeout = WriteTimeout;
}
private int GetDataLength(IEnumerable<DataItem> dataItems)
{
// Odd length variables are 0-padded
return dataItems.Select(di => VarTypeToByteLength(di.VarType, di.Count))
.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

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DateTime = S7.Net.Types.DateTime;
namespace S7.Net
{
@@ -144,7 +145,25 @@ namespace S7.Net
}
else
{
return Bit.ToBitArray(bytes);
return Bit.ToBitArray(bytes, varCount);
}
case VarType.DateTime:
if (varCount == 1)
{
return DateTime.FromByteArray(bytes);
}
else
{
return DateTime.ToArray(bytes);
}
case VarType.DateTimeLong:
if (varCount == 1)
{
return DateTimeLong.FromByteArray(bytes);
}
else
{
return DateTimeLong.ToArray(bytes);
}
default:
return null;
@@ -162,7 +181,7 @@ namespace S7.Net
switch (varType)
{
case VarType.Bit:
return varCount; //TODO
return varCount + 7 / 8;
case VarType.Byte:
return (varCount < 1) ? 1 : varCount;
case VarType.String:
@@ -178,6 +197,10 @@ namespace S7.Net
case VarType.DInt:
case VarType.Real:
return varCount * 4;
case VarType.DateTime:
return varCount * 8;
case VarType.DateTimeLong:
return varCount * 12;
default:
return 0;
}
@@ -186,7 +209,7 @@ namespace S7.Net
private byte[] GetS7ConnectionSetup()
{
return new byte[] { 3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 255, 255, 0, 8, 0, 0, 240, 0, 0, 3, 0, 3,
7, 128 //Try 1920 PDU Size. Same as libnodave.
3, 192 // Use 960 PDU size
};
}

View File

@@ -25,23 +25,27 @@ namespace S7.Net
var response = await COTP.TPDU.ReadAsync(stream);
if (response.PDUType != 0xd0) //Connect Confirm
{
throw new WrongNumberOfBytesException("Waiting for COTP connect confirm");
throw new InvalidDataException("Error reading Connection Confirm", response.TPkt.Data, 1, 0x0d);
}
await stream.WriteAsync(GetS7ConnectionSetup(), 0, 25);
var s7data = await COTP.TSDU.ReadAsync(stream);
if (s7data == null || s7data[1] != 0x03) //Check for S7 Ack Data
{
throw new WrongNumberOfBytesException("Waiting for S7 connection setup");
}
if (s7data == null)
throw new WrongNumberOfBytesException("No data received in response to Communication Setup");
//Check for S7 Ack Data
if (s7data[1] != 0x03)
throw new InvalidDataException("Error reading Communication Setup response", s7data, 1, 0x03);
MaxPDUSize = (short)(s7data[18] * 256 + s7data[19]);
}
private async Task ConnectAsync()
{
tcpClient = new TcpClient();
await tcpClient.ConnectAsync(IP, 102);
ConfigureConnection();
await tcpClient.ConnectAsync(IP, Port);
stream = tcpClient.GetStream();
}
@@ -141,7 +145,7 @@ namespace S7.Net
/// <returns>The number of read bytes</returns>
public async Task<Tuple<int, object>> ReadClassAsync(object sourceClass, int db, int startByteAdr = 0)
{
int numBytes = Class.GetClassSize(sourceClass);
int numBytes = (int)Class.GetClassSize(sourceClass);
if (numBytes <= 0)
{
throw new Exception("The size of the class is less than 1 byte and therefore cannot be read");
@@ -201,15 +205,10 @@ namespace S7.Net
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read. Maximum 20 dataitems are accepted.</param>
public async Task<List<DataItem>> ReadMultipleVarsAsync(List<DataItem> dataItems)
{
int cntBytes = dataItems.Sum(dataItem => VarTypeToByteLength(dataItem.VarType, dataItem.Count));
//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.
if (dataItems.Count > 20)
throw new Exception("Too many vars requested");
if (cntBytes > 222)
throw new Exception("Too many bytes requested"); // TODO: proper TDU check + split in multiple requests
AssertPduSizeForRead(dataItems);
try
{
// first create the header
@@ -370,8 +369,9 @@ namespace S7.Net
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteClassAsync(object classValue, int db, int startByteAdr = 0)
{
var bytes = Types.Class.ToBytes(classValue).ToList();
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray());
byte[] bytes = new byte[(int)Class.GetClassSize(classValue)];
Types.Class.ToBytes(classValue, bytes);
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes);
}
private async Task<byte[]> ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, int count)
@@ -388,8 +388,7 @@ namespace S7.Net
await stream.WriteAsync(package.Array, 0, package.Array.Length);
var s7data = await COTP.TSDU.ReadAsync(stream);
if (s7data == null || s7data[14] != 0xff)
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
AssertReadResponse(s7data, count);
for (int cnt = 0; cnt < count; cnt++)
bytes[cnt] = s7data[cnt + 18];
@@ -405,6 +404,8 @@ namespace S7.Net
/// <returns>Task that completes when response from PLC is parsed.</returns>
public async Task WriteAsync(params DataItem[] dataItems)
{
AssertPduSizeForWrite(dataItems);
var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems);
await stream.WriteAsync(message.Array, 0, length).ConfigureAwait(false);

View File

@@ -23,16 +23,19 @@ namespace S7.Net
var response = COTP.TPDU.Read(stream);
if (response.PDUType != 0xd0) //Connect Confirm
{
throw new WrongNumberOfBytesException("Waiting for COTP connect confirm");
throw new InvalidDataException("Error reading Connection Confirm", response.TPkt.Data, 1, 0x0d);
}
stream.Write(GetS7ConnectionSetup(), 0, 25);
var s7data = COTP.TSDU.Read(stream);
if (s7data == null || s7data[1] != 0x03) //Check for S7 Ack Data
{
throw new WrongNumberOfBytesException("Waiting for S7 connection setup");
}
if (s7data == null)
throw new WrongNumberOfBytesException("No data received in response to Communication Setup");
//Check for S7 Ack Data
if (s7data[1] != 0x03)
throw new InvalidDataException("Error reading Communication Setup response", s7data, 1, 0x03);
MaxPDUSize = (short)(s7data[18] * 256 + s7data[19]);
}
catch (Exception exc)
@@ -47,7 +50,8 @@ namespace S7.Net
try
{
tcpClient = new TcpClient();
tcpClient.Connect(IP, 102);
ConfigureConnection();
tcpClient.Connect(IP, Port);
stream = tcpClient.GetStream();
}
catch (SocketException sex)
@@ -162,7 +166,7 @@ namespace S7.Net
/// <returns>The number of read bytes</returns>
public int ReadClass(object sourceClass, int db, int startByteAdr = 0)
{
int numBytes = Class.GetClassSize(sourceClass);
int numBytes = (int)Class.GetClassSize(sourceClass);
if (numBytes <= 0)
{
throw new Exception("The size of the class is less than 1 byte and therefore cannot be read");
@@ -226,7 +230,7 @@ namespace S7.Net
//TODO: Figure out how to use MaxPDUSize here
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//replies with bigger PDU size in connection setup.
var maxToWrite = Math.Min(count, 200);
var maxToWrite = Math.Min(count, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480
WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value.Skip(localIndex).Take(maxToWrite).ToArray());
count -= maxToWrite;
localIndex += maxToWrite;
@@ -349,8 +353,7 @@ namespace S7.Net
stream.Write(package.Array, 0, package.Array.Length);
var s7data = COTP.TSDU.Read(stream);
if (s7data == null || s7data[14] != 0xff)
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
AssertReadResponse(s7data, count);
for (int cnt = 0; cnt < count; cnt++)
bytes[cnt] = s7data[cnt + 18];
@@ -370,6 +373,8 @@ namespace S7.Net
/// <param name="dataItems">The DataItem(s) to write to the PLC.</param>
public void Write(params DataItem[] dataItems)
{
AssertPduSizeForWrite(dataItems);
var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems);
stream.Write(message.Array, 0, length);
@@ -388,8 +393,9 @@ namespace S7.Net
int packageSize = 35 + value.Length;
ByteArray package = new ByteArray(packageSize);
package.Add(new byte[] { 3, 0, 0 });
package.Add((byte)packageSize);
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 });
@@ -474,15 +480,10 @@ namespace S7.Net
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read. Maximum 20 dataitems are accepted.</param>
public void ReadMultipleVars(List<DataItem> dataItems)
{
int cntBytes = dataItems.Sum(dataItem => VarTypeToByteLength(dataItem.VarType, dataItem.Count));
//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.
if (dataItems.Count > 20)
throw new Exception("Too many vars requested");
if (cntBytes > 222)
throw new Exception("Too many bytes requested"); // TODO: proper TDU check + split in multiple requests
AssertPduSizeForRead(dataItems);
try
{
// first create the header

View File

@@ -21,7 +21,7 @@ namespace S7.Net.Protocol
3, 0, //Destination TASP
192, //Parameter Code (tpdu-size)
1, //Parameter Length
11 //TPDU Size (2^11 = 2048)
10 //TPDU Size (2^10 = 1024)
};
switch (cpu)
@@ -34,6 +34,14 @@ namespace S7.Net.Protocol
bSend1[17] = 0x10;
bSend1[18] = 0x00;
break;
case CpuType.Logo0BA8:
// These values are taken from NodeS7, it's not verified if these are
// exact requirements to connect to the Logo0BA8.
bSend1[13] = 0x01;
bSend1[14] = 0x00;
bSend1[17] = 0x01;
bSend1[18] = 0x02;
break;
case CpuType.S71200:
case CpuType.S7300:
case CpuType.S7400:

View File

@@ -20,8 +20,11 @@ namespace S7.Net.Protocol
var dataOffset = paramOffset + paramSize;
var data = new ByteArray();
var itemCount = 0;
foreach (var item in dataItems)
{
itemCount++;
message.Add(Parameter.Template);
var value = Serialization.SerializeDataItem(item);
var wordLen = item.Value is bool ? 1 : 2;
@@ -30,33 +33,42 @@ namespace S7.Net.Protocol
Serialization.SetWordAt(message, paramOffset + Parameter.Offsets.Amount, (ushort) value.Length);
Serialization.SetWordAt(message, paramOffset + Parameter.Offsets.DbNumber, (ushort) item.DB);
message[paramOffset + Parameter.Offsets.Area] = (byte) item.DataType;
Serialization.SetAddressAt(message, paramOffset + Parameter.Offsets.Address, item.StartByteAdr, item.BitAdr);
paramOffset += Parameter.Template.Length;
data.Add(0x00);
if (item.Value is bool b)
{
if (item.BitAdr > 7)
throw new ArgumentException(
$"Cannot read bit with invalid {nameof(item.BitAdr)} '{item.BitAdr}'.", nameof(dataItems));
Serialization.SetAddressAt(message, paramOffset + Parameter.Offsets.Address, item.StartByteAdr,
item.BitAdr);
data.Add(0x03);
data.AddWord(1);
data.Add(b ? (byte) 1 : (byte) 0);
data.Add(0);
data.Add(b ? (byte)1 : (byte)0);
if (itemCount != dataItems.Length) {
data.Add(0);
}
}
else
{
Serialization.SetAddressAt(message, paramOffset + Parameter.Offsets.Address, item.StartByteAdr, 0);
var len = value.Length;
data.Add(0x04);
data.AddWord((ushort) (len << 3));
data.Add(value);
if ((len & 0b1) == 1)
if ((len & 0b1) == 1 && itemCount != dataItems.Length)
{
data.Add(0);
}
}
}
paramOffset += Parameter.Template.Length;
}
message.Add(data.Array);

View File

@@ -41,6 +41,8 @@ namespace S7.Net.Protocol
return Types.Double.ToByteArray((double)value);
case "Single":
return Types.Single.ToByteArray((float)value);
case "DateTime":
return Types.DateTime.ToByteArray((System.DateTime) value);
case "Byte[]":
return (byte[])value;
case "Int16[]":
@@ -60,6 +62,10 @@ namespace S7.Net.Protocol
// if the consumer does not pay attention to string length.
var stringVal = (string) value;
return Types.String.ToByteArray(stringVal, stringVal.Length);
case "DateTime[]":
return Types.DateTime.ToByteArray((System.DateTime[]) value);
case "DateTimeLong[]":
return Types.DateTimeLong.ToByteArray((System.DateTime[])value);
default:
throw new InvalidVariableTypeException();
}

View File

@@ -1,4 +1,5 @@
using System.Collections;
using System;
using System.Collections;
namespace S7.Net.Types
{
@@ -16,12 +17,27 @@ namespace S7.Net.Types
}
/// <summary>
/// Converts an array of bytes to a BitArray
/// Converts an array of bytes to a BitArray.
/// </summary>
public static BitArray ToBitArray(byte[] bytes)
/// <param name="bytes">The bytes to convert.</param>
/// <returns>A BitArray with the same number of bits and equal values as <paramref name="bytes"/>.</returns>
public static BitArray ToBitArray(byte[] bytes) => ToBitArray(bytes, bytes.Length * 8);
/// <summary>
/// Converts an array of bytes to a BitArray.
/// </summary>
/// <param name="bytes">The bytes to convert.</param>
/// <param name="length">The number of bits to return.</param>
/// <returns>A BitArray with <paramref name="length"/> bits.</returns>
public static BitArray ToBitArray(byte[] bytes, int length)
{
BitArray bitArr = new BitArray(bytes);
return bitArr;
if (length > bytes.Length * 8) throw new ArgumentException($"Not enough data in bytes to return {length} bits.", nameof(bytes));
var bitArr = new BitArray(bytes);
var bools = new bool[length];
for (var i = 0; i < length; i++) bools[i] = bitArr[i];
return new BitArray(bools);
}
}
}

View File

@@ -25,10 +25,8 @@ namespace S7.Net.Types
}
private static double GetIncreasedNumberOfBytes(double startingNumberOfBytes, Type type)
private static double GetIncreasedNumberOfBytes(double numBytes, Type type)
{
double numBytes = startingNumberOfBytes;
switch (type.Name)
{
case "Boolean":
@@ -61,7 +59,7 @@ namespace S7.Net.Types
break;
default:
var propertyClass = Activator.CreateInstance(type);
numBytes += GetClassSize(propertyClass);
numBytes = GetClassSize(propertyClass, numBytes, true);
break;
}
@@ -73,10 +71,8 @@ namespace S7.Net.Types
/// </summary>
/// <param name="instance">An instance of the class</param>
/// <returns>the number of bytes</returns>
public static int GetClassSize(object instance)
public static double GetClassSize(object instance, double numBytes = 0.0, bool isInnerProperty = false)
{
double numBytes = 0.0;
var properties = GetAccessableProperties(instance.GetType());
foreach (var property in properties)
{
@@ -89,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);
@@ -99,11 +96,14 @@ namespace S7.Net.Types
numBytes = GetIncreasedNumberOfBytes(numBytes, property.PropertyType);
}
}
// enlarge numBytes to next even number because S7-Structs in a DB always will be resized to an even byte count
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
return (int)numBytes;
if (false == isInnerProperty)
{
// enlarge numBytes to next even number because S7-Structs in a DB always will be resized to an even byte count
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
}
return numBytes;
}
private static object GetPropertyValue(Type propertyType, byte[] bytes, ref double numBytes)
@@ -196,14 +196,8 @@ namespace S7.Net.Types
break;
default:
var propClass = Activator.CreateInstance(propertyType);
var buffer = new byte[GetClassSize(propClass)];
if (buffer.Length > 0)
{
Buffer.BlockCopy(bytes, (int)Math.Ceiling(numBytes), buffer, 0, buffer.Length);
FromBytes(propClass, buffer);
value = propClass;
numBytes += buffer.Length;
}
numBytes = FromBytes(propClass, bytes, numBytes);
value = propClass;
break;
}
@@ -215,16 +209,10 @@ namespace S7.Net.Types
/// </summary>
/// <param name="sourceClass">The object to fill in the given array of bytes</param>
/// <param name="bytes">The array of bytes</param>
public static void FromBytes(object sourceClass, byte[] bytes)
public static double FromBytes(object sourceClass, byte[] bytes, double numBytes = 0, bool isInnerClass = false)
{
if (bytes == null)
return;
if (bytes.Length != GetClassSize(sourceClass))
return;
// and decode it
double numBytes = 0.0;
return numBytes;
var properties = GetAccessableProperties(sourceClass.GetType());
foreach (var property in properties)
@@ -232,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++)
{
@@ -248,9 +237,11 @@ namespace S7.Net.Types
null);
}
}
return numBytes;
}
private static void ToBytes(object propertyValue, byte[] bytes, ref double numBytes)
private static double SetBytesFromProperty(object propertyValue, byte[] bytes, double numBytes)
{
int bytePos = 0;
int bitPos = 0;
@@ -293,21 +284,21 @@ namespace S7.Net.Types
bytes2 = Single.ToByteArray((float)propertyValue);
break;
default:
bytes2 = ToBytes(propertyValue);
numBytes = ToBytes(propertyValue, bytes, numBytes);
break;
}
if (bytes2 != null)
{
// add them
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
IncrementToEven(ref numBytes);
bytePos = (int)numBytes;
for (int bCnt = 0; bCnt < bytes2.Length; bCnt++)
bytes[bytePos + bCnt] = bytes2[bCnt];
numBytes += bytes2.Length;
}
return numBytes;
}
/// <summary>
@@ -315,30 +306,33 @@ namespace S7.Net.Types
/// </summary>
/// <param name="sourceClass">The struct object</param>
/// <returns>A byte array or null if fails.</returns>
public static byte[] ToBytes(object sourceClass)
public static double ToBytes(object sourceClass, byte[] bytes, double numBytes = 0.0)
{
int size = GetClassSize(sourceClass);
byte[] bytes = new byte[size];
double numBytes = 0.0;
var properties = GetAccessableProperties(sourceClass.GetType());
foreach (var property in properties)
{
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++)
{
ToBytes(array.GetValue(i), bytes, ref numBytes);
numBytes = SetBytesFromProperty(array.GetValue(i), bytes, numBytes);
}
}
else
{
ToBytes(property.GetValue(sourceClass, null), bytes, ref numBytes);
numBytes = SetBytesFromProperty(property.GetValue(sourceClass, null), bytes, numBytes);
}
}
return bytes;
return numBytes;
}
private static void IncrementToEven(ref double numBytes)
{
numBytes = Math.Ceiling(numBytes);
if (numBytes % 2 > 0) numBytes++;
}
}
}

View File

@@ -68,7 +68,7 @@ namespace S7.Net.Types
DB = dbNumber,
VarType = varType,
StartByteAdr = startByte,
BitAdr = (byte) bitNumber
BitAdr = (byte) (bitNumber == -1 ? 0 : bitNumber)
};
}

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

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

View File

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

View File

@@ -22,7 +22,17 @@ namespace S7.Net.Types
int size = bytes[0];
int length = bytes[1];
return System.Text.Encoding.ASCII.GetString(bytes, 2, length);
try
{
return Encoding.ASCII.GetString(bytes, 2, length);
}
catch (Exception e)
{
throw new PlcException(ErrorCode.ReadData,
$"Failed to parse {VarType.StringEx} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.",
e);
}
}
/// <summary>

View File

@@ -3,7 +3,7 @@ configuration: Release
install:
- choco install gitversion.portable -y
before_build:
- cmd: gitversion /l console /output buildserver /b %APPVEYOR_REPO_BRANCH%
- cmd: gitversion /l console /output buildserver
- nuget restore
build_script:
msbuild /nologo /v:m /p:AssemblyVersion=%GitVersion_AssemblySemVer% /p:FileVersion=%GitVersion_MajorMinorPatch% /p:InformationalVersion=%GitVersion_InformationalVersion% /p:Configuration=%CONFIGURATION% S7.sln