Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests/Fakes/FakeCipTarget.cs
T

132 lines
6.2 KiB
C#

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;
/// <summary>
/// A CIP target on top of <see cref="FakeEipServer"/>: answers Forward Open / Forward Close /
/// Unconnected Send / Direct (bare SendRRData) / connected SendUnitData requests and counts each kind.
/// </summary>
internal sealed class FakeCipTarget : IAsyncDisposable
{
public const uint OTConnectionId = 0xC0FFEE01;
private readonly List<byte[]> _forwardOpens = [];
private readonly List<byte[]> _forwardCloses = [];
private readonly List<byte[]> _unconnectedEmbedded = [];
private readonly List<byte[]> _direct = [];
private readonly List<byte[]> _connected = [];
public FakeCipTarget()
{
Server.HandlerAsync = HandleAsync;
}
public FakeEipServer Server { get; } = new();
public int Port => Server.Port;
/// <summary>Forward Open request CIP → reply CIP. Defaults to <see cref="Accept"/>.</summary>
public Func<byte[], byte[]> ForwardOpenReply { get; set; } = Accept;
/// <summary>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).</summary>
public Func<byte[], byte[]> ServiceReply { get; set; } = req => [(byte)(req[0] | 0x80), 0, 0, 0, 0x01, 0x00];
/// <summary>Connected requests only: may delay, or return null to never answer. Defaults to <see cref="ServiceReply"/>.</summary>
public Func<byte[], Task<byte[]?>>? ConnectedHandler { get; set; }
/// <summary>Awaited before a Forward Open is answered (the request is already counted).</summary>
public Func<Task>? ForwardOpenGate { get; set; }
/// <summary>Awaited before a Forward Close is answered (the request is already counted).</summary>
public Func<Task>? 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<byte[]> ForwardOpenRequests { get { lock (_forwardOpens) return [.. _forwardOpens]; } }
public IReadOnlyList<byte[]> ForwardCloseRequests { get { lock (_forwardCloses) return [.. _forwardCloses]; } }
public IReadOnlyList<byte[]> UnconnectedEmbedded { get { lock (_unconnectedEmbedded) return [.. _unconnectedEmbedded]; } }
public IReadOnlyList<byte[]> DirectCips { get { lock (_direct) return [.. _direct]; } }
public IReadOnlyList<byte[]> ConnectedCips { get { lock (_connected) return [.. _connected]; } }
/// <summary>A successful (Large) Forward Open reply echoing the request's ids and serials.</summary>
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();
/// <summary>A refusal with the given general and extended status.</summary>
public static Func<byte[], byte[]> 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<byte[]?> 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));
}
/// <summary>Polls (real time) until <paramref name="condition"/> holds.</summary>
public static async Task Until(Func<bool> 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();
}