Auto: abcip-2.5 — online tag-DB refresh trigger

Add IDriverControl capability interface in Core.Abstractions with a
RebrowseAsync(IAddressSpaceBuilder, CancellationToken) hook so operators
can force a controller-side @tags re-walk without restarting the driver.

AbCipDriver now implements IDriverControl. RebrowseAsync clears the UDT
template cache (so stale shapes from a pre-download program don't
survive) then runs the same enumerator + builder fan-out as
DiscoverAsync, serialised against concurrent discovery / rebrowse via
a new SemaphoreSlim.

Driver.AbCip.Cli ships a `rebrowse` subcommand mirroring the existing
probe / read shape: connects to a single gateway, runs RebrowseAsync
against an in-memory builder, and prints discovered tag names so
operators can sanity-check the controller's symbol table from a shell.

Tests cover: two consecutive RebrowseAsync calls bump the enumerator's
Create / Enumerate counters once each, discovered tags reach the
supplied builder, the template cache is dropped on rebrowse, and the
driver exposes IDriverControl. 313 AbCip unit tests + 17 CLI tests +
37 Core.Abstractions tests pass.

Closes #233
This commit is contained in:
Joseph Doherty
2026-04-25 18:45:48 -04:00
parent 27878d0faf
commit 6e244e0c01
4 changed files with 315 additions and 1 deletions

View File

@@ -22,7 +22,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// <see cref="PlcTagHandle"/> and reconnects each device.</para>
/// </remarks>
public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable,
IHostConnectivityProbe, IPerCallHostResolver, IAlarmSource, IDisposable, IAsyncDisposable
IHostConnectivityProbe, IPerCallHostResolver, IAlarmSource, IDriverControl, IDisposable, IAsyncDisposable
{
private readonly AbCipDriverOptions _options;
private readonly string _driverInstanceId;
@@ -34,6 +34,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
private readonly Dictionary<string, DeviceState> _devices = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, AbCipTagDefinition> _tagsByName = new(StringComparer.OrdinalIgnoreCase);
private readonly AbCipAlarmProjection _alarmProjection;
private readonly SemaphoreSlim _discoverySemaphore = new(1, 1);
private DriverHealth _health = new(DriverState.Unknown, null, null);
public event EventHandler<DataChangeEventArgs>? OnDataChange;
@@ -967,6 +968,43 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(builder);
await _discoverySemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
await DiscoverCoreAsync(builder, cancellationToken).ConfigureAwait(false);
}
finally
{
_discoverySemaphore.Release();
}
}
/// <summary>
/// PR abcip-2.5 — operator-triggered rebrowse. Drops the cached UDT template shapes so
/// the next read re-fetches them from the controller, then runs the same enumerator
/// walk + builder fan-out that <see cref="DiscoverAsync"/> drives. Serialised against
/// other rebrowse / discovery passes via <see cref="_discoverySemaphore"/> so two
/// concurrent triggers don't double-issue the @tags read.
/// </summary>
public async Task RebrowseAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(builder);
await _discoverySemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// Stale template shapes can outlive a controller program-download, so a rebrowse
// is the natural moment to drop them; subsequent UDT reads re-populate on demand.
_templateCache.Clear();
await DiscoverCoreAsync(builder, cancellationToken).ConfigureAwait(false);
}
finally
{
_discoverySemaphore.Release();
}
}
private async Task DiscoverCoreAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
{
var root = builder.Folder("AbCip", "AbCip");
foreach (var device in _options.Devices)
@@ -1076,6 +1114,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
public async ValueTask DisposeAsync()
{
await ShutdownAsync(CancellationToken.None).ConfigureAwait(false);
_discoverySemaphore.Dispose();
}
/// <summary>