51 Commits

Author SHA1 Message Date
Michael Croes
f03ba93a96 Release S7NetPlus 0.8.0
Release highlights:
- Replace Double type with LReal
- Fixes for PDU size assertions
- Don't embed sources
- Fixes for StringEx
- Rename StringEx to S7String
- Possible null reference fixes
- Fixes for compiler warnings
- Preparations for async method cancellation
- Extended response code validation
- Structural changes, minor improvements
2021-01-04 20:44:16 +01:00
Michael Croes
9cd63d906f Merge pull request #338 from scamille/fixOpen
Fix OpenAsync function to rethrow exception during connection establish.
2020-10-17 20:04:57 +02:00
Serge Camille
d051b93bdc Fix OpenAsync function to rethrow exception during connection establish.
This bug was introduced in 106e9912
2020-10-17 17:54:30 +02:00
Michael Croes
c9bab7523a Merge pull request #317 from scamille/read-request-header
Unify async read request header building.
2020-10-14 21:14:17 +02:00
Serge Camille
a7608e3cb7 Rename and Move DataRequestItem 2020-09-26 21:28:31 +02:00
Serge Camille
4d37679c75 Move BuildReadRequestPackage to PlcHelpers, cleanup. 2020-09-26 21:21:37 +02:00
Serge Camille
e2a0ed548d Unify async read request header building.
Add concept of DataRequestItem which contains the necessary data to build the request.
2020-09-26 21:21:37 +02:00
Michael Croes
4124bae1bc Merge pull request #314 from scamille/cleanupOpen
Cleanup OpenAsync function.
2020-09-21 23:09:07 +02:00
Michael Croes
2bcc5e6b9c Merge branch 'develop' into cleanupOpen 2020-09-21 23:06:06 +02:00
Serge Camille
33981ab4f9 Use int for MaxPduSize and parse communication response for it as uint16 2020-09-16 23:09:23 +02:00
Serge Camille
de60a7b6b0 Add .ConfigureAwait(false) throughout OpenAsync callstack. 2020-09-16 22:38:05 +02:00
Michael Croes
de87409458 Merge pull request #321 from FalcoGoodbody/patch-1
Remove outdated documentation link.
2020-09-16 22:32:55 +02:00
Serge Camille
ca89736c7c Use GetAwaiter().GetResult(); 2020-09-16 22:31:51 +02:00
Serge Camille
4a72c3596b Revert MaxPDUSize parse change. 2020-09-16 22:28:42 +02:00
Michael Croes
786e012179 Merge branch 'develop' into patch-1 2020-09-16 22:07:15 +02:00
Michael Croes
f46833606f Merge pull request #316 from scamille/responseValidation
Add more extensive response code validation.
2020-09-16 20:53:46 +02:00
Serge Camille
b95b71e9aa Remove unnecessary string interpolation in ValidateResponseCode 2020-09-16 20:26:53 +02:00
Serge Camille
1069641606 Add more extensive response code validation.
Fixes #310
2020-09-16 20:25:20 +02:00
Michael Croes
36a9ecb2c8 Merge pull request #315 from scamille/stringSerialization
Rename StringEx to S7String.
2020-09-16 20:15:11 +02:00
Michael Croes
243e868488 Merge branch 'develop' into stringSerialization 2020-09-16 20:13:19 +02:00
Michael Croes
ff65f06b7d Merge pull request #318 from scamille/netcoretests
Linux timeout fix & Multi-Platform Unit Test support
2020-09-16 20:12:48 +02:00
FalcoGoodbody
065b1fbdf8 Update README.md
delete the link to PDF documentation because it is outdated. The actual docu can be found in the wiki
2020-09-15 09:19:51 +02:00
Serge Camille
8f3c701a2f Revert UnitTest IP address to 127.0.0.1.
Seems there are sometimes performance regressions when using localhost. Might be related to IPv6, who know.

Fix some TestContext not being public.
2020-09-13 10:28:06 +02:00
Serge Camille
023530322e Remove Port 102 dependencies from Unit Test.
- Adjust readme.
- It is no longer be necessary to shut down service s7oiehsx64.
2020-09-13 10:15:53 +02:00
Serge Camille
9198fc1686 Test project: Enable netcore3.1 testing, switch win64 Snap7 and use custom port.
- Add netcoreapp3.1 target framework, allowing this to run on linux and macos as well.
- Switch windows snap7 DLL to 64bit version 1.4.2. This also improves UnitTest stability (reduces false positives) on the CI (including appveyor)
- Changing the port used for S7NetTests when communicating with Snap7 to a value > 1000 allows tests to run on Linux without elevated privileges.
2020-09-13 10:01:38 +02:00
Serge Camille
12a2e3c0b1 Fix runtime error on linux with default Socket timeouts.
The default value for socket timeout API is 0 and not -1, so revert to that value to give the intended infinite timeout on all platforms.
2020-09-13 09:57:13 +02:00
Serge Camille
b088fe276b Rename StringEx to S7String.
This name is already somewhat used in code and gives this a better name.
2020-09-12 19:27:15 +02:00
Serge Camille
80ad95372b Use Stream.Dispose() for NetStandard 1.3 2020-09-12 11:19:08 +02:00
Serge Camille
106e9912ab Cleanup OpenAsync function.
- Separate into Connect and EstablishConnection step.
- Remove redundant null checks for returned data.
-  Only assing PLC stream object once we fully established a connection, and Close otherwise.
- Replace sync implementation with Async call.
2020-09-12 11:14:41 +02:00
Michael Croes
af39659944 Merge pull request #307 from scamille/fb-asyncCancellationTokens
Add CancellationToken support to async functions.
2020-09-10 23:50:41 +02:00
Serge Camille
e5bdb10ce3 Add cancellation UnitTest.
This is very fragile with time-dependent cancellation, but I don't know of a better way without messing with the code to be tested.

Seems to only work when the test is run as a single run, not when running the whole test suit.
2020-09-09 21:24:39 +02:00
Serge Camille
faea428e4c Try not to replace OperationCanceledException with PlcException. 2020-09-09 21:24:39 +02:00
Serge Camille
4da76c7f6d Add missing cancellationToken to Stream.WriteAsync call. 2020-09-09 21:24:39 +02:00
Serge Camille
6c4d4605f0 Add CancellationToken support to async functions. 2020-09-09 21:24:36 +02:00
Michael Croes
730ccbf9fc Merge pull request #308 from scamille/fb-fixWarnings
Fix some null deref warnings.
2020-09-09 21:03:25 +02:00
Michael Croes
b36c4a98ec Merge branch 'develop' into fb-fixWarnings 2020-09-09 21:01:54 +02:00
Michael Croes
70c3f8e996 Merge pull request #309 from scamille/fb-varHints
Apply some of the hints that the compiler is giving.
2020-09-09 20:42:33 +02:00
Michael Croes
cf1b71220a Merge branch 'develop' into fb-varHints 2020-09-09 20:40:09 +02:00
Michael Croes
88c45bd995 Merge pull request #292 from scamille/fb-StringEx
Adjust StringEx ToByteArray.
2020-09-08 21:59:28 +02:00
Serge Camille
cf493d47f0 Apply some of the hints that the compiler is giving. 2020-09-06 15:04:20 +02:00
Serge Camille
9e2f17fdf3 Fix some null deref warnings. 2020-09-06 15:00:08 +02:00
Serge Camille
1919b0083a StringEx more strict malformed checks when parsing byte array.
- min length of 2.
- capacity >= length
2020-09-04 22:33:54 +02:00
Serge Camille
fd4bc0fe84 Adjust StringEx ToByteArray.
- Do not allow null string to be passed, raise ArgumentNullException.
- Do not allow string whose ASCII representation is longer than the reserved length, since this currently leads to silent data loss.
- Always write the full binary data length of 2 + reservedLength, since that is what the binary representation of that string is in S7 memory, even if some tail bytes are unused by the current string.

I also suspect that S7WriteMultiple would have chocked on that last bit, but I am not sure. There aren't any tests for writing multiple Dataitems right now.

Adjust tests accordingly. Mostly add some tail bytes where necessary, and assert on exceptions where this is now required.
2020-09-04 22:33:54 +02:00
Michael Croes
9ff73ff3f7 Merge pull request #303 from mycroes/remove-embed-sources
Remove EmbedAllSources from S7.Net
2020-09-04 22:10:52 +02:00
Michael Croes
c31353bed2 Remove EmbedAllSources from S7.Net
Sources are already provided via SourceLink, no need to embed them as
well.
2020-09-04 22:06:58 +02:00
Michael Croes
b16097092b Merge pull request #290 from scamille/fb-AdjustDataItemsPduCheck
Adjust AssertPduSize checks for Reading multiple DataItems.
2020-09-04 21:39:55 +02:00
Serge Camille
eca2ed6474 Add required request & response sizes to PDU limit exception messages. 2020-09-04 21:06:06 +02:00
Serge Camille
b92242f911 Adjust AssertPduSize checks for Reading multiple DataItems.
The limit calculations did not match what the send and parsing code expected.

sending request header seems to be 19 byte in general.

Also adjust XML comments somewhat, since max PDU really differs a lot between PLC types, from 240 to 960 afaik.
2020-09-04 21:06:06 +02:00
Michael Croes
c99c3d745a Merge pull request #298 from scamille/fb-real
Add LReal support, kill old "Double" sham.
2020-09-04 21:03:00 +02:00
Serge Camille
64c781ec8b Remove unnecessary includes and commented-out test. 2020-09-04 20:46:16 +02:00
Serge Camille
e8a9983367 Add LReal support, kill old "Double" sham.
- Adds true support for 64bit double / LReal datatype.
- Set old Types.Single and Types.Double to obselete. Both class names use .NET types instead of S7 type names, contrary to all other types.
- Remove already obsoleted conversion from DWord to Real. Why is this even necessary?
  For users caring about converting from DWord, they can still convert to single. But unless we get LWord support, there won't be a direct conversion to double/LReal.
- Adjust unit tests by removing rounding, testing directly double read/writes.

There is quite a bit of breaking changes at least in the automated Struct and object functions which automatically translate .NET types to appropriate S7 types.
My consideration was that if we ever want to support 64bit types, there is no way against breaking those existing incorrect conversions from 64bit .NET double to 32 bit S7 Real variables.
2020-09-04 20:46:16 +02:00
39 changed files with 923 additions and 632 deletions

View File

