From ed1fe43d28e110b122006ee4fe62e27f194558e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=BA=E8=83=BD=E5=A4=A7=E7=9F=B3=E5=A4=B4?= Date: Fri, 29 Dec 2023 02:06:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=88=90=E5=8A=9F=E8=A7=A3=E6=9E=90=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E5=92=8C=E7=A1=AE=E8=AE=A4=E6=8C=87=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NewLife.Siemens/Protocols/COTP.cs | 256 ++++++++++++++++++--------- NewLife.Siemens/Protocols/PduType.cs | 14 ++ NewLife.Siemens/Protocols/S7PLC.cs | 38 ++-- NewLife.Siemens/Protocols/TLV.cs | 14 ++ NewLife.Siemens/Protocols/TPKT.cs | 21 ++- XUnitTest/COTPTests.cs | 128 ++++++++++++++ 6 files changed, 362 insertions(+), 109 deletions(-) create mode 100644 NewLife.Siemens/Protocols/PduType.cs create mode 100644 NewLife.Siemens/Protocols/TLV.cs create mode 100644 XUnitTest/COTPTests.cs diff --git a/NewLife.Siemens/Protocols/COTP.cs b/NewLife.Siemens/Protocols/COTP.cs index ff58284..fe36d2d 100644 --- a/NewLife.Siemens/Protocols/COTP.cs +++ b/NewLife.Siemens/Protocols/COTP.cs @@ -1,124 +1,210 @@ -using NewLife.Siemens.Common; +using NewLife.Data; +using NewLife.Serialization; +using NewLife.Siemens.Common; namespace NewLife.Siemens.Protocols; /// 面向连接的传输协议(Connection-Oriented Transport Protocol) -public class COTP +public class COTP : IAccessor { - /// PDU类型 - public enum PduType : Byte - { - /// 数据帧 - Data = 0xf0, + #region 属性 + /// 包类型 + public PduType Type { get; set; } - /// CR连接请求帧 - ConnectionRequest = 0xe0, + /// 目标的引用,可以认为是用来唯一标识目标 + public UInt16 Destination { get; set; } - /// CC连接确认帧 - ConnectionConfirmed = 0xd0 - } + /// 源的引用 + public UInt16 Source { get; set; } - /// - /// Describes a COTP TPDU (Transport protocol data unit) - /// - public class TPDU - { - ///// TPKT头 - //public TPKT TPkt { get; } + /// 选项 + public Byte Option { get; set; } - ///// 头部长度 - //public Byte HeaderLength { get; set; } + /// 参数集合 + public IList Parameters { get; set; } - /// 包类型 - public PduType PDUType { get; set; } + /// 编码 + public Int32 Number { get; set; } - /// 编码 - public Int32 Number { get; set; } + /// 是否最后数据单元 + public Boolean LastDataUnit { get; set; } - /// 数据 - public Byte[] Data { get; set; } + /// 数据 + public Packet Data { get; set; } + #endregion - /// 是否最后数据单元 - public Boolean LastDataUnit { get; set; } + #region 读写 + /// 解析数据 + /// + /// + public Boolean Read(Packet data) => (this as IAccessor).Read(null, data); - /// 实例化 - /// - public TPDU(TPKT tPKT) - { - //TPkt = tPKT; + /// 序列化 + /// + public Byte[] ToArray() + { + var ms = new MemoryStream(); + (this as IAccessor).Write(ms, null); - var len = tPKT.Data[0]; // Header length excluding this length byte - if (len >= 2) - { - PDUType = (PduType)tPKT.Data[1]; + return ms.ToArray(); + } + + Boolean IAccessor.Read(Stream stream, Object context) + { + var pk = context as Packet; + stream ??= pk?.GetStream(); + var reader = new Binary { Stream = stream, IsLittleEndian = false }; - // 解析不同数据帧 - if (PDUType == PduType.Data) //DT Data + // 头部长度。当前字节之后就是头部,然后是数据 + var len = reader.ReadByte(); + Type = (PduType)reader.ReadByte(); + + // 解析不同数据帧 + switch (Type) + { + case PduType.Data: { - var flags = tPKT.Data[2]; + var flags = reader.ReadByte(); Number = flags & 0x7F; LastDataUnit = (flags & 0x80) > 0; - Data = new Byte[tPKT.Data.Length - len - 1]; // substract header length byte + header length. - Array.Copy(tPKT.Data, len + 1, Data, 0, Data.Length); - return; + if (pk != null) + Data = pk.Slice(4, pk.Total - 1 - len); + else + Data = stream.ReadBytes(-1); } - //TODO: Handle other PDUTypes - } - Data = new Byte[0]; + break; + case PduType.ConnectionRequest: + case PduType.ConnectionConfirmed: + default: + { + Destination = reader.ReadUInt16(); + Source = reader.ReadUInt16(); + Option = reader.ReadByte(); + + if (stream.Position < stream.Length) + { + var list = new List(); + while (stream.Position + 3 <= stream.Length) + { + var tlv = new TLV + { + Type = reader.ReadByte(), + Length = reader.ReadByte() + }; + var buf = reader.ReadBytes(tlv.Length); + tlv.Value = tlv.Length switch + { + 1 => buf[0], + 2 => buf.ToUInt16(0, false), + 4 => buf.ToUInt32(0, false), + _ => buf, + }; + list.Add(tlv); + } + Parameters = list; + } + } + break; } - /// - /// Reads COTP TPDU (Transport protocol data unit) from the network stream - /// See: https://tools.ietf.org/html/rfc905 - /// - /// The socket to read from - /// - /// COTP DPDU instance - public static async Task ReadAsync(Stream stream, CancellationToken cancellationToken) + return true; + } + + Boolean IAccessor.Write(Stream stream, Object context) + { + switch (Type) { - var tpkt = await TPKT.ReadAsync(stream, cancellationToken).ConfigureAwait(false); - if (tpkt.Length == 0) throw new TPDUInvalidException("No protocol data received"); + case PduType.Data: + { + var len = 2; + stream.WriteByte((Byte)len); + stream.WriteByte((Byte)Type); + + var flags = (Byte)(Number & 0x7F); + if (LastDataUnit) flags |= 0x80; + stream.WriteByte(flags); - return new TPDU(tpkt); + Data?.CopyTo(stream); + } + break; + case PduType.ConnectionRequest: + { + var len = 2; + stream.WriteByte((Byte)len); + stream.WriteByte((Byte)Type); + + var flags = (Byte)(Number & 0x7F); + if (LastDataUnit) flags |= 0x80; + stream.WriteByte(flags); + + Data?.CopyTo(stream); + } + break; + case PduType.ConnectionConfirmed: + { + var len = 2; + stream.WriteByte((Byte)len); + stream.WriteByte((Byte)Type); + + var flags = (Byte)(Number & 0x7F); + if (LastDataUnit) flags |= 0x80; + stream.WriteByte(flags); + + Data?.CopyTo(stream); + } + break; + default: + break; } - /// 已重载。 - /// - public override String ToString() => $"[{PDUType}] TPDUNumber: {Number} Last: {LastDataUnit} Segment Data: {BitConverter.ToString(Data)}"; + return true; } + #endregion /// - /// Describes a COTP TSDU (Transport service data unit). One TSDU consist of 1 ore more TPDUs + /// Reads COTP TPDU (Transport protocol data unit) from the network stream + /// See: https://tools.ietf.org/html/rfc905 /// - public class TSDU + /// The socket to read from + /// + /// COTP DPDU instance + public static async Task ReadAsync(Stream stream, CancellationToken cancellationToken) { - /// - /// Reads the full COTP TSDU (Transport service data unit) - /// See: https://tools.ietf.org/html/rfc905 - /// - /// The stream to read from - /// - /// Data in TSDU - public static async Task ReadAsync(Stream stream, CancellationToken cancellationToken) - { - var segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + var tpkt = await TPKT.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + if (tpkt.Length == 0) throw new TPDUInvalidException("No protocol data received"); - if (segment.LastDataUnit) return segment.Data; + var cotp = new COTP(); + if (!cotp.Read(tpkt.Data)) throw new TPDUInvalidException("Invalid protocol data received"); - // More segments are expected, prepare a buffer to store all data - var buffer = new Byte[segment.Data.Length]; - Array.Copy(segment.Data, buffer, segment.Data.Length); + return cotp; + } + + /// 已重载。 + /// + public override String ToString() => $"[{Type}] TPDUNumber: {Number} Last: {LastDataUnit} Data[{Data?.Total}]"; - while (!segment.LastDataUnit) - { - 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); - } + /// + /// Reads the full COTP TSDU (Transport service data unit) + /// See: https://tools.ietf.org/html/rfc905 + /// + /// The stream to read from + /// + /// Data in TSDU + public static async Task ReadTsduAsync(Stream stream, CancellationToken cancellationToken) + { + var cotp = await ReadAsync(stream, cancellationToken).ConfigureAwait(false); + if (cotp.LastDataUnit) return cotp.Data; - return buffer; + while (!cotp.LastDataUnit) + { + var seg = await ReadAsync(stream, cancellationToken).ConfigureAwait(false); + if (seg != null && seg.Data != null) cotp.Data.Append(seg.Data); + + cotp = seg; } + + return cotp.Data; } + } \ No newline at end of file diff --git a/NewLife.Siemens/Protocols/PduType.cs b/NewLife.Siemens/Protocols/PduType.cs new file mode 100644 index 0000000..34ab30c --- /dev/null +++ b/NewLife.Siemens/Protocols/PduType.cs @@ -0,0 +1,14 @@ +namespace NewLife.Siemens.Protocols; + +/// PDU类型 +public enum PduType : Byte +{ + /// 数据帧 + Data = 0xf0, + + /// CR连接请求帧 + ConnectionRequest = 0xe0, + + /// CC连接确认帧 + ConnectionConfirmed = 0xd0 +} diff --git a/NewLife.Siemens/Protocols/S7PLC.cs b/NewLife.Siemens/Protocols/S7PLC.cs index 96ab30d..0b76ac8 100644 --- a/NewLife.Siemens/Protocols/S7PLC.cs +++ b/NewLife.Siemens/Protocols/S7PLC.cs @@ -1,4 +1,5 @@ using System.Net.Sockets; +using NewLife.Data; using NewLife.Siemens.Common; using NewLife.Siemens.Models; @@ -108,9 +109,9 @@ private async Task RequestConnection(Stream stream, CancellationToken cancellati var requestData = GetCOTPConnectionRequest(TSAP); var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false); - if (response.PDUType != COTP.PduType.ConnectionConfirmed) + if (response.Type != PduType.ConnectionConfirmed) { - throw new InvalidDataException($"Connection request was denied (PDUType={response.PDUType})"); + throw new InvalidDataException($"Connection request was denied (PDUType={response.Type})"); } } @@ -146,14 +147,14 @@ private Byte[] GetCOTPConnectionRequest(TsapAddress tsap) return buf; } - private async Task NoLockRequestTpduAsync(Stream stream, Byte[] requestData, CancellationToken cancellationToken = default) + private async Task NoLockRequestTpduAsync(Stream stream, Byte[] requestData, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); try { using var closeOnCancellation = cancellationToken.Register(Close); await stream.WriteAsync(requestData, 0, requestData.Length, cancellationToken).ConfigureAwait(false); - return await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + return await COTP.ReadAsync(stream, cancellationToken).ConfigureAwait(false); } catch (Exception exc) { @@ -173,14 +174,14 @@ private async Task SetupConnection(Stream stream, CancellationToken cancellation var s7data = await NoLockRequestTsduAsync(stream, setupData, 0, setupData.Length, cancellationToken) .ConfigureAwait(false); - if (s7data.Length < 2) + if (s7data.Count < 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"); - if (s7data.Length < 20) + if (s7data.Count < 20) throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup"); // TODO: check if this should not rather be UInt16. @@ -217,17 +218,13 @@ private static void BuildHeaderPackage(MemoryStream stream, Int32 amount = 1) stream.WriteByte((Byte)amount); } - private Byte[] RequestTsdu(Byte[] requestData) => RequestTsdu(requestData, 0, requestData.Length); + private Packet RequestTsdu(Byte[] requestData) => RequestTsdu(requestData, 0, requestData.Length); - private Byte[] RequestTsdu(Byte[] requestData, Int32 offset, Int32 length, CancellationToken cancellationToken = default) + private Packet RequestTsdu(Byte[] requestData, Int32 offset, Int32 length, CancellationToken cancellationToken = default) { var stream = GetStreamIfAvailable(); - return - //queue.Enqueue(() => - NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken).GetAwaiter().GetResult(); - //) - ; + return NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken).GetAwaiter().GetResult(); } #endregion @@ -268,7 +265,8 @@ private void ReadBytesWithSingleRequest(DataType dataType, Int32 db, Int32 start var s7data = RequestTsdu(dataToSend); AssertReadResponse(s7data, count); - Array.Copy(s7data, 18, buffer, offset, count); + //Array.Copy(s7data, 18, buffer, offset, count); + s7data.Slice(18).WriteTo(buffer, offset, count); } catch (Exception exc) { @@ -399,23 +397,23 @@ public static Byte[] ToByteArray(UInt16 value) return bytes; } - private static void AssertReadResponse(Byte[] s7Data, Int32 dataLength) + private static void AssertReadResponse(Packet s7Data, Int32 dataLength) { var expectedLength = dataLength + 18; PlcException NotEnoughBytes() { - return new PlcException(ErrorCode.WrongNumberReceivedBytes, $"Received {s7Data.Length} bytes: '{BitConverter.ToString(s7Data)}', expected {expectedLength} bytes."); + return new PlcException(ErrorCode.WrongNumberReceivedBytes, $"Received {s7Data.Count} bytes: '{s7Data.ToHex()}', expected {expectedLength} bytes."); } if (s7Data == null) throw new PlcException(ErrorCode.WrongNumberReceivedBytes, "No s7Data received."); - if (s7Data.Length < 15) throw NotEnoughBytes(); + if (s7Data.Count < 15) throw NotEnoughBytes(); ValidateResponseCode((ReadWriteErrorCode)s7Data[14]); - if (s7Data.Length < expectedLength) throw NotEnoughBytes(); + if (s7Data.Count < expectedLength) throw NotEnoughBytes(); } internal static void ValidateResponseCode(ReadWriteErrorCode statusCode) @@ -456,7 +454,7 @@ private Byte[] GetS7ConnectionSetup() return [3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 255, 255, 0, 8, 0, 0, 240, 0, 0, 3, 0, 3, 3, 192]; } - private async Task NoLockRequestTsduAsync(Stream stream, Byte[] requestData, Int32 offset, Int32 length, + private async Task NoLockRequestTsduAsync(Stream stream, Byte[] requestData, Int32 offset, Int32 length, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -464,7 +462,7 @@ private async Task NoLockRequestTsduAsync(Stream stream, Byte[] requestD { using var closeOnCancellation = cancellationToken.Register(Close); await stream.WriteAsync(requestData, offset, length, cancellationToken).ConfigureAwait(false); - return await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false); + return await COTP.ReadTsduAsync(stream, cancellationToken).ConfigureAwait(false); } catch (Exception exc) { diff --git a/NewLife.Siemens/Protocols/TLV.cs b/NewLife.Siemens/Protocols/TLV.cs new file mode 100644 index 0000000..2b3bd54 --- /dev/null +++ b/NewLife.Siemens/Protocols/TLV.cs @@ -0,0 +1,14 @@ +namespace NewLife.Siemens.Protocols; + +/// 数据项(类型+长度+数值) +public class TLV +{ + /// 类型 + public Byte Type { get; set; } + + /// 长度 + public Byte Length { get; set; } + + /// 数值 + public Object Value { get; set; } +} diff --git a/NewLife.Siemens/Protocols/TPKT.cs b/NewLife.Siemens/Protocols/TPKT.cs index 102801a..469db91 100644 --- a/NewLife.Siemens/Protocols/TPKT.cs +++ b/NewLife.Siemens/Protocols/TPKT.cs @@ -1,4 +1,5 @@ -using NewLife.Siemens.Common; +using NewLife.Data; +using NewLife.Siemens.Common; namespace NewLife.Siemens.Protocols; @@ -15,13 +16,13 @@ public class TPKT public Byte Version { get; set; } /// 保留 - public Byte Reserved1 { get; set; } + public Byte Reserved { get; set; } /// 长度 public UInt16 Length { get; set; } /// 数据 - public Byte[] Data { get; set; } + public Packet Data { get; set; } /// 实例化 public TPKT() { } @@ -29,11 +30,23 @@ public TPKT() { } private TPKT(Byte version, Byte reserved1, Int32 length, Byte[] data) { Version = version; - Reserved1 = reserved1; + Reserved = reserved1; Length = (UInt16)length; Data = data; } + /// 解析数据 + /// + public void Read(Packet pk) + { + var buf = pk.ReadBytes(0, 4); + Version = buf[0]; + Reserved = buf[1]; + Length = buf.ToUInt16(2, false); + + Data = pk.Slice(4, Length); + } + /// /// Reads a TPKT from the socket Async /// diff --git a/XUnitTest/COTPTests.cs b/XUnitTest/COTPTests.cs new file mode 100644 index 0000000..cb99f50 --- /dev/null +++ b/XUnitTest/COTPTests.cs @@ -0,0 +1,128 @@ +using System; +using NewLife; +using NewLife.Data; +using NewLife.Siemens.Protocols; +using Xunit; + +namespace XUnitTest; + +public class COTPTests +{ + [Fact] + public void DtTest() + { + var cotp = new COTP + { + Type = PduType.Data + }; + + var buf = cotp.ToArray(); + + var cotp2 = new COTP(); + var rs = cotp2.Read(buf); + Assert.True(rs); + Assert.Equal(cotp.Type, cotp2.Type); + } + + [Fact] + public void DecodeCR() + { + var str = "03 00 00 16 11 e0 00 00 00 01 00 c1 02 10 00 c2 02 03 00 c0 01 0a"; + var buf = str.ToHex(); + var pk = new Packet(buf); + + // 前面有TPKT头 + var tpkt = new TPKT(); + tpkt.Read(pk); + Assert.Equal(3, tpkt.Version); + Assert.Equal(0, tpkt.Reserved); + Assert.Equal(0x16, tpkt.Length); + + var cotp = new COTP(); + var rs = cotp.Read(tpkt.Data); + Assert.True(rs); + Assert.Equal(PduType.ConnectionRequest, cotp.Type); + Assert.Equal(0x0000, cotp.Destination); + Assert.Equal(0x0001, cotp.Source); + Assert.Equal(0x00, cotp.Option); + + var ps = cotp.Parameters; + Assert.NotEmpty(ps); + + Assert.Equal(0xC1, ps[0].Type); + Assert.Equal(0x1000, (UInt16)ps[0].Value); + + Assert.Equal(0xC2, ps[1].Type); + Assert.Equal(0x0300, (UInt16)ps[1].Value); + + Assert.Equal(0xC0, ps[2].Type); + Assert.Equal(0x0A, (Byte)ps[2].Value); + } + + [Fact] + public void DecodeCC() + { + var str = "03 00 00 16 11 d0 00 01 00 07 00 c0 01 0a c1 02 10 00 c2 02 03 00"; + var buf = str.ToHex(); + var pk = new Packet(buf); + + // 前面有TPKT头 + var tpkt = new TPKT(); + tpkt.Read(pk); + Assert.Equal(3, tpkt.Version); + Assert.Equal(0, tpkt.Reserved); + Assert.Equal(0x16, tpkt.Length); + + var cotp = new COTP(); + var rs = cotp.Read(tpkt.Data); + Assert.True(rs); + Assert.Equal(PduType.ConnectionConfirmed, cotp.Type); + Assert.Equal(0x0001, cotp.Destination); + Assert.Equal(0x0007, cotp.Source); + Assert.Equal(0x00, cotp.Option); + + var ps = cotp.Parameters; + Assert.NotEmpty(ps); + + Assert.Equal(0xC0, ps[0].Type); + Assert.Equal(0x0A, (Byte)ps[0].Value); + + Assert.Equal(0xC1, ps[1].Type); + Assert.Equal(0x1000, (UInt16)ps[1].Value); + + Assert.Equal(0xC2, ps[2].Type); + Assert.Equal(0x0300, (UInt16)ps[2].Value); + } + + [Fact] + public void ConnectTest() + { + var cotp = new COTP + { + Type = PduType.ConnectionRequest + }; + + var buf = cotp.ToArray(); + + var cotp2 = new COTP(); + var rs = cotp2.Read(buf); + Assert.True(rs); + Assert.Equal(cotp.Type, cotp2.Type); + } + + [Fact] + public void ConfirmedTest() + { + var cotp = new COTP + { + Type = PduType.ConnectionConfirmed + }; + + var buf = cotp.ToArray(); + + var cotp2 = new COTP(); + var rs = cotp2.Read(buf); + Assert.True(rs); + Assert.Equal(cotp.Type, cotp2.Type); + } +}