using System.Buffers.Binary; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Eip; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Wire; namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes; /// /// A CIP target on top of : answers Forward Open / Forward Close / /// Unconnected Send / Direct (bare SendRRData) / connected SendUnitData requests and counts each kind. /// internal sealed class FakeCipTarget : IAsyncDisposable { public const uint OTConnectionId = 0xC0FFEE01; private readonly List _forwardOpens = []; private readonly List _forwardCloses = []; private readonly List _unconnectedEmbedded = []; private readonly List _direct = []; private readonly List _connected = []; public FakeCipTarget() { Server.HandlerAsync = HandleAsync; } public FakeEipServer Server { get; } = new(); public int Port => Server.Port; /// Forward Open request CIP → reply CIP. Defaults to . public Func ForwardOpenReply { get; set; } = Accept; /// Reply to an ordinary service request (Direct, embedded in Unconnected Send, or connected). /// Defaults to success with two data bytes (a Vendor ID, for the keep-alive). public Func ServiceReply { get; set; } = req => [(byte)(req[0] | 0x80), 0, 0, 0, 0x01, 0x00]; /// Connected requests only: may delay, or return null to never answer. Defaults to . public Func>? ConnectedHandler { get; set; } /// Awaited before a Forward Open is answered (the request is already counted). public Func? ForwardOpenGate { get; set; } /// Awaited before a Forward Close is answered (the request is already counted). public Func? ForwardCloseGate { get; set; } public int ForwardOpens { get { lock (_forwardOpens) return _forwardOpens.Count; } } public int ForwardCloses { get { lock (_forwardCloses) return _forwardCloses.Count; } } public int UnconnectedSends { get { lock (_unconnectedEmbedded) return _unconnectedEmbedded.Count; } } public int DirectRequests { get { lock (_direct) return _direct.Count; } } public int ConnectedRequests { get { lock (_connected) return _connected.Count; } } public IReadOnlyList ForwardOpenRequests { get { lock (_forwardOpens) return [.. _forwardOpens]; } } public IReadOnlyList ForwardCloseRequests { get { lock (_forwardCloses) return [.. _forwardCloses]; } } public IReadOnlyList UnconnectedEmbedded { get { lock (_unconnectedEmbedded) return [.. _unconnectedEmbedded]; } } public IReadOnlyList DirectCips { get { lock (_direct) return [.. _direct]; } } public IReadOnlyList ConnectedCips { get { lock (_connected) return [.. _connected]; } } /// A successful (Large) Forward Open reply echoing the request's ids and serials. public static byte[] Accept(byte[] fo) => new CipWriter() .U8((byte)(fo[0] | 0x80)).U8(0).U8(0).U8(0) .U32(OTConnectionId) .U32(BinaryPrimitives.ReadUInt32LittleEndian(fo.AsSpan(12))) .U16(BinaryPrimitives.ReadUInt16LittleEndian(fo.AsSpan(16))) .U16(BinaryPrimitives.ReadUInt16LittleEndian(fo.AsSpan(18))) .U32(BinaryPrimitives.ReadUInt32LittleEndian(fo.AsSpan(20))) .U32(2_000_000).U32(2_000_000).U8(0).U8(0) .ToArray(); /// A refusal with the given general and extended status. public static Func Refuse(byte general, params ushort[] extended) => fo => { var w = new CipWriter().U8((byte)(fo[0] | 0x80)).U8(0).U8(general).U8((byte)extended.Length); foreach (var e in extended) w.U16(e); return w.ToArray(); }; public static bool IsForwardOpen(byte[] cip) => cip[0] is 0x54 or 0x5B && AddressesConnectionManager(cip); public static bool IsForwardClose(byte[] cip) => cip[0] == 0x4E && AddressesConnectionManager(cip); private static bool AddressesConnectionManager(byte[] cip) => cip.Length >= 6 && cip.AsSpan(2, 4).SequenceEqual(CipPath.ConnectionManager); private async Task HandleAsync(byte[] frame) { var cip = FakeEipServer.RequestCip(frame); if (FakeEipServer.Command(frame) == EncapCommand.SendUnitData) { lock (_connected) _connected.Add(cip); var reply = ConnectedHandler is null ? ServiceReply(cip) : await ConnectedHandler(cip); return reply is null ? null : FakeEipServer.UnitReply(frame, reply); } if (IsForwardOpen(cip)) { lock (_forwardOpens) _forwardOpens.Add(cip); if (ForwardOpenGate is { } openGate) await openGate(); return FakeEipServer.RRReply(frame, ForwardOpenReply(cip)); } if (IsForwardClose(cip)) { lock (_forwardCloses) _forwardCloses.Add(cip); if (ForwardCloseGate is { } closeGate) await closeGate(); var w = new CipWriter().U8(0xCE).U8(0).U8(0).U8(0).Bytes(cip.AsSpan(8, 8)).U8(0).U8(0); return FakeEipServer.RRReply(frame, w.ToArray()); } if (cip[0] == 0x52 && AddressesConnectionManager(cip)) { var len = BinaryPrimitives.ReadUInt16LittleEndian(cip.AsSpan(8)); var embedded = cip.AsSpan(10, len).ToArray(); lock (_unconnectedEmbedded) _unconnectedEmbedded.Add(embedded); return FakeEipServer.RRReply(frame, ServiceReply(embedded)); } lock (_direct) _direct.Add(cip); return FakeEipServer.RRReply(frame, ServiceReply(cip)); } /// Polls (real time) until holds. public static async Task Until(Func condition, TimeSpan timeout, string what) { var giveUp = DateTime.UtcNow + timeout; while (!condition()) { if (DateTime.UtcNow > giveUp) throw new TimeoutException($"Timed out waiting for {what}."); await Task.Delay(5); } } public ValueTask DisposeAsync() => Server.DisposeAsync(); }