Use TcpClient and implemnt async methods

Note: This keeps the old methods to be backward compatible.
Note: Unforntunatly a lot of whitespace fixes, refactoring and other trivial stuff is
included. It was to hard to split of in a seperate commit.

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

* Use TcpClient and use Async methods (ReadAsync/WriteAsync)
* Implemnt async methods for all existing methods
* Implemnt existing methods using tcpclient.
* Split Plc.cs in more files. (Common, Async, Sync, Helpers)
* Mark old methods as Obsolete
* Split tests in two files
* Implement Async tests
This commit is contained in:
Thomas Jäger
2018-04-18 16:25:29 +02:00
parent 50b026d7a5
commit f53a3bd320
11 changed files with 2532 additions and 1258 deletions

View File

@@ -8,7 +8,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>S7.UnitTest</RootNamespace>
<AssemblyName>S7Net.UnitTest</AssemblyName>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
<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>
@@ -16,6 +16,7 @@
<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>
@@ -25,6 +26,7 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
@@ -33,8 +35,12 @@
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<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" />
@@ -45,11 +51,7 @@
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework" />
</ItemGroup>
</Otherwise>
<Otherwise />
</Choose>
<ItemGroup>
<Compile Include="ConvertersUnitTest.cs" />
@@ -60,10 +62,11 @@
<Compile Include="Helpers\TestClassWithCustomType.cs" />
<Compile Include="Helpers\TestClassWithPrivateSetters.cs" />
<Compile Include="Helpers\TestLongClass.cs" />
<Compile Include="S7NetTestsAsync.cs" />
<Compile Include="Snap7\snap7.net.cs" />
<Compile Include="Helpers\TestClass.cs" />
<Compile Include="Helpers\TestStruct.cs" />
<Compile Include="S7NetTests.cs" />
<Compile Include="S7NetTestsSync.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Helpers\TestLongStruct.cs" />
</ItemGroup>

View File

