using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Logix; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// Native : Get Attribute List then chunked Read Template on the /// device's shared scheduler, returning the 14-byte-header buffer /// parses. By default the lease is held for one read and released when it completes; a reader built for a /// batch () instead keeps one lease per device from its /// first read until it is disposed. /// internal sealed class NativeAbCipTemplateReader : IAbCipTemplateReader { /// Request bytes of a Read Template that are not data room (service, path, offset, count). private const int RequestOverhead = 16; /// Bytes of the template definition header not counted in the Read Template length. private const int DefinitionHeaderBytes = 23; /// Upper bound on a template's Read Template length; Logix templates are far smaller. internal const long MaxTemplateBytes = 64 * 1024; private readonly Func Release)> _acquire; private readonly ILogger _logger; private readonly bool _holdLeases; private readonly object _heldLock = new(); private readonly Dictionary Release)> _held = new(); private bool _disposed; /// Production constructor: leases from . /// The connection pool (the factory passes ). /// True for a batch reader: keep each device's lease until disposed. public NativeAbCipTemplateReader(DeviceSessionPool pool, bool holdLeases = false) : this(p => { var lease = pool.Acquire(NativeAbCipDevice.Key(p), NativeAbCipDevice.Options(p)); return (lease.Scheduler, lease.Channel, lease.DisposeAsync); }, holdLeases: holdLeases) { } /// Test seam: supplies the scheduler, channel and release directly. /// Acquires the connection parts for a device. /// Receives release failures; null discards them. /// True for a batch reader: keep each device's lease until disposed. internal NativeAbCipTemplateReader( Func Release)> acquire, ILogger? logger = null, bool holdLeases = false) { _acquire = acquire; _logger = logger ?? NullLogger.Instance; _holdLeases = holdLeases; } /// The connection parts for one read, and whether this read owns (must release) them. private (RequestScheduler Scheduler, ICipChannel Channel, Func Release, bool Owned) Lease(AbCipTagCreateParams p) { if (!_holdLeases) { var one = _acquire(p); return (one.Scheduler, one.Channel, one.Release, true); } var key = NativeAbCipDevice.Key(p); lock (_heldLock) { ObjectDisposedException.ThrowIf(_disposed, this); if (!_held.TryGetValue(key, out var held)) _held[key] = held = _acquire(p); return (held.Scheduler, held.Channel, held.Release, false); } } /// public async Task ReadAsync( AbCipTagCreateParams deviceParams, uint templateInstanceId, CancellationToken cancellationToken) { if (templateInstanceId > ushort.MaxValue) throw new ArgumentOutOfRangeException(nameof(templateInstanceId), "Template ids are 16-bit."); var id = (ushort)templateInstanceId; var (scheduler, channel, release, owned) = Lease(deviceParams); try { var attrReply = await scheduler.SendAsync(TemplateObjectServices.GetAttributes(id), cancellationToken) .ConfigureAwait(false); var attrs = TemplateObjectServices.ParseAttributes(attrReply); // The CIP-documented Read Template length: definition words * 4 - 23. var total = checked((long)attrs.DefinitionWords * 4 - DefinitionHeaderBytes); if (total < 0 || total > MaxTemplateBytes) throw new AbTransportException(AbStatus.BadReply, $"Template {id} reports an impossible definition size of {attrs.DefinitionWords} words."); using var raw = new MemoryStream(); long offset = 0; while (offset < total) { var chunk = (ushort)Math.Min(total - offset, channel.MaxRequestBytes - RequestOverhead); var reply = await scheduler .SendAsync(TemplateObjectServices.ReadTemplate(id, (uint)offset, chunk), cancellationToken, exclusive: true) .ConfigureAwait(false); if (reply.GeneralStatus is not (0x00 or 0x06)) throw new AbTransportException(reply.Status, $"Read Template {id} failed: general 0x{reply.GeneralStatus:X2}."); var more = TemplateObjectServices.ReadHasMore(reply); if (more && reply.Data.IsEmpty) throw new AbTransportException(AbStatus.BadReply, $"Read Template {id} reported more data but returned none."); raw.Write(reply.Data.Span); offset += reply.Data.Length; if (!more) break; } return TemplateObjectServices.BuildDecoderBuffer(id, attrs, raw.ToArray()); } finally { if (owned) NativeLease.ReleaseBounded(release, _logger); } } /// Releases the leases a batch reader held; a per-read reader has none. public void Dispose() { List> releases; lock (_heldLock) { _disposed = true; releases = [.. _held.Values.Select(h => h.Release)]; _held.Clear(); } foreach (var release in releases) NativeLease.ReleaseBounded(release, _logger); } } /// Factory for . internal sealed class NativeAbCipTemplateReaderFactory : IAbCipTemplateReaderFactory { /// public IAbCipTemplateReader Create() => new NativeAbCipTemplateReader(DeviceSessionPool.Shared); /// public IAbCipTemplateReader CreateBatch() => new NativeAbCipTemplateReader(DeviceSessionPool.Shared, holdLeases: true); }