@@ -12,8 +12,6 @@ to my request for committing code, I decided to pick up where he left off here o
## Documentation
Check the Wiki and feel free to edit it: https://github.com/killnine/s7netplus/wiki
S7.Net Plus has a [User Manual](https://github.com/killnine/s7netplus/blob/master/Documentation/Documentation.pdf), check it out.
## Supported PLC
+ Compatible S7 PLC (S7-200, S7-300, S7-400, S7-1200, S7-1500)
@@ -36,6 +34,5 @@ PM> Install-Package S7netplus
## Running the tests
Unit tests use Snap7 server, so port 102 must be not in use.
If you have Siemens Step7 installed, the service s7oiehsx64 is stopped when running unit tests.
You have to restart the service manually if you need it.
Unit tests use Snap7 server.
On Windows, the DLL is included with the test project. On other platforms, Snap7 must be installed manually before running tests.

View File

@@ -29,7 +29,7 @@ namespace S7.Net.UnitTest.Helpers
Console.WriteLine(Server.EventText(ref Event));
}
public static void Start()
public static void Start(short port)
{
Server = new S7Server();
// Share some resources with our virtual PLC
@@ -59,7 +59,14 @@ namespace S7.Net.UnitTest.Helpers
// Start the server onto the default adapter.
// To select an adapter we have to use Server->StartTo("192.168.x.y").
// Start() is the same of StartTo("0.0.0.0");
Server.SetParam(S7Consts.p_u16_LocalPort, ref port);
int Error = Server.Start();
if (Error != 0)
{
throw new Exception($"Error starting Snap7 server: {Server.ErrorText(Error)}");
}
//if (Error == 0)
//{
// // Now the server is running ... wait a key to terminate

View File

@@ -35,12 +35,12 @@ namespace S7.Net.UnitTest.Helpers
/// <summary>
/// DB1.DBD4
/// </summary>
public double RealVariableDouble { get; set; }
public double LRealVariable { get; set; }
/// <summary>
/// DB1.DBD8
/// </summary>
public float RealVariableFloat { get; set; }
public float RealVariable { get; set; }
/// <summary>
/// DB1.DBD12

View File

@@ -35,12 +35,12 @@ namespace S7.Net.UnitTest.Helpers
/// <summary>
/// DB1.DBD4
/// </summary>
public double RealVariableDouble;
public double LRealVariable;
/// <summary>
/// DB1.DBD8
/// </summary>
public float RealVariableFloat;
public float RealVariable;
/// <summary>
/// DB1.DBD12

View File

@@ -7,21 +7,25 @@ using S7.Net;
using System.IO;
using System.Threading.Tasks;
using S7.Net.Protocol;
using System.Collections;
namespace S7.Net.UnitTest
{
[TestClass]
public class ProtocolUnitTest
{
public TestContext TestContext { get; set; }
[TestMethod]
public void TPKT_Read()
public async Task TPKT_Read()
{
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd"));
var t = TPKT.Read(m);
Assert.AreEqual(0x03, t.Version);
Assert.AreEqual(0x29, t.Length);
m.Position = 0;
t = TPKT.ReadAsync(m).Result;
t = await TPKT.ReadAsync(m, TestContext.CancellationTokenSource.Token);
Assert.AreEqual(0x03, t.Version);
Assert.AreEqual(0x29, t.Length);
}
@@ -40,7 +44,7 @@ namespace S7.Net.UnitTest
public async Task TPKT_ReadShortAsync()
{
var m = new MemoryStream(StringToByteArray("0300002902f0803203000000010002001400000401ff040080"));
var t = await TPKT.ReadAsync(m);
var t = await TPKT.ReadAsync(m, TestContext.CancellationTokenSource.Token);
}
[TestMethod]
@@ -51,7 +55,7 @@ namespace S7.Net.UnitTest
var t = COTP.TSDU.Read(m);
Assert.IsTrue(expected.SequenceEqual(t));
m.Position = 0;
t = COTP.TSDU.ReadAsync(m).Result;
t = COTP.TSDU.ReadAsync(m, TestContext.CancellationTokenSource.Token).Result;
Assert.IsTrue(expected.SequenceEqual(t));
}
@@ -62,6 +66,32 @@ namespace S7.Net.UnitTest
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
[TestMethod]
public void TestResponseCode()
{
var expected = StringToByteArray("320700000400000800080001120411440100ff09000400000000");
var m = new MemoryStream(StringToByteArray("0300000702f0000300000702f0000300002102f080320700000400000800080001120411440100ff09000400000000"));
var t = COTP.TSDU.Read(m);
Assert.IsTrue(expected.SequenceEqual(t));
// Test all possible byte values. Everything except 0xff should throw an exception.
var testData = Enumerable.Range(0, 256).Select(i => new { StatusCode = (ReadWriteErrorCode)i, ThrowsException = i != (byte)ReadWriteErrorCode.Success });
foreach (var entry in testData)
{
if (entry.ThrowsException)
{
Assert.ThrowsException<Exception>(() => Plc.ValidateResponseCode(entry.StatusCode));
}
else
{
Plc.ValidateResponseCode(entry.StatusCode);
}
}
}
}
}

View File

@@ -1,15 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net452</TargetFrameworks>
<TargetFrameworks>net452;netcoreapp3.1</TargetFrameworks>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>Properties\S7.Net.snk</AssemblyOriginatorKeyFile>
<IsPackable>false</IsPackable>
<Copyright>Copyright © 2014</Copyright>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitHubActionsTestLogger" Version="1.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
@@ -24,13 +26,8 @@
</ItemGroup>
<ItemGroup>
<Reference Include="System.ServiceProcess" />
</ItemGroup>
<ItemGroup>
<None Update="snap7.dll">
<None Update="runtimes\win-x64\native\snap7.dll" Link="snap7.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -1,15 +1,12 @@
#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;
using System.Threading;
#endregion
@@ -101,20 +98,13 @@ namespace S7.Net.UnitTest
/// <summary>
/// Read/Write a single REAL with a single request.
/// Test that writing a double and reading it gives the correct value.
/// Test that writing a float 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.68729;
await plc.WriteAsync("DB1.DBD40", val.ConvertToUInt());
double result = ((uint)await plc.ReadAsync("DB1.DBD40")).ConvertToDouble();
Assert.AreEqual(val, Math.Round(result, 5)); // float lose precision, so i need to round it
// Reading and writing a float is quite complicated, because it needs to be converted to DWord before the write,
// then reconvert to float after the read. Float values can contain only 7 digits, so no precision is lost.
float val2 = 1234567;
@@ -162,8 +152,8 @@ namespace S7.Net.UnitTest
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariableDouble = -154.789,
RealVariableFloat = -154.789f,
LRealVariable = -154.789,
RealVariable = -154.789f,
DWordVariable = 850
};
@@ -175,8 +165,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
Assert.AreEqual(tc.RealVariableDouble, Math.Round(tc2.RealVariableDouble, 3));
Assert.AreEqual(tc.RealVariableFloat, tc2.RealVariableFloat);
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable);
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
}
@@ -219,8 +209,8 @@ namespace S7.Net.UnitTest
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariableDouble = -154.789,
RealVariableFloat = -154.789f,
LRealVariable = -154.789,
RealVariable = -154.789f,
DWordVariable = 850
};
plc.WriteStruct(tc, DB2);
@@ -230,8 +220,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
Assert.AreEqual(tc.RealVariableDouble, Math.Round(tc2.RealVariableDouble, 3));
Assert.AreEqual(tc.RealVariableFloat, tc2.RealVariableFloat);
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable);
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
}
@@ -584,8 +574,8 @@ namespace S7.Net.UnitTest
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariableDouble = -154.789,
RealVariableFloat = -154.789f,
LRealVariable = -154.789,
RealVariable = -154.789f,
DWordVariable = 850
};
@@ -599,8 +589,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
Assert.AreEqual(tc.RealVariableDouble, tc2.RealVariableDouble, 0.1);
Assert.AreEqual(tc.RealVariableFloat, tc2.RealVariableFloat);
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable, 0.1);
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
Assert.AreEqual(TestClassWithPrivateSetters.PRIVATE_SETTER_VALUE, tc2.PrivateSetterProperty);
@@ -632,8 +622,8 @@ namespace S7.Net.UnitTest
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariableDouble = -154.789,
RealVariableFloat = -154.789f,
LRealVariable = -154.789,
RealVariable = -154.789f,
DWordVariable = 850
};
@@ -649,8 +639,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(tc2.BitVariable10, tc2Generic.BitVariable10);
Assert.AreEqual(tc2.DIntVariable, tc2Generic.DIntVariable);
Assert.AreEqual(tc2.IntVariable, tc2Generic.IntVariable);
Assert.AreEqual(Math.Round(tc2.RealVariableDouble, 3), Math.Round(tc2Generic.RealVariableDouble, 3));
Assert.AreEqual(tc2.RealVariableFloat, tc2Generic.RealVariableFloat);
Assert.AreEqual(Math.Round(tc2.LRealVariable, 3), Math.Round(tc2Generic.LRealVariable, 3));
Assert.AreEqual(tc2.RealVariable, tc2Generic.RealVariable);
Assert.AreEqual(tc2.DWordVariable, tc2Generic.DWordVariable);
}
@@ -675,8 +665,8 @@ namespace S7.Net.UnitTest
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariableDouble = -154.789,
RealVariableFloat = -154.789f,
LRealVariable = -154.789,
RealVariable = -154.789f,
DWordVariable = 850
};
@@ -689,8 +679,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(tc2Generic.BitVariable00, tc2GenericWithClassFactory.BitVariable00);
Assert.AreEqual(tc2Generic.BitVariable10, tc2GenericWithClassFactory.BitVariable10);
Assert.AreEqual(tc2Generic.DIntVariable, tc2GenericWithClassFactory.DIntVariable);
Assert.AreEqual(Math.Round(tc2Generic.RealVariableDouble, 3), Math.Round(tc2GenericWithClassFactory.RealVariableDouble, 3));
Assert.AreEqual(tc2Generic.RealVariableFloat, tc2GenericWithClassFactory.RealVariableFloat);
Assert.AreEqual(Math.Round(tc2Generic.LRealVariable, 3), Math.Round(tc2GenericWithClassFactory.LRealVariable, 3));
Assert.AreEqual(tc2Generic.RealVariable, tc2GenericWithClassFactory.RealVariable);
Assert.AreEqual(tc2Generic.DWordVariable, tc2GenericWithClassFactory.DWordVariable);
}
@@ -747,8 +737,8 @@ namespace S7.Net.UnitTest
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariableDouble = -154.789,
RealVariableFloat = -154.789f,
LRealVariable = -154.789,
RealVariable = -154.789f,
DWordVariable = 850
};
@@ -763,8 +753,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(ts2.BitVariable10, ts2Generic.BitVariable10);
Assert.AreEqual(ts2.DIntVariable, ts2Generic.DIntVariable);
Assert.AreEqual(ts2.IntVariable, ts2Generic.IntVariable);
Assert.AreEqual(Math.Round(ts2.RealVariableDouble, 3), Math.Round(ts2Generic.RealVariableDouble, 3));
Assert.AreEqual(ts2.RealVariableFloat, ts2Generic.RealVariableFloat);
Assert.AreEqual(ts2.LRealVariable, ts2Generic.LRealVariable);
Assert.AreEqual(ts2.RealVariable, ts2Generic.RealVariable);
Assert.AreEqual(ts2.DWordVariable, ts2Generic.DWordVariable);
}
@@ -792,8 +782,8 @@ namespace S7.Net.UnitTest
BitVariable10 = true,
DIntVariable = -100000,
IntVariable = -15000,
RealVariableDouble = -154.789,
RealVariableFloat = -154.789f,
LRealVariable = -154.789,
RealVariable = -154.789f,
DWordVariable = 850
};
plc.WriteClass(tc, DB2);
@@ -883,17 +873,6 @@ namespace S7.Net.UnitTest
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);
var helper = await plc.ReadAsync("DB1.DBD0");
double test_value2 = Conversion.ConvertToDouble((uint)helper);
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_ReadWriteSingle()
{
@@ -926,6 +905,42 @@ namespace S7.Net.UnitTest
Assert.AreEqual(x % 256, res[x], string.Format("Bit {0} failed", x));
}
}
/// <summary>
/// Write a large amount of data and test cancellation
/// </summary>
[TestMethod]
public async Task Test_Async_WriteLargeByteArrayWithCancellation()
{
Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor.");
var cancellationSource = new CancellationTokenSource();
var cancellationToken = cancellationSource.Token;
var randomEngine = new Random();
var data = new byte[8192];
var db = 2;
randomEngine.NextBytes(data);
cancellationSource.CancelAfter(TimeSpan.FromMilliseconds(5));
try
{
await plc.WriteBytesAsync(DataType.DataBlock, db, 0, data, cancellationToken);
}
catch(TaskCanceledException)
{
// everything is good, that is the exception we expect
Console.WriteLine("Task was cancelled as expected.");
return;
}
catch(Exception e)
{
Assert.Fail($"Wrong exception type received. Expected {typeof(TaskCanceledException)}, received {e.GetType()}.");
}
// Depending on how tests run, this can also just succeed without getting cancelled at all. Do nothing in this case.
Console.WriteLine("Task was not cancelled as expected.");
}
#endregion
}
}