@@ -0,0 +1,795 @@
#region Using
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S7.Net;
using S7.Net.UnitTest.Helpers;
using S7.Net.UnitTest;
using System.ServiceProcess;
using S7.Net.Types;
using S7.UnitTest.Helpers;
using System.Threading.Tasks;
#endregion
/**
* About the tests:
* ---------------------------------------------------------------------------
* The tests were written to show how to use this library to read and write
* different types of values, how to box and unbox values and of course to
* address some of the bugs of the library.
* These tests are not meant to cover 100% the code, but to check that once a
* variable is written, it stores the correct value.
* ----------------------------------------------------------------------------
* The plc used for the tests is the S7 "server" provided by Snap7 opensource
* library, that you can get for free here:http://snap7.sourceforge.net/
* The implementation of the server will not be discussed here, but there are
* some issues with the interop that cause the server, and unit test, to fail
* under some circumstances, like "click on Run all tests" too much.
* This doesn't mean that S7.Net has bugs, but that the implementation of the
* server has problems.
*
*/
//Tests for Async Methods
namespace S7.Net.UnitTest
{
public partial class S7NetTests
{
#region Tests
[TestMethod]
public async Task Test_Async_Connection()
{
if (plc.IsConnected == false)
{
await plc.OpenAsync();
}
}
/// <summary>
/// Read/Write a single Int16 or UInt16 with a single request.
/// Test that writing a UInt16 (ushort) and reading it gives the correct value.
/// Test also that writing a Int16 (short) and reading it gives the correct value.
/// </summary>
[TestMethod]
public async Task Test_Async_WriteAndReadInt16Variable()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
// To write a ushort i don't need any cast, only unboxing must be done
ushort val = 40000;
await plc.WriteAsync("DB1.DBW0", val);
ushort result = (ushort)await plc.ReadAsync("DB1.DBW0");
Assert.AreEqual(val, result, "A ushort goes from 0 to 64512");
// To write a short i need to convert it to UShort, then i need to reconvert the readed value to get
// the negative sign back
// Depending if i'm writing on a DWORD or on a DEC, i will see ushort or short value in the plc
short value = -100;
Assert.IsTrue(plc.IsConnected, "After connecting, IsConnected must be set to true");
await plc.WriteAsync("DB1.DBW0", value.ConvertToUshort());
short result2 = ((ushort)await plc.ReadAsync("DB1.DBW0")).ConvertToShort();
Assert.AreEqual(value, result2, "A short goes from -32767 to 32766");
}
/// <summary>
/// Read/Write a single Int32 or UInt32 with a single request.
/// Test that writing a UInt32 (uint) and reading it gives the correct value.
/// Test also that writing a Int32 (int) and reading it gives the correct value.
/// </summary>
[TestMethod]
public async Task Test_Async_WriteAndReadInt32Variable()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
// To write a uint I don't need any cast, only unboxing must be done
uint val = 1000;
await plc.WriteAsync("DB1.DBD40", val);
uint result = (uint)await plc.ReadAsync("DB1.DBD40");
Assert.AreEqual(val, result);
// To write a int I need to convert it to uint, then I need to reconvert the readed value to get
// the negative sign back
// Depending if I'm writing on a DBD or on a LONG, I will see uint or int value in the plc
int value = -60000;
await plc.WriteAsync("DB1.DBD60", value);
int result2 = ((uint)await plc.ReadAsync("DB1.DBD60")).ConvertToInt();
Assert.AreEqual(value, result2);
}
/// <summary>
/// Read/Write a single REAL with a single request.
/// Test that writing a double and reading it gives the correct value.
/// </summary>
[TestMethod]
public async Task Test_Async_WriteAndReadRealVariables()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
// Reading and writing a double is quite complicated, because it needs to be converted to DWord before the write,
// then reconvert to double after the read.
double val = 35.687;
await plc.WriteAsync ("DB1.DBD40", val.ConvertToUInt());
double result = ((uint)await plc.ReadAsync("DB1.DBD40")).ConvertToDouble();
Assert.AreEqual(val, Math.Round(result, 3)); // float lose precision, so i need to round it
}
/// <summary>
/// Read/Write a class that has the same properties of a DB with the same field in the same order
/// </summary>
[TestMethod]
public async Task Test_Async_ReadAndWriteClass()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestClass tc = new TestClass
{
BitVariable00 = true,
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariable = -154.789,
DWordVariable = 850
};
await plc.WriteClassAsync(tc, DB2);
TestClass tc2 = new TestClass();
// Values that are read from a class are stored inside the class itself, that is passed by reference
await plc.ReadClassAsync(tc2, DB2);
Assert.AreEqual(tc.BitVariable00, tc2.BitVariable00);
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
Assert.AreEqual(tc.RealVariable, Math.Round(tc2.RealVariable, 3));
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
}
/// <summary>
/// Read/Write a struct that has the same properties of a DB with the same field in the same order
/// </summary>
[TestMethod]
public async Task Test_Async_ReadAndWriteStruct()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestStruct tc = new TestStruct
{
BitVariable00 = true,
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariable = -154.789,
DWordVariable = 850
};
plc.WriteStruct(tc, DB2);
// Values that are read from a struct are stored in a new struct, returned by the funcion ReadStruct
TestStruct tc2 = (TestStruct)await plc.ReadStructAsync(typeof(TestStruct), DB2);
Assert.AreEqual(tc.BitVariable00, tc2.BitVariable00);
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
Assert.AreEqual(tc.RealVariable, Math.Round(tc2.RealVariable, 3));
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
}
/// <summary>
/// Read/Write a struct that has the same properties of a DB with the same field in the same order
/// </summary>
[TestMethod]
public async Task Test_Async_ReadAndWriteLongStruct()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestLongStruct tc = new TestLongStruct
{
IntVariable0 = 0,
IntVariable1 = 1,
IntVariable10 = 10,
IntVariable11 = 11,
IntVariable20 = 20,
IntVariable21 = 21,
IntVariable30 = 30,
IntVariable31 = 31,
IntVariable40 = 40,
IntVariable41 = 41,
IntVariable50 = 50,
IntVariable51 = 51,
IntVariable60 = 60,
IntVariable61 = 61,
IntVariable70 = 70,
IntVariable71 = 71,
IntVariable80 = 80,
IntVariable81 = 81,
IntVariable90 = 90,
IntVariable91 = 91,
IntVariable100 = 100,
IntVariable101 = 101,
IntVariable110 = 200,
IntVariable111 = 201
};
plc.WriteStruct(tc, DB2);
Assert.AreEqual(ErrorCode.NoError, plc.LastErrorCode);
// Values that are read from a struct are stored in a new struct, returned by the funcion ReadStruct
TestLongStruct tc2 = (TestLongStruct)await plc.ReadStructAsync(typeof(TestLongStruct), DB2);
Assert.AreEqual(ErrorCode.NoError, plc.LastErrorCode);
Assert.AreEqual( tc.IntVariable0, tc2.IntVariable0 );
Assert.AreEqual( tc.IntVariable1, tc2.IntVariable1 );
Assert.AreEqual( tc.IntVariable10, tc2.IntVariable10);
Assert.AreEqual( tc.IntVariable11, tc2.IntVariable11);
Assert.AreEqual( tc.IntVariable20, tc2.IntVariable20);
Assert.AreEqual( tc.IntVariable21, tc2.IntVariable21);
Assert.AreEqual( tc.IntVariable30, tc2.IntVariable30);
Assert.AreEqual( tc.IntVariable31, tc2.IntVariable31);
Assert.AreEqual( tc.IntVariable40, tc2.IntVariable40);
Assert.AreEqual( tc.IntVariable41, tc2.IntVariable41);
Assert.AreEqual( tc.IntVariable50, tc2.IntVariable50);
Assert.AreEqual( tc.IntVariable51, tc2.IntVariable51);
Assert.AreEqual( tc.IntVariable60, tc2.IntVariable60);
Assert.AreEqual( tc.IntVariable61, tc2.IntVariable61);
Assert.AreEqual( tc.IntVariable70, tc2.IntVariable70);
Assert.AreEqual( tc.IntVariable71, tc2.IntVariable71);
Assert.AreEqual( tc.IntVariable80, tc2.IntVariable80);
Assert.AreEqual( tc.IntVariable81, tc2.IntVariable81);
Assert.AreEqual( tc.IntVariable90, tc2.IntVariable90);
Assert.AreEqual(tc.IntVariable91, tc2.IntVariable91);
Assert.AreEqual(tc.IntVariable100, tc2.IntVariable100);
Assert.AreEqual(tc.IntVariable101, tc2.IntVariable101);
Assert.AreEqual(tc.IntVariable110, tc2.IntVariable110);
Assert.AreEqual(tc.IntVariable111, tc2.IntVariable111);
}
/// <summary>
/// Read/Write a class that has the same properties of a DB with the same field in the same order
/// </summary>
[TestMethod]
public async Task Test_Async_ReadAndWriteLongClass()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestLongClass tc = new TestLongClass
{
IntVariable0 = 0,
IntVariable1 = 1,
IntVariable10 = 10,
IntVariable11 = 11,
IntVariable20 = 20,
IntVariable21 = 21,
IntVariable30 = 30,
IntVariable31 = 31,
IntVariable40 = 40,
IntVariable41 = 41,
IntVariable50 = 50,
IntVariable51 = 51,
IntVariable60 = 60,
IntVariable61 = 61,
IntVariable70 = 70,
IntVariable71 = 71,
IntVariable80 = 80,
IntVariable81 = 81,
IntVariable90 = 90,
IntVariable91 = 91,
IntVariable100 = 100,
IntVariable101 = 101,
IntVariable110 = 200,
IntVariable111 = 201
};
await plc.WriteClassAsync(tc, DB2);
Assert.AreEqual(ErrorCode.NoError, plc.LastErrorCode);
// Values that are read from a struct are stored in a new struct, returned by the funcion ReadStruct
TestLongClass tc2 = new TestLongClass();
await plc.ReadClassAsync(tc2, DB2);
Assert.AreEqual(ErrorCode.NoError, plc.LastErrorCode);
Assert.AreEqual(tc.IntVariable0, tc2.IntVariable0);
Assert.AreEqual(tc.IntVariable1, tc2.IntVariable1);
Assert.AreEqual(tc.IntVariable10, tc2.IntVariable10);
Assert.AreEqual(tc.IntVariable11, tc2.IntVariable11);
Assert.AreEqual(tc.IntVariable20, tc2.IntVariable20);
Assert.AreEqual(tc.IntVariable21, tc2.IntVariable21);
Assert.AreEqual(tc.IntVariable30, tc2.IntVariable30);
Assert.AreEqual(tc.IntVariable31, tc2.IntVariable31);
Assert.AreEqual(tc.IntVariable40, tc2.IntVariable40);
Assert.AreEqual(tc.IntVariable41, tc2.IntVariable41);
Assert.AreEqual(tc.IntVariable50, tc2.IntVariable50);
Assert.AreEqual(tc.IntVariable51, tc2.IntVariable51);
Assert.AreEqual(tc.IntVariable60, tc2.IntVariable60);
Assert.AreEqual(tc.IntVariable61, tc2.IntVariable61);
Assert.AreEqual(tc.IntVariable70, tc2.IntVariable70);
Assert.AreEqual(tc.IntVariable71, tc2.IntVariable71);
Assert.AreEqual(tc.IntVariable80, tc2.IntVariable80);
Assert.AreEqual(tc.IntVariable81, tc2.IntVariable81);
Assert.AreEqual(tc.IntVariable90, tc2.IntVariable90);
Assert.AreEqual(tc.IntVariable91, tc2.IntVariable91);
Assert.AreEqual(tc.IntVariable100, tc2.IntVariable100);
Assert.AreEqual(tc.IntVariable101, tc2.IntVariable101);
Assert.AreEqual(tc.IntVariable110, tc2.IntVariable110);
Assert.AreEqual(tc.IntVariable111, tc2.IntVariable111);
}
/// <summary>
/// Tests that a read and a write on addresses bigger than 8192 are executed correctly
/// </summary>
[TestMethod]
public async Task Test_Async_WriteAndReadInt16VariableAddress8192()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
// To write a ushort i don't need any cast, only unboxing must be done
ushort val = 8192;
await plc.WriteAsync("DB2.DBW8192", val);
ushort result = (ushort)await plc.ReadAsync("DB2.DBW8192");
Assert.AreEqual(val, result, "A ushort goes from 0 to 64512");
// To write a short i need to convert it to UShort, then i need to reconvert the readed value to get
// the negative sign back
// Depending if i'm writing on a DWORD or on a DEC, i will see ushort or short value in the plc
short value = -8192;
Assert.IsTrue(plc.IsConnected, "After connecting, IsConnected must be set to true");
await plc.WriteAsync("DB2.DBW8192", value.ConvertToUshort());
short result2 = ((ushort)await plc.ReadAsync("DB2.DBW8192")).ConvertToShort();
Assert.AreEqual(value, result2, "A short goes from -32767 to 32766");
}
/// <summary>
/// Tests that a read and a write on addresses bigger than 8192 are executed correctly
/// </summary>
[TestMethod]
public async Task Test_Async_WriteAndReadInt16VariableAddress16384()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
// To write a ushort i don't need any cast, only unboxing must be done
ushort val = 16384;
await plc.WriteAsync("DB2.DBW16384", val);
ushort result = (ushort)await plc.ReadAsync("DB2.DBW16384");
Assert.AreEqual(val, result, "A ushort goes from 0 to 64512");
// To write a short i need to convert it to UShort, then i need to reconvert the readed value to get
// the negative sign back
// Depending if i'm writing on a DWORD or on a DEC, i will see ushort or short value in the plc
short value = -16384;
Assert.IsTrue(plc.IsConnected, "After connecting, IsConnected must be set to true");
await plc.WriteAsync("DB2.DBW16384", value.ConvertToUshort());
short result2 = ((ushort)await plc.ReadAsync("DB2.DBW16384")).ConvertToShort();
Assert.AreEqual(value, result2, "A short goes from -32767 to 32766");
}
[TestMethod]
public async Task Test_Async_ReadMultipleBytes()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
ushort val = 16384;
await plc.WriteAsync("DB2.DBW16384", val);
ushort result = (ushort)await plc.ReadAsync("DB2.DBW16384");
Assert.AreEqual(val, result, "A ushort goes from 0 to 64512");
ushort val2 = 129;
await plc.WriteAsync("DB2.DBW16", val2);
ushort result2 = (ushort)await plc.ReadAsync("DB2.DBW16");
Assert.AreEqual(val2, result2, "A ushort goes from 0 to 64512");
var dataItems = new List<DataItem>()
{
new DataItem
{
Count = 1,
DataType = DataType.DataBlock,
DB = 2,
StartByteAdr = 16384,
VarType = VarType.Word
},
new DataItem
{
Count = 1,
DataType = DataType.DataBlock,
DB = 2,
StartByteAdr = 16,
VarType = VarType.Word
}
};
dataItems = await plc.ReadMultipleVarsAsync(dataItems);
Assert.AreEqual(dataItems[0].Value, val);
Assert.AreEqual(dataItems[1].Value, val2);
}
/// <summary>
/// Tests that a read and a write on addresses bigger than 8192 are executed correctly
/// </summary>
[TestMethod]
public async Task Test_Async_WriteAndReadBooleanVariable()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
// tests when writing true/false
await plc.WriteAsync("DB1.DBX0.0", false);
var boolVariable = (bool)await plc.ReadAsync("DB1.DBX0.0");
Assert.IsFalse(boolVariable);
await plc.WriteAsync("DB1.DBX0.0", true);
boolVariable = (bool)await plc.ReadAsync("DB1.DBX0.0");
Assert.IsTrue(boolVariable);
// tests when writing 0/1
await plc.WriteAsync("DB1.DBX0.0", 0);
boolVariable = (bool)await plc.ReadAsync("DB1.DBX0.0");
Assert.IsFalse(boolVariable);
await plc.WriteAsync("DB1.DBX0.0", 1);
boolVariable = (bool)await plc.ReadAsync("DB1.DBX0.0");
Assert.IsTrue(boolVariable);
await plc.WriteAsync("DB1.DBX0.7", 1);
boolVariable = (bool)await plc.ReadAsync("DB1.DBX0.7");
Assert.IsTrue(boolVariable);
await plc.WriteAsync("DB1.DBX0.7", 0);
boolVariable = (bool)await plc.ReadAsync("DB1.DBX0.7");
Assert.IsFalse(boolVariable);
await plc.WriteAsync("DB1.DBX658.0", 1);
boolVariable = (bool)await plc.ReadAsync("DB1.DBX658.0");
Assert.IsTrue(boolVariable);
await plc.WriteAsync("DB1.DBX658.7", 1);
boolVariable = (bool)await plc.ReadAsync("DB1.DBX658.7");
Assert.IsTrue(boolVariable);
await plc.WriteAsync("DB2.DBX9658.0", 1);
boolVariable = (bool)await plc.ReadAsync("DB2.DBX9658.0");
Assert.IsTrue(boolVariable);
}
[TestMethod]
public async Task Test_Async_ReadClassIgnoresNonPublicSetters()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestClassWithPrivateSetters tc = new TestClassWithPrivateSetters
{
BitVariable00 = true,
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariable = -154.789,
DWordVariable = 850
};
await plc.WriteClassAsync(tc, DB2);
TestClassWithPrivateSetters tc2 = new TestClassWithPrivateSetters();
// Values that are read from a class are stored inside the class itself, that is passed by reference
var res = await plc.ReadClassAsync(tc2, DB2);
tc = (TestClassWithPrivateSetters)res.Item2;
Assert.AreEqual(tc.BitVariable00, tc2.BitVariable00);
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
Assert.AreEqual(tc.RealVariable, tc2.RealVariable, 0.1);
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
Assert.AreEqual(TestClassWithPrivateSetters.PRIVATE_SETTER_VALUE, tc2.PrivateSetterProperty);
Assert.AreEqual(TestClassWithPrivateSetters.PROTECTED_SETTER_VALUE, tc2.ProtectedSetterProperty);
Assert.AreEqual(TestClassWithPrivateSetters.INTERNAL_SETTER_VALUE, tc2.InternalSetterProperty);
Assert.AreEqual(TestClassWithPrivateSetters.JUST_A_GETTER_VALUE, tc2.JustAGetterProperty);
}
[TestMethod]
public async Task Test_Async_ReadBytesThrowsExceptionIfPlcIsNotConnected()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
Assert.IsFalse(notConnectedPlc.IsConnected);
TestClass tc = new TestClass();
try
{
var res = await notConnectedPlc.ReadClassAsync(tc, DB2);
Assert.Fail();
}
catch
{
}
}
}
[TestMethod]
public async Task Test_Async_ReadClassWithGenericReturnsSameResultAsReadClassWithoutGeneric()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestClass tc = new TestClass
{
BitVariable00 = true,
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariable = -154.789,
DWordVariable = 850
};
await plc.WriteClassAsync(tc, DB2);
// Values that are read from a class are stored inside the class itself, that is passed by reference
TestClass tc2 = new TestClass();
var res = await plc.ReadClassAsync(tc2, DB2);
tc2 = (TestClass)res.Item2;
TestClass tc2Generic = await plc.ReadClassAsync<TestClass>(DB2);
Assert.AreEqual(tc2.BitVariable00, tc2Generic.BitVariable00);
Assert.AreEqual(tc2.BitVariable10, tc2Generic.BitVariable10);
Assert.AreEqual(tc2.DIntVariable, tc2Generic.DIntVariable);
Assert.AreEqual(tc2.IntVariable, tc2Generic.IntVariable);
Assert.AreEqual(Math.Round(tc2.RealVariable, 3), Math.Round(tc2Generic.RealVariable, 3));
Assert.AreEqual(tc2.DWordVariable, tc2Generic.DWordVariable);
}
[TestMethod]
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.");
try
{
TestClass tc = await notConnectedPlc.ReadClassAsync<TestClass>(DB2);
Assert.Fail();
}
catch { }
}
}
[TestMethod]
public async Task Test_Async_ReadClassWithGenericAndClassFactoryReturnsSameResultAsReadClassWithoutGeneric()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestClass tc = new TestClass
{
BitVariable00 = true,
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariable = -154.789,
DWordVariable = 850
};
await plc.WriteClassAsync(tc, DB2);
// Values that are read from a class are stored inside the class itself, that is passed by reference
TestClass tc2Generic = await plc.ReadClassAsync<TestClass>(DB2);
TestClass tc2GenericWithClassFactory = await plc.ReadClassAsync(() => new TestClass(), DB2);
Assert.AreEqual(tc2Generic.BitVariable00, tc2GenericWithClassFactory.BitVariable00);
Assert.AreEqual(tc2Generic.BitVariable10, tc2GenericWithClassFactory.BitVariable10);
Assert.AreEqual(tc2Generic.DIntVariable, tc2GenericWithClassFactory.DIntVariable);
Assert.AreEqual(tc2Generic.IntVariable, tc2GenericWithClassFactory.IntVariable);
Assert.AreEqual(Math.Round(tc2Generic.RealVariable, 3), Math.Round(tc2GenericWithClassFactory.RealVariable, 3));
Assert.AreEqual(tc2Generic.DWordVariable, tc2GenericWithClassFactory.DWordVariable);
}
[TestMethod]
public async Task Test_Async_ReadClassWithGenericAndClassFactoryThrowsExceptionPlcIsNotConnected()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
Assert.IsFalse(notConnectedPlc.IsConnected);
try
{
TestClass tc = await notConnectedPlc.ReadClassAsync(() => new TestClass(), DB2);
Assert.Fail();
}
catch
{
}
}
}
[TestMethod]
public async Task Test_Async_ReadStructThrowsExceptionPlcIsNotConnected()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
Assert.IsFalse(notConnectedPlc.IsConnected);
try
{
object tsObj = await notConnectedPlc.ReadStructAsync(typeof(TestStruct), DB2);
Assert.Fail();
} catch
{
}
}
}
[TestMethod]
public async Task Test_Async_ReadStructWithGenericReturnsSameResultAsReadStructWithoutGeneric()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestStruct ts = new TestStruct
{
BitVariable00 = true,
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariable = -154.789,
DWordVariable = 850
};
plc.WriteStruct(ts, DB2);
// Values that are read from a struct are stored in a new struct, returned by the funcion ReadStruct
TestStruct ts2 = (TestStruct)await plc.ReadStructAsync(typeof(TestStruct), DB2);
var test = await plc.ReadStructAsync<TestStruct>(DB2);
TestStruct ts2Generic = test.Value;
Assert.AreEqual(ts2.BitVariable00, ts2Generic.BitVariable00);
Assert.AreEqual(ts2.BitVariable10, ts2Generic.BitVariable10);
Assert.AreEqual(ts2.DIntVariable, ts2Generic.DIntVariable);
Assert.AreEqual(ts2.IntVariable, ts2Generic.IntVariable);
Assert.AreEqual(Math.Round(ts2.RealVariable, 3), Math.Round(ts2Generic.RealVariable, 3));
Assert.AreEqual(ts2.DWordVariable, ts2Generic.DWordVariable);
}
[TestMethod]
public async Task Test_Async_ReadStructWithGenericThrowsExceptionIfPlcIsNotConnected()
{
using (var notConnectedPlc = new Plc(CpuType.S7300, "255.255.255.255", 0, 0))
{
Assert.IsFalse(notConnectedPlc.IsConnected);
try
{
object tsObj = await notConnectedPlc.ReadStructAsync<TestStruct>(DB2);
Assert.Fail();
} catch { }
}
}
/// <summary>
/// Tests that the method ReadClass returns the number of bytes read from the plc
/// </summary>
[TestMethod]
public async Task Test_Async_ReadClassReturnsNumberOfReadBytesFromThePlc()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestClass tc = new TestClass
{
BitVariable00 = true,
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariable = -154.789,
DWordVariable = 850
};
plc.WriteClass(tc, DB2);
int expectedReadBytes = 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
var res = await plc.ReadClassAsync(tc2, DB2);
int actualReadBytes = res.Item1;
Assert.AreEqual(expectedReadBytes, actualReadBytes);
}
[TestMethod]
public async Task Test_Async_ReadClassWithArray()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestClassWithArrays tc = new TestClassWithArrays
{
Bool = true
};
tc.BoolValues[1] = true;
tc.Int = int.MinValue;
tc.Ints[0] = int.MinValue;
tc.Ints[1] = int.MaxValue;
tc.Short = short.MinValue;
tc.Shorts[0] = short.MinValue;
tc.Shorts[1] = short.MaxValue;
tc.Double = float.MinValue;
tc.Doubles[0] = float.MinValue + 1;
tc.Doubles[1] = float.MaxValue;
tc.UShort = ushort.MinValue + 1;
tc.UShorts[0] = ushort.MinValue + 1;
tc.UShorts[1] = ushort.MaxValue;
plc.WriteClass(tc, DB2);
TestClassWithArrays tc2 = await plc.ReadClassAsync<TestClassWithArrays>(DB2);
Assert.AreEqual(tc.Bool, tc2.Bool);
Assert.AreEqual(tc.BoolValues[0], tc2.BoolValues[0]);
Assert.AreEqual(tc.BoolValues[1], tc2.BoolValues[1]);
Assert.AreEqual(tc.Int, tc2.Int);
Assert.AreEqual(tc.Ints[0], tc2.Ints[0]);
Assert.AreEqual(tc.Ints[1], tc.Ints[1]);
Assert.AreEqual(tc.Short, tc2.Short);
Assert.AreEqual(tc.Shorts[0], tc2.Shorts[0]);
Assert.AreEqual(tc.Shorts[1], tc2.Shorts[1]);
Assert.AreEqual(tc.Double, tc2.Double);
Assert.AreEqual(tc.Doubles[0], tc2.Doubles[0]);
Assert.AreEqual(tc.Doubles[1], tc2.Doubles[1]);
Assert.AreEqual(tc.UShort, tc2.UShort);
Assert.AreEqual(tc.UShorts[0], tc2.UShorts[0]);
Assert.AreEqual(tc.UShorts[1], tc2.UShorts[1]);
}
[TestMethod]
public async Task Test_Async_ReadClassWithArrayAndCustomType()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
TestClassWithCustomType tc = new TestClassWithCustomType
{
Int = int.MinValue,
CustomType = new CustomType()
};
tc.CustomType.Bools[1] = true;
tc.CustomTypes[0] = new CustomType();
tc.CustomTypes[1] = new CustomType();
tc.CustomTypes[0].Bools[0] = true;
tc.CustomTypes[1].Bools[1] = true;
plc.WriteClass(tc, DB2);
TestClassWithCustomType tc2 = await plc.ReadClassAsync<TestClassWithCustomType>(DB2);
Assert.AreEqual(tc.Int, tc2.Int);
Assert.AreEqual(tc.CustomType.Bools[0], tc2.CustomType.Bools[0]);
Assert.AreEqual(tc.CustomType.Bools[1], tc2.CustomType.Bools[1]);
Assert.AreEqual(tc.CustomTypes[0].Bools[0], tc2.CustomTypes[0].Bools[0]);
Assert.AreEqual(tc.CustomTypes[0].Bools[1], tc2.CustomTypes[0].Bools[1]);
Assert.AreEqual(tc.CustomTypes[1].Bools[0], tc2.CustomTypes[1].Bools[0]);
Assert.AreEqual(tc.CustomTypes[1].Bools[1], tc2.CustomTypes[1].Bools[1]);
}
[TestMethod]
public async Task Test_Async_ReadWriteDouble()
{
double test_value = 55.66;
await plc.WriteAsync("DB1.DBD0", test_value);
Assert.AreEqual(plc.LastErrorCode, ErrorCode.NoError, "Write Double");
var helper = await plc.ReadAsync("DB1.DBD0");
double test_value2 = Conversion.ConvertToDouble((uint)helper);
Assert.AreEqual(plc.LastErrorCode, ErrorCode.NoError, "Read Double");
Assert.AreEqual(test_value, test_value2, 0.01, "Compare Write/Read"); //Need delta here because S7 only has 32 bit reals
}
[TestMethod]
public async Task Test_Async_ReadWriteBytesMany()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
var count = 2000;
var dataItems = new List<byte>();
for (int i = 0; i < count; i++)
{
dataItems.Add((byte)(i%256));
}
await plc.WriteBytesAsync(DataType.DataBlock, 2, 0, dataItems.ToArray());
var res = await plc.ReadBytesAsync(DataType.DataBlock, 2, 0, count);
for (int x = 0; x < count; x++)
{
Assert.AreEqual(x % 256, res[x]);
}
}
#endregion
}
}

