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();
}