Merge branch 'develop' into patch-1

This commit is contained in:
Michael Croes
2020-09-16 22:07:15 +02:00
committed by GitHub
22 changed files with 206 additions and 150 deletions

View File

@@ -34,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

@@ -7,13 +7,15 @@ 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
{
private TestContext TestContext { get; set; }
public TestContext TestContext { get; set; }
[TestMethod]
public async Task TPKT_Read()
@@ -64,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,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;
using System.Threading.Tasks;

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();
}
@@ -931,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);
}
@@ -1026,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,7 +65,7 @@ namespace S7.Net.UnitTest
[TestClass]
public class StreamTests
{
private TestContext TestContext { get; set; }
public TestContext TestContext { get; set; }
[TestMethod]
public async Task TPKT_ReadRestrictedStreamAsync()

View File

@@ -7,7 +7,7 @@ using System.Linq;
namespace S7.Net.UnitTest.TypeTests
{
[TestClass]
public class StringExTests
public class S7StringTests
{
[TestMethod]
public void ReadEmptyStringWithZeroByteLength()
@@ -36,7 +36,7 @@ namespace S7.Net.UnitTest.TypeTests
[TestMethod]
public void ReadMalformedStringSizeLargerThanCapacity()
{
Assert.ThrowsException<PlcException>(() => StringEx.FromByteArray(new byte[] { 3, 5, 0, 1, 2 }));
Assert.ThrowsException<PlcException>(() => S7String.FromByteArray(new byte[] { 3, 5, 0, 1, 2 }));
}
[TestMethod]
@@ -102,7 +102,7 @@ namespace S7.Net.UnitTest.TypeTests
[TestMethod]
public void WriteAbcWithStringLargetThanReservedLength()
{
Assert.ThrowsException<ArgumentException>(() => StringEx.ToByteArray("Abc", 2));
Assert.ThrowsException<ArgumentException>(() => S7String.ToByteArray("Abc", 2));
}
[TestMethod]
@@ -119,16 +119,16 @@ namespace S7.Net.UnitTest.TypeTests
private static void AssertFromByteArrayEquals(string expected, params byte[] bytes)
{
var convertedString = StringEx.FromByteArray(bytes);
var convertedString = S7String.FromByteArray(bytes);
Assert.AreEqual(expected, convertedString);
}
private static void AssertToByteArrayAndBackEquals(string value, int reservedLength, params byte[] expected)
{
var convertedData = StringEx.ToByteArray(value, reservedLength);
var convertedData = S7String.ToByteArray(value, reservedLength);
CollectionAssert.AreEqual(expected, convertedData);
var convertedBack = StringEx.FromByteArray(convertedData);
var convertedBack = S7String.FromByteArray(convertedData);
Assert.AreEqual(value, convertedBack);
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -169,14 +169,14 @@
LReal,
/// <summary>
/// String variable type (variable)
/// 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
@@ -234,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

@@ -122,8 +122,8 @@ namespace S7.Net
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)
@@ -186,7 +186,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:

View File

@@ -259,8 +259,7 @@ namespace S7.Net
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken); //TODO use Async
if (s7data == null || s7data[14] != 0xff)
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
ParseDataIntoDataItems(s7data, dataItems);
}
@@ -483,10 +482,7 @@ namespace S7.Net
await stream.WriteAsync(dataToSend, 0, dataToSend.Length, cancellationToken);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken);
if (s7data == null || s7data[14] != 0xff)
{
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
}
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
{
@@ -509,10 +505,7 @@ namespace S7.Net
await stream.WriteAsync(dataToSend, 0, dataToSend.Length);
var s7data = await COTP.TSDU.ReadAsync(stream, cancellationToken);
if (s7data == null || s7data[14] != 0xff)
{
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
}
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
{

View File

@@ -398,10 +398,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)
{
@@ -483,10 +480,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)
{
@@ -524,8 +518,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

@@ -94,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 with error code {result}."));
errors.Add(new Exception($"Write of dataItem {dataItems[i]} failed: {e.Message}."));
}
}
if (errors != null)

View File

@@ -18,8 +18,8 @@ namespace S7.Net.Protocol
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);

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

@@ -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,70 +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)
{
throw new PlcException(ErrorCode.ReadData, "Malformed S7 String / too short");
}
/// <inheritdoc cref="S7String.FromByteArray(byte[])"/>
public static string FromByteArray(byte[] bytes) => S7String.FromByteArray(bytes);
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.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 (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;
}
/// <inheritdoc cref="S7String.ToByteArray(string, int)"/>
public static byte[] ToByteArray(string value, int reservedLength) => S7String.ToByteArray(value, reservedLength);
}
}