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.
46 lines
2.5 KiB
C#
46 lines
2.5 KiB
C#
using System.Buffers.Binary;
|
|
using System.Text;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
|
|
|
/// <summary>
|
|
/// Builds an <c>@udt/<id></c> template buffer in the layout libplctag really returns —
|
|
/// the layout pinned by the real-controller captures in <c>CipTemplateObjectWireCaptureTests</c>.
|
|
/// Keep it in step with those captures, never with the decoder: a builder that restates the
|
|
/// decoder's assumption proves nothing (#468, and the template decoder after it).
|
|
/// </summary>
|
|
internal static class TemplateBufferBuilder
|
|
{
|
|
/// <summary>Build a template buffer.</summary>
|
|
/// <param name="udtName">UDT type name (written as <c>name;n</c> + an encoded tail, as a controller does).</param>
|
|
/// <param name="instanceSize">Bytes per instance.</param>
|
|
/// <param name="members">Per member: name, CIP type word (atomic code, or <c>0x8000 | nestedTemplateId</c>),
|
|
/// array length (0 = scalar; > 0 sets the array flag and the info word), byte offset.</param>
|
|
/// <returns>The buffer.</returns>
|
|
public static byte[] Build(
|
|
string udtName, uint instanceSize, params (string name, ushort type, ushort arraySize, uint offset)[] members)
|
|
{
|
|
const int header = 14;
|
|
var buf = new MemoryStream();
|
|
var head = new byte[header + (8 * members.Length)];
|
|
BinaryPrimitives.WriteUInt16LittleEndian(head.AsSpan(0), 0x0123); // template id
|
|
BinaryPrimitives.WriteUInt32LittleEndian(head.AsSpan(2), 0); // def size (words)
|
|
BinaryPrimitives.WriteUInt32LittleEndian(head.AsSpan(6), instanceSize);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(head.AsSpan(10), (ushort)members.Length);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(head.AsSpan(12), 0xABCD); // struct handle
|
|
for (var i = 0; i < members.Length; i++)
|
|
{
|
|
var o = header + (i * 8);
|
|
var (_, type, arraySize, offset) = members[i];
|
|
var typeWord = arraySize > 0 ? (ushort)(type | 0x2000) : type;
|
|
BinaryPrimitives.WriteUInt16LittleEndian(head.AsSpan(o), arraySize);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(head.AsSpan(o + 2), typeWord);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(head.AsSpan(o + 4), offset);
|
|
}
|
|
buf.Write(head);
|
|
buf.Write(Encoding.ASCII.GetBytes(udtName + ";nEAIA\0"));
|
|
foreach (var m in members) buf.Write(Encoding.ASCII.GetBytes(m.name + "\0"));
|
|
return buf.ToArray();
|
|
}
|
|
}
|