3b48e05e9a
Live read-only check against production CompactLogix 5069-L320ERS2 rev 34.11 presses (Z-35162 / 10.209.70.160): every @udt/<id> template the controller returned decoded to null, so controller-discovered UDTs never fanned out. The decoder assumed a 12-byte header led by the member count and [info][array][offset] blocks with ';'-terminated names; libplctag actually returns a 14-byte header (u16 id, u32 def words, u32 instance size, u16 member count, u16 handle), [info][type][offset] blocks, and NUL-terminated names after a 'Name;<tail>' type name. All three test builders restated the old assumption, so the unit suite agreed with itself (the #468 trap again); they now share a TemplateBufferBuilder pinned by real captures (CipTemplateObjectWireCaptureTests). Discovery follow-ons that the working decoder exposed: - STRING (and user string types, LEN DINT + DATA SINT[n]) browse as one String variable, top-level and nested, not as a LEN/DATA folder. - Controller-generated members (ZZZZZZZZZZ* BOOL hosts, __* AOI backing) are not browsed. - A UDT array is not fanned out into element-free Tag.Member paths.
140 lines
7.0 KiB
C#
140 lines
7.0 KiB
C#
using System.Buffers.Binary;
|
|
using System.Text;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
|
|
|
/// <summary>
|
|
/// Decoder for the UDT template buffer libplctag returns for an <c>@udt/<id></c> tag (CIP
|
|
/// Template Object, class 0x6C). Produces an <see cref="AbCipUdtShape"/> describing the UDT's
|
|
/// name, total size, + ordered member list with per-member offset + type + array length.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>The layout below is what libplctag hands back from <c>Tag.GetBuffer()</c> — its own
|
|
/// header followed by the controller's Read Template reply — matching libplctag's
|
|
/// <c>list_tags_logix</c> example and pinned by real captures in
|
|
/// <c>CipTemplateObjectWireCaptureTests</c> (CompactLogix 5069-L320ERS2 rev 34.11). An earlier
|
|
/// version assumed a 12-byte header led by the member count and <c>[info][array][offset]</c>
|
|
/// blocks; every real buffer decoded to <c>null</c> under that assumption.</para>
|
|
///
|
|
/// <para>Header (14 bytes, little-endian):</para>
|
|
/// <list type="table">
|
|
/// <item><term>u16</term><description>Template instance id.</description></item>
|
|
/// <item><term>u32</term><description>Member-definition size in 32-bit words — not used here.</description></item>
|
|
/// <item><term>u32</term><description>Instance size — bytes per structure instance.</description></item>
|
|
/// <item><term>u16</term><description>Member count.</description></item>
|
|
/// <item><term>u16</term><description>Struct handle (opaque).</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para>Then <c>member_count</c> member blocks (8 bytes each):</para>
|
|
/// <list type="table">
|
|
/// <item><term>u16</term><description>Info — the array length for an array member, the bit
|
|
/// number for a BOOL packed into a host integer, otherwise 0.</description></item>
|
|
/// <item><term>u16</term><description>Type — bit 15 = struct, bit 13 = array, low 12 bits =
|
|
/// CIP atomic type code, or for a struct the nested template instance id.</description></item>
|
|
/// <item><term>u32</term><description>Byte offset from the struct start.</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para>Then the strings: the UDT name terminated by <c>;</c> (followed by an encoded tail that
|
|
/// runs to a NUL — discarded), then each member name terminated by a NUL. Anything after the
|
|
/// last member name is ignored.</para>
|
|
/// </remarks>
|
|
public static class CipTemplateObjectDecoder
|
|
{
|
|
private const int HeaderSize = 14; // u16 id + u32 def size + u32 instance size + u16 count + u16 handle
|
|
private const int MemberBlockSize = 8; // u16 info + u16 type + u32 offset
|
|
|
|
private const ushort TypeStructFlag = 0x8000;
|
|
private const ushort TypeArrayFlag = 0x2000;
|
|
private const ushort TypeCodeMask = 0x0FFF;
|
|
|
|
/// <summary>
|
|
/// Decode the raw template buffer. Returns <c>null</c> when the header indicates zero
|
|
/// members or the buffer is too short to hold the header + member blocks + type name.
|
|
/// </summary>
|
|
/// <param name="buffer">The raw template buffer to decode.</param>
|
|
/// <returns>An AbCipUdtShape describing the structure, or null if decoding fails.</returns>
|
|
public static AbCipUdtShape? Decode(byte[] buffer)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(buffer);
|
|
if (buffer.Length < HeaderSize) return null;
|
|
|
|
// bytes 0-1: template instance id; bytes 2-5: definition size in words — neither needed.
|
|
var instanceSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(6));
|
|
var memberCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(10));
|
|
// bytes 12-13: struct handle — opaque.
|
|
|
|
if (memberCount == 0) return null;
|
|
|
|
var namesOffset = HeaderSize + (MemberBlockSize * memberCount);
|
|
if (namesOffset > buffer.Length) return null;
|
|
|
|
var strings = ParseTemplateStrings(buffer.AsSpan(namesOffset), memberCount);
|
|
if (strings.Count == 0) return null;
|
|
|
|
var udtName = strings[0];
|
|
var members = new List<AbCipUdtMember>(memberCount);
|
|
for (var i = 0; i < memberCount; i++)
|
|
{
|
|
var blockOffset = HeaderSize + (i * MemberBlockSize);
|
|
var info = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(blockOffset));
|
|
var type = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(blockOffset + 2));
|
|
var offset = (int)BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(blockOffset + 4));
|
|
|
|
var isStruct = (type & TypeStructFlag) != 0;
|
|
var isArray = (type & TypeArrayFlag) != 0;
|
|
// Atomic CIP type codes (0xC1-0xD0) fit a byte; for a struct the low 12 bits are the nested
|
|
// template id, which can exceed a byte, so it is taken from the full 12-bit mask below.
|
|
var dataType = isStruct
|
|
? AbCipDataType.Structure
|
|
: (CipSymbolObjectDecoder.MapTypeCode((byte)(type & TypeCodeMask)) ?? AbCipDataType.Structure);
|
|
var nestedTemplateId = isStruct ? (uint?)(type & TypeCodeMask) : null;
|
|
|
|
// info is the element count only for an array member; for a BOOL it is the bit number,
|
|
// which must never be read as a length.
|
|
var arrayLength = isArray && info > 0 ? info : 1;
|
|
|
|
var memberName = i + 1 < strings.Count ? strings[i + 1] : $"<member_{i}>";
|
|
members.Add(new AbCipUdtMember(
|
|
Name: memberName,
|
|
Offset: offset,
|
|
DataType: dataType,
|
|
ArrayLength: arrayLength,
|
|
NestedTemplateId: nestedTemplateId));
|
|
}
|
|
|
|
return new AbCipUdtShape(
|
|
TypeName: udtName,
|
|
TotalSize: (int)instanceSize,
|
|
Members: members);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Split the template string section into the UDT name (cut at its first <c>;</c>) followed
|
|
/// by up to <paramref name="memberCount"/> NUL-terminated member names. Stops at the end of
|
|
/// the buffer; trailing bytes past the last member name are ignored.
|
|
/// </summary>
|
|
/// <param name="span">The string section, starting right after the last member block.</param>
|
|
/// <param name="memberCount">Member names to read after the UDT name.</param>
|
|
/// <returns>The UDT name followed by the member names found.</returns>
|
|
internal static List<string> ParseTemplateStrings(ReadOnlySpan<byte> span, int memberCount)
|
|
{
|
|
var result = new List<string>(memberCount + 1);
|
|
var pos = 0;
|
|
while (pos < span.Length && result.Count < memberCount + 1)
|
|
{
|
|
var nul = span[pos..].IndexOf((byte)0);
|
|
var end = nul < 0 ? span.Length : pos + nul;
|
|
var raw = span[pos..end];
|
|
if (result.Count == 0)
|
|
{
|
|
// The type name carries an encoded tail after ';' ("STRING;nEAIA").
|
|
var semi = raw.IndexOf((byte)';');
|
|
if (semi >= 0) raw = raw[..semi];
|
|
}
|
|
result.Add(Encoding.ASCII.GetString(raw));
|
|
pos = end + 1;
|
|
}
|
|
return result;
|
|
}
|
|
}
|