View File

@@ -1,12 +1,8 @@
#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;
@@ -41,6 +37,8 @@ namespace S7.Net.UnitTest
#region Constants
const int DB2 = 2;
const int DB4 = 4;
const short TestServerPort = 31122;
const string TestServerIp = "127.0.0.1";
#endregion
#region Private fields
@@ -53,16 +51,19 @@ namespace S7.Net.UnitTest
/// </summary>
public S7NetTests()
{
plc = new Plc(CpuType.S7300, "127.0.0.1", 0, 2);
//ConsoleManager.Show();
ShutDownServiceS7oiehsx64();
plc = CreatePlc();
}
private static Plc CreatePlc()
{
return new Plc(CpuType.S7300, TestServerIp, TestServerPort, 0, 2);
}
[TestInitialize]
public void Setup()
{
S7TestServer.Start();
S7TestServer.Start(TestServerPort);
plc.Open();
}
@@ -146,20 +147,13 @@ namespace S7.Net.UnitTest
/// <summary>
/// Read/Write a single REAL with a single request.
/// Test that writing a double and reading it gives the correct value.
/// Test that writing a float and reading it gives the correct value.
/// </summary>
[TestMethod]
public void T03_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.68729;
plc.Write("DB1.DBD40", val.ConvertToUInt());
double result = ((uint)plc.Read("DB1.DBD40")).ConvertToDouble();
Assert.AreEqual(val, Math.Round(result, 5)); // float lose precision, so i need to round it
// Reading and writing a float is quite complicated, because it needs to be converted to DWord before the write,
// then reconvert to float after the read. Float values can contain only 7 digits, so no precision is lost.
float val2 = 1234567;
@@ -186,8 +180,8 @@ namespace S7.Net.UnitTest
tc.BitVariable10 = true;
tc.DIntVariable = -100000;
tc.IntVariable = -15000;
tc.RealVariableDouble = -154.789;
tc.RealVariableFloat = -154.789f;
tc.LRealVariable = -154.789;
tc.RealVariable = -154.789f;
tc.DWordVariable = 850;
plc.WriteClass(tc, DB2);
TestClass tc2 = new TestClass();
@@ -197,8 +191,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
Assert.AreEqual(Math.Round(tc.RealVariableDouble, 3), Math.Round(tc2.RealVariableDouble, 3));
Assert.AreEqual(tc.RealVariableFloat, tc2.RealVariableFloat);
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable);
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
}
@@ -215,8 +209,8 @@ namespace S7.Net.UnitTest
tc.BitVariable10 = true;
tc.DIntVariable = -100000;
tc.IntVariable = -15000;
tc.RealVariableDouble = -154.789;
tc.RealVariableFloat = -154.789f;
tc.LRealVariable = -154.789;
tc.RealVariable = -154.789f;
tc.DWordVariable = 850;
plc.WriteStruct(tc, DB2);
// Values that are read from a struct are stored in a new struct, returned by the funcion ReadStruct
@@ -225,8 +219,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
Assert.AreEqual(tc.RealVariableDouble, Math.Round(tc2.RealVariableDouble, 3));
Assert.AreEqual(tc.RealVariableFloat, tc2.RealVariableFloat);
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable);
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
}
@@ -575,8 +569,8 @@ namespace S7.Net.UnitTest
tc.BitVariable10 = true;
tc.DIntVariable = -100000;
tc.IntVariable = -15000;
tc.RealVariableDouble = -154.789;
tc.RealVariableFloat = -154.789f;
tc.LRealVariable = -154.789;
tc.RealVariable = -154.789f;
tc.DWordVariable = 850;
plc.WriteClass(tc, DB2);
@@ -588,8 +582,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(tc.BitVariable10, tc2.BitVariable10);
Assert.AreEqual(tc.DIntVariable, tc2.DIntVariable);
Assert.AreEqual(tc.IntVariable, tc2.IntVariable);
Assert.AreEqual(Math.Round(tc.RealVariableDouble, 3), Math.Round(tc2.RealVariableDouble, 3));
Assert.AreEqual(tc.RealVariableFloat, tc2.RealVariableFloat);
Assert.AreEqual(tc.LRealVariable, tc2.LRealVariable);
Assert.AreEqual(tc.RealVariable, tc2.RealVariable);
Assert.AreEqual(tc.DWordVariable, tc2.DWordVariable);
Assert.AreEqual(TestClassWithPrivateSetters.PRIVATE_SETTER_VALUE, tc2.PrivateSetterProperty);
@@ -620,8 +614,8 @@ namespace S7.Net.UnitTest
tc.BitVariable10 = true;
tc.DIntVariable = -100000;
tc.IntVariable = -15000;
tc.RealVariableDouble = -154.789;
tc.RealVariableFloat = -154.789f;
tc.LRealVariable = -154.789;
tc.RealVariable = -154.789f;
tc.DWordVariable = 850;
plc.WriteClass(tc, DB2);
@@ -635,8 +629,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(tc2.BitVariable10, tc2Generic.BitVariable10);
Assert.AreEqual(tc2.DIntVariable, tc2Generic.DIntVariable);
Assert.AreEqual(tc2.IntVariable, tc2Generic.IntVariable);
Assert.AreEqual(Math.Round(tc2.RealVariableDouble, 3), Math.Round(tc2Generic.RealVariableDouble, 3));
Assert.AreEqual(tc2.RealVariableFloat, tc2Generic.RealVariableFloat);
Assert.AreEqual(Math.Round(tc2.LRealVariable, 3), Math.Round(tc2Generic.LRealVariable, 3));
Assert.AreEqual(tc2.RealVariable, tc2Generic.RealVariable);
Assert.AreEqual(tc2.DWordVariable, tc2Generic.DWordVariable);
}
@@ -663,8 +657,8 @@ namespace S7.Net.UnitTest
tc.BitVariable10 = true;
tc.DIntVariable = -100000;
tc.IntVariable = -15000;
tc.RealVariableDouble = -154.789;
tc.RealVariableFloat = -154.789f;
tc.LRealVariable = -154.789;
tc.RealVariable = -154.789f;
tc.DWordVariable = 850;
plc.WriteClass(tc, DB2);
@@ -677,8 +671,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(tc2Generic.BitVariable10, tc2GenericWithClassFactory.BitVariable10);
Assert.AreEqual(tc2Generic.DIntVariable, tc2GenericWithClassFactory.DIntVariable);
Assert.AreEqual(tc2Generic.IntVariable, tc2GenericWithClassFactory.IntVariable);
Assert.AreEqual(Math.Round(tc2Generic.RealVariableDouble, 3), Math.Round(tc2GenericWithClassFactory.RealVariableDouble, 3));
Assert.AreEqual(tc2Generic.RealVariableFloat, tc2GenericWithClassFactory.RealVariableFloat);
Assert.AreEqual(Math.Round(tc2Generic.LRealVariable, 3), Math.Round(tc2GenericWithClassFactory.LRealVariable, 3));
Assert.AreEqual(tc2Generic.RealVariable, tc2GenericWithClassFactory.RealVariable);
Assert.AreEqual(tc2Generic.DWordVariable, tc2GenericWithClassFactory.DWordVariable);
}
@@ -786,8 +780,8 @@ namespace S7.Net.UnitTest
ts.BitVariable10 = true;
ts.DIntVariable = -100000;
ts.IntVariable = -15000;
ts.RealVariableDouble = -154.789;
ts.RealVariableFloat = -154.789f;
ts.LRealVariable = -154.789;
ts.RealVariable = -154.789f;
ts.DWordVariable = 850;
plc.WriteStruct(ts, DB2);
@@ -800,8 +794,8 @@ namespace S7.Net.UnitTest
Assert.AreEqual(ts2.BitVariable10, ts2Generic.BitVariable10);
Assert.AreEqual(ts2.DIntVariable, ts2Generic.DIntVariable);
Assert.AreEqual(ts2.IntVariable, ts2Generic.IntVariable);
Assert.AreEqual(Math.Round(ts2.RealVariableDouble, 3), Math.Round(ts2Generic.RealVariableDouble, 3));
Assert.AreEqual(ts2.RealVariableFloat, ts2Generic.RealVariableFloat);
Assert.AreEqual(ts2.LRealVariable, ts2Generic.LRealVariable);
Assert.AreEqual(ts2.RealVariable, ts2Generic.RealVariable);
Assert.AreEqual(ts2.DWordVariable, ts2Generic.DWordVariable);
}
@@ -831,8 +825,8 @@ namespace S7.Net.UnitTest
tc.BitVariable10 = true;
tc.DIntVariable = -100000;
tc.IntVariable = -15000;
tc.RealVariableDouble = -154.789;
tc.RealVariableFloat = -154.789f;
tc.LRealVariable = -154.789;
tc.RealVariable = -154.789f;
tc.DWordVariable = 850;
plc.WriteClass(tc, DB2);
@@ -938,9 +932,9 @@ namespace S7.Net.UnitTest
{
plc.Close();
S7TestServer.Stop();
S7TestServer.Start();
S7TestServer.Start(TestServerPort);
var reachablePlc = new Plc(CpuType.S7300, "127.0.0.1", 0, 2);
var reachablePlc = CreatePlc();
Assert.IsTrue(reachablePlc.IsAvailable);
}
@@ -948,11 +942,10 @@ namespace S7.Net.UnitTest
public void T26_ReadWriteDouble()
{
double test_value = 55.66;
plc.Write("DB1.DBD0", test_value);
var helper = plc.Read("DB1.DBD0");
double test_value2 = Conversion.ConvertToDouble((uint)helper);
plc.Write(DataType.DataBlock, 1, 0, test_value);
var result = (double)plc.Read(DataType.DataBlock, 1, 0, VarType.LReal, 1);
Assert.AreEqual(test_value, test_value2, 0.01, "Compare Write/Read"); //Need delta here because S7 only has 32 bit reals
Assert.AreEqual(test_value, result, "Compare Write/Read");
}
[TestMethod]
@@ -1034,18 +1027,6 @@ namespace S7.Net.UnitTest
#endregion
#region Private methods
private static void ShutDownServiceS7oiehsx64()
{
ServiceController[] services = ServiceController.GetServices();
var service = services.FirstOrDefault(s => s.ServiceName == "s7oiehsx64");
if (service != null)
{
if (service.Status == ServiceControllerStatus.Running)
{
service.Stop();
}
}
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls

View File

@@ -36,11 +36,8 @@ namespace Snap7
public class S7Consts
{
#if __MonoCS__ // Assuming that we are using Unix release of Mono (otherwise modify it)
public const string Snap7LibName = "libsnap7.so";
#else
public const string Snap7LibName = "snap7.dll";
#endif
public const string Snap7LibName = "snap7";
//------------------------------------------------------------------------------
// PARAMS LIST
//------------------------------------------------------------------------------

View File

@@ -65,13 +65,14 @@ namespace S7.Net.UnitTest
[TestClass]
public class StreamTests
{
public TestContext TestContext { get; set; }
[TestMethod]
public async Task TPKT_ReadRestrictedStreamAsync()
{
var fullMessage = ProtocolUnitTest.StringToByteArray("0300002902f0803203000000010002001400000401ff0400807710000100000103000000033f8ccccd");
var m = new TestStream1BytePerRead(fullMessage);
var t = await TPKT.ReadAsync(m);
var t = await TPKT.ReadAsync(m, TestContext.CancellationTokenSource.Token);
Assert.AreEqual(fullMessage.Length, t.Length);
Assert.AreEqual(fullMessage.Last(), t.Data.Last());
}

View File

@@ -0,0 +1,135 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S7.Net.Types;
using System;
using System.Collections;
using System.Linq;
namespace S7.Net.UnitTest.TypeTests
{
[TestClass]
public class S7StringTests
{
[TestMethod]
public void ReadEmptyStringWithZeroByteLength()
{
AssertFromByteArrayEquals("", 0, 0);
}
[TestMethod]
public void ReadEmptyStringWithOneByteLength()
{
AssertFromByteArrayEquals("", 1, 0, 0);
}
[TestMethod]
public void ReadEmptyStringWithOneByteGarbage()
{
AssertFromByteArrayEquals("", 1, 0, (byte) 'a');
}
[TestMethod]
public void ReadMalformedStringTooShort()
{
Assert.ThrowsException<PlcException>(() => AssertFromByteArrayEquals("", 1));
}
[TestMethod]
public void ReadMalformedStringSizeLargerThanCapacity()
{
Assert.ThrowsException<PlcException>(() => S7String.FromByteArray(new byte[] { 3, 5, 0, 1, 2 }));
}
[TestMethod]
public void ReadMalformedStringCapacityTooLarge()
{
Assert.ThrowsException<ArgumentException>(() => AssertToByteArrayAndBackEquals("", 300, 0));
}
[TestMethod]
public void ReadA()
{
AssertFromByteArrayEquals("A", 1, 1, (byte) 'A');
}
[TestMethod]
public void ReadAbc()
{
AssertFromByteArrayEquals("Abc", 3, 3, (byte) 'A', (byte) 'b', (byte) 'c');
}
[TestMethod]
public void WriteNullWithReservedLengthZero()
{
Assert.ThrowsException<ArgumentNullException>(() => AssertToByteArrayAndBackEquals(null, 0, 0, 0));
}
[TestMethod]
public void WriteEmptyStringWithReservedLengthZero()
{
AssertToByteArrayAndBackEquals("", 0, 0, 0);
}
[TestMethod]
public void WriteAWithReservedLengthZero()
{
AssertToByteArrayAndBackEquals("", 0, 0, 0);
}
[TestMethod]
public void WriteNullWithReservedLengthOne()
{
Assert.ThrowsException<ArgumentNullException>(() => AssertToByteArrayAndBackEquals(null, 1, 1, 0));
}
[TestMethod]
public void WriteEmptyStringWithReservedLengthOne()
{
AssertToByteArrayAndBackEquals("", 1, 1, 0, 0);
}
[TestMethod]
public void WriteAWithReservedLengthOne()
{
AssertToByteArrayAndBackEquals("A", 1, 1, 1, (byte) 'A');
}
[TestMethod]
public void WriteAWithReservedLengthTwo()
{
AssertToByteArrayAndBackEquals("A", 2, 2, 1, (byte) 'A', 0);
}
[TestMethod]
public void WriteAbcWithStringLargetThanReservedLength()
{
Assert.ThrowsException<ArgumentException>(() => S7String.ToByteArray("Abc", 2));
}
[TestMethod]
public void WriteAbcWithReservedLengthThree()
{
AssertToByteArrayAndBackEquals("Abc", 3, 3, 3, (byte) 'A', (byte) 'b', (byte) 'c');
}
[TestMethod]
public void WriteAbcWithReservedLengthFour()
{
AssertToByteArrayAndBackEquals("Abc", 4, 4, 3, (byte) 'A', (byte) 'b', (byte) 'c', 0);
}
private static void AssertFromByteArrayEquals(string expected, params byte[] bytes)
{
var convertedString = S7String.FromByteArray(bytes);
Assert.AreEqual(expected, convertedString);
}
private static void AssertToByteArrayAndBackEquals(string value, int reservedLength, params byte[] expected)
{
var convertedData = S7String.ToByteArray(value, reservedLength);
CollectionAssert.AreEqual(expected, convertedData);
var convertedBack = S7String.FromByteArray(convertedData);
Assert.AreEqual(value, convertedBack);
}
}
}

View File

@@ -1,115 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using S7.Net.Types;
namespace S7.Net.UnitTest.TypeTests
{
[TestClass]
public class StringExTests
{
[TestMethod]
public void ReadEmptyStringWithZeroByteLength()
{
AssertFromByteArrayEquals("", 0, 0);
}
[TestMethod]
public void ReadEmptyStringWithOneByteLength()
{
AssertFromByteArrayEquals("", 1, 0, 0);
}
[TestMethod]
public void ReadEmptyStringWithOneByteGarbage()
{
AssertFromByteArrayEquals("", 1, 0, (byte) 'a');
}
[TestMethod]
public void ReadA()
{
AssertFromByteArrayEquals("A", 1, 1, (byte) 'A');
}
[TestMethod]
public void ReadAbc()
{
AssertFromByteArrayEquals("Abc", 1, 3, (byte) 'A', (byte) 'b', (byte) 'c');
}
[TestMethod]
public void WriteNullWithReservedLengthZero()
{
AssertToByteArrayEquals(null, 0, 0, 0);
}
[TestMethod]
public void WriteEmptyStringWithReservedLengthZero()
{
AssertToByteArrayEquals("", 0, 0, 0);
}
[TestMethod]
public void WriteAWithReservedLengthZero()
{
AssertToByteArrayEquals("A", 0, 0, 0);
}
[TestMethod]
public void WriteNullWithReservedLengthOne()
{
AssertToByteArrayEquals(null, 1, 1, 0);
}
[TestMethod]
public void WriteEmptyStringWithReservedLengthOne()
{
AssertToByteArrayEquals("", 1, 1, 0);
}
[TestMethod]
public void WriteAWithReservedLengthOne()
{
AssertToByteArrayEquals("A", 1, 1, 1, (byte) 'A');
}
[TestMethod]
public void WriteAWithReservedLengthTwo()
{
AssertToByteArrayEquals("A", 2, 2, 1, (byte) 'A');
}
[TestMethod]
public void WriteAbcWithReservedLengthOne()
{
AssertToByteArrayEquals("Abc", 1, 1, 1, (byte) 'A');
}
[TestMethod]
public void WriteAbcWithReservedLengthTwo()
{
AssertToByteArrayEquals("Abc", 2, 2, 2, (byte) 'A', (byte) 'b');
}
[TestMethod]
public void WriteAbcWithReservedLengthThree()
{
AssertToByteArrayEquals("Abc", 3, 3, 3, (byte) 'A', (byte) 'b', (byte) 'c');
}
[TestMethod]
public void WriteAbcWithReservedLengthFour()
{
AssertToByteArrayEquals("Abc", 4, 4, 3, (byte) 'A', (byte) 'b', (byte) 'c');
}
private static void AssertFromByteArrayEquals(string expected, params byte[] bytes)
{
Assert.AreEqual(expected, StringEx.FromByteArray(bytes));
}
private static void AssertToByteArrayEquals(string value, int reservedLength, params byte[] expected)
{
CollectionAssert.AreEqual(expected, StringEx.ToByteArray(value, reservedLength));
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net
@@ -10,6 +11,11 @@ namespace S7.Net
/// </summary>
internal class COTP
{
public enum PduType : byte
{
Data = 0xf0,
ConnectionConfirmed = 0xd0
}
/// <summary>
/// Describes a COTP TPDU (Transport protocol data unit)
/// </summary>
@@ -17,7 +23,7 @@ namespace S7.Net
{
public TPKT TPkt { get; }
public byte HeaderLength;
public byte PDUType;
public PduType PDUType;
public int TPDUNumber;
public byte[] Data;
public bool LastDataUnit;
@@ -29,8 +35,8 @@ namespace S7.Net
HeaderLength = tPKT.Data[0]; // Header length excluding this length byte
if (HeaderLength >= 2)
{
PDUType = tPKT.Data[1];
if (PDUType == 0xf0) //DT Data
PDUType = (PduType)tPKT.Data[1];
if (PDUType == PduType.Data) //DT Data
{
var flags = tPKT.Data[2];
TPDUNumber = flags & 0x7F;
@@ -66,9 +72,9 @@ namespace S7.Net
/// </summary>
/// <param name="stream">The socket to read from</param>
/// <returns>COTP DPDU instance</returns>
public static async Task<TPDU> ReadAsync(Stream stream)
public static async Task<TPDU> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
var tpkt = await TPKT.ReadAsync(stream);
var tpkt = await TPKT.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
if (tpkt.Length == 0)
{
throw new TPDUInvalidException("No protocol data received");
@@ -130,9 +136,9 @@ namespace S7.Net
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <returns>Data in TSDU</returns>
public static async Task<byte[]> ReadAsync(Stream stream)
public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
var segment = await TPDU.ReadAsync(stream);
var segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
if (segment.LastDataUnit)
{
@@ -145,7 +151,7 @@ namespace S7.Net
while (!segment.LastDataUnit)
{
segment = await TPDU.ReadAsync(stream);
segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
var previousLength = buffer.Length;
Array.Resize(ref buffer, buffer.Length + segment.Data.Length);
Array.Copy(segment.Data, 0, buffer, previousLength, segment.Data.Length);

View File

@@ -199,19 +199,6 @@ namespace S7.Net
return output;
}
/// <summary>
/// Converts from double to DWord (DBD)
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[Obsolete("Double support is obsolete. Use ConvertToUInt(float) instead.")]
public static UInt32 ConvertToUInt(this double input)
{
uint output;
output = S7.Net.Types.DWord.FromByteArray(S7.Net.Types.Double.ToByteArray(input));
return output;
}
/// <summary>
/// Converts from float to DWord (DBD)
/// </summary>
@@ -220,20 +207,7 @@ namespace S7.Net
public static UInt32 ConvertToUInt(this float input)
{
uint output;
output = S7.Net.Types.DWord.FromByteArray(S7.Net.Types.Single.ToByteArray(input));
return output;
}
/// <summary>
/// Converts from DWord (DBD) to double
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[Obsolete("Double support is obsolete. Use ConvertToFloat(uint) instead.")]
public static double ConvertToDouble(this uint input)
{
double output;
output = S7.Net.Types.Double.FromByteArray(S7.Net.Types.DWord.ToByteArray(input));
output = S7.Net.Types.DWord.FromByteArray(S7.Net.Types.Real.ToByteArray(input));
return output;
}
@@ -245,7 +219,7 @@ namespace S7.Net
public static float ConvertToFloat(this uint input)
{
float output;
output = S7.Net.Types.Single.FromByteArray(S7.Net.Types.DWord.ToByteArray(input));
output = S7.Net.Types.Real.FromByteArray(S7.Net.Types.DWord.ToByteArray(input));
return output;
}
}

View File

@@ -164,14 +164,19 @@
Real,
/// <summary>
/// String variable type (variable)
/// LReal variable type (64 bits, 8 bytes)
/// </summary>
LReal,
/// <summary>
/// Char Array / C-String variable type (variable)
/// </summary>
String,
/// <summary>
/// String variable type (variable)
/// S7 String variable type (variable)
/// </summary>
StringEx,
S7String,
/// <summary>
/// Timer variable type

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using S7.Net.Protocol;
using S7.Net.Types;
@@ -18,8 +19,8 @@ namespace S7.Net
private TcpClient? tcpClient;
private NetworkStream? _stream;
private int readTimeout = System.Threading.Timeout.Infinite;
private int writeTimeout = System.Threading.Timeout.Infinite;
private int readTimeout = 0; // default no timeout
private int writeTimeout = 0; // default no timeout
/// <summary>
/// IP address of the PLC
@@ -49,7 +50,7 @@ namespace S7.Net
/// <summary>
/// Max PDU size this cpu supports
/// </summary>
public Int16 MaxPDUSize { get; set; }
public int MaxPDUSize { get; private set; }
/// <summary>Gets or sets the amount of time that a read operation blocks waiting for data from PLC.</summary>
/// <returns>A <see cref="T:System.Int32" /> that specifies the amount of time, in milliseconds, that will elapse before a read operation fails. The default value, <see cref="F:System.Threading.Timeout.Infinite" />, specifies that the read operation does not time out.</returns>
@@ -85,7 +86,7 @@ namespace S7.Net
{
try
{
Connect();
OpenAsync().GetAwaiter().GetResult();
return true;
}
catch
@@ -183,11 +184,13 @@ namespace S7.Net
private void AssertPduSizeForRead(ICollection<DataItem> dataItems)
{
// 12 bytes of header data, 12 bytes of parameter data for each dataItem
if ((dataItems.Count + 1) * 12 > MaxPDUSize) throw new Exception("Too many vars requested for read");
// 14 bytes of header data, 4 bytes of result data for each dataItem and the actual data
if (GetDataLength(dataItems) + dataItems.Count * 4 + 14 > MaxPDUSize) throw new Exception("Too much data requested for read");
// send request limit: 19 bytes of header data, 12 bytes of parameter data for each dataItem
var requiredRequestSize = 19 + dataItems.Count * 12;
if (requiredRequestSize > MaxPDUSize) throw new Exception($"Too many vars requested for read. Request size ({requiredRequestSize}) is larger than protocol limit ({MaxPDUSize}).");
// response limit: 14 bytes of header data, 4 bytes of result data for each dataItem and the actual data
var requiredResponseSize = GetDataLength(dataItems) + dataItems.Count * 4 + 14;
if (requiredResponseSize > MaxPDUSize) throw new Exception($"Too much data requested for read. Response size ({requiredResponseSize}) is larger than protocol limit ({MaxPDUSize}).");
}
private void AssertPduSizeForWrite(ICollection<DataItem> dataItems)
@@ -232,13 +235,34 @@ namespace S7.Net
if (s7Data.Length < 15) throw NotEnoughBytes();
if (s7Data[14] != 0xff)
throw new PlcException(ErrorCode.ReadData,
$"Invalid response from PLC: '{BitConverter.ToString(s7Data)}'.");
ValidateResponseCode((ReadWriteErrorCode)s7Data[14]);
if (s7Data.Length < expectedLength) throw NotEnoughBytes();
}
internal static void ValidateResponseCode(ReadWriteErrorCode statusCode)
{
switch (statusCode)
{
case ReadWriteErrorCode.ObjectDoesNotExist:
throw new Exception("Received error from PLC: Object does not exist.");
case ReadWriteErrorCode.DataTypeInconsistent:
throw new Exception("Received error from PLC: Data type inconsistent.");
case ReadWriteErrorCode.DataTypeNotSupported:
throw new Exception("Received error from PLC: Data type not supported.");
case ReadWriteErrorCode.AccessingObjectNotAllowed:
throw new Exception("Received error from PLC: Accessing object not allowed.");
case ReadWriteErrorCode.AddressOutOfRange:
throw new Exception("Received error from PLC: Address out of range.");
case ReadWriteErrorCode.HardwareFault:
throw new Exception("Received error from PLC: Hardware fault.");
case ReadWriteErrorCode.Success:
break;
default:
throw new Exception( $"Invalid response from PLC: statusCode={(byte)statusCode}.");
}
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls

View File

@@ -1,4 +1,5 @@
using S7.Net.Helper;
using S7.Net.Protocol.S7;
using S7.Net.Types;
using System;
using System.Collections.Generic;
@@ -14,7 +15,7 @@ namespace S7.Net
/// </summary>
/// <param name="amount"></param>
/// <returns></returns>
private void BuildHeaderPackage(System.IO.MemoryStream stream, int amount = 1)
private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount = 1)
{
//header size = 19 bytes
stream.WriteByteArray(new byte[] { 0x03, 0x00 });
@@ -37,7 +38,7 @@ namespace S7.Net
/// <param name="startByteAdr">Start address of the byte</param>
/// <param name="count">Number of bytes to be read</param>
/// <returns></returns>
private void BuildReadDataRequestPackage(System.IO.MemoryStream stream, DataType dataType, int db, int startByteAdr, int count = 1)
private static void BuildReadDataRequestPackage(System.IO.MemoryStream stream, DataType dataType, int db, int startByteAdr, int count = 1)
{
//single data req = 12
stream.WriteByteArray(new byte[] { 0x12, 0x0a, 0x10 });
@@ -111,14 +112,19 @@ namespace S7.Net
return DInt.ToArray(bytes);
case VarType.Real:
if (varCount == 1)
return Types.Single.FromByteArray(bytes);
return Types.Real.FromByteArray(bytes);
else
return Types.Single.ToArray(bytes);
return Types.Real.ToArray(bytes);
case VarType.LReal:
if (varCount == 1)
return Types.LReal.FromByteArray(bytes);
else
return Types.LReal.ToArray(bytes);
case VarType.String:
return Types.String.FromByteArray(bytes);
case VarType.StringEx:
return StringEx.FromByteArray(bytes);
case VarType.S7String:
return S7String.FromByteArray(bytes);
case VarType.Timer:
if (varCount == 1)
@@ -171,7 +177,7 @@ namespace S7.Net
/// <param name="varType"></param>
/// <param name="varCount"></param>
/// <returns>Byte lenght of variable</returns>
private int VarTypeToByteLength(VarType varType, int varCount = 1)
internal static int VarTypeToByteLength(VarType varType, int varCount = 1)
{
switch (varType)
{
@@ -181,7 +187,7 @@ namespace S7.Net
return (varCount < 1) ? 1 : varCount;
case VarType.String:
return varCount;
case VarType.StringEx:
case VarType.S7String:
return varCount + 2;
case VarType.Word:
case VarType.Timer:
@@ -192,6 +198,7 @@ namespace S7.Net
case VarType.DInt:
case VarType.Real:
return varCount * 4;
case VarType.LReal:
case VarType.DateTime:
return varCount * 8;
case VarType.DateTimeLong:
@@ -236,5 +243,20 @@ namespace S7.Net
offset++;
}
}
private static byte[] BuildReadRequestPackage(IList<DataItemAddress> dataItems)
{
int packageSize = 19 + (dataItems.Count * 12);
var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package, dataItems.Count);
foreach (var dataItem in dataItems)
{
BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAddress, dataItem.ByteLength);
}
return package.ToArray();
}
}
}

View File

@@ -6,6 +6,8 @@ using System.Net.Sockets;
using System.Threading.Tasks;
using S7.Net.Protocol;
using System.IO;
using System.Threading;
using S7.Net.Protocol.S7;
namespace S7.Net
{
@@ -17,28 +19,57 @@ namespace S7.Net
/// <summary>
/// Connects to the PLC and performs a COTP ConnectionRequest and S7 CommunicationSetup.
/// </summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that the cancellation will not affect opening the socket in any way and only affects data transfers for configuring the connection after the socket connection is successfully established.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous open operation.</returns>
public async Task OpenAsync()
public async Task OpenAsync(CancellationToken cancellationToken = default)
{
await ConnectAsync();
var stream = GetStreamIfAvailable();
await stream.WriteAsync(ConnectionRequest.GetCOTPConnectionRequest(CPU, Rack, Slot), 0, 22);
var response = await COTP.TPDU.ReadAsync(stream);
if (response == null)
var stream = await ConnectAsync().ConfigureAwait(false);
try
{
throw new Exception("Error reading Connection Confirm. Malformed TPDU packet");
cancellationToken.ThrowIfCancellationRequested();
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
_stream = stream;
}
if (response.PDUType != 0xd0) //Connect Confirm
catch(Exception)
{
throw new InvalidDataException("Error reading Connection Confirm", response.TPkt.Data, 1, 0x0d);
stream.Dispose();
throw;
}
}
await stream.WriteAsync(GetS7ConnectionSetup(), 0, 25);
private async Task<NetworkStream> ConnectAsync()
{
tcpClient = new TcpClient();
ConfigureConnection();
await tcpClient.ConnectAsync(IP, Port).ConfigureAwait(false);
return tcpClient.GetStream();
}
var s7data = await COTP.TSDU.ReadAsync(stream);
if (s7data == null)
throw new WrongNumberOfBytesException("No data received in response to Communication Setup");
private async Task EstablishConnection(NetworkStream stream, CancellationToken cancellationToken)
{
await RequestConnection(stream, cancellationToken).ConfigureAwait(false);
await SetupConnection(stream, cancellationToken).ConfigureAwait(false);
}
private async Task RequestConnection(NetworkStream stream, CancellationToken cancellationToken)
{
var requestData = ConnectionRequest.GetCOTPConnectionRequest(CPU, Rack, Slot);
await stream.WriteAsync(requestData, 0, requestData.Length).ConfigureAwait(false);
var response = await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
if (response.PDUType != COTP.PduType.ConnectionConfirmed)
{
throw new InvalidDataException("Connection request was denied", response.TPkt.Data, 1, 0x0d);
}
}
private async Task SetupConnection(NetworkStream stream, CancellationToken cancellationToken)
{
var setupData = GetS7ConnectionSetup();
await stream.WriteAsync(setupData, 0, setupData.Length).ConfigureAwait(false);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
if (s7data.Length < 2)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
@@ -49,15 +80,8 @@ namespace S7.Net
if (s7data.Length < 20)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
MaxPDUSize = (short)(s7data[18] * 256 + s7data[19]);
}
private async Task ConnectAsync()
{
tcpClient = new TcpClient();
ConfigureConnection();
await tcpClient.ConnectAsync(IP, Port);
_stream = tcpClient.GetStream();
// TODO: check if this should not rather be UInt16.
MaxPDUSize = s7data[18] * 256 + s7data[19];
}
@@ -69,8 +93,10 @@ namespace S7.Net
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="count">Byte count, if you want to read 120 bytes, set this to 120.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>Returns the bytes in an array</returns>
public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count)
public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken cancellationToken = default)
{
var resultBytes = new byte[count];
int index = 0;
@@ -78,7 +104,7 @@ namespace S7.Net
{
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
var maxToRead = Math.Min(count, MaxPDUSize - 18);
await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, resultBytes, index, maxToRead);
await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, resultBytes, index, maxToRead, cancellationToken);
count -= maxToRead;
index += maxToRead;
}
@@ -96,10 +122,12 @@ namespace S7.Net
/// <param name="varType">Type of the variable/s that you are reading</param>
/// <param name="bitAdr">Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter.</param>
/// <param name="varCount"></param>
public async Task<object?> ReadAsync(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0)
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
public async Task<object?> ReadAsync(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0, CancellationToken cancellationToken = default)
{
int cntBytes = VarTypeToByteLength(varType, varCount);
byte[] bytes = await ReadBytesAsync(dataType, db, startByteAdr, cntBytes);
byte[] bytes = await ReadBytesAsync(dataType, db, startByteAdr, cntBytes, cancellationToken);
return ParseBytes(varType, bytes, varCount, bitAdr);
}
@@ -108,11 +136,13 @@ namespace S7.Net
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>Returns an object that contains the value. This object must be cast accordingly.</returns>
public async Task<object?> ReadAsync(string variable)
public async Task<object?> ReadAsync(string variable, CancellationToken cancellationToken = default)
{
var adr = new PLCAddress(variable);
return await ReadAsync(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber);
return await ReadAsync(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber, cancellationToken);
}
/// <summary>
@@ -121,12 +151,14 @@ namespace S7.Net
/// <param name="structType">Type of the struct to be readed (es.: TypeOf(MyStruct)).</param>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>Returns a struct that must be cast.</returns>
public async Task<object?> ReadStructAsync(Type structType, int db, int startByteAdr = 0)
public async Task<object?> ReadStructAsync(Type structType, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
{
int numBytes = Types.Struct.GetStructSize(structType);
// now read the package
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes);
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken);
// and decode it
return Types.Struct.FromBytes(structType, resultBytes);
@@ -138,10 +170,12 @@ namespace S7.Net
/// <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>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>Returns 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
public async Task<T?> ReadStructAsync<T>(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : struct
{
return await ReadStructAsync(typeof(T), db, startByteAdr) as T?;
return await ReadStructAsync(typeof(T), db, startByteAdr, cancellationToken) as T?;
}
/// <summary>
@@ -151,8 +185,10 @@ namespace S7.Net
/// <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>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>The number of read bytes</returns>
public async Task<Tuple<int, object>> ReadClassAsync(object sourceClass, int db, int startByteAdr = 0)
public async Task<Tuple<int, object>> ReadClassAsync(object sourceClass, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
{
int numBytes = (int)Class.GetClassSize(sourceClass);
if (numBytes <= 0)
@@ -161,7 +197,7 @@ namespace S7.Net
}
// now read the package
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes);
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken);
// and decode it
Class.FromBytes(sourceClass, resultBytes);
@@ -176,10 +212,12 @@ namespace S7.Net
/// <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>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public async Task<T?> ReadClassAsync<T>(int db, int startByteAdr = 0) where T : class
public async Task<T?> ReadClassAsync<T>(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class
{
return await ReadClassAsync(() => Activator.CreateInstance<T>(), db, startByteAdr);
return await ReadClassAsync(() => Activator.CreateInstance<T>(), db, startByteAdr, cancellationToken);
}
/// <summary>
@@ -190,11 +228,13 @@ namespace S7.Net
/// <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>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public async Task<T?> ReadClassAsync<T>(Func<T> classFactory, int db, int startByteAdr = 0) where T : class
public async Task<T?> ReadClassAsync<T>(Func<T> classFactory, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class
{
var instance = classFactory();
var res = await ReadClassAsync(instance, db, startByteAdr);
var res = await ReadClassAsync(instance, db, startByteAdr, cancellationToken);
int readBytes = res.Item1;
if (readBytes <= 0)
{
@@ -209,10 +249,12 @@ namespace S7.Net
/// 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).
/// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (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)
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
public async Task<List<DataItem>> ReadMultipleVarsAsync(List<DataItem> dataItems, CancellationToken cancellationToken = default)
{
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//replies with bigger PDU size in connection setup.
@@ -222,22 +264,11 @@ namespace S7.Net
try
{
// first create the header
int packageSize = 19 + (dataItems.Count * 12);
var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package, dataItems.Count);
// package.Add(0x02); // datenart
foreach (var dataItem in dataItems)
{
BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count));
}
var dataToSend = package.ToArray();
var dataToSend = BuildReadRequestPackage(dataItems.Select(d => DataItem.GetDataItemAddress(d)).ToList());
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
var s7data = await COTP.TSDU.ReadAsync(stream); //TODO use Async
if (s7data == null || s7data[14] != 0xff)
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
ParseDataIntoDataItems(s7data, dataItems);
}
@@ -245,6 +276,10 @@ namespace S7.Net
{
throw new PlcException(ErrorCode.ReadData, socketException);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.ReadData, exc);
@@ -252,6 +287,7 @@ namespace S7.Net
return dataItems;
}
/// <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.
@@ -260,15 +296,17 @@ namespace S7.Net
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value)
public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken = default)
{
int localIndex = 0;
int count = value.Length;
while (count > 0)
{
var maxToWrite = (int)Math.Min(count, MaxPDUSize - 35);
await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite);
await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite, cancellationToken);
count -= maxToWrite;
localIndex += maxToWrite;
}
@@ -282,13 +320,15 @@ namespace S7.Net
/// <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>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool value)
public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool value, CancellationToken cancellationToken = default)
{
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));
await WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, value);
await WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, value, cancellationToken);
}
/// <summary>
@@ -299,13 +339,15 @@ namespace S7.Net
/// <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>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, int value)
public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, int value, CancellationToken cancellationToken = default)
{
if (value < 0 || value > 1)
throw new ArgumentException("Value must be 0 or 1", nameof(value));
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, value == 1);
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, value == 1, cancellationToken);
}
/// <summary>
@@ -318,15 +360,17 @@ namespace S7.Net
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteAsync(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1)
public async Task WriteAsync(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1, CancellationToken cancellationToken = default)
{
if (bitAdr != -1)
{
//Must be writing a bit value as bitAdr is specified
if (value is bool)
if (value is bool boolean)
{
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, (bool) value);
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, boolean, cancellationToken);
}
else if (value is int intValue)
{
@@ -336,11 +380,11 @@ namespace S7.Net
"Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid",
bitAdr), nameof(bitAdr));
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, intValue == 1);
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, intValue == 1, cancellationToken);
}
else throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value));
}
else await WriteBytesAsync(dataType, db, startByteAdr, Serialization.SerializeValue(value));
else await WriteBytesAsync(dataType, db, startByteAdr, Serialization.SerializeValue(value), cancellationToken);
}
/// <summary>
@@ -349,11 +393,13 @@ namespace S7.Net
/// </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>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteAsync(string variable, object value)
public async Task WriteAsync(string variable, object value, CancellationToken cancellationToken = default)
{
var adr = new PLCAddress(variable);
await WriteAsync(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber);
await WriteAsync(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber, cancellationToken);
}
/// <summary>
@@ -362,11 +408,13 @@ namespace S7.Net
/// <param name="structValue">The struct to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteStructAsync(object structValue, int db, int startByteAdr = 0)
public async Task WriteStructAsync(object structValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
{
var bytes = Struct.ToBytes(structValue).ToList();
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray());
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray(), cancellationToken);
}
/// <summary>
@@ -375,29 +423,24 @@ namespace S7.Net
/// <param name="classValue">The class to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteClassAsync(object classValue, int db, int startByteAdr = 0)
public async Task WriteClassAsync(object classValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
{
byte[] bytes = new byte[(int)Class.GetClassSize(classValue)];
Types.Class.ToBytes(classValue, bytes);
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes);
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes, cancellationToken);
}
private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count)
private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
var stream = GetStreamIfAvailable();
// first create the header
int packageSize = 31;
var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package);
// package.Add(0x02); // datenart
BuildReadDataRequestPackage(package, dataType, db, startByteAdr, count);
var dataToSend = BuildReadRequestPackage(new [] { new DataItemAddress(dataType, db, startByteAdr, count)});
await stream.WriteAsync(dataToSend, 0, dataToSend.Length, cancellationToken);
var dataToSend = package.ToArray();
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
var s7data = await COTP.TSDU.ReadAsync(stream);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken);
AssertReadResponse(s7data, count);
Array.Copy(s7data, 18, buffer, offset, count);
@@ -419,7 +462,7 @@ namespace S7.Net
var length = S7WriteMultiple.CreateRequest(message, dataItems);
await stream.WriteAsync(message.Array, 0, length).ConfigureAwait(false);
var response = await COTP.TSDU.ReadAsync(stream).ConfigureAwait(false);
var response = await COTP.TSDU.ReadAsync(stream, CancellationToken.None).ConfigureAwait(false);
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
}
@@ -431,7 +474,7 @@ namespace S7.Net
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count)
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count, CancellationToken cancellationToken)
{
try
@@ -439,13 +482,14 @@ namespace S7.Net
var stream = GetStreamIfAvailable();
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count);
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
await stream.WriteAsync(dataToSend, 0, dataToSend.Length, cancellationToken);
var s7data = await COTP.TSDU.ReadAsync(stream);
if (s7data == null || s7data[14] != 0xff)
{
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
}
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exc)
{
@@ -453,7 +497,7 @@ namespace S7.Net
}
}
private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue)
private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken)
{
var stream = GetStreamIfAvailable();
@@ -463,11 +507,12 @@ namespace S7.Net
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
var s7data = await COTP.TSDU.ReadAsync(stream);
if (s7data == null || s7data[14] != 0xff)
{
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
}
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exc)
{

View File

@@ -17,34 +17,9 @@ namespace S7.Net
/// </summary>
public void Open()
{
Connect();
try
{
var stream = GetStreamIfAvailable();
stream.Write(ConnectionRequest.GetCOTPConnectionRequest(CPU, Rack, Slot), 0, 22);
var response = COTP.TPDU.Read(stream);
if (response.PDUType != 0xd0) //Connect Confirm
{
throw new InvalidDataException("Error reading Connection Confirm", response.TPkt.Data, 1, 0x0d);
}
stream.Write(GetS7ConnectionSetup(), 0, 25);
var s7data = COTP.TSDU.Read(stream);
if (s7data == null)
throw new WrongNumberOfBytesException("No data received in response to Communication Setup");
if (s7data.Length < 2)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
//Check for S7 Ack Data
if (s7data[1] != 0x03)
throw new InvalidDataException("Error reading Communication Setup response", s7data, 1, 0x03);
if (s7data.Length < 20)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
MaxPDUSize = (short)(s7data[18] * 256 + s7data[19]);
OpenAsync().GetAwaiter().GetResult();
}
catch (Exception exc)
{
@@ -53,28 +28,6 @@ namespace S7.Net
}
}
private void Connect()
{
try
{
tcpClient = new TcpClient();
ConfigureConnection();
tcpClient.Connect(IP, Port);
_stream = tcpClient.GetStream();
}
catch (SocketException sex)
{
// see https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668(v=vs.85).aspx
throw new PlcException(
sex.SocketErrorCode == SocketError.TimedOut
? ErrorCode.IPAddressNotAvailable
: ErrorCode.ConnectionError, sex);
}
catch (Exception ex)
{
throw new PlcException(ErrorCode.ConnectionError, ex);
}
}
/// <summary>
/// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
@@ -289,9 +242,9 @@ namespace S7.Net
if (bitAdr != -1)
{
//Must be writing a bit value as bitAdr is specified
if (value is bool)
if (value is bool boolean)
{
WriteBit(dataType, db, startByteAdr, bitAdr, (bool) value);
WriteBit(dataType, db, startByteAdr, bitAdr, boolean);
}
else if (value is int intValue)
{
@@ -398,10 +351,7 @@ namespace S7.Net
stream.Write(dataToSend, 0, dataToSend.Length);
var s7data = COTP.TSDU.Read(stream);
if (s7data == null || s7data[14] != 0xff)
{
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
}
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (Exception exc)
{
@@ -442,7 +392,6 @@ namespace S7.Net
private byte[] BuildWriteBitPackage(DataType dataType, int db, int startByteAdr, bool bitValue, int bitAdr)
{
var stream = GetStreamIfAvailable();
var value = new[] { bitValue ? (byte)1 : (byte)0 };
int varCount = 1;
// first create the header
@@ -484,10 +433,7 @@ namespace S7.Net
stream.Write(dataToSend, 0, dataToSend.Length);
var s7data = COTP.TSDU.Read(stream);
if (s7data == null || s7data[14] != 0xff)
{
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
}
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (Exception exc)
{
@@ -500,13 +446,11 @@ namespace S7.Net
/// 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).
/// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (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>
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
public void ReadMultipleVars(List<DataItem> dataItems)
{
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//replies with bigger PDU size in connection setup.
AssertPduSizeForRead(dataItems);
var stream = GetStreamIfAvailable();
@@ -527,8 +471,8 @@ namespace S7.Net
stream.Write(dataToSend, 0, dataToSend.Length);
var s7data = COTP.TSDU.Read(stream); //TODO use Async
if (s7data == null || s7data[14] != 0xff)
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
ParseDataIntoDataItems(s7data, dataItems);
}

View File

@@ -0,0 +1,15 @@

namespace S7.Net.Protocol
{
internal enum ReadWriteErrorCode : byte
{
Reserved = 0x00,
HardwareFault = 0x01,
AccessingObjectNotAllowed = 0x03,
AddressOutOfRange = 0x05,
DataTypeNotSupported = 0x06,
DataTypeInconsistent = 0x07,
ObjectDoesNotExist = 0x0a,
Success = 0xff
}
}

View File

@@ -0,0 +1,37 @@
namespace S7.Net.Protocol.S7
{
/// <summary>
/// Represents an area of memory in the PLC
/// </summary>
internal class DataItemAddress
{
public DataItemAddress(DataType dataType, int db, int startByteAddress, int byteLength)
{
DataType = dataType;
DB = db;
StartByteAddress = startByteAddress;
ByteLength = byteLength;
}
/// <summary>
/// Memory area to read
/// </summary>
public DataType DataType { get; }
/// <summary>
/// Address of memory area to read (example: for DB1 this value is 1, for T45 this value is 45)
/// </summary>
public int DB { get; }
/// <summary>
/// Address of the first byte to read
/// </summary>
public int StartByteAddress { get; }
/// <summary>
/// Length of data to read
/// </summary>
public int ByteLength { get; }
}
}

View File

@@ -17,7 +17,6 @@ namespace S7.Net.Protocol
(ushort) (2 + paramSize));
var paramOffset = Header.Template.Length;
var dataOffset = paramOffset + paramSize;
var data = new ByteArray();
var itemCount = 0;
@@ -95,11 +94,16 @@ namespace S7.Net.Protocol
for (int i = 0; i < dataItems.Length; i++)
{
var result = itemResults[i];
if (result == 0xff) continue;
try
{
Plc.ValidateResponseCode((ReadWriteErrorCode)itemResults[i]);
}
catch(Exception e)
{
if (errors == null) errors = new List<Exception>();
errors.Add(new Exception($"Write of dataItem {dataItems[i]} failed: {e.Message}."));
}
if (errors == null) errors = new List<Exception>();
errors.Add(new Exception($"Write of dataItem {dataItems[i]} failed with error code {result}."));
}
if (errors != null)

View File

@@ -13,9 +13,13 @@ namespace S7.Net.Protocol
public static byte[] SerializeDataItem(DataItem dataItem)
{
if (dataItem.Value == null)
{
throw new Exception($"DataItem.Value is null, cannot serialize. StartAddr={dataItem.StartByteAdr} VarType={dataItem.VarType}");
}
if (dataItem.Value is string s)
return dataItem.VarType == VarType.StringEx
? StringEx.ToByteArray(s, dataItem.Count)
return dataItem.VarType == VarType.S7String
? S7String.ToByteArray(s, dataItem.Count)
: Types.String.ToByteArray(s, dataItem.Count);
return SerializeValue(dataItem.Value);
@@ -37,10 +41,10 @@ namespace S7.Net.Protocol
return Types.DInt.ToByteArray((Int32)value);
case "UInt32":
return Types.DWord.ToByteArray((UInt32)value);
case "Double":
return Types.Double.ToByteArray((double)value);
case "Single":
return Types.Single.ToByteArray((float)value);
return Types.Real.ToByteArray((float)value);
case "Double":
return Types.LReal.ToByteArray((double)value);
case "DateTime":
return Types.DateTime.ToByteArray((System.DateTime) value);
case "Byte[]":
@@ -53,10 +57,10 @@ namespace S7.Net.Protocol
return Types.DInt.ToByteArray((Int32[])value);
case "UInt32[]":
return Types.DWord.ToByteArray((UInt32[])value);
case "Double[]":
return Types.Double.ToByteArray((double[])value);
case "Single[]":
return Types.Single.ToByteArray((float[])value);
return Types.Real.ToByteArray((float[])value);
case "Double[]":
return Types.LReal.ToByteArray((double[])value);
case "String":
// Hack: This is backwards compatible with the old code, but functionally it's broken
// if the consumer does not pay attention to string length.
@@ -75,9 +79,9 @@ namespace S7.Net.Protocol
{
var start = startByte * 8 + bitNumber;
buffer[index + 2] = (byte)start;
start = start >> 8;
start >>= 8;
buffer[index + 1] = (byte)start;
start = start >> 8;
start >>= 8;
buffer[index] = (byte)start;
}

View File

@@ -18,7 +18,6 @@
<LangVersion>8.0</LangVersion>
<Nullable>Enable</Nullable>
<DebugType>portable</DebugType>
<EmbedAllSources>true</EmbedAllSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net
@@ -39,13 +40,13 @@ namespace S7.Net
/// <param name="offset">the offset in the buffer to read into</param>
/// <param name="count">the amount of bytes to read into the buffer</param>
/// <returns>returns the amount of read bytes</returns>
public static async Task<int> ReadExactAsync(this Stream stream, byte[] buffer, int offset, int count)
public static async Task<int> ReadExactAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
int read = 0;
int received;
do
{
received = await stream.ReadAsync(buffer, offset + read, count - read);
received = await stream.ReadAsync(buffer, offset + read, count - read, cancellationToken).ConfigureAwait(false);
read += received;
}
while (read < count && received > 0);

View File

@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net
@@ -57,10 +58,10 @@ namespace S7.Net
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <returns>Task TPKT Instace</returns>
public static async Task<TPKT> ReadAsync(Stream stream)
public static async Task<TPKT> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
var buf = new byte[4];
int len = await stream.ReadExactAsync(buf, 0, 4);
int len = await stream.ReadExactAsync(buf, 0, 4, cancellationToken).ConfigureAwait(false);
if (len < 4) throw new TPKTInvalidException("TPKT is incomplete / invalid");
var version = buf[0];
@@ -68,7 +69,7 @@ namespace S7.Net
var length = buf[2] * 256 + buf[3]; //BigEndian
var data = new byte[length - 4];
len = await stream.ReadExactAsync(data, 0, data.Length);
len = await stream.ReadExactAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(false);
if (len < data.Length)
throw new TPKTInvalidException("TPKT payload incomplete / invalid");

View File

@@ -51,12 +51,17 @@ namespace S7.Net.Types
numBytes += 4;
break;
case "Single":
case "Double":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
numBytes += 4;
break;
case "Double":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
numBytes += 8;
break;
default:
var propertyClass = Activator.CreateInstance(type);
numBytes = GetClassSize(propertyClass, numBytes, true);
@@ -168,12 +173,12 @@ namespace S7.Net.Types
bytes[(int)numBytes + 3]);
numBytes += 4;
break;
case "Double":
case "Single":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// hier auswerten
value = Double.FromByteArray(
value = Real.FromByteArray(
new byte[] {
bytes[(int)numBytes],
bytes[(int)numBytes + 1],
@@ -181,18 +186,15 @@ namespace S7.Net.Types
bytes[(int)numBytes + 3] });
numBytes += 4;
break;
case "Single":
case "Double":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
var buffer = new byte[8];
Array.Copy(bytes, (int)numBytes, buffer, 0, 8);
// hier auswerten
value = Single.FromByteArray(
new byte[] {
bytes[(int)numBytes],
bytes[(int)numBytes + 1],
bytes[(int)numBytes + 2],
bytes[(int)numBytes + 3] });
numBytes += 4;
value = LReal.FromByteArray(buffer);
numBytes += 8;
break;
default:
var propClass = Activator.CreateInstance(propertyType);
@@ -277,11 +279,11 @@ namespace S7.Net.Types
case "UInt32":
bytes2 = DWord.ToByteArray((UInt32)propertyValue);
break;
case "Double":
bytes2 = Double.ToByteArray((double)propertyValue);
break;
case "Single":
bytes2 = Single.ToByteArray((float)propertyValue);
bytes2 = Real.ToByteArray((float)propertyValue);
break;
case "Double":
bytes2 = LReal.ToByteArray((double)propertyValue);
break;
default:
numBytes = ToBytes(propertyValue, bytes, numBytes);

View File

@@ -1,4 +1,5 @@
using System;
using S7.Net.Protocol.S7;
using System;
namespace S7.Net.Types
{
@@ -94,5 +95,10 @@ namespace S7.Net.Types
return dataItem;
}
internal static DataItemAddress GetDataItemAddress(DataItem dataItem)
{
return new DataItemAddress(dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, Plc.VarTypeToByteLength(dataItem.VarType, dataItem.Count));
}
}
}

View File

@@ -5,27 +5,13 @@ namespace S7.Net.Types
/// <summary>
/// Contains the conversion methods to convert Real from S7 plc to C# double.
/// </summary>
[Obsolete("Class Double is obsolete. Use Real instead for 32bit floating point, or LReal for 64bit floating point.")]
public static class Double
{
/// <summary>
/// Converts a S7 Real (4 bytes) to double
/// </summary>
public static double FromByteArray(byte[] bytes)
{
if (bytes.Length != 4)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 4 bytes.");
}
// sps uses bigending so we have to reverse if platform needs
if (BitConverter.IsLittleEndian)
{
// create deep copy of the array and reverse
bytes = new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
}
return BitConverter.ToSingle(bytes, 0);
}
public static double FromByteArray(byte[] bytes) => Real.FromByteArray(bytes);
/// <summary>
/// Converts a S7 DInt to double
@@ -51,16 +37,7 @@ namespace S7.Net.Types
/// <summary>
/// Converts a double to S7 Real (4 bytes)
/// </summary>
public static byte[] ToByteArray(double value)
{
byte[] bytes = BitConverter.GetBytes((float)(value));
// sps uses bigending so we have to check if platform is same
if (!BitConverter.IsLittleEndian) return bytes;
// create deep copy of the array and reverse
return new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
}
public static byte[] ToByteArray(double value) => Real.ToByteArray((float)value);
/// <summary>
/// Converts an array of double to an array of bytes

57
S7.Net/Types/LReal.cs Normal file
View File

@@ -0,0 +1,57 @@
using System;
using System.IO;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Real from S7 plc to C# double.
/// </summary>
public static class LReal
{
/// <summary>
/// Converts a S7 LReal (8 bytes) to double
/// </summary>
public static double FromByteArray(byte[] bytes)
{
if (bytes.Length != 8)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 8 bytes.");
}
var buffer = bytes;
// sps uses bigending so we have to reverse if platform needs
if (BitConverter.IsLittleEndian)
{
Array.Reverse(buffer);
}
return BitConverter.ToDouble(buffer, 0);
}
/// <summary>
/// Converts a double to S7 LReal (8 bytes)
/// </summary>
public static byte[] ToByteArray(double value)
{
var bytes = BitConverter.GetBytes(value);
// sps uses bigending so we have to check if platform is same
if (BitConverter.IsLittleEndian)
{
Array.Reverse(bytes);
}
return bytes;
}
/// <summary>
/// Converts an array of double to an array of bytes
/// </summary>
public static byte[] ToByteArray(double[] value) => TypeHelper.ToByteArray(value, ToByteArray);
/// <summary>
/// Converts an array of S7 LReal to an array of double
/// </summary>
public static double[] ToArray(byte[] bytes) => TypeHelper.ToArray(bytes, FromByteArray);
}
}

75
S7.Net/Types/Real.cs Normal file
View File

@@ -0,0 +1,75 @@
using System;
using System.IO;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Real from S7 plc to C# double.
/// </summary>
public static class Real
{
/// <summary>
/// Converts a S7 Real (4 bytes) to float
/// </summary>
public static float FromByteArray(byte[] bytes)
{
if (bytes.Length != 4)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 4 bytes.");
}
// sps uses bigending so we have to reverse if platform needs
if (BitConverter.IsLittleEndian)
{
// create deep copy of the array and reverse
bytes = new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
}
return BitConverter.ToSingle(bytes, 0);
}
/// <summary>
/// Converts a float to S7 Real (4 bytes)
/// </summary>
public static byte[] ToByteArray(float value)
{
byte[] bytes = BitConverter.GetBytes(value);
// sps uses bigending so we have to check if platform is same
if (!BitConverter.IsLittleEndian) return bytes;
// create deep copy of the array and reverse
return new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
}
/// <summary>
/// Converts an array of float to an array of bytes
/// </summary>
public static byte[] ToByteArray(float[] value)
{
var buffer = new byte[4 * value.Length];
var stream = new MemoryStream(buffer);
foreach (var val in value)
{
stream.Write(ToByteArray(val), 0, 4);
}
return buffer;
}
/// <summary>
/// Converts an array of S7 Real to an array of float
/// </summary>
public static float[] ToArray(byte[] bytes)
{
var values = new float[bytes.Length / 4];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 4; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] });
return values;
}
}
}

