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);