be421a4cb0
#572: listing continues at max(instance in page)+1 (no ascending-order assumption, no-progress guard kept); browsePageDelayMs (default 5 ms) paces pages; DiscoverAsync reads templates through one batch reader (IAbCipTemplateReaderFactory.CreateBatch) holding one lease. #575: differential harness reports a same-status template refusal as an equal refusal; offline test pins the open-attempt budget without relying on the abort ending the phase. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
148 lines
6.9 KiB
C#
148 lines
6.9 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Native <see cref="IAbCipTemplateReader"/>: Get Attribute List then chunked Read Template on the
|
|
/// device's shared scheduler, returning the 14-byte-header buffer <see cref="CipTemplateObjectDecoder"/>
|
|
/// parses. By default the lease is held for one read and released when it completes; a reader built for a
|
|
/// batch (<see cref="IAbCipTemplateReaderFactory.CreateBatch"/>) instead keeps one lease per device from its
|
|
/// first read until it is disposed.
|
|
/// </summary>
|
|
internal sealed class NativeAbCipTemplateReader : IAbCipTemplateReader
|
|
{
|
|
/// <summary>Request bytes of a Read Template that are not data room (service, path, offset, count).</summary>
|
|
private const int RequestOverhead = 16;
|
|
|
|
/// <summary>Bytes of the template definition header not counted in the Read Template length.</summary>
|
|
private const int DefinitionHeaderBytes = 23;
|
|
|
|
/// <summary>Upper bound on a template's Read Template length; Logix templates are far smaller.</summary>
|
|
internal const long MaxTemplateBytes = 64 * 1024;
|
|
|
|
private readonly Func<AbCipTagCreateParams, (RequestScheduler Scheduler, ICipChannel Channel, Func<ValueTask> Release)> _acquire;
|
|
private readonly ILogger _logger;
|
|
private readonly bool _holdLeases;
|
|
private readonly object _heldLock = new();
|
|
private readonly Dictionary<DeviceKey, (RequestScheduler Scheduler, ICipChannel Channel, Func<ValueTask> Release)> _held = new();
|
|
private bool _disposed;
|
|
|
|
/// <summary>Production constructor: leases from <paramref name="pool"/>.</summary>
|
|
/// <param name="pool">The connection pool (the factory passes <see cref="DeviceSessionPool.Shared"/>).</param>
|
|
/// <param name="holdLeases">True for a batch reader: keep each device's lease until disposed.</param>
|
|
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)
|
|
{
|
|
}
|
|
|
|
/// <summary>Test seam: supplies the scheduler, channel and release directly.</summary>
|
|
/// <param name="acquire">Acquires the connection parts for a device.</param>
|
|
/// <param name="logger">Receives release failures; null discards them.</param>
|
|
/// <param name="holdLeases">True for a batch reader: keep each device's lease until disposed.</param>
|
|
internal NativeAbCipTemplateReader(
|
|
Func<AbCipTagCreateParams, (RequestScheduler Scheduler, ICipChannel Channel, Func<ValueTask> Release)> acquire,
|
|
ILogger? logger = null, bool holdLeases = false)
|
|
{
|
|
_acquire = acquire;
|
|
_logger = logger ?? NullLogger.Instance;
|
|
_holdLeases = holdLeases;
|
|
}
|
|
|
|
/// <summary>The connection parts for one read, and whether this read owns (must release) them.</summary>
|
|
private (RequestScheduler Scheduler, ICipChannel Channel, Func<ValueTask> 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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<byte[]> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>Releases the leases a batch reader held; a per-read reader has none.</summary>
|
|
public void Dispose()
|
|
{
|
|
List<Func<ValueTask>> releases;
|
|
lock (_heldLock)
|
|
{
|
|
_disposed = true;
|
|
releases = [.. _held.Values.Select(h => h.Release)];
|
|
_held.Clear();
|
|
}
|
|
foreach (var release in releases) NativeLease.ReleaseBounded(release, _logger);
|
|
}
|
|
}
|
|
|
|
/// <summary>Factory for <see cref="NativeAbCipTemplateReader"/>.</summary>
|
|
internal sealed class NativeAbCipTemplateReaderFactory : IAbCipTemplateReaderFactory
|
|
{
|
|
/// <inheritdoc />
|
|
public IAbCipTemplateReader Create() => new NativeAbCipTemplateReader(DeviceSessionPool.Shared);
|
|
|
|
/// <inheritdoc />
|
|
public IAbCipTemplateReader CreateBatch() => new NativeAbCipTemplateReader(DeviceSessionPool.Shared, holdLeases: true);
|
|
}
|