using System.Collections.Concurrent; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// Cache of UDT shape descriptors keyed by (deviceHostAddress, templateInstanceId). /// Populated on demand during discovery + whole-UDT reads. /// Nothing in production clears it. Its only caller was /// AbCipDriver.FlushOptionalCachesAsync, deleted in Gitea #525 as an unconsumed /// IDriver 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 /// ReinitializeAsync 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. /// /// /// Templates are decoded by (CIP Template Object /// class 0x6C, GetAttributeList + Read Template); the live reader is /// , and /// populates this cache on first fetch. /// public sealed class AbCipTemplateCache { private readonly ConcurrentDictionary<(string device, uint instanceId), AbCipUdtShape> _shapes = new(); /// /// Retrieve a cached UDT shape, or null if not yet read. /// /// The device host address and port. /// The template instance ID. /// The cached UDT shape, or null if not yet read. public AbCipUdtShape? TryGet(string deviceHostAddress, uint templateInstanceId) => _shapes.TryGetValue((deviceHostAddress, templateInstanceId), out var shape) ? shape : null; /// Store a freshly-decoded UDT shape. /// The device host address and port. /// The template instance ID. /// The UDT shape to cache. public void Put(string deviceHostAddress, uint templateInstanceId, AbCipUdtShape shape) => _shapes[(deviceHostAddress, templateInstanceId)] = shape; /// Drop every cached shape. Retained as ordinary cache API + the tests' reset seam; /// see the type remarks for why no production path calls it. public void Clear() => _shapes.Clear(); /// Count of cached shapes — exposed for diagnostics + tests. public int Count => _shapes.Count; } /// /// Decoded shape of one Logix UDT — member list + each member's offset + type. Populated /// by from a Template Object response buffer /// read via . /// /// UDT name as reported by the Template Object. /// Bytes the UDT occupies in a whole-UDT read buffer. /// Ordered list of members, each with its byte offset + type. public sealed record AbCipUdtShape( string TypeName, int TotalSize, IReadOnlyList Members) { /// /// true when the shape is a Logix string-family type — the built-in STRING or /// a user string type such as STR0016: exactly a LEN DINT at offset 0 and a /// DATA 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. /// 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 }; } /// One member of a Logix UDT. /// Member name as reported by the Template Object. /// Byte offset of the member from the struct start. /// Decoded member data type ( for a nested UDT). /// Element count (1 for a scalar member). /// 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 ; null for a scalar member. public sealed record AbCipUdtMember( string Name, int Offset, AbCipDataType DataType, int ArrayLength, uint? NestedTemplateId = null);