141 lines
6.3 KiB
C#
141 lines
6.3 KiB
C#
using System.Buffers.Binary;
|
|
using System.Text;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
|
|
|
/// <summary>
|
|
/// Decoder for the CIP Template Object (class 0x6C) blob returned by a <c>Read Template</c>
|
|
/// service. 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>Wire format per Rockwell CIP Vol 1 §5A + Logix 5000 CIP Programming Manual
|
|
/// 1756-PM019 §"Template Object", cross-checked against libplctag's <c>ab/cip.c</c>
|
|
/// <c>handle_read_template_reply</c>:</para>
|
|
///
|
|
/// <para>Header (fixed-size, little-endian):</para>
|
|
/// <list type="table">
|
|
/// <item><term>u16</term><description>Member count.</description></item>
|
|
/// <item><term>u16</term><description>Struct handle (opaque id).</description></item>
|
|
/// <item><term>u32</term><description>Instance size — bytes per structure instance.</description></item>
|
|
/// <item><term>u32</term><description>Member-definition total size — not used here.</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para>Then <c>member_count</c> member blocks (8 bytes each):</para>
|
|
/// <list type="table">
|
|
/// <item><term>u16</term><description>Member info — type code + flags (same encoding
|
|
/// as Symbol Object: bit 15 = struct, lower 12 = CIP type code).</description></item>
|
|
/// <item><term>u16</term><description>Array size — 0 for scalar members.</description></item>
|
|
/// <item><term>u32</term><description>Struct offset — byte offset from struct start.</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para>Then strings: UDT name followed by each member name, each terminated by a
|
|
/// semicolon <c>;</c> followed by a null <c>\0</c>. The UDT name may itself contain the
|
|
/// sequence <c>UDTName;0\0</c> where <c>0</c> after the semicolon is an ASCII flag byte.
|
|
/// Decoder trims to the first semicolon.</para>
|
|
/// </remarks>
|
|
public static class CipTemplateObjectDecoder
|
|
{
|
|
private const int HeaderSize = 12; // u16 + u16 + u32 + u32
|
|
private const int MemberBlockSize = 8; // u16 + u16 + u32
|
|
|
|
private const ushort MemberInfoStructFlag = 0x8000;
|
|
private const ushort MemberInfoTypeCodeMask = 0x0FFF;
|
|
|
|
/// <summary>
|
|
/// Decode the raw Template Object blob. Returns <c>null</c> when the header indicates
|
|
/// zero members or the buffer is too short to hold the fixed header.
|
|
/// </summary>
|
|
public static AbCipUdtShape? Decode(byte[] buffer)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(buffer);
|
|
if (buffer.Length < HeaderSize) return null;
|
|
|
|
var memberCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(0));
|
|
// bytes 2-3: struct handle — opaque, not needed for the shape record
|
|
var instanceSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(4));
|
|
// bytes 8-11: member-definition total size — inferred from names list instead
|
|
|
|
if (memberCount == 0) return null;
|
|
|
|
var memberBlocksOffset = HeaderSize;
|
|
var namesOffset = memberBlocksOffset + MemberBlockSize * memberCount;
|
|
if (namesOffset > buffer.Length) return null;
|
|
|
|
var stringsSpan = buffer.AsSpan(namesOffset);
|
|
var names = ParseSemicolonTerminatedStrings(stringsSpan);
|
|
if (names.Count == 0) return null;
|
|
|
|
// Strings layout: UDT name first, then one per member (in the same order as the
|
|
// member-info blocks). Always consume the first entry as the UDT name; missing
|
|
// trailing member names get <member_N> placeholders below.
|
|
var udtName = names[0];
|
|
var memberNames = names.Skip(1).ToArray();
|
|
|
|
var members = new List<AbCipUdtMember>(memberCount);
|
|
for (var i = 0; i < memberCount; i++)
|
|
{
|
|
var blockOffset = memberBlocksOffset + (i * MemberBlockSize);
|
|
var info = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(blockOffset));
|
|
var arraySize = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(blockOffset + 2));
|
|
var offset = (int)BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(blockOffset + 4));
|
|
|
|
var isStruct = (info & MemberInfoStructFlag) != 0;
|
|
var typeCode = (byte)(info & MemberInfoTypeCodeMask);
|
|
var dataType = isStruct
|
|
? AbCipDataType.Structure
|
|
: (CipSymbolObjectDecoder.MapTypeCode(typeCode) ?? AbCipDataType.Structure);
|
|
|
|
var memberName = i < memberNames.Length ? memberNames[i] : $"<member_{i}>";
|
|
members.Add(new AbCipUdtMember(
|
|
Name: memberName,
|
|
Offset: offset,
|
|
DataType: dataType,
|
|
ArrayLength: arraySize == 0 ? 1 : arraySize));
|
|
}
|
|
|
|
return new AbCipUdtShape(
|
|
TypeName: udtName,
|
|
TotalSize: (int)instanceSize,
|
|
Members: members);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Walk a span of <c>NAME;\0NAME;\0…</c> byte sequences. Splits at each semicolon —
|
|
/// the null byte after each semicolon is optional padding per Rockwell's string
|
|
/// encoding convention. Stops at a trailing null / end of buffer.
|
|
/// </summary>
|
|
internal static List<string> ParseSemicolonTerminatedStrings(ReadOnlySpan<byte> span)
|
|
{
|
|
var result = new List<string>();
|
|
var start = 0;
|
|
for (var i = 0; i < span.Length; i++)
|
|
{
|
|
var b = span[i];
|
|
if (b == ';')
|
|
{
|
|
if (i > start)
|
|
result.Add(Encoding.ASCII.GetString(span[start..i]));
|
|
// Skip the optional null/space padding following the semicolon.
|
|
while (i + 1 < span.Length && (span[i + 1] == '\0' || span[i + 1] == ' '))
|
|
i++;
|
|
start = i + 1;
|
|
}
|
|
else if (b == 0 && start == i)
|
|
{
|
|
// Trailing null at a string boundary — done.
|
|
break;
|
|
}
|
|
}
|
|
// Trailing name without a semicolon (unlikely but observed on some firmwares).
|
|
if (start < span.Length)
|
|
{
|
|
var zeroAt = span[start..].IndexOf((byte)0);
|
|
var end = zeroAt < 0 ? span.Length : start + zeroAt;
|
|
if (end > start)
|
|
result.Add(Encoding.ASCII.GetString(span[start..end]));
|
|
}
|
|
return result;
|
|
}
|
|
}
|