69
S7.Net/Types/S7String.cs Normal file
View File

@@ -0,0 +1,69 @@
using System;
using System.Text;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert from S7 strings to C# strings
/// An S7 String has a preceeding 2 byte header containing its capacity and length
/// </summary>
public static class S7String
{
/// <summary>
/// Converts S7 bytes to a string
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string FromByteArray(byte[] bytes)
{
if (bytes.Length < 2)
{
throw new PlcException(ErrorCode.ReadData, "Malformed S7 String / too short");
}
int size = bytes[0];
int length = bytes[1];
if (length > size)
{
throw new PlcException(ErrorCode.ReadData, "Malformed S7 String / length larger than capacity");
}
try
{
return Encoding.ASCII.GetString(bytes, 2, length);
}
catch (Exception e)
{
throw new PlcException(ErrorCode.ReadData,
$"Failed to parse {VarType.S7String} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.",
e);
}
}
/// <summary>
/// Converts a <see cref="T:string"/> to S7 string with 2-byte header.
/// </summary>
/// <param name="value">The string to convert to byte array.</param>
/// <param name="reservedLength">The length (in bytes) allocated in PLC for string excluding header.</param>
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 2.</returns>
public static byte[] ToByteArray(string value, int reservedLength)
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
if (reservedLength > byte.MaxValue) throw new ArgumentException($"The maximum string length supported is {byte.MaxValue}.");
var bytes = Encoding.ASCII.GetBytes(value);
if (bytes.Length > reservedLength) throw new ArgumentException($"The provided string length ({bytes.Length} is larger than the specified reserved length ({reservedLength}).");
var buffer = new byte[2 + reservedLength];
Array.Copy(bytes, 0, buffer, 2, bytes.Length);
buffer[0] = (byte)reservedLength;
buffer[1] = (byte)bytes.Length;
return buffer;
}
}
}