View File

@@ -30,10 +30,13 @@ using S7.UnitTest.Helpers;
* server has problems.
*
*/
//This file contains tests for the obsolete synchronous methods
#pragma warning disable CS0618
namespace S7.Net.UnitTest
{
[TestClass]
public class S7NetTests
public partial class S7NetTests : IDisposable
{
#region Constants
const int DB2 = 2;
@@ -242,26 +245,26 @@ namespace S7.Net.UnitTest
// Values that are read from a struct are stored in a new struct, returned by the funcion ReadStruct
TestLongStruct tc2 = (TestLongStruct)plc.ReadStruct(typeof(TestLongStruct), DB2);
Assert.AreEqual(ErrorCode.NoError, plc.LastErrorCode);
Assert.AreEqual( tc.IntVariable0, tc2.IntVariable0 );
Assert.AreEqual( tc.IntVariable1, tc2.IntVariable1 );
Assert.AreEqual( tc.IntVariable10, tc2.IntVariable10);
Assert.AreEqual( tc.IntVariable11, tc2.IntVariable11);
Assert.AreEqual( tc.IntVariable20, tc2.IntVariable20);
Assert.AreEqual( tc.IntVariable21, tc2.IntVariable21);
Assert.AreEqual( tc.IntVariable30, tc2.IntVariable30);
Assert.AreEqual( tc.IntVariable31, tc2.IntVariable31);
Assert.AreEqual( tc.IntVariable40, tc2.IntVariable40);
Assert.AreEqual( tc.IntVariable41, tc2.IntVariable41);
Assert.AreEqual( tc.IntVariable50, tc2.IntVariable50);
Assert.AreEqual( tc.IntVariable51, tc2.IntVariable51);
Assert.AreEqual( tc.IntVariable60, tc2.IntVariable60);
Assert.AreEqual( tc.IntVariable61, tc2.IntVariable61);
Assert.AreEqual( tc.IntVariable70, tc2.IntVariable70);
Assert.AreEqual( tc.IntVariable71, tc2.IntVariable71);
Assert.AreEqual( tc.IntVariable80, tc2.IntVariable80);
Assert.AreEqual( tc.IntVariable81, tc2.IntVariable81);
Assert.AreEqual( tc.IntVariable90, tc2.IntVariable90);
Assert.AreEqual(tc.IntVariable91, tc2.IntVariable91);
Assert.AreEqual(tc.IntVariable0, tc2.IntVariable0);
Assert.AreEqual(tc.IntVariable1, tc2.IntVariable1);
Assert.AreEqual(tc.IntVariable10, tc2.IntVariable10);
Assert.AreEqual(tc.IntVariable11, tc2.IntVariable11);
Assert.AreEqual(tc.IntVariable20, tc2.IntVariable20);
Assert.AreEqual(tc.IntVariable21, tc2.IntVariable21);
Assert.AreEqual(tc.IntVariable30, tc2.IntVariable30);
Assert.AreEqual(tc.IntVariable31, tc2.IntVariable31);
Assert.AreEqual(tc.IntVariable40, tc2.IntVariable40);
Assert.AreEqual(tc.IntVariable41, tc2.IntVariable41);
Assert.AreEqual(tc.IntVariable50, tc2.IntVariable50);
Assert.AreEqual(tc.IntVariable51, tc2.IntVariable51);
Assert.AreEqual(tc.IntVariable60, tc2.IntVariable60);
Assert.AreEqual(tc.IntVariable61, tc2.IntVariable61);
Assert.AreEqual(tc.IntVariable70, tc2.IntVariable70);
Assert.AreEqual(tc.IntVariable71, tc2.IntVariable71);
Assert.AreEqual(tc.IntVariable80, tc2.IntVariable80);
Assert.AreEqual(tc.IntVariable81, tc2.IntVariable81);
Assert.AreEqual(tc.IntVariable90, tc2.IntVariable90);
Assert.AreEqual(tc.IntVariable91, tc2.IntVariable91);
Assert.AreEqual(tc.IntVariable100, tc2.IntVariable100);
Assert.AreEqual(tc.IntVariable101, tc2.IntVariable101);
Assert.AreEqual(tc.IntVariable110, tc2.IntVariable110);
@@ -676,7 +679,7 @@ namespace S7.Net.UnitTest
Assert.AreEqual(expectedReadBytes, actualReadBytes);
}
[TestMethod]
public void T22_ReadClassWithArray()
{
@@ -791,14 +794,14 @@ namespace S7.Net.UnitTest
var dataItems = new List<byte>();
for (int i = 0; i < count; i++)
{
dataItems.Add((byte)(i%256));
dataItems.Add((byte)(i % 256));
}
plc.WriteBytes(DataType.DataBlock, 2, 0, dataItems.ToArray());
var res = plc.ReadBytes(DataType.DataBlock, 2, 0, count);
for (int x = 0; x < count; x++)
for (int x = 0; x < count; x++)
{
Assert.AreEqual(x % 256, res[x]);
}
@@ -807,22 +810,53 @@ namespace S7.Net.UnitTest
#endregion
#region Private methods
private static void ShutDownServiceS7oiehsx64()
{
try
ServiceController[] services = ServiceController.GetServices();
var service = services.FirstOrDefault(s => s.ServiceName == "s7oiehsx64");
if (service != null)
{
ServiceController sc = new ServiceController("s7oiehsx64");
switch (sc.Status)
if (service.Status == ServiceControllerStatus.Running)
{
case ServiceControllerStatus.Running:
sc.Stop();
break;
service.Stop();
}
}
catch { } // service not found
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
plc.Close();
}
// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
disposedValue = true;
}
}
// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~S7NetTests() {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// TODO: uncomment the following line if the finalizer is overridden above.
// GC.SuppressFinalize(this);
}
#endregion
#endregion
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading.Tasks;
namespace S7.Net
{
@@ -45,11 +46,24 @@ namespace S7.Net
/// Reads COTP TPDU (Transport protocol data unit) from the network stream
/// See: https://tools.ietf.org/html/rfc905
/// </summary>
/// <param name="socket">The socket to read from</param>
/// <param name="stream">The socket to read from</param>
/// <returns>COTP DPDU instance</returns>
public static TPDU Read(Socket socket)
public static TPDU Read(Stream stream)
{
var tpkt = TPKT.Read(socket);
var tpkt = TPKT.Read(stream);
if (tpkt.Length > 0) return new TPDU(tpkt);
return null;
}
/// <summary>
/// Reads COTP TPDU (Transport protocol data unit) from the network stream
/// See: https://tools.ietf.org/html/rfc905
/// </summary>
/// <param name="stream">The socket to read from</param>
/// <returns>COTP DPDU instance</returns>
public static async Task<TPDU> ReadAsync(Stream stream)
{
var tpkt = await TPKT.ReadAsync(stream);
if (tpkt.Length > 0) return new TPDU(tpkt);
return null;
}
@@ -77,9 +91,9 @@ namespace S7.Net
/// See: https://tools.ietf.org/html/rfc905
/// </summary>
/// <returns>Data in TSDU</returns>
public static byte[] Read(Socket socket)
{
var segment = TPDU.Read(socket);
public static byte[] Read(Stream stream)
{
var segment = TPDU.Read(stream);
if (segment == null) return null;
var output = new MemoryStream(segment.Data.Length);
@@ -87,7 +101,28 @@ namespace S7.Net
while (!segment.LastDataUnit)
{
segment = TPDU.Read(socket);
segment = TPDU.Read(stream);
output.Write(segment.Data, (int)output.Position, segment.Data.Length);
}
return output.GetBuffer();
}
/// <summary>
/// Reads the full COTP TSDU (Transport service data unit)
/// See: https://tools.ietf.org/html/rfc905
/// </summary>
/// <returns>Data in TSDU</returns>
public static async Task<byte[]> ReadAsync(Stream stream)
{
var segment = await TPDU.ReadAsync(stream);
if (segment == null) return null;
var output = new MemoryStream(segment.Data.Length);
output.Write(segment.Data, 0, segment.Data.Length);
while (!segment.LastDataUnit)
{
segment = await TPDU.ReadAsync(stream);
output.Write(segment.Data, (int)output.Position, segment.Data.Length);
}
return output.GetBuffer();

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
namespace S7.Net
{
internal class WrongNumberOfBytesException : Exception
{
public WrongNumberOfBytesException() : base()
{
}
public WrongNumberOfBytesException(string message) : base(message)
{
}
public WrongNumberOfBytesException(string message, Exception innerException) : base(message, innerException)
{
}
protected WrongNumberOfBytesException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
internal class InvalidAddressException : Exception
{
public InvalidAddressException() : base ()
{
}
public InvalidAddressException(string message) : base(message)
{
}
public InvalidAddressException(string message, Exception innerException) : base(message, innerException)
{
}
protected InvalidAddressException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
internal class InvalidVariableTypeException : Exception
{
public InvalidVariableTypeException() : base()
{
}
public InvalidVariableTypeException(string message) : base(message)
{
}
public InvalidVariableTypeException(string message, Exception innerException) : base(message, innerException)
{
}
protected InvalidVariableTypeException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

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

@@ -0,0 +1,480 @@
using S7.Net.Types;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net.Sockets;
using System.Threading.Tasks;
namespace S7.Net
{
internal class PLCAddress
{
public DataType dataType;
public int DBNumber;
public int Address;
public int BitNumber;
public VarType varType;
public PLCAddress(string address)
{
ParseString(address);
}
private void ParseString(string address)
{
BitNumber = -1;
switch (address.Substring(0, 2))
{
case "DB":
string[] strings = address.Split(new char[] { '.' });
if (strings.Length < 2)
throw new InvalidAddressException("To few periods for DB address");
dataType = DataType.DataBlock;
DBNumber = int.Parse(strings[0].Substring(2));
Address = int.Parse(strings[1].Substring(3));
string dbType = strings[1].Substring(0, 3);
switch (dbType)
{
case "DBB":
varType = VarType.Byte;
return;
case "DBW":
varType = VarType.Word;
return;
case "DBD":
varType = VarType.DWord;
return;
case "DBX":
BitNumber = int.Parse(strings[2]);
if (BitNumber > 7)
throw new InvalidAddressException("Bit can only be 0-7");
varType = VarType.Bit;
return;
default:
throw new InvalidAddressException();
}
case "EB":
// Input byte
dataType = DataType.Input;
DBNumber = 0;
Address = int.Parse(address.Substring(2));
varType = VarType.Byte;
return;
case "EW":
// Input word
dataType = DataType.Input;
DBNumber = 0;
Address = int.Parse(address.Substring(2));
varType = VarType.Word;
return;
case "ED":
// Input double-word
dataType = DataType.Input;
DBNumber = 0;
Address = int.Parse(address.Substring(2));
varType = VarType.DWord;
return;
case "AB":
// Output byte
dataType = DataType.Output;
DBNumber = 0;
Address = int.Parse(address.Substring(2));
varType = VarType.Byte;
return;
case "AW":
// Output word
dataType = DataType.Output;
DBNumber = 0;
Address = int.Parse(address.Substring(2));
varType = VarType.Word;
return;
case "AD":
// Output double-word
dataType = DataType.Output;
DBNumber = 0;
Address = int.Parse(address.Substring(2));
varType = VarType.DWord;
return;
case "MB":
// Memory byte
dataType = DataType.Memory;
DBNumber = 0;
Address = int.Parse(address.Substring(2));
varType = VarType.Byte;
return;
case "MW":
// Memory word
dataType = DataType.Memory;
DBNumber = 0;
Address = int.Parse(address.Substring(2));
varType = VarType.Word;
return;
case "MD":
// Memory double-word
dataType = DataType.Memory;
DBNumber = 0;
Address = int.Parse(address.Substring(2));
varType = VarType.DWord;
return;
default:
switch (address.Substring(0, 1))
{
case "E":
case "I":
// Input
dataType = DataType.Input;
break;
case "A":
case "O":
// Output
dataType = DataType.Output;
break;
case "M":
// Memory
dataType = DataType.Memory;
break;
case "T":
// Timer
dataType = DataType.Timer;
DBNumber = 0;
Address = int.Parse(address.Substring(1));
varType = VarType.Timer;
return;
case "Z":
case "C":
// Counter
dataType = DataType.Timer;
DBNumber = 0;
Address = int.Parse(address.Substring(1));
varType = VarType.Counter;
return;
default:
throw new InvalidAddressException(string.Format("{0} is not av valid address", address.Substring(0, 1)));
}
string txt2 = address.Substring(1);
if (txt2.IndexOf(".") == -1)
throw new InvalidAddressException("To few periods for DB address");
Address = int.Parse(txt2.Substring(0, txt2.IndexOf(".")));
BitNumber = int.Parse(txt2.Substring(txt2.IndexOf(".") + 1));
if (BitNumber > 7)
throw new InvalidAddressException("Bit can only be 0-7");
return;
}
}
}
public partial class Plc
{
/// <summary>
/// Creates the header to read bytes from the PLC
/// </summary>
/// <param name="amount"></param>
/// <returns></returns>
private ByteArray ReadHeaderPackage(int amount = 1)
{
//header size = 19 bytes
var package = new Types.ByteArray(19);
package.Add(new byte[] { 0x03, 0x00, 0x00 });
//complete package size
package.Add((byte)(19 + (12 * amount)));
package.Add(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 });
//amount of requests
package.Add((byte)amount);
return package;
}
/// <summary>
/// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType),
/// the address of the memory, the address of the byte and the bytes count.
/// </summary>
/// <param name="dataType">MemoryType (DB, Timer, Counter, etc.)</param>
/// <param name="db">Address of the memory to be read</param>
/// <param name="startByteAdr">Start address of the byte</param>
/// <param name="count">Number of bytes to be read</param>
/// <returns></returns>
private ByteArray CreateReadDataRequestPackage(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 });
switch (dataType)
{
case DataType.Timer:
case DataType.Counter:
package.Add((byte)dataType);
break;
default:
package.Add(0x02);
break;
}
package.Add(Word.ToByteArray((ushort)(count)));
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);
switch (dataType)
{
case DataType.Timer:
case DataType.Counter:
package.Add(Types.Word.ToByteArray((ushort)(startByteAdr)));
break;
default:
package.Add(Types.Word.ToByteArray((ushort)((startByteAdr) * 8)));
break;
}
return package;
}
/// <summary>
/// Given a S7 variable type (Bool, Word, DWord, etc.), it converts the bytes in the appropriate C# format.
/// </summary>
/// <param name="varType"></param>
/// <param name="bytes"></param>
/// <param name="varCount"></param>
/// <param name="bitAdr"></param>
/// <returns></returns>
private object ParseBytes(VarType varType, byte[] bytes, int varCount, byte bitAdr = 0)
{
if (bytes == null)
return null;
switch (varType)
{
case VarType.Byte:
if (varCount == 1)
return bytes[0];
else
return bytes;
case VarType.Word:
if (varCount == 1)
return Word.FromByteArray(bytes);
else
return Word.ToArray(bytes);
case VarType.Int:
if (varCount == 1)
return Int.FromByteArray(bytes);
else
return Int.ToArray(bytes);
case VarType.DWord:
if (varCount == 1)
return DWord.FromByteArray(bytes);
else
return DWord.ToArray(bytes);
case VarType.DInt:
if (varCount == 1)
return DInt.FromByteArray(bytes);
else
return DInt.ToArray(bytes);
case VarType.Real:
if (varCount == 1)
return Types.Double.FromByteArray(bytes);
else
return Types.Double.ToArray(bytes);
case VarType.String:
return Types.String.FromByteArray(bytes);
case VarType.StringEx:
return StringEx.FromByteArray(bytes);
case VarType.Timer:
if (varCount == 1)
return Timer.FromByteArray(bytes);
else
return Timer.ToArray(bytes);
case VarType.Counter:
if (varCount == 1)
return Counter.FromByteArray(bytes);
else
return Counter.ToArray(bytes);
case VarType.Bit:
if (varCount == 1)
{
if (bitAdr > 7)
return null;
else
return Bit.FromByte(bytes[0], bitAdr);
}
else
{
return Bit.ToBitArray(bytes);
}
default:
return null;
}
}
public byte[] GetPackage(object value)
{
switch (value.GetType().Name)
{
case "Byte":
return Types.Byte.ToByteArray((byte)value);
case "Int16":
return Types.Int.ToByteArray((Int16)value);
case "UInt16":
return Types.Word.ToByteArray((UInt16)value);
case "Int32":
return Types.DInt.ToByteArray((Int32)value);
case "UInt32":
return Types.DWord.ToByteArray((UInt32)value);
case "Double":
return Types.Double.ToByteArray((double)value);
case "Byte[]":
return (byte[])value;
case "Int16[]":
return Types.Int.ToByteArray((Int16[])value);
case "UInt16[]":
return Types.Word.ToByteArray((UInt16[])value);
case "Int32[]":
return Types.DInt.ToByteArray((Int32[])value);
case "UInt32[]":
return Types.DWord.ToByteArray((UInt32[])value);
case "Double[]":
return Types.Double.ToByteArray((double[])value);
case "String":
return Types.String.ToByteArray(value as string);
default:
throw new InvalidVariableTypeException();
}
}
/// <summary>
/// Sets the <see cref="LastErrorCode"/> to <see cref="ErrorCode.NoError"/> and <see cref="LastErrorString"/> to <see cref="string.Empty"/>.
/// </summary>
public void ClearLastError()
{
LastErrorCode = ErrorCode.NoError;
LastErrorString = string.Empty;
}
/// <summary>
/// Given a S7 <see cref="VarType"/> (Bool, Word, DWord, etc.), it returns how many bytes to read.
/// </summary>
/// <param name="varType"></param>
/// <param name="varCount"></param>
/// <returns>Byte lenght of variable</returns>
private int VarTypeToByteLength(VarType varType, int varCount = 1)
{
switch (varType)
{
case VarType.Bit:
return varCount; //TODO
case VarType.Byte:
return (varCount < 1) ? 1 : varCount;
case VarType.String:
return varCount;
case VarType.StringEx:
return varCount + 2;
case VarType.Word:
case VarType.Timer:
case VarType.Int:
case VarType.Counter:
return varCount * 2;
case VarType.DWord:
case VarType.DInt:
case VarType.Real:
return varCount * 4;
default:
return 0;
}
}
private byte[] GetCOPTConnectionRequest(CpuType CPU)
{
byte[] bSend1 = {
3, 0, 0, 22, //TPKT
17, //COTP Header Length
224, //Connect Request
0, 0, //Destination Reference
0, 46, //Source Reference
0, //Flags
193, //Parameter Code (src-tasp)
2, //Parameter Length
1, 0, //Source TASP
194, //Parameter Code (dst-tasp)
2, //Parameter Length
3, 0, //Destination TASP
192, //Parameter Code (tpdu-size)
1, //Parameter Length
9 //TPDU Size (2^9 = 512)
};
switch (CPU)
{
case CpuType.S7200:
//S7200: Chr(193) & Chr(2) & Chr(16) & Chr(0) 'Eigener Tsap
bSend1[11] = 193;
bSend1[12] = 2;
bSend1[13] = 16;
bSend1[14] = 0;
//S7200: Chr(194) & Chr(2) & Chr(16) & Chr(0) 'Fremder Tsap
bSend1[15] = 194;
bSend1[16] = 2;
bSend1[17] = 16;
bSend1[18] = 0;
break;
case CpuType.S71200:
case CpuType.S7300:
//S7300: Chr(193) & Chr(2) & Chr(1) & Chr(0) 'Eigener Tsap
bSend1[11] = 193;
bSend1[12] = 2;
bSend1[13] = 1;
bSend1[14] = 0;
//S7300: Chr(194) & Chr(2) & Chr(3) & Chr(2) 'Fremder Tsap
bSend1[15] = 194;
bSend1[16] = 2;
bSend1[17] = 3;
bSend1[18] = (byte)(Rack * 2 * 16 + Slot);
break;
case CpuType.S7400:
//S7400: Chr(193) & Chr(2) & Chr(1) & Chr(0) 'Eigener Tsap
bSend1[11] = 193;
bSend1[12] = 2;
bSend1[13] = 1;
bSend1[14] = 0;
//S7400: Chr(194) & Chr(2) & Chr(3) & Chr(3) 'Fremder Tsap
bSend1[15] = 194;
bSend1[16] = 2;
bSend1[17] = 3;
bSend1[18] = (byte)(Rack * 2 * 16 + Slot);
break;
case CpuType.S71500:
// Eigener Tsap
bSend1[11] = 193;
bSend1[12] = 2;
bSend1[13] = 0x10;
bSend1[14] = 0x2;
// Fredmer Tsap
bSend1[15] = 194;
bSend1[16] = 2;
bSend1[17] = 0x3;
bSend1[18] = (byte)(Rack * 2 * 16 + Slot);
break;
default:
throw new Exception("Wrong CPU Type Secified");
}
return bSend1;
}
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, 80 //Try 1920 PDU Size. Same as libnodave.
};
}
}
}

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

@@ -0,0 +1,536 @@
using S7.Net.Types;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net.Sockets;
using System.Threading.Tasks;
namespace S7.Net
{
/// <summary>
/// Creates an instance of S7.Net driver
/// </summary>
public partial class Plc
{
/// <summary>
/// Open a <see cref="Socket"/> and connects to the PLC, sending all the corrected package
/// and returning if the connection was successful (<see cref="ErrorCode.NoError"/>) of it was wrong.
/// </summary>
/// <returns>Returns ErrorCode.NoError if the connection was successful, otherwise check the ErrorCode</returns>
public async Task OpenAsync()
{
await ConnectAsync();
await stream.WriteAsync(GetCOPTConnectionRequest(CPU), 0, 22);
var response = await COTP.TPDU.ReadAsync(stream);
if (response.PDUType != 0xd0) //Connect Confirm
{
throw new WrongNumberOfBytesException("Waiting for COTP connect confirm");
}
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");
}
MaxPDUSize = (short)(s7data[18] * 256 + s7data[19]);
}
private async Task ConnectAsync()
{
tcpClient = new TcpClient();
await tcpClient.ConnectAsync(IP, 102);
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.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="count">Byte count, if you want to read 120 bytes, set this to 120.</param>
/// <returns>Returns the bytes in an array</returns>
public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count)
{
List<byte> resultBytes = new List<byte>();
int index = startByteAdr;
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);
count -= maxToRead;
index += maxToRead;
}
return resultBytes.ToArray();
}
/// <summary>
/// Read and decode a certain number of bytes of the "VarType" provided.
/// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="varType">Type of the variable/s that you are reading</param>
/// <param name="bitAdr">Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter.</param>
/// <param name="varCount"></param>
public 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);
return ParseBytes(varType, bytes, varCount, bitAdr);
}
/// <summary>
/// Reads a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <returns>Returns an object that contains the value. This object must be cast accordingly.</returns>
public async Task<object> ReadAsync(string variable)
{
var adr = new PLCAddress(variable);
return await ReadAsync(adr.dataType, adr.DBNumber, adr.Address, adr.varType, 1, (byte)adr.BitNumber);
}
/// <summary>
/// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and return an object that can be casted to the struct.
/// </summary>
/// <param name="structType">Type of the struct to be readed (es.: TypeOf(MyStruct)).</param>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>Returns a struct that must be cast.</returns>
public async Task<object> ReadStructAsync(Type structType, int db, int startByteAdr = 0)
{
int numBytes = Types.Struct.GetStructSize(structType);
// now read the package
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes);
// and decode it
return Types.Struct.FromBytes(structType, resultBytes);
}
/// <summary>
/// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and returns the struct or null if nothing was read.
/// </summary>
/// <typeparam name="T">The struct type</typeparam>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>Returns a nulable struct. If nothing was read null will be returned.</returns>
public async Task<T?> ReadStructAsync<T>(int db, int startByteAdr = 0) where T : struct
{
return await ReadStructAsync(typeof(T), db, startByteAdr) as T?;
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary>
/// <param name="sourceClass">Instance of the class that will store the values</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>The number of read bytes</returns>
public async Task<Tuple<int, object>> ReadClassAsync(object sourceClass, int db, int startByteAdr = 0)
{
int numBytes = Class.GetClassSize(sourceClass);
if (numBytes <= 0)
{
throw new Exception("The size of the class is less than 1 byte and therefore cannot be read");
}
// now read the package
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes);
// and decode it
Class.FromBytes(sourceClass, resultBytes);
return new Tuple<int, object>(resultBytes.Length, sourceClass);
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic
/// type, the class needs a default constructor.
/// </summary>
/// <typeparam name="T">The class that will be instantiated. Requires a default constructor</typeparam>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <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
{
return await ReadClassAsync(() => Activator.CreateInstance<T>(), db, startByteAdr);
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary>
/// <typeparam name="T">The class that will be instantiated</typeparam>
/// <param name="classFactory">Function to instantiate the class</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public 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);
int readBytes = res.Item1;
if (readBytes <= 0)
{
return null;
}
return (T)res.Item2;
}
/// <summary>
/// Reads multiple vars in a single request.
/// You have to create and pass a list of DataItems and you obtain in response the same list with the values.
/// Values are stored in the property "Value" of the dataItem and are already converted.
/// If you don't want the conversion, just create a dataItem of bytes.
/// DataItems must not be more than 20 (protocol restriction) and bytes must not be more than 200 + 22 of header (protocol restriction).
/// </summary>
/// <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
try
{
// first create the header
int packageSize = 19 + (dataItems.Count * 12);
Types.ByteArray package = new ByteArray(packageSize);
package.Add(ReadHeaderPackage(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)));
}
await stream.WriteAsync(package.array, 0, package.array.Length);
var s7data = await COTP.TSDU.ReadAsync(stream); //TODO use Async
if (s7data == null || s7data[14] != 0xff)
throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString());
int offset = 18;
foreach (var dataItem in dataItems)
{
int byteCnt = VarTypeToByteLength(dataItem.VarType, dataItem.Count);
byte[] bytes = new byte[byteCnt];
for (int i = 0; i < byteCnt; i++)
{
bytes[i] = s7data[i + offset];
}
offset += byteCnt + 4;
dataItem.Value = ParseBytes(dataItem.VarType, bytes, dataItem.Count);
}
return dataItems;
}
catch (SocketException socketException)
{
LastErrorCode = ErrorCode.WriteData;
LastErrorString = socketException.Message;
}
catch (Exception exc)
{
LastErrorCode = ErrorCode.WriteData;
LastErrorString = exc.Message;
}
return null;
}
/// <summary>
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
/// If the write was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public async Task<ErrorCode> WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value)
{
int localIndex = 0;
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);
ErrorCode lastError = await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value.Skip(localIndex).Take(maxToWrite).ToArray());
if (lastError != ErrorCode.NoError)
{
return lastError;
}
count -= maxToWrite;
localIndex += maxToWrite;
}
return ErrorCode.NoError;
}
/// <summary>
/// Write a single bit from a DB with the specified index.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public async Task<ErrorCode> WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool value)
{
if (bitAdr < 0 || bitAdr > 7)
throw new InvalidAddressException(string.Format("Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr));
ErrorCode lastError = await WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, value);
if (lastError != ErrorCode.NoError)
{
return lastError;
}
return ErrorCode.NoError;
}
/// <summary>
/// Write a single bit from a DB with the specified index.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public async Task<ErrorCode> WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, int value)
{
if (value < 0 || value > 1)
throw new ArgumentException("Value must be 0 or 1", nameof(value));
return await WriteBitAsync(dataType, db, startByteAdr, bitAdr, value == 1);
}
/// <summary>
/// Takes in input an object and tries to parse it to an array of values. This can be used to write many data, all of the same type.
/// You must specify the memory area type, memory are address, byte start address and bytes count.
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public async Task<ErrorCode> WriteAsync(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1)
{
if (bitAdr != -1)
{
//Must be writing a bit value as bitAdr is specified
if (value is bool)
{
return await WriteBitAsync(dataType, db, startByteAdr, bitAdr, (bool)value);
}
else if (value is int intValue)
{
if (intValue < 0 || intValue > 7)
throw new ArgumentOutOfRangeException(string.Format("Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr), nameof(bitAdr));
return await WriteBitAsync(dataType, db, startByteAdr, bitAdr, intValue == 1);
}
throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value));
}
return await WriteBytesAsync(dataType, db, startByteAdr, GetPackage(value));
}
/// <summary>
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the write was not successful, check <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <param name="value">Value to be written to the PLC</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public async Task<ErrorCode> WriteAsync(string variable, object value)
{
var adr = new PLCAddress(variable);
return await WriteAsync(adr.dataType, adr.DBNumber, adr.Address, value, adr.BitNumber);
}
/// <summary>
/// Writes a C# struct to a DB in the PLC
/// </summary>
/// <param name="structValue">The struct to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public async Task<ErrorCode> WriteStructAsync(object structValue, int db, int startByteAdr = 0)
{
var bytes = Struct.ToBytes(structValue).ToList();
var errCode = await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray());
return errCode;
}
/// <summary>
/// Writes a C# class to a DB in the PLC
/// </summary>
/// <param name="classValue">The class to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public async Task<ErrorCode> WriteClassAsync(object classValue, int db, int startByteAdr = 0)
{
var bytes = Types.Class.ToBytes(classValue).ToList();
var errCode = await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray());
return errCode;
}
private async Task<byte[]> ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, int count)
{
byte[] bytes = new byte[count];
// first create the header
int packageSize = 31;
ByteArray package = new ByteArray(packageSize);
package.Add(ReadHeaderPackage());
// package.Add(0x02); // datenart
package.Add(CreateReadDataRequestPackage(dataType, db, startByteAdr, count));
await stream.WriteAsync(package.array, 0, package.array.Length);
var s7data = await COTP.TSDU.ReadAsync(stream);
if (s7data == null || s7data[14] != 0xff)
throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString());
for (int cnt = 0; cnt < count; cnt++)
bytes[cnt] = s7data[cnt + 18];
return bytes;
}
/// <summary>
/// Writes up to 200 bytes to the PLC and returns NoError if successful. You must specify the memory area type, memory are address, byte start address and bytes count.
/// If the write was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to 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>NoError if it was successful, or the error is specified</returns>
private async Task<ErrorCode> WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value)
{
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);
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);
var s7data = await COTP.TSDU.ReadAsync(stream);
if (s7data == null || s7data[14] != 0xff)
{
throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString());
}
return ErrorCode.NoError;
}
catch (Exception exc)
{
LastErrorCode = ErrorCode.WriteData;
LastErrorString = exc.Message;
return LastErrorCode;
}
}
private async Task<ErrorCode> WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue)
{
byte[] bReceive = new byte[513];
int varCount = 0;
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);
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);
var s7data = await COTP.TSDU.ReadAsync(stream);
if (s7data == null || s7data[14] != 0xff)
throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString());
return ErrorCode.NoError;
}
catch (Exception exc)
{
LastErrorCode = ErrorCode.WriteData;
LastErrorString = exc.Message;
return LastErrorCode;
}
}
}
}

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

