diff --git a/S7.Net.UnitTest/ConnectionRequestTest.cs b/S7.Net.UnitTest/ConnectionRequestTest.cs index 440a962..ec17125 100644 --- a/S7.Net.UnitTest/ConnectionRequestTest.cs +++ b/S7.Net.UnitTest/ConnectionRequestTest.cs @@ -9,52 +9,52 @@ namespace S7.Net.UnitTest [TestMethod] public void Test_ConnectionRequest_S7_200() { - CollectionAssert.AreEqual(MakeConnectionRequest(16, 0, 16, 0), - ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7200, 0, 0)); + CollectionAssert.AreEqual(MakeConnectionRequest(16, 0, 16, 1), + ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7200, 0, 0))); } [TestMethod] public void Test_ConnectionRequest_S7_300() { CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0), - ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7300, 0, 0)); + ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 0, 0))); CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1), - ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7300, 0, 1)); + ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 0, 1))); CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33), - ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7300, 1, 1)); + ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7300, 1, 1))); } [TestMethod] public void Test_ConnectionRequest_S7_400() { CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0), - ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7400, 0, 0)); + ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 0, 0))); CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1), - ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7400, 0, 1)); + ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 0, 1))); CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33), - ConnectionRequest.GetCOTPConnectionRequest(CpuType.S7400, 1, 1)); + ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S7400, 1, 1))); } [TestMethod] public void Test_ConnectionRequest_S7_1200() { CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0), - ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71200, 0, 0)); + ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 0, 0))); CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1), - ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71200, 0, 1)); + ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 0, 1))); CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33), - ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71200, 1, 1)); + ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71200, 1, 1))); } [TestMethod] public void Test_ConnectionRequest_S7_1500() { - CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 0), - ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 0, 0)); - CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 1), - ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 0, 1)); - CollectionAssert.AreEqual(MakeConnectionRequest(0x10, 0x2, 3, 33), - ConnectionRequest.GetCOTPConnectionRequest(CpuType.S71500, 1, 1)); + CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 0), + ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 0, 0))); + CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 1), + ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 0, 1))); + CollectionAssert.AreEqual(MakeConnectionRequest(1, 0, 3, 33), + ConnectionRequest.GetCOTPConnectionRequest(TsapPair.GetDefaultTsapPair(CpuType.S71500, 1, 1))); } private static byte[] MakeConnectionRequest(byte sourceTsap1, byte sourceTsap2, byte destTsap1, byte destTsap2) @@ -63,7 +63,7 @@ namespace S7.Net.UnitTest { 3, 0, 0, 22, //TPKT 17, //COTP Header Length - 224, //Connect Request + 224, //Connect Request 0, 0, //Destination Reference 0, 46, //Source Reference 0, //Flags diff --git a/S7.Net.UnitTest/S7NetTestsAsync.cs b/S7.Net.UnitTest/S7NetTestsAsync.cs index cb63660..5ebbf4a 100644 --- a/S7.Net.UnitTest/S7NetTestsAsync.cs +++ b/S7.Net.UnitTest/S7NetTestsAsync.cs @@ -949,6 +949,39 @@ namespace S7.Net.UnitTest // 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."); } + + /// + /// Write a large amount of data and test cancellation + /// + [TestMethod] + public async Task Test_Async_ParseDataIntoDataItemsAlignment() + { + Assert.IsTrue(plc.IsConnected, "Before executing this test, the plc must be connected. Check constructor."); + + var db = 2; + // First write a sensible S7 string capacity + await plc.WriteBytesAsync(DataType.DataBlock, db, 0, new byte[] {5, 0}); + + // Read two data items, with the first having odd number of bytes (7), + // and the second has to be aligned on a even address + var dataItems = new List + { + new DataItem + { + DataType = DataType.DataBlock, + DB = db, + VarType = VarType.S7String, + Count = 5 + }, + new DataItem + { + DataType = DataType.DataBlock, + DB = db, + VarType = VarType.Word, + } + }; + await plc.ReadMultipleVarsAsync(dataItems, CancellationToken.None); + } #endregion } } diff --git a/S7.Net/Enums.cs b/S7.Net/Enums.cs index 436d94b..fc412d3 100644 --- a/S7.Net/Enums.cs +++ b/S7.Net/Enums.cs @@ -15,6 +15,11 @@ /// Logo0BA8 = 1, + /// + /// S7 200 Smart + /// + S7200Smart = 2, + /// /// S7 300 cpu type /// diff --git a/S7.Net/PLC.cs b/S7.Net/PLC.cs index 89cf62f..7cabc88 100644 --- a/S7.Net/PLC.cs +++ b/S7.Net/PLC.cs @@ -15,6 +15,11 @@ namespace S7.Net /// public partial class Plc : IDisposable { + /// + /// The default port for the S7 protocol. + /// + public const int DefaultPort = 102; + private readonly TaskQueue queue = new TaskQueue(); private const int CONNECTION_TIMED_OUT_ERROR_CODE = 10060; @@ -36,6 +41,11 @@ namespace S7.Net /// public int Port { get; } + /// + /// The TSAP addresses used during the connection request. + /// + public TsapPair TsapPair { get; set; } + /// /// CPU type of the PLC /// @@ -108,25 +118,14 @@ namespace S7.Net /// /// CpuType of the PLC (select from the enum) /// Ip address of the PLC - /// Port address of the PLC, default 102 /// rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal /// slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500. /// If you use an external ethernet card, this must be set accordingly. - public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot) + public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot) + : this(cpu, ip, DefaultPort, rack, slot) { - if (!Enum.IsDefined(typeof(CpuType), cpu)) - throw new ArgumentException($"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.", nameof(cpu)); - - if (string.IsNullOrEmpty(ip)) - throw new ArgumentException("IP address must valid.", nameof(ip)); - - CPU = cpu; - IP = ip; - Port = port; - Rack = rack; - Slot = slot; - MaxPDUSize = 240; } + /// /// Creates a PLC object with all the parameters needed for connections. /// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0. @@ -135,23 +134,51 @@ namespace S7.Net /// /// CpuType of the PLC (select from the enum) /// Ip address of the PLC + /// Port number used for the connection, default 102. /// rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal /// slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500. /// If you use an external ethernet card, this must be set accordingly. - public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot) + public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot) + : this(ip, port, TsapPair.GetDefaultTsapPair(cpu, rack, slot)) { if (!Enum.IsDefined(typeof(CpuType), cpu)) - throw new ArgumentException($"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.", nameof(cpu)); + throw new ArgumentException( + $"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.", + nameof(cpu)); + CPU = cpu; + Rack = rack; + Slot = slot; + } + + /// + /// Creates a PLC object with all the parameters needed for connections. + /// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0. + /// You need slot > 0 if you are connecting to external ethernet card (CP). + /// For S7-300 and S7-400 the default is rack = 0 and slot = 2. + /// + /// Ip address of the PLC + /// The TSAP addresses used for the connection request. + public Plc(string ip, TsapPair tsapPair) : this(ip, DefaultPort, tsapPair) + { + } + + /// + /// Creates a PLC object with all the parameters needed for connections. Use this constructor + /// if you want to manually override the TSAP addresses used during the connection request. + /// + /// Ip address of the PLC + /// Port number used for the connection, default 102. + /// The TSAP addresses used for the connection request. + public Plc(string ip, int port, TsapPair tsapPair) + { if (string.IsNullOrEmpty(ip)) throw new ArgumentException("IP address must valid.", nameof(ip)); - CPU = cpu; IP = ip; - Port = 102; - Rack = rack; - Slot = slot; + Port = port; MaxPDUSize = 240; + TsapPair = tsapPair; } /// diff --git a/S7.Net/PLCHelpers.cs b/S7.Net/PLCHelpers.cs index 5b2d77f..7715922 100644 --- a/S7.Net/PLCHelpers.cs +++ b/S7.Net/PLCHelpers.cs @@ -242,8 +242,8 @@ namespace S7.Net // next Item offset += byteCnt; - // Fill byte in response when bytecount is odd - if (dataItem.Count % 2 != 0 && (dataItem.VarType == VarType.Byte || dataItem.VarType == VarType.Bit)) + // Always align to even offset + if (offset % 2 != 0) offset++; } } diff --git a/S7.Net/PlcAsynchronous.cs b/S7.Net/PlcAsynchronous.cs index 77f757f..a58f629 100644 --- a/S7.Net/PlcAsynchronous.cs +++ b/S7.Net/PlcAsynchronous.cs @@ -60,7 +60,7 @@ namespace S7.Net private async Task RequestConnection(Stream stream, CancellationToken cancellationToken) { - var requestData = ConnectionRequest.GetCOTPConnectionRequest(CPU, Rack, Slot); + var requestData = ConnectionRequest.GetCOTPConnectionRequest(TsapPair); var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false); if (response.PDUType != COTP.PduType.ConnectionConfirmed) diff --git a/S7.Net/Protocol/ConnectionRequest.cs b/S7.Net/Protocol/ConnectionRequest.cs index 64e2b5d..9dbd396 100644 --- a/S7.Net/Protocol/ConnectionRequest.cs +++ b/S7.Net/Protocol/ConnectionRequest.cs @@ -1,68 +1,27 @@ -using System; - -namespace S7.Net.Protocol +namespace S7.Net.Protocol { internal static class ConnectionRequest { - public static byte[] GetCOTPConnectionRequest(CpuType cpu, Int16 rack, Int16 slot) + public static byte[] GetCOTPConnectionRequest(TsapPair tsapPair) { byte[] bSend1 = { 3, 0, 0, 22, //TPKT 17, //COTP Header Length - 224, //Connect Request + 224, //Connect Request 0, 0, //Destination Reference 0, 46, //Source Reference 0, //Flags 193, //Parameter Code (src-tasp) 2, //Parameter Length - 1, 0, //Source TASP + tsapPair.Local.FirstByte, tsapPair.Local.SecondByte, //Source TASP 194, //Parameter Code (dst-tasp) 2, //Parameter Length - 3, 0, //Destination TASP + tsapPair.Remote.FirstByte, tsapPair.Remote.SecondByte, //Destination TASP 192, //Parameter Code (tpdu-size) 1, //Parameter Length 10 //TPDU Size (2^10 = 1024) }; - switch (cpu) - { - case CpuType.S7200: - //S7200: Chr(193) & Chr(2) & Chr(16) & Chr(0) 'Eigener Tsap - bSend1[13] = 0x10; - bSend1[14] = 0x00; - //S7200: Chr(194) & Chr(2) & Chr(16) & Chr(0) 'Fremder Tsap - bSend1[17] = 0x10; - bSend1[18] = 0x00; - break; - case CpuType.Logo0BA8: - // These values are taken from NodeS7, it's not verified if these are - // exact requirements to connect to the Logo0BA8. - bSend1[13] = 0x01; - bSend1[14] = 0x00; - bSend1[17] = 0x01; - bSend1[18] = 0x02; - break; - case CpuType.S71200: - case CpuType.S7300: - case CpuType.S7400: - //S7300: Chr(193) & Chr(2) & Chr(1) & Chr(0) 'Eigener Tsap - bSend1[13] = 0x01; - bSend1[14] = 0x00; - //S7300: Chr(194) & Chr(2) & Chr(3) & Chr(2) 'Fremder Tsap - bSend1[17] = 0x03; - bSend1[18] = (byte) ((rack << 5) | (int) slot); - break; - case CpuType.S71500: - // Eigener Tsap - bSend1[13] = 0x10; - bSend1[14] = 0x02; - // Fredmer Tsap - bSend1[17] = 0x03; - bSend1[18] = (byte) ((rack << 5) | (int) slot); - break; - default: - throw new Exception("Wrong CPU Type Secified"); - } return bSend1; } } diff --git a/S7.Net/Protocol/Tsap.cs b/S7.Net/Protocol/Tsap.cs new file mode 100644 index 0000000..dc9d46c --- /dev/null +++ b/S7.Net/Protocol/Tsap.cs @@ -0,0 +1,31 @@ +namespace S7.Net.Protocol +{ + /// + /// Provides a representation of the Transport Service Access Point, or TSAP in short. TSAP's are used + /// to specify a client and server address. For most PLC types a default TSAP is available that allows + /// connection from any IP and can be calculated using the rack and slot numbers. + /// + public struct Tsap + { + /// + /// First byte of the TSAP. + /// + public byte FirstByte { get; set; } + + /// + /// Second byte of the TSAP. + /// + public byte SecondByte { get; set; } + + /// + /// Initializes a new instance of the class using the specified values. + /// + /// The first byte of the TSAP. + /// The second byte of the TSAP. + public Tsap(byte firstByte, byte secondByte) + { + FirstByte = firstByte; + SecondByte = secondByte; + } + } +} \ No newline at end of file diff --git a/S7.Net/Protocol/TsapPair.cs b/S7.Net/Protocol/TsapPair.cs new file mode 100644 index 0000000..e54fc32 --- /dev/null +++ b/S7.Net/Protocol/TsapPair.cs @@ -0,0 +1,96 @@ +using System; + +namespace S7.Net.Protocol +{ + /// + /// Implements a pair of TSAP addresses used to connect to a PLC. + /// + public class TsapPair + { + /// + /// The local . + /// + public Tsap Local { get; set; } + + /// + /// The remote + /// + public Tsap Remote { get; set; } + + /// + /// Initializes a new instance of the class using the specified local and + /// remote TSAP. + /// + /// The local TSAP. + /// The remote TSAP. + public TsapPair(Tsap local, Tsap remote) + { + Local = local; + Remote = remote; + } + + /// + /// Builds a that can be used to connect to a PLC using the default connection + /// addresses. + /// + /// + /// The remote TSAP is constructed using new Tsap(0x03, (byte) ((rack << 5) | slot)). + /// + /// The CPU type of the PLC. + /// The rack of the PLC's network card. + /// The slot of the PLC's network card. + /// A TSAP pair that matches the given parameters. + /// The is invalid. + /// + /// -or- + /// + /// The parameter is less than 0. + /// + /// -or- + /// + /// The parameter is greater than 15. + /// + /// -or- + /// + /// The parameter is less than 0. + /// + /// -or- + /// + /// The parameter is greater than 15. + public static TsapPair GetDefaultTsapPair(CpuType cpuType, int rack, int slot) + { + if (rack < 0) throw InvalidRackOrSlot(rack, nameof(rack), "minimum", 0); + if (rack > 0x0F) throw InvalidRackOrSlot(rack, nameof(rack), "maximum", 0x0F); + + if (slot < 0) throw InvalidRackOrSlot(slot, nameof(slot), "minimum", 0); + if (slot > 0x0F) throw InvalidRackOrSlot(slot, nameof(slot), "maximum", 0x0F); + + switch (cpuType) + { + case CpuType.S7200: + return new TsapPair(new Tsap(0x10, 0x00), new Tsap(0x10, 0x01)); + case CpuType.Logo0BA8: + // The actual values are probably on a per-project basis + return new TsapPair(new Tsap(0x01, 0x00), new Tsap(0x01, 0x02)); + case CpuType.S7200Smart: + case CpuType.S71200: + case CpuType.S71500: + case CpuType.S7300: + case CpuType.S7400: + // Testing with S7 1500 shows only the remote TSAP needs to match. This might differ for other + // PLC types. + return new TsapPair(new Tsap(0x01, 0x00), new Tsap(0x03, (byte) ((rack << 5) | slot))); + default: + throw new ArgumentOutOfRangeException(nameof(cpuType), "Invalid CPU Type specified"); + } + } + + private static ArgumentOutOfRangeException InvalidRackOrSlot(int value, string name, string extrema, + int extremaValue) + { + return new ArgumentOutOfRangeException(name, + $"Invalid {name} value specified (decimal: {value}, hexadecimal: {value:X}), {extrema} value " + + $"is {extremaValue} (decimal) or {extremaValue:X} (hexadecimal)."); + } + } +} \ No newline at end of file