Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTemplateCache.cs
T
Joseph Doherty 3b48e05e9a fix(abcip): decode real @udt template buffers; browse STRINGs as String, hide BOOL hosts
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.
2026-10-07 14:04:11 -04:00

88 lines
4.8 KiB
C#

using System.Collections.Concurrent;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// <summary>
/// Cache of UDT shape descriptors keyed by <c>(deviceHostAddress, templateInstanceId)</c>.
/// Populated on demand during discovery + whole-UDT reads.
/// <para><b>Nothing in production clears it.</b> Its only caller was
/// <c>AbCipDriver.FlushOptionalCachesAsync</c>, deleted in Gitea #525 as an unconsumed
/// <c>IDriver</c> member — but that member never had a caller either, so this is not a change in
/// behaviour, only in how visible the fact is. The comment here used to claim a device
/// <c>ReinitializeAsync</c> also flushed it; no such call ever existed. A UDT redefined on a live
/// controller therefore keeps its stale shape until the driver instance is rebuilt.</para>
/// </summary>
/// <remarks>
/// Templates are decoded by <see cref="CipTemplateObjectDecoder"/> (CIP Template Object
/// class 0x6C, <c>GetAttributeList</c> + <c>Read Template</c>); the live reader is
/// <see cref="LibplctagTemplateReader"/>, and <see cref="AbCipDriver.FetchUdtShapeAsync"/>
/// populates this cache on first fetch.
/// </remarks>
public sealed class AbCipTemplateCache
{
private readonly ConcurrentDictionary<(string device, uint instanceId), AbCipUdtShape> _shapes = new();
/// <summary>
/// Retrieve a cached UDT shape, or <c>null</c> if not yet read.
/// </summary>
/// <param name="deviceHostAddress">The device host address and port.</param>
/// <param name="templateInstanceId">The template instance ID.</param>
/// <returns>The cached UDT shape, or <c>null</c> if not yet read.</returns>
public AbCipUdtShape? TryGet(string deviceHostAddress, uint templateInstanceId) =>
_shapes.TryGetValue((deviceHostAddress, templateInstanceId), out var shape) ? shape : null;
/// <summary>Store a freshly-decoded UDT shape.</summary>
/// <param name="deviceHostAddress">The device host address and port.</param>
/// <param name="templateInstanceId">The template instance ID.</param>
/// <param name="shape">The UDT shape to cache.</param>
public void Put(string deviceHostAddress, uint templateInstanceId, AbCipUdtShape shape) =>
_shapes[(deviceHostAddress, templateInstanceId)] = shape;
/// <summary>Drop every cached shape. Retained as ordinary cache API + the tests' reset seam;
/// see the type remarks for why no production path calls it.</summary>
public void Clear() => _shapes.Clear();
/// <summary>Count of cached shapes — exposed for diagnostics + tests.</summary>
public int Count => _shapes.Count;
}
/// <summary>
/// Decoded shape of one Logix UDT — member list + each member's offset + type. Populated
/// by <see cref="CipTemplateObjectDecoder.Decode"/> from a Template Object response buffer
/// read via <see cref="LibplctagTemplateReader"/>.
/// </summary>
/// <param name="TypeName">UDT name as reported by the Template Object.</param>
/// <param name="TotalSize">Bytes the UDT occupies in a whole-UDT read buffer.</param>
/// <param name="Members">Ordered list of members, each with its byte offset + type.</param>
public sealed record AbCipUdtShape(
string TypeName,
int TotalSize,
IReadOnlyList<AbCipUdtMember> Members)
{
/// <summary>
/// <c>true</c> when the shape is a Logix string-family type — the built-in <c>STRING</c> or
/// a user string type such as <c>STR0016</c>: exactly a <c>LEN</c> DINT at offset 0 and a
/// <c>DATA</c> SINT array at offset 4. A controller reports every string tag as a struct of
/// this shape, but it is read and presented as one String value, never as LEN + DATA leaves.
/// </summary>
public bool IsStringFamily =>
Members.Count == 2
&& Members[0] is { Name: "LEN", DataType: AbCipDataType.DInt, Offset: 0 }
&& Members[1] is { Name: "DATA", DataType: AbCipDataType.SInt, Offset: 4, ArrayLength: > 1 };
}
/// <summary>One member of a Logix UDT.</summary>
/// <param name="Name">Member name as reported by the Template Object.</param>
/// <param name="Offset">Byte offset of the member from the struct start.</param>
/// <param name="DataType">Decoded member data type (<see cref="AbCipDataType.Structure"/> for a nested UDT).</param>
/// <param name="ArrayLength">Element count (1 for a scalar member).</param>
/// <param name="NestedTemplateId">For a nested-struct member, the nested UDT's CIP template instance id
/// (low 12 bits of the member info, same encoding as the Symbol Object) so the nested shape can be
/// fetched via <see cref="AbCipDriver.FetchUdtShapeAsync"/>; <c>null</c> for a scalar member.</param>
public sealed record AbCipUdtMember(
string Name,
int Offset,
AbCipDataType DataType,
int ArrayLength,
uint? NestedTemplateId = null);