@@ -0,0 +1,479 @@
using S7.Net.Types;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net.Sockets;
using System.Threading.Tasks;
//Implement obsolete synchronous methods here
namespace S7.Net
{
public partial class Plc
{
/// <summary>
/// Open a <see cref="Socket"/> and connects to the PLC, sending all the corrected package
/// and returning if the connection was successful (<see cref="ErrorCode.NoError"/>) of it was wrong.
/// </summary>
/// <returns>Returns ErrorCode.NoError if the connection was successful, otherwise check the ErrorCode</returns>
public ErrorCode Open()
{
if (Connect() != ErrorCode.NoError)
{
return LastErrorCode;
}
try
{
stream.Write(GetCOPTConnectionRequest(CPU), 0, 22);
var response = COTP.TPDU.Read(stream);
if (response.PDUType != 0xd0) //Connect Confirm
{
throw new WrongNumberOfBytesException("Waiting for COTP connect confirm");
}
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");
}
MaxPDUSize = (short)(s7data[18] * 256 + s7data[19]);
}
catch (Exception exc)
{
LastErrorCode = ErrorCode.ConnectionError;
LastErrorString = string.Format("Couldn't establish the connection to {0}.\nMessage: {1}", IP, exc.Message);
return ErrorCode.ConnectionError;
}
return ErrorCode.NoError;
}
private ErrorCode Connect()
{
try
{
tcpClient = new TcpClient();
tcpClient.Connect(IP, 102);
stream = tcpClient.GetStream();
}
catch (SocketException sex)
{
// see https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668(v=vs.85).aspx
if (sex.SocketErrorCode == SocketError.TimedOut)
{
LastErrorCode = ErrorCode.IPAddressNotAvailable;
}
else
{
LastErrorCode = ErrorCode.ConnectionError;
}
LastErrorString = sex.Message;
}
catch (Exception ex)
{
LastErrorCode = ErrorCode.ConnectionError;
LastErrorString = ex.Message;
}
return LastErrorCode;
}
/// <summary>
/// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="count">Byte count, if you want to read 120 bytes, set this to 120.</param>
/// <returns>Returns the bytes in an array</returns>
public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count)
{
List<byte> resultBytes = new List<byte>();
int index = startByteAdr;
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);
count -= maxToRead;
index += maxToRead;
}
return resultBytes.ToArray();
}
/// <summary>
/// Read and decode a certain number of bytes of the "VarType" provided.
/// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="varType">Type of the variable/s that you are reading</param>
/// <param name="bitAdr">Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter.</param>
/// <param name="varCount"></param>
public object Read(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0)
{
int cntBytes = VarTypeToByteLength(varType, varCount);
byte[] bytes = ReadBytes(dataType, db, startByteAdr, cntBytes);
return ParseBytes(varType, bytes, varCount, bitAdr);
}
/// <summary>
/// Reads a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <returns>Returns an object that contains the value. This object must be cast accordingly.</returns>
public object Read(string variable)
{
var adr = new PLCAddress(variable);
return Read(adr.dataType, adr.DBNumber, adr.Address, adr.varType, 1, (byte)adr.BitNumber);
}
/// <summary>
/// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and return an object that can be casted to the struct.
/// </summary>
/// <param name="structType">Type of the struct to be readed (es.: TypeOf(MyStruct)).</param>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>Returns a struct that must be cast.</returns>
public object ReadStruct(Type structType, int db, int startByteAdr = 0)
{
int numBytes = Struct.GetStructSize(structType);
// now read the package
var resultBytes = ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes);
// and decode it
return Struct.FromBytes(structType, resultBytes);
}
/// <summary>
/// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and returns the struct or null if nothing was read.
/// </summary>
/// <typeparam name="T">The struct type</typeparam>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>Returns a nullable struct. If nothing was read null will be returned.</returns>
public T? ReadStruct<T>(int db, int startByteAdr = 0) where T : struct
{
return ReadStruct(typeof(T), db, startByteAdr) as T?;
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary>
/// <param name="sourceClass">Instance of the class that will store the values</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>The number of read bytes</returns>
public int ReadClass(object sourceClass, int db, int startByteAdr = 0)
{
int numBytes = Class.GetClassSize(sourceClass);
if (numBytes <= 0)
{
throw new Exception("The size of the class is less than 1 byte and therefore cannot be read");
}
// now read the package
var resultBytes = ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes);
// and decode it
Class.FromBytes(sourceClass, resultBytes);
return resultBytes.Length;
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic
/// type, the class needs a default constructor.
/// </summary>
/// <typeparam name="T">The class that will be instantiated. Requires a default constructor</typeparam>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public T ReadClass<T>(int db, int startByteAdr = 0) where T : class
{
return ReadClass(() => Activator.CreateInstance<T>(), db, startByteAdr);
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary>
/// <typeparam name="T">The class that will be instantiated</typeparam>
/// <param name="classFactory">Function to instantiate the class</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public T ReadClass<T>(Func<T> classFactory, int db, int startByteAdr = 0) where T : class
{
var instance = classFactory();
int readBytes = ReadClass(instance, db, startByteAdr);
if (readBytes <= 0)
{
return null;
}
return instance;
}
/// <summary>
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
/// If the write was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public ErrorCode WriteBytes(DataType dataType, int db, int startByteAdr, byte[] value)
{
int localIndex = 0;
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);
ErrorCode lastError = WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value.Skip(localIndex).Take(maxToWrite).ToArray());
if (lastError != ErrorCode.NoError)
{
return lastError;
}
count -= maxToWrite;
localIndex += maxToWrite;
}
return ErrorCode.NoError;
}
/// <summary>
/// Write a single bit from a DB with the specified index.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public ErrorCode WriteBit(DataType dataType, int db, int startByteAdr, int bitAdr, bool value)
{
if (bitAdr < 0 || bitAdr > 7)
throw new InvalidAddressException(string.Format("Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr));
ErrorCode lastError = WriteBitWithASingleRequest(dataType, db, startByteAdr, bitAdr, value);
if (lastError != ErrorCode.NoError)
{
return lastError; }
return ErrorCode.NoError;
}
/// <summary>
/// Write a single bit from a DB with the specified index.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public ErrorCode WriteBit(DataType dataType, int db, int startByteAdr, int bitAdr, int value)
{
if (value < 0 || value > 1)
throw new ArgumentException("Value must be 0 or 1", nameof(value));
return WriteBit(dataType, db, startByteAdr, bitAdr, value == 1);
}
/// <summary>
/// Takes in input an object and tries to parse it to an array of values. This can be used to write many data, all of the same type.
/// You must specify the memory area type, memory are address, byte start address and bytes count.
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public ErrorCode Write(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1)
{
if (bitAdr != -1)
{
//Must be writing a bit value as bitAdr is specified
if (value is bool)
{
return WriteBit(dataType, db, startByteAdr, bitAdr, (bool)value);
}
else if (value is int intValue)
{
if (intValue < 0 || intValue > 7)
throw new ArgumentOutOfRangeException(string.Format("Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr), nameof(bitAdr));
return WriteBit(dataType, db, startByteAdr, bitAdr, intValue == 1);
}
throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value));
}
return WriteBytes(dataType, db, startByteAdr, GetPackage(value));
}
/// <summary>
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the write was not successful, check <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <param name="value">Value to be written to the PLC</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public ErrorCode Write(string variable, object value)
{
var adr = new PLCAddress(variable);
return Write(adr.dataType, adr.DBNumber, adr.Address, value, adr.BitNumber);
}
/// <summary>
/// Writes a C# struct to a DB in the PLC
/// </summary>
/// <param name="structValue">The struct to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public ErrorCode WriteStruct(object structValue, int db, int startByteAdr = 0)
{
return WriteStructAsync(structValue, db, startByteAdr).Result;
}
/// <summary>
/// Writes a C# class to a DB in the PLC
/// </summary>
/// <param name="classValue">The class to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
/// <returns>NoError if it was successful, or the error is specified</returns>
public ErrorCode WriteClass(object classValue, int db, int startByteAdr = 0)
{
return WriteClassAsync(classValue, db, startByteAdr).Result;
}
private byte[] ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, int count)
{
byte[] bytes = new byte[count];
try {
// first create the header
int packageSize = 31;
ByteArray package = new ByteArray(packageSize);
package.Add(ReadHeaderPackage());
// package.Add(0x02); // datenart
package.Add(CreateReadDataRequestPackage(dataType, db, startByteAdr, count));
stream.Write(package.array, 0, package.array.Length);
var s7data = COTP.TSDU.Read(stream);
if (s7data == null || s7data[14] != 0xff)
throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString());
for (int cnt = 0; cnt < count; cnt++)
bytes[cnt] = s7data[cnt + 18];
return bytes;
}
catch (SocketException socketException)
{
LastErrorCode = ErrorCode.WriteData;
LastErrorString = socketException.Message;
return null;
}
catch (Exception exc)
{
LastErrorCode = ErrorCode.WriteData;
LastErrorString = exc.Message;
return null;
}
}
private ErrorCode WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, byte[] value)
{
return WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr, value).Result;
}
private ErrorCode WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue)
{
return WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, bitValue).Result;
}
/// <summary>
/// Reads multiple vars in a single request.
/// You have to create and pass a list of DataItems and you obtain in response the same list with the values.
/// Values are stored in the property "Value" of the dataItem and are already converted.
/// If you don't want the conversion, just create a dataItem of bytes.
/// DataItems must not be more than 20 (protocol restriction) and bytes must not be more than 200 + 22 of header (protocol restriction).
/// </summary>
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read. Maximum 20 dataitems are accepted.</param>
[Obsolete("Use ReadMultipleVarsAsync. Note: different function signature")]
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
try
{
// first create the header
int packageSize = 19 + (dataItems.Count * 12);
Types.ByteArray package = new ByteArray(packageSize);
package.Add(ReadHeaderPackage(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)));
}
stream.Write(package.array, 0, package.array.Length);
var s7data = COTP.TSDU.Read(stream); //TODO use Async
if (s7data == null || s7data[14] != 0xff)
throw new Exception(ErrorCode.WrongNumberReceivedBytes.ToString());
int offset = 18;
foreach (var dataItem in dataItems)
{
int byteCnt = VarTypeToByteLength(dataItem.VarType, dataItem.Count);
byte[] bytes = new byte[byteCnt];
for (int i = 0; i < byteCnt; i++)
{
bytes[i] = s7data[i + offset];
}
offset += byteCnt + 4;
dataItem.Value = ParseBytes(dataItem.VarType, bytes, dataItem.Count);
}
}
catch (SocketException socketException)
{
LastErrorCode = ErrorCode.WriteData;
LastErrorString = socketException.Message;
}
catch (Exception exc)
{
LastErrorCode = ErrorCode.WriteData;
LastErrorString = exc.Message;
}
}
}
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<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>
@@ -10,7 +10,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>S7.Net</RootNamespace>
<AssemblyName>S7.Net</AssemblyName>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<FileUpgradeFlags>
</FileUpgradeFlags>
@@ -79,7 +79,11 @@
<Compile Include="Conversion.cs" />
<Compile Include="COTP.cs" />
<Compile Include="Enums.cs" />
<Compile Include="PLC.cs" />
<Compile Include="Plc.cs" />
<Compile Include="PlcAsynchronous.cs" />
<Compile Include="PLCExceptions.cs" />
<Compile Include="PlcHelpers.cs" />
<Compile Include="PlcSynchronous.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TPKT.cs" />
<Compile Include="Types\Bit.cs" />