View File

@@ -5,27 +5,13 @@ namespace S7.Net.Types
/// <summary>
/// Contains the conversion methods to convert Real from S7 plc to C# float.
/// </summary>
[Obsolete("Class Single is obsolete. Use Real instead.")]
public static class Single
{
/// <summary>
/// Converts a S7 Real (4 bytes) to float
/// </summary>
public static float FromByteArray(byte[] bytes)
{
if (bytes.Length != 4)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 4 bytes.");
}
// sps uses bigending so we have to reverse if platform needs
if (BitConverter.IsLittleEndian)
{
// create deep copy of the array and reverse
bytes = new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
}
return BitConverter.ToSingle(bytes, 0);
}
public static float FromByteArray(byte[] bytes) => Real.FromByteArray(bytes);
/// <summary>
/// Converts a S7 DInt to float
@@ -51,16 +37,7 @@ namespace S7.Net.Types
/// <summary>
/// Converts a double to S7 Real (4 bytes)
/// </summary>
public static byte[] ToByteArray(float value)
{
byte[] bytes = BitConverter.GetBytes((float)(value));
// sps uses bigending so we have to check if platform is same
if (!BitConverter.IsLittleEndian) return bytes;
// create deep copy of the array and reverse
return new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
}
public static byte[] ToByteArray(float value) => Real.ToByteArray(value);
/// <summary>
/// Converts an array of float to an array of bytes

