132 lines
6.2 KiB
C#
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();
|
|
}
|