using System.Buffers.Binary; using System.Text; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// Decoder for the UDT template buffer libplctag returns for an @udt/<id> tag (CIP /// Template Object, class 0x6C). Produces an describing the UDT's /// name, total size, + ordered member list with per-member offset + type + array length. /// /// /// The layout below is what libplctag hands back from Tag.GetBuffer() — its own /// header followed by the controller's Read Template reply — matching libplctag's /// list_tags_logix example and pinned by real captures in /// CipTemplateObjectWireCaptureTests (CompactLogix 5069-L320ERS2 rev 34.11). An earlier /// version assumed a 12-byte header led by the member count and [info][array][offset] /// blocks; every real buffer decoded to null under that assumption. /// /// Header (14 bytes, little-endian): /// /// u16Template instance id. /// u32Member-definition size in 32-bit words — not used here. /// u32Instance size — bytes per structure instance. /// u16Member count. /// u16Struct handle (opaque). /// /// /// Then member_count member blocks (8 bytes each): /// /// u16Info — the array length for an array member, the bit /// number for a BOOL packed into a host integer, otherwise 0. /// u16Type — bit 15 = struct, bit 13 = array, low 12 bits = /// CIP atomic type code, or for a struct the nested template instance id. /// u32Byte offset from the struct start. /// /// /// Then the strings: the UDT name terminated by ; (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. /// 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; /// /// Decode the raw template buffer. Returns null when the header indicates zero /// members or the buffer is too short to hold the header + member blocks + type name. /// /// The raw template buffer to decode. /// An AbCipUdtShape describing the structure, or null if decoding fails. 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(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] : $""; members.Add(new AbCipUdtMember( Name: memberName, Offset: offset, DataType: dataType, ArrayLength: arrayLength, NestedTemplateId: nestedTemplateId)); } return new AbCipUdtShape( TypeName: udtName, TotalSize: (int)instanceSize, Members: members); } /// /// Split the template string section into the UDT name (cut at its first ;) followed /// by up to NUL-terminated member names. Stops at the end of /// the buffer; trailing bytes past the last member name are ignored. /// /// The string section, starting right after the last member block. /// Member names to read after the UDT name. /// The UDT name followed by the member names found. internal static List ParseTemplateStrings(ReadOnlySpan span, int memberCount) { var result = new List(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; } }