View File

@@ -1,7 +1,7 @@
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert from S7 strings to C# strings
/// Contains the methods to convert from S7 Array of Chars (like a const char[N] C-String) to C# strings
/// </summary>
public class String
{

View File

@@ -1,60 +1,15 @@
using System;
using System.Text;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert from S7 strings to C# strings
/// there are two kinds how strings a send. This one is with a pre of two bytes
/// they contain the length of the string
/// </summary>
/// <inheritdoc cref="S7String"/>
[Obsolete("Please use S7String class")]
public static class StringEx
{
/// <summary>
/// Converts S7 bytes to a string
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string FromByteArray(byte[] bytes)
{
if (bytes.Length < 2) return "";
/// <inheritdoc cref="S7String.FromByteArray(byte[])"/>
public static string FromByteArray(byte[] bytes) => S7String.FromByteArray(bytes);
int size = bytes[0];
int length = bytes[1];
try
{
return Encoding.ASCII.GetString(bytes, 2, length);
}
catch (Exception e)
{
throw new PlcException(ErrorCode.ReadData,
$"Failed to parse {VarType.StringEx} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.",
e);
}
}
/// <summary>
/// Converts a <see cref="T:string"/> to S7 string with 2-byte header.
/// </summary>
/// <param name="value">The string to convert to byte array.</param>
/// <param name="reservedLength">The length (in bytes) allocated in PLC for string excluding header.</param>
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 2.</returns>
public static byte[] ToByteArray(string value, int reservedLength)
{
if (reservedLength > byte.MaxValue) throw new ArgumentException($"The maximum string length supported is {byte.MaxValue}.");
var length = value?.Length;
if (length > reservedLength) length = reservedLength;
var bytes = new byte[(length ?? 0) + 2];
bytes[0] = (byte) reservedLength;
if (value == null) return bytes;
bytes[1] = (byte) Encoding.ASCII.GetBytes(value, 0, length.Value, bytes, 2);
return bytes;
}
/// <inheritdoc cref="S7String.ToByteArray(string, int)"/>
public static byte[] ToByteArray(string value, int reservedLength) => S7String.ToByteArray(value, reservedLength);
}
}

View File

@@ -50,12 +50,17 @@ namespace S7.Net.Types
numBytes += 4;
break;
case "Single":
case "Double":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
numBytes += 4;
break;
case "Double":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
numBytes += 8;
break;
default:
numBytes += GetStructSize(info.FieldType);
break;
@@ -152,28 +157,27 @@ namespace S7.Net.Types
bytes[(int)numBytes + 3]));
numBytes += 4;
break;
case "Double":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// hier auswerten
info.SetValue(structValue, Double.FromByteArray(new byte[] { bytes[(int)numBytes],
bytes[(int)numBytes + 1],
bytes[(int)numBytes + 2],
bytes[(int)numBytes + 3] }));
numBytes += 4;
break;
case "Single":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// hier auswerten
info.SetValue(structValue, Single.FromByteArray(new byte[] { bytes[(int)numBytes],
info.SetValue(structValue, Real.FromByteArray(new byte[] { bytes[(int)numBytes],
bytes[(int)numBytes + 1],
bytes[(int)numBytes + 2],
bytes[(int)numBytes + 3] }));
numBytes += 4;
break;
case "Double":
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
// hier auswerten
var data = new byte[8];
Array.Copy(bytes, (int)numBytes, data, 0, 8);
info.SetValue(structValue, LReal.FromByteArray(data));
numBytes += 8;
break;
default:
var buffer = new byte[GetStructSize(info.FieldType)];
if (buffer.Length == 0)
@@ -244,11 +248,11 @@ namespace S7.Net.Types
case "UInt32":
bytes2 = DWord.ToByteArray((UInt32)info.GetValue(structValue));
break;
case "Double":
bytes2 = Double.ToByteArray((double)info.GetValue(structValue));
break;
case "Single":
bytes2 = Single.ToByteArray((float)info.GetValue(structValue));
bytes2 = Real.ToByteArray((float)info.GetValue(structValue));
break;
case "Double":
bytes2 = LReal.ToByteArray((double)info.GetValue(structValue));
break;
}
if (bytes2 != null)

View File

@@ -0,0 +1,43 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace S7.Net.Types
{
internal static class TypeHelper
{
/// <summary>
/// Converts an array of T to an array of bytes
/// </summary>
public static byte[] ToByteArray<T>(T[] value, Func<T, byte[]> converter) where T : struct
{
var buffer = new byte[Marshal.SizeOf(default(T)) * value.Length];
var stream = new MemoryStream(buffer);
foreach (var val in value)
{
stream.Write(converter(val), 0, 4);
}
return buffer;
}
/// <summary>
/// Converts an array of T repesented as S7 binary data to an array of T
/// </summary>
public static T[] ToArray<T>(byte[] bytes, Func<byte[], T> converter) where T : struct
{
var typeSize = Marshal.SizeOf(default(T));
var entries = bytes.Length / typeSize;
var values = new T[entries];
for(int i = 0; i < entries; ++i)
{
var buffer = new byte[typeSize];
Array.Copy(bytes, i * typeSize, buffer, 0, typeSize);
values[i] = converter(buffer);
}
return values;
}
}
}