Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests/Captures/CaptureFile.cs
T
Joseph Doherty 1201d5fcfb test(ab-transport): golden libplctag<->ab_server frame captures for Logix, Micro800 and PCCC
17 .hex fixtures captured from libplctag 1.5.2 (native 2.6.0) against
otopcua-ab-server:libplctag-release, plus the CaptureFile loader, its
length/alternation tests, and the re-runnable scripts/ab-capture/capture.sh.

Observed and recorded in Captures/README.md "Routing observed":
- Micro800 is NOT UCMM under libplctag 2.6.0: it opens a Large Forward Open
  (path 20 02 24 01) and reads connected; use_connected_msg=0 is ignored.
- SLC500/MicroLogix/PLC-5 send Execute PCCC 0x4B unconnected and unwrapped;
  a configured path=1,0 never reaches the wire.
- Only Large FO 0x5B was observed; no 0x54 fallback occurred.
- LogixPccc is unreachable against ab_server (CIP 0x08); no fixture committed.
2026-10-07 15:50:00 -04:00

49 lines
2.4 KiB
C#

namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Captures;
/// <summary>One EtherNet/IP encapsulation frame from a golden capture.</summary>
/// <param name="FromClient"><c>true</c> for a client→server (<c>&gt;</c>) frame.</param>
/// <param name="Bytes">The full frame: 24-byte encapsulation header plus data.</param>
internal sealed record CapturedFrame(bool FromClient, byte[] Bytes);
/// <summary>
/// Loads the golden libplctag ⇄ ab_server captures in <c>Captures/*.hex</c>. See
/// <c>Captures/README.md</c> for how each was taken.
/// </summary>
internal static class CaptureFile
{
/// <summary>The directory the <c>.hex</c> fixtures are copied to.</summary>
public static string Dir => Path.Combine(AppContext.BaseDirectory, "Captures");
/// <summary>Loads every frame of a capture, in wire order; <c>#</c> lines are skipped.</summary>
/// <param name="name">The capture's file name, e.g. <c>logix-read-dint.hex</c>.</param>
/// <returns>The frames.</returns>
public static IReadOnlyList<CapturedFrame> Load(string name) =>
File.ReadAllLines(Path.Combine(Dir, name))
.Where(l => l.Length > 2 && l[0] is '>' or '<')
.Select(l => new CapturedFrame(l[0] == '>', Convert.FromHexString(l[2..].Trim())))
.ToList();
/// <summary>The CIP payload of a SendRRData / SendUnitData frame (the last CPF item's data,
/// minus the 2-byte sequence count for connected data).</summary>
/// <param name="f">The frame.</param>
/// <returns>The CIP payload, or empty for any other encapsulation command.</returns>
public static byte[] CipPayload(CapturedFrame f)
{
var b = f.Bytes;
var cmd = BitConverter.ToUInt16(b, 0); // test-only; fixtures are LE on a LE host
if (cmd is not (0x6F or 0x70)) return []; // only SendRRData / SendUnitData carry CPF items
var pos = 24 + 4 + 2; // header + interface handle + timeout
var itemCount = BitConverter.ToUInt16(b, pos); pos += 2;
byte[] last = [];
for (var i = 0; i < itemCount; i++)
{
var type = BitConverter.ToUInt16(b, pos);
var len = BitConverter.ToUInt16(b, pos + 2);
last = b.AsSpan(pos + 4, len).ToArray();
if (type == 0x00B1) last = last[2..]; // strip connected sequence count
pos += 4 + len;
}
return last;
}
}