diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 16e7a33..8116e8b 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -64,77 +64,49 @@ jobs: ${{ env.NuGetDirectory }}/*.snupkg run_test: - name: test-${{ matrix.os }}-${{ matrix.test-framework }} + name: test-${{ matrix.os }} runs-on: ${{ matrix.os }} env: configuration: Release artifacts: ${{ github.workspace }}/artifacts strategy: matrix: - os: [windows-latest, ubuntu-22.04, macos-latest] - test-framework: [net6.0, net7.0] - include: - - os: ubuntu-22.04 - test-framework: net6.0 - installSnap7: true - dotnet-sdk: '6.x' - - os: ubuntu-22.04 - test-framework: net7.0 - installSnap7: true - dotnet-sdk: '7.x' - - os: macos-latest - test-framework: net6.0 - installSnap7: true - dotnet-sdk: '6.x' - - os: macos-latest - test-framework: net7.0 - installSnap7: true - dotnet-sdk: '7.x' - - os: windows-latest - test-framework: net6.0 - dotnet-sdk: '6.x' - - os: windows-latest - test-framework: net7.0 - dotnet-sdk: '7.x' - - os: windows-latest - test-framework: net462 - dotnet-sdk: '7.x' + os: [windows-latest, ubuntu-20.04, macos-latest] fail-fast: false steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Snap7 Linux - if: ${{ matrix.installSnap7 && matrix.os == 'ubuntu-20.04' }} + if: ${{ matrix.os == 'ubuntu-20.04' }} run: | sudo add-apt-repository ppa:gijzelaar/snap7 sudo apt-get update sudo apt-get install libsnap7-1 libsnap7-dev - name: Install Snap7 MacOs - if: ${{ matrix.installSnap7 && matrix.os == 'macos-latest' }} + if: ${{ matrix.os == 'macos-latest' }} run: | brew install snap7 - name: Setup Dotnet - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ matrix.dotnet-sdk }} + dotnet-version: | + 6.x + 7.x - name: Nuget Cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.nuget/packages # Look to see if there is a cache hit for the corresponding requirements file - key: ${{ runner.os }}-${{ matrix.test-framework }}-nuget-${{ hashFiles('**/packages.lock.json') }} + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} restore-keys: | - ${{ runner.os }}-${{ matrix.test-framework }}-nuget - - - name: Restore - run: dotnet restore S7.Net.UnitTest + ${{ runner.os }}-nuget - name: Test - run: dotnet test --no-restore --nologo --verbosity normal --logger GitHubActions --framework ${{ matrix.test-framework }} + run: dotnet test --nologo --verbosity normal --logger GitHubActions deploy: # Publish only when creating a GitHub Release diff --git a/S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs b/S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs new file mode 100644 index 0000000..15568d5 --- /dev/null +++ b/S7.Net.UnitTest/CommunicationTests/ConnectionOpen.cs @@ -0,0 +1,28 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using S7.Net.Protocol; + +namespace S7.Net.UnitTest.CommunicationTests; + +[TestClass] +public class ConnectionOpen +{ + [TestMethod] + public async Task Does_Not_Throw() + { + var cs = new CommunicationSequence { + ConnectionOpenTemplates.ConnectionRequestConfirm, + ConnectionOpenTemplates.CommunicationSetup + }; + + async Task Client(int port) + { + var conn = new Plc(IPAddress.Loopback.ToString(), port, new TsapPair(new Tsap(1, 2), new Tsap(3, 4))); + await conn.OpenAsync(); + conn.Close(); + } + + await Task.WhenAll(cs.Serve(out var port), Client(port)); + } +} \ No newline at end of file diff --git a/S7.Net.UnitTest/CommunicationTests/ConnectionOpenTemplates.cs b/S7.Net.UnitTest/CommunicationTests/ConnectionOpenTemplates.cs new file mode 100644 index 0000000..cb9cc0a --- /dev/null +++ b/S7.Net.UnitTest/CommunicationTests/ConnectionOpenTemplates.cs @@ -0,0 +1,107 @@ +namespace S7.Net.UnitTest.CommunicationTests; + +internal static class ConnectionOpenTemplates +{ + public static RequestResponsePair ConnectionRequestConfirm { get; } = new RequestResponsePair( + """ + // TPKT + 03 // Version + 00 // Reserved + 00 16 // Length + + // CR + 11 // Number of bytes following + E0 // CR / Credit + 00 00 // Destination reference, unused + __ __ // Source reference, unused + 00 // Class / Option + + // Source TSAP + C1 // Parameter code + 02 // Parameter length + TSAP_SRC_CHAN // Channel + TSAP_SRC_POS // Position + + // Destination TSAP + C2 // Parameter code + 02 // Parameter length + TSAP_DEST_CHAN // Channel + TSAP_DEST_POS // Position + + // PDU Size parameter + C0 // Parameter code + 01 // Parameter length + 0A // 1024 byte PDU (2 ^ 10) + """, + """ + // TPKT + 03 // Version + 00 // Reserved + 00 0B // Length + + // CC + 06 // Length + D0 // CC / Credit + 00 00 // Destination reference + 00 00 // Source reference + 00 // Class / Option + """ + ); + + public static RequestResponsePair CommunicationSetup { get; } = new RequestResponsePair( + """ + // TPKT + 03 // Version + 00 // Reserved + 00 19 // Length + + // Data header + 02 // Length + F0 // Data identifier + 80 // PDU number and end of transmission + + // S7 header + 32 // Protocol ID + 01 // Message type job request + 00 00 // Reserved + PDU1 PDU2 // PDU reference + 00 08 // Parameter length (Communication Setup) + 00 00 // Data length + + // Communication Setup + F0 // Function code + 00 // Reserved + 00 03 // Max AMQ caller + 00 03 // Max AMQ callee + 03 C0 // PDU size (960) + """, + """ + // TPKT + 03 // Version + 00 // Reserved + 00 1B // Length + + // Data header + 02 // Length + F0 // Data identifier + 80 // PDU number and end of transmission + + // S7 header + 32 // Protocol ID + 03 // Message type ack data + 00 00 // Reserved + PDU1 PDU2 // PDU reference + 00 08 // Parameter length (Communication Setup) + 00 00 // Data length + 00 // Error class + 00 // Error code + + // Communication Setup + F0 // Function code + 00 // Reserved + 00 03 // Max AMQ caller + 00 03 // Max AMQ callee + 03 C0 // PDU size (960) + """ + ); +} \ No newline at end of file diff --git a/S7.Net.UnitTest/CommunicationTests/ReadPlcStatus.cs b/S7.Net.UnitTest/CommunicationTests/ReadPlcStatus.cs new file mode 100644 index 0000000..13cc733 --- /dev/null +++ b/S7.Net.UnitTest/CommunicationTests/ReadPlcStatus.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using S7.Net.Protocol; + +namespace S7.Net.UnitTest.CommunicationTests; + +[TestClass] +public class ReadPlcStatus +{ + [TestMethod] + public async Task Read_Status_Run() + { + var cs = new CommunicationSequence { + ConnectionOpenTemplates.ConnectionRequestConfirm, + ConnectionOpenTemplates.CommunicationSetup, + { + """ + // TPKT + 03 00 00 21 + + // COTP + 02 f0 80 + + // S7 SZL read + 32 07 00 00 PDU1 PDU2 00 08 00 08 00 01 12 04 11 44 + 01 00 ff 09 00 04 04 24 00 00 + """, + """ + // TPKT + 03 00 00 3d + + // COTP + 02 f0 80 + + // S7 SZL response + 32 07 00 00 PDU1 PDU2 00 0c 00 20 00 01 12 08 12 84 + 01 02 00 00 00 00 ff 09 00 1c 04 24 00 00 00 14 + 00 01 51 44 ff 08 00 00 00 00 00 00 00 00 14 08 + 20 12 05 28 34 94 + """ + } + }; + + async Task Client(int port) + { + var conn = new Plc(IPAddress.Loopback.ToString(), port, new TsapPair(new Tsap(1, 2), new Tsap(3, 4))); + await conn.OpenAsync(); + var status = await conn.ReadStatusAsync(); + + Assert.AreEqual(0x08, status); + conn.Close(); + } + + await Task.WhenAll(cs.Serve(out var port), Client(port)); + } +} \ No newline at end of file diff --git a/S7.Net.UnitTest/Framework/IsExternalInit.cs b/S7.Net.UnitTest/Framework/IsExternalInit.cs new file mode 100644 index 0000000..f70856c --- /dev/null +++ b/S7.Net.UnitTest/Framework/IsExternalInit.cs @@ -0,0 +1,7 @@ +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + [EditorBrowsable(EditorBrowsableState.Never)] + internal record IsExternalInit; +} \ No newline at end of file diff --git a/S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs b/S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs new file mode 100644 index 0000000..c3e4488 --- /dev/null +++ b/S7.Net.UnitTest/Infrastructure/CommunicationSequence.cs @@ -0,0 +1,82 @@ +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace S7.Net.UnitTest; + +internal class CommunicationSequence : IEnumerable +{ + private readonly List _requestResponsePairs = new List(); + + public void Add(RequestResponsePair requestResponsePair) + { + _requestResponsePairs.Add(requestResponsePair); + } + + public void Add(string requestPattern, string responsePattern) + { + _requestResponsePairs.Add(new RequestResponsePair(requestPattern, responsePattern)); + } + + public IEnumerator GetEnumerator() + { + return _requestResponsePairs.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public Task Serve(out int port) + { + var socket = CreateBoundListenSocket(out port); + socket.Listen(0); + + async Task Impl() + { + await Task.Yield(); + var socketIn = socket.Accept(); + + var buffer = ArrayPool.Shared.Rent(1024); + try + { + foreach (var pair in _requestResponsePairs) + { + var bytesReceived = socketIn.Receive(buffer, SocketFlags.None); + + var received = buffer.Take(bytesReceived).ToArray(); + Console.WriteLine($"=> {BitConverter.ToString(received)}"); + + var response = Responder.Respond(pair, received); + + Console.WriteLine($"<= {BitConverter.ToString(response)}"); + socketIn.Send(response); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + socketIn.Close(); + } + + return Impl(); + } + + private static Socket CreateBoundListenSocket(out int port) + { + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); + + socket.Bind(endpoint); + + var localEndpoint = (IPEndPoint)socket.LocalEndPoint!; + port = localEndpoint.Port; + + return socket; + } +} diff --git a/S7.Net.UnitTest/Infrastructure/RequestResponsePair.cs b/S7.Net.UnitTest/Infrastructure/RequestResponsePair.cs new file mode 100644 index 0000000..390ee62 --- /dev/null +++ b/S7.Net.UnitTest/Infrastructure/RequestResponsePair.cs @@ -0,0 +1,3 @@ +namespace S7.Net.UnitTest; + +internal record RequestResponsePair(string RequestPattern, string ResponsePattern); diff --git a/S7.Net.UnitTest/Infrastructure/Responder.cs b/S7.Net.UnitTest/Infrastructure/Responder.cs new file mode 100644 index 0000000..0fa15ea --- /dev/null +++ b/S7.Net.UnitTest/Infrastructure/Responder.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; + +namespace S7.Net.UnitTest; + +internal static class Responder +{ + private const string Comment = "//"; + private static char[] Space = " ".ToCharArray(); + + public static byte[] Respond(RequestResponsePair pair, byte[] request) + { + var offset = 0; + var matches = new Dictionary(); + var res = new List(); + using var requestReader = new StringReader(pair.RequestPattern); + + string line; + while ((line = requestReader.ReadLine()) != null) + { + var tokens = line.Split(Space, StringSplitOptions.RemoveEmptyEntries); + foreach (var token in tokens) + { + if (token.StartsWith(Comment)) break; + + if (offset >= request.Length) + { + throw new Exception("Request pattern has more data than request."); + } + + var received = request[offset]; + + if (token.Length == 2 && byte.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)) + { + // Number, exact match + if (value != received) + { + throw new Exception($"Incorrect data at offset {offset}. Expected {value:X2}, received {received:X2}."); + } + } + else + { + matches[token] = received; + } + + offset++; + } + } + + if (offset != request.Length) throw new Exception("Request contained more data than request pattern."); + + using var responseReader = new StringReader(pair.ResponsePattern); + while ((line = responseReader.ReadLine()) != null) + { + var tokens = line.Split(Space, StringSplitOptions.RemoveEmptyEntries); + foreach (var token in tokens) + { + if (token.StartsWith(Comment)) break; + + if (token.Length == 2 && byte.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)) + { + res.Add(value); + } + else + { + if (!matches.TryGetValue(token, out var match)) + { + throw new Exception($"Unmatched token '{token}' in response."); + } + + res.Add(match); + } + } + } + + return res.ToArray(); + } +} \ No newline at end of file diff --git a/S7.Net.UnitTest/S7.Net.UnitTest.csproj b/S7.Net.UnitTest/S7.Net.UnitTest.csproj index eb63e48..3b43284 100644 --- a/S7.Net.UnitTest/S7.Net.UnitTest.csproj +++ b/S7.Net.UnitTest/S7.Net.UnitTest.csproj @@ -1,8 +1,14 @@  - - net452;netcoreapp3.1;net5.0 + + net6.0;net7.0 + + + net6.0;net7.0;net462 + + + latest true Properties\S7.Net.snk false @@ -11,7 +17,10 @@ - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/S7.Net/COTP.cs b/S7.Net/COTP.cs index 3e5abbb..4f8b1cd 100644 --- a/S7.Net/COTP.cs +++ b/S7.Net/COTP.cs @@ -55,6 +55,7 @@ namespace S7.Net /// See: https://tools.ietf.org/html/rfc905 /// /// The socket to read from + /// A cancellation token that can be used to cancel the asynchronous operation. /// COTP DPDU instance public static async Task ReadAsync(Stream stream, CancellationToken cancellationToken) { @@ -89,6 +90,7 @@ namespace S7.Net /// See: https://tools.ietf.org/html/rfc905 /// /// The stream to read from + /// A cancellation token that can be used to cancel the asynchronous operation. /// Data in TSDU public static async Task ReadAsync(Stream stream, CancellationToken cancellationToken) { diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index 69fc890..fa01672 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -9,29 +9,97 @@ namespace S7.Net { public partial class Plc { - /// - /// Creates the header to read bytes from the PLC - /// - /// - /// - private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount = 1) + private static void WriteTpktHeader(System.IO.MemoryStream stream, int length) { - //header size = 19 bytes stream.Write(new byte[] { 0x03, 0x00 }); - //complete package size - stream.Write(Int.ToByteArray((short)(19 + (12 * amount)))); - stream.Write(new byte[] { 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 }); - //data part size - stream.Write(Word.ToByteArray((ushort)(2 + (amount * 12)))); - stream.Write(new byte[] { 0x00, 0x00, 0x04 }); + stream.Write(Word.ToByteArray((ushort) length)); + } + + private static void WriteDataHeader(System.IO.MemoryStream stream) + { + stream.Write(new byte[] { 0x02, 0xf0, 0x80 }); + } + + private static void WriteS7Header(System.IO.MemoryStream stream, byte messageType, int parameterLength, int dataLength) + { + stream.WriteByte(0x32); // S7 protocol ID + stream.WriteByte(messageType); // Message type + stream.Write(new byte[] { 0x00, 0x00 }); // Reserved + stream.Write(new byte[] { 0x00, 0x00 }); // PDU ref + stream.Write(Word.ToByteArray((ushort) parameterLength)); + stream.Write(Word.ToByteArray((ushort) dataLength)); + } + + /// + /// Creates the header to read bytes from the PLC. + /// + /// The stream to write to. + /// The number of items to read. + private static void WriteReadHeader(System.IO.MemoryStream stream, int amount = 1) + { + // Header size 19, 12 bytes per item + WriteTpktHeader(stream, 19 + 12 * amount); + WriteDataHeader(stream); + WriteS7Header(stream, 0x01, 2 + 12 * amount, 0); + // Function code: read request + stream.WriteByte(0x04); //amount of requests stream.WriteByte((byte)amount); } + private static void WriteUserDataHeader(System.IO.MemoryStream stream, int parameterLength, int dataLength) + { + const byte s7MessageTypeUserData = 0x07; + + WriteTpktHeader(stream, 17 + parameterLength + dataLength); + WriteDataHeader(stream); + WriteS7Header(stream, s7MessageTypeUserData, parameterLength, dataLength); + } + + private static void WriteSzlReadRequest(System.IO.MemoryStream stream, ushort szlId, ushort szlIndex) + { + WriteUserDataHeader(stream, 8, 8); + + // Parameter + const byte szlMethodRequest = 0x11; + const byte szlTypeRequest = 0b100; + const byte szlFunctionGroupCpuFunctions = 0b100; + const byte subFunctionReadSzl = 0x01; + + // Parameter head + stream.Write(new byte[] { 0x00, 0x01, 0x12 }); + // Parameter length + stream.WriteByte(0x04); + // Method + stream.WriteByte(szlMethodRequest); + // Type / function group + stream.WriteByte(szlTypeRequest << 4 | szlFunctionGroupCpuFunctions); + // Subfunction + stream.WriteByte(subFunctionReadSzl); + // Sequence number + stream.WriteByte(0); + + // Data + const byte success = 0xff; + const byte transportSizeOctetString = 0x09; + + // Return code + stream.WriteByte(success); + // Transport size + stream.WriteByte(transportSizeOctetString); + // Length + stream.Write(Word.ToByteArray(4)); + // SZL-ID + stream.Write(Word.ToByteArray(szlId)); + // SZL-Index + stream.Write(Word.ToByteArray(szlIndex)); + } + /// /// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType), /// the address of the memory, the address of the byte and the bytes count. /// + /// The stream to write the read data request to. /// MemoryType (DB, Timer, Counter, etc.) /// Address of the memory to be read /// Start address of the byte @@ -262,7 +330,7 @@ namespace S7.Net int packageSize = 19 + (dataItems.Count * 12); var package = new System.IO.MemoryStream(packageSize); - BuildHeaderPackage(package, dataItems.Count); + WriteReadHeader(package, dataItems.Count); foreach (var dataItem in dataItems) { @@ -271,5 +339,15 @@ namespace S7.Net return package.ToArray(); } + + private static byte[] BuildSzlReadRequestPackage(ushort szlId, ushort szlIndex) + { + var stream = new System.IO.MemoryStream(); + + WriteSzlReadRequest(stream, szlId, szlIndex); + stream.SetLength(stream.Position); + + return stream.ToArray(); + } } } diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index 36fef8c..eb49e5d 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -312,6 +312,20 @@ namespace S7.Net return dataItems; } + /// + /// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type. + /// + /// 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. + /// A task that represents the asynchronous operation, with it's result set to the current PLC status on completion. + public async Task ReadStatusAsync(CancellationToken cancellationToken = default) + { + var dataToSend = BuildSzlReadRequestPackage(0x0424, 0); + var s7data = await RequestTsduAsync(dataToSend, cancellationToken); + + return (byte) (s7data[37] & 0x0f); + } + /// /// 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. @@ -428,7 +442,6 @@ namespace S7.Net /// /// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. - /// If the write was not successful, check or . /// /// Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// Value to be written to the PLC @@ -507,6 +520,7 @@ namespace S7.Net /// 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. /// Start byte address. If you want to read DB1.DBW200, this is 200. /// Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion. + /// A cancellation token that can be used to cancel the asynchronous operation. /// A task that represents the asynchronous write operation. private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, ReadOnlyMemory value, CancellationToken cancellationToken) { diff --git a/S7.Net/PlcSynchronous.cs b/S7.Net/PlcSynchronous.cs index 4a3aaad..1b3af97 100644 --- a/S7.Net/PlcSynchronous.cs +++ b/S7.Net/PlcSynchronous.cs @@ -289,7 +289,6 @@ namespace S7.Net /// /// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. - /// If the write was not successful, check or . /// /// Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc. /// Value to be written to the PLC @@ -329,7 +328,7 @@ namespace S7.Net const int packageSize = 19 + 12; // 19 header + 12 for 1 request var dataToSend = new byte[packageSize]; var package = new MemoryStream(dataToSend); - BuildHeaderPackage(package); + WriteReadHeader(package); // package.Add(0x02); // datenart BuildReadDataRequestPackage(package, dataType, db, startByteAdr, buffer.Length); @@ -474,7 +473,7 @@ namespace S7.Net int packageSize = 19 + (dataItems.Count * 12); var dataToSend = new byte[packageSize]; var package = new MemoryStream(dataToSend); - BuildHeaderPackage(package, dataItems.Count); + WriteReadHeader(package, dataItems.Count); // package.Add(0x02); // datenart foreach (var dataItem in dataItems) { @@ -493,6 +492,18 @@ namespace S7.Net } } + /// + /// Read the current status from the PLC. A value of 0x08 indicates the PLC is in run status, regardless of the PLC type. + /// + /// The current PLC status. + public byte ReadStatus() + { + var dataToSend = BuildSzlReadRequestPackage(0x0424, 0); + var s7data = RequestTsdu(dataToSend); + + return (byte) (s7data[37] & 0x0f); + } + private byte[] RequestTsdu(byte[] requestData) => RequestTsdu(requestData, 0, requestData.Length); private byte[] RequestTsdu(byte[] requestData, int offset, int length) diff --git a/S7.Net/S7.Net.csproj b/S7.Net/S7.Net.csproj index d41f08f..a5643f5 100644 --- a/S7.Net/S7.Net.csproj +++ b/S7.Net/S7.Net.csproj @@ -1,7 +1,7 @@  - net452;netstandard2.0;netstandard1.3;net5.0 + net452;net462;netstandard2.0;netstandard1.3;net5.0;net6.0;net7.0 true Properties\S7.Net.snk S7.Net.UnitTest @@ -15,19 +15,20 @@ git PLC Siemens Communication S7 Derek Heiser 2015 - 8.0 + latest Enable portable true snupkg true + $(NoWarn);CS1591;NETSDK1138 - + NET_FULL - + diff --git a/S7.Net/StreamExtensions.cs b/S7.Net/StreamExtensions.cs index 749b915..504e4dc 100644 --- a/S7.Net/StreamExtensions.cs +++ b/S7.Net/StreamExtensions.cs @@ -39,6 +39,7 @@ namespace S7.Net /// the buffer to read into /// the offset in the buffer to read into /// the amount of bytes to read into the buffer + /// A cancellation token that can be used to cancel the asynchronous operation. /// returns the amount of read bytes public static async Task ReadExactAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken) { diff --git a/S7.Net/TPKT.cs b/S7.Net/TPKT.cs index a311dce..f24b4c0 100644 --- a/S7.Net/TPKT.cs +++ b/S7.Net/TPKT.cs @@ -29,6 +29,7 @@ namespace S7.Net /// Reads a TPKT from the socket Async /// /// The stream to read from + /// A cancellation token that can be used to cancel the asynchronous operation. /// Task TPKT Instace public static async Task ReadAsync(Stream stream, CancellationToken cancellationToken) { diff --git a/S7.Net/Types/Class.cs b/S7.Net/Types/Class.cs index 819b626..be84c2b 100644 --- a/S7.Net/Types/Class.cs +++ b/S7.Net/Types/Class.cs @@ -64,7 +64,8 @@ namespace S7.Net.Types numBytes += attribute.ReservedLengthInBytes; break; default: - var propertyClass = Activator.CreateInstance(type); + var propertyClass = Activator.CreateInstance(type) ?? + throw new ArgumentException($"Failed to create instance of type {type}.", nameof(type)); numBytes = GetClassSize(propertyClass, numBytes, true); break; } @@ -76,6 +77,8 @@ namespace S7.Net.Types /// Gets the size of the class in bytes. /// /// An instance of the class + /// The offset of the current field. + /// if this property belongs to a class being serialized as member of the class requested for serialization; otherwise, . /// the number of bytes public static double GetClassSize(object instance, double numBytes = 0.0, bool isInnerProperty = false) { @@ -84,8 +87,10 @@ namespace S7.Net.Types { if (property.PropertyType.IsArray) { - Type elementType = property.PropertyType.GetElementType(); - Array array = (Array)property.GetValue(instance, null); + Type elementType = property.PropertyType.GetElementType()!; + Array array = (Array?) property.GetValue(instance, null) ?? + throw new ArgumentException($"Property {property.Name} on {instance} must have a non-null value to get it's size.", nameof(instance)); + if (array.Length <= 0) { throw new Exception("Cannot determine size of class, because an array is defined which has no fixed size greater than zero."); @@ -199,7 +204,9 @@ namespace S7.Net.Types numBytes += sData.Length; break; default: - var propClass = Activator.CreateInstance(propertyType); + var propClass = Activator.CreateInstance(propertyType) ?? + throw new ArgumentException($"Failed to create instance of type {propertyType}.", nameof(propertyType)); + numBytes = FromBytes(propClass, bytes, numBytes); value = propClass; break; @@ -213,6 +220,8 @@ namespace S7.Net.Types /// /// The object to fill in the given array of bytes /// The array of bytes + /// The offset for the current field. + /// if this class is the type of a member of the class to be serialized; otherwise, . public static double FromBytes(object sourceClass, byte[] bytes, double numBytes = 0, bool isInnerClass = false) { if (bytes == null) @@ -223,9 +232,11 @@ namespace S7.Net.Types { if (property.PropertyType.IsArray) { - Array array = (Array)property.GetValue(sourceClass, null); + Array array = (Array?) property.GetValue(sourceClass, null) ?? + throw new ArgumentException($"Property {property.Name} on sourceClass must be an array instance.", nameof(sourceClass)); + IncrementToEven(ref numBytes); - Type elementType = property.PropertyType.GetElementType(); + Type elementType = property.PropertyType.GetElementType()!; for (int i = 0; i < array.Length && numBytes < bytes.Length; i++) { array.SetValue( @@ -320,26 +331,30 @@ namespace S7.Net.Types /// /// Creates a byte array depending on the struct type. /// - /// The struct object + /// The struct object. + /// The target byte array. + /// The offset for the current field. /// A byte array or null if fails. public static double ToBytes(object sourceClass, byte[] bytes, double numBytes = 0.0) { var properties = GetAccessableProperties(sourceClass.GetType()); foreach (var property in properties) { + var value = property.GetValue(sourceClass, null) ?? + throw new ArgumentException($"Property {property.Name} on sourceClass can't be null.", nameof(sourceClass)); + if (property.PropertyType.IsArray) { - Array array = (Array)property.GetValue(sourceClass, null); + Array array = (Array) value; IncrementToEven(ref numBytes); - Type elementType = property.PropertyType.GetElementType(); for (int i = 0; i < array.Length && numBytes < bytes.Length; i++) { - numBytes = SetBytesFromProperty(array.GetValue(i), property, bytes, numBytes); + numBytes = SetBytesFromProperty(array.GetValue(i)!, property, bytes, numBytes); } } else { - numBytes = SetBytesFromProperty(property.GetValue(sourceClass, null), property, bytes, numBytes); + numBytes = SetBytesFromProperty(value, property, bytes, numBytes); } } return numBytes; diff --git a/S7.Net/Types/DateTime.cs b/S7.Net/Types/DateTime.cs index 9cafa67..a685a21 100644 --- a/S7.Net/Types/DateTime.cs +++ b/S7.Net/Types/DateTime.cs @@ -141,7 +141,7 @@ namespace S7.Net.Types /// Converts an array of values to a byte array. /// /// The DateTime values to convert. - /// A byte array containing the S7 date time representations of . + /// A byte array containing the S7 date time representations of . /// Thrown when any value of /// is before /// or after . diff --git a/S7.Net/Types/S7String.cs b/S7.Net/Types/S7String.cs index 46c4808..d45c534 100644 --- a/S7.Net/Types/S7String.cs +++ b/S7.Net/Types/S7String.cs @@ -8,17 +8,17 @@ namespace S7.Net.Types /// An S7 String has a preceeding 2 byte header containing its capacity and length /// public static class S7String - { - private static Encoding stringEncoding = Encoding.ASCII; - + { + private static Encoding stringEncoding = Encoding.ASCII; + /// /// The Encoding used when serializing and deserializing S7String (Encoding.ASCII by default) /// - /// StringEncoding must not be null - public static Encoding StringEncoding - { - get => stringEncoding; - set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding)); + /// StringEncoding must not be null + public static Encoding StringEncoding + { + get => stringEncoding; + set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding)); } /// @@ -58,7 +58,7 @@ namespace S7.Net.Types /// The string to convert to byte array. /// The length (in characters) allocated in PLC for the string. /// A containing the string header and string value with a maximum length of + 2. - public static byte[] ToByteArray(string value, int reservedLength) + public static byte[] ToByteArray(string? value, int reservedLength) { if (value is null) { diff --git a/S7.Net/Types/S7WString.cs b/S7.Net/Types/S7WString.cs index 8d8aabf..001c7ef 100644 --- a/S7.Net/Types/S7WString.cs +++ b/S7.Net/Types/S7WString.cs @@ -48,7 +48,7 @@ namespace S7.Net.Types /// The string to convert to byte array. /// The length (in characters) allocated in PLC for the string. /// A containing the string header and string value with a maximum length of + 4. - public static byte[] ToByteArray(string value, int reservedLength) + public static byte[] ToByteArray(string? value, int reservedLength) { if (value is null) { diff --git a/S7.Net/Types/String.cs b/S7.Net/Types/String.cs index 3917635..b0ccc19 100644 --- a/S7.Net/Types/String.cs +++ b/S7.Net/Types/String.cs @@ -12,13 +12,15 @@ /// The amount of bytes reserved for the in the PLC. public static byte[] ToByteArray(string value, int reservedLength) { - var length = value?.Length; - if (length > reservedLength) length = reservedLength; var bytes = new byte[reservedLength]; + if (value == null) return bytes; - if (length == null || length == 0) return bytes; + var length = value.Length; + if (length == 0) return bytes; - System.Text.Encoding.ASCII.GetBytes(value, 0, length.Value, bytes, 0); + if (length > reservedLength) length = reservedLength; + + System.Text.Encoding.ASCII.GetBytes(value, 0, length, bytes, 0); return bytes; } diff --git a/S7.Net/Types/Struct.cs b/S7.Net/Types/Struct.cs index 8fe67fa..136638a 100644 --- a/S7.Net/Types/Struct.cs +++ b/S7.Net/Types/Struct.cs @@ -99,8 +99,8 @@ namespace S7.Net.Types int bytePos = 0; int bitPos = 0; double numBytes = 0.0; - object structValue = Activator.CreateInstance(structType); - + object structValue = Activator.CreateInstance(structType) ?? + throw new ArgumentException($"Failed to create an instance of the type {structType}.", nameof(structType)); var infos = structValue.GetType() #if NETSTANDARD1_3 @@ -270,6 +270,14 @@ namespace S7.Net.Types foreach (var info in infos) { + static TValue GetValueOrThrow(FieldInfo fi, object structValue) where TValue : struct + { + var value = fi.GetValue(structValue) as TValue? ?? + throw new ArgumentException($"Failed to convert value of field {fi.Name} of {structValue} to type {typeof(TValue)}"); + + return value; + } + bytes2 = null; switch (info.FieldType.Name) { @@ -277,7 +285,7 @@ namespace S7.Net.Types // get the value bytePos = (int)Math.Floor(numBytes); bitPos = (int)((numBytes - (double)bytePos) / 0.125); - if ((bool)info.GetValue(structValue)) + if (GetValueOrThrow(info, structValue)) bytes[bytePos] |= (byte)Math.Pow(2, bitPos); // is true else bytes[bytePos] &= (byte)(~(byte)Math.Pow(2, bitPos)); // is false @@ -286,26 +294,26 @@ namespace S7.Net.Types case "Byte": numBytes = (int)Math.Ceiling(numBytes); bytePos = (int)numBytes; - bytes[bytePos] = (byte)info.GetValue(structValue); + bytes[bytePos] = GetValueOrThrow(info, structValue); numBytes++; break; case "Int16": - bytes2 = Int.ToByteArray((Int16)info.GetValue(structValue)); + bytes2 = Int.ToByteArray(GetValueOrThrow(info, structValue)); break; case "UInt16": - bytes2 = Word.ToByteArray((UInt16)info.GetValue(structValue)); + bytes2 = Word.ToByteArray(GetValueOrThrow(info, structValue)); break; case "Int32": - bytes2 = DInt.ToByteArray((Int32)info.GetValue(structValue)); + bytes2 = DInt.ToByteArray(GetValueOrThrow(info, structValue)); break; case "UInt32": - bytes2 = DWord.ToByteArray((UInt32)info.GetValue(structValue)); + bytes2 = DWord.ToByteArray(GetValueOrThrow(info, structValue)); break; case "Single": - bytes2 = Real.ToByteArray((float)info.GetValue(structValue)); + bytes2 = Real.ToByteArray(GetValueOrThrow(info, structValue)); break; case "Double": - bytes2 = LReal.ToByteArray((double)info.GetValue(structValue)); + bytes2 = LReal.ToByteArray(GetValueOrThrow(info, structValue)); break; case "String": S7StringAttribute? attribute = info.GetCustomAttributes().SingleOrDefault(); @@ -314,8 +322,8 @@ namespace S7.Net.Types bytes2 = attribute.Type switch { - S7StringType.S7String => S7String.ToByteArray((string)info.GetValue(structValue), attribute.ReservedLength), - S7StringType.S7WString => S7WString.ToByteArray((string)info.GetValue(structValue), attribute.ReservedLength), + S7StringType.S7String => S7String.ToByteArray((string?)info.GetValue(structValue), attribute.ReservedLength), + S7StringType.S7WString => S7WString.ToByteArray((string?)info.GetValue(structValue), attribute.ReservedLength), _ => throw new ArgumentException("Please use a valid string type for the S7StringAttribute") }; break;