View File

@@ -1,5 +1,7 @@
using System;
using System.Net.Sockets;
using System.IO;
using System.Threading.Tasks;
namespace S7.Net
{
@@ -15,14 +17,14 @@ namespace S7.Net
public byte[] Data;
/// <summary>
/// Reds a TPKT from the socket
/// Reads a TPKT from the socket
/// </summary>
/// <param name="socket">The socket to read from</param>
/// <returns>TPKT Instace</returns>
public static TPKT Read(Socket socket)
/// <param name="stream"></param>
/// <returns>TPKT Instance</returns>
public static TPKT Read(Stream stream)
{
var buf = new byte[4];
socket.Receive(buf, 4, SocketFlags.None);
stream.Read(buf, 0, 4);
var pkt = new TPKT
{
Version = buf[0],
@@ -32,7 +34,30 @@ namespace S7.Net
if (pkt.Length > 0)
{
pkt.Data = new byte[pkt.Length - 4];
socket.Receive(pkt.Data, pkt.Length - 4, SocketFlags.None);
stream.Read(pkt.Data, 0, pkt.Length - 4);
}
return pkt;
}
/// <summary>
/// Reads a TPKT from the socket Async
/// </summary>
/// <param name="stream">The socket to read from</param>
/// <returns>Task TPKT Instace</returns>
public static async Task<TPKT> ReadAsync(Stream stream)
{
var buf = new byte[4];
await stream.ReadAsync(buf, 0, 4);
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];
await stream.ReadAsync(pkt.Data, 0, pkt.Length - 4);
}
return pkt;
}