2171fc8ce3
ab_server answers every Execute PCCC with a fixed requestor id (pccc.c PCCC_RESP_PREFIX: 07 3D F3 45 43 50 21) instead of echoing the request's, so the R14-3 echo check rejected every native reply sent under the 0x1337 CIP originator identity as BadReply. The pooled runtime now sends libplctag's identity (PcccServices.RequestorVendor/RequestorSerial), which passes the echo check against ab_server and against a real controller that echoes. Pinned by a FakeEipServer round trip that answers with ab_server's fixed prefix.
121 lines
6.7 KiB
C#
121 lines
6.7 KiB
C#
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Wire;
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Pccc;
|
|
|
|
/// <summary>Pure builders/parsers for Execute PCCC (CIP 0x4B on class 0x67) carrying SLC/MicroLogix
|
|
/// protected typed logical commands and PLC-5 range reads. Framing is taken from the golden captures.
|
|
/// The Execute PCCC request is sent bare (no Unconnected Send, no route) per the captures.</summary>
|
|
public static class PcccServices
|
|
{
|
|
/// <summary>Largest data byte count one typed read/write may carry (single-byte count on the wire).</summary>
|
|
public const int MaxDataBytes = 236;
|
|
|
|
/// <summary>True: PLC-5 range READ (cmd 0x0F fnc 0x01) is capture-verified.</summary>
|
|
public static bool Plc5Verified { get; } = true;
|
|
|
|
/// <summary>False: no PLC-5 write capture exists, so no PLC-5 write builder ships.</summary>
|
|
public static bool Plc5WriteVerified { get; } = false;
|
|
|
|
/// <summary>Requestor vendor id the native transport puts in Execute PCCC: libplctag's, from the captures
|
|
/// (<c>07 3D F3 45 43 50 21</c> in <c>slc500-read-n7.hex</c>).</summary>
|
|
/// <remarks>Not the CIP originator vendor (<see cref="ConnectionOptions.VendorId"/>). ab_server answers every
|
|
/// PCCC request with this fixed requestor id (<c>PCCC_RESP_PREFIX</c> in its <c>pccc.c</c>) instead of
|
|
/// echoing the request's, while real controllers echo whatever was sent. Sending libplctag's identity is
|
|
/// the only choice that lets the echo check in <see cref="ParseReply"/> pass against both (found by
|
|
/// Task 17 slc500: with the 0x1337 originator identity every native PCCC reply was BadReply).</remarks>
|
|
public const ushort RequestorVendor = 0xF33D;
|
|
|
|
/// <summary>Requestor serial number paired with <see cref="RequestorVendor"/> (libplctag's, from the captures).</summary>
|
|
public const uint RequestorSerial = 0x21504345;
|
|
|
|
/// <summary>Wraps a PCCC command in Execute PCCC: <c>4B 02 20 67 24 01</c> + requestor id (7, vendor, serial) + PCCC.</summary>
|
|
public static byte[] ExecutePccc(ushort requestorVendor, uint requestorSerial, ReadOnlySpan<byte> pccc) =>
|
|
new CipWriter().U8(0x4B).U8(2).U8(0x20).U8(0x67).U8(0x24).U8(0x01)
|
|
.U8(7).U16(requestorVendor).U32(requestorSerial).Bytes(pccc).ToArray();
|
|
|
|
private static void Field(CipWriter w, int v)
|
|
{
|
|
if (v < 0) throw new AbTransportException(AbStatus.OutOfBounds, $"Negative PCCC field {v}.");
|
|
if (v > ushort.MaxValue) throw new AbTransportException(AbStatus.OutOfBounds, $"PCCC field {v} exceeds 65535.");
|
|
if (v < 0xFF) w.U8((byte)v); else w.U8(0xFF).U16((ushort)v);
|
|
}
|
|
|
|
private static void CheckCount(int n)
|
|
{
|
|
if (n < 0 || n > MaxDataBytes)
|
|
throw new AbTransportException(AbStatus.TooLarge, $"PCCC byte count {n} exceeds {MaxDataBytes}.");
|
|
}
|
|
|
|
private static void Logical(CipWriter w, PcccAddress a)
|
|
{
|
|
Field(w, a.EffectiveFileNumber);
|
|
w.U8(a.FileTypeCode);
|
|
Field(w, a.Element);
|
|
Field(w, a.ResolveSubElement().Word);
|
|
}
|
|
|
|
/// <summary>Protected typed logical read with 3 address fields (0x0F/0xA2).</summary>
|
|
public static byte[] SlcTypedRead(ushort tns, PcccAddress addr, int byteCount)
|
|
{
|
|
CheckCount(byteCount);
|
|
var w = new CipWriter().U8(0x0F).U8(0).U16(tns).U8(0xA2).U8((byte)byteCount);
|
|
Logical(w, addr);
|
|
return w.ToArray();
|
|
}
|
|
|
|
/// <summary>Protected typed logical write with 3 address fields (0x0F/0xAA). SLC/MicroLogix/LogixPccc
|
|
/// only: it must not be used for PLC-5 (the family guard lives in the AbLegacy native runtime, Task 14).</summary>
|
|
public static byte[] SlcTypedWrite(ushort tns, PcccAddress addr, ReadOnlySpan<byte> data)
|
|
{
|
|
CheckCount(data.Length);
|
|
var w = new CipWriter().U8(0x0F).U8(0).U16(tns).U8(0xAA).U8((byte)data.Length);
|
|
Logical(w, addr);
|
|
return w.Bytes(data).ToArray();
|
|
}
|
|
|
|
/// <summary>PLC-5 range read (0x0F/0x01), as captured from libplctag:
|
|
/// <c>offset u16 (0), totalTrans u16, levelMask u8 (0x06 = file+element), file*, element*, byteCount u8</c>.
|
|
/// <c>totalTrans</c> counts 16-bit WORDS (DF1 word range read; the capture shows 1 for one 2-byte N element).
|
|
/// Only N-file reads are capture-verified, other PLC-5 file types are unverified; sub-element addressing is not and throws Unsupported.</summary>
|
|
public static byte[] Plc5RangeRead(ushort tns, PcccAddress addr, ushort totalTrans, int byteCount)
|
|
{
|
|
CheckCount(byteCount);
|
|
if (addr.ResolveSubElement().Word != 0)
|
|
throw new AbTransportException(AbStatus.Unsupported, "PLC-5 sub-element addressing is not capture-verified.");
|
|
var w = new CipWriter().U8(0x0F).U8(0).U16(tns).U8(0x01).U16(0).U16(totalTrans).U8(0x06);
|
|
Field(w, addr.EffectiveFileNumber);
|
|
Field(w, addr.Element);
|
|
return w.U8((byte)byteCount).ToArray();
|
|
}
|
|
|
|
/// <summary>Validates an Execute PCCC reply and returns the PCCC data bytes.</summary>
|
|
/// <param name="reply">The parsed CIP reply.</param>
|
|
/// <param name="expectedTns">The request's TNS; checked before STS so a stale error reply is BadReply.</param>
|
|
/// <param name="expectedRequestorId">Optional full echoed requestor id (length byte included) to compare.</param>
|
|
public static ReadOnlyMemory<byte> ParseReply(CipReply reply, ushort expectedTns, byte[]? expectedRequestorId = null)
|
|
{
|
|
if (reply.GeneralStatus != 0)
|
|
throw new AbTransportException(reply.Status, $"Execute PCCC CIP status 0x{reply.GeneralStatus:X2}.");
|
|
var r = new CipReader(reply.Data.Span);
|
|
var idLen = r.U8();
|
|
if (idLen < 1) throw new AbTransportException(AbStatus.BadReply, "Empty requestor id in PCCC reply.");
|
|
var idBody = r.Bytes(idLen - 1);
|
|
if (expectedRequestorId is not null
|
|
&& !(expectedRequestorId.Length == idLen && expectedRequestorId[0] == idLen
|
|
&& idBody.SequenceEqual(expectedRequestorId.AsSpan(1))))
|
|
throw new AbTransportException(AbStatus.BadReply, "PCCC reply requestor id does not match the request.");
|
|
var cmd = r.U8();
|
|
if (cmd != 0x4F)
|
|
throw new AbTransportException(AbStatus.BadReply, $"PCCC reply command 0x{cmd:X2} != 0x4F.");
|
|
var sts = r.U8();
|
|
var tns = r.U16();
|
|
if (tns != expectedTns)
|
|
throw new AbTransportException(AbStatus.BadReply, $"PCCC TNS {tns} != {expectedTns}.");
|
|
byte? ext = sts == 0xF0 ? r.U8() : null;
|
|
if (sts != 0)
|
|
throw new AbTransportException(AbStatusCodes.FromPccc(sts, ext),
|
|
$"PCCC STS 0x{sts:X2}{(ext is { } e ? $" EXT 0x{e:X2}" : "")}.");
|
|
return reply.Data[r.Position..];
|
|
}
|
|
}
|