fix(driver-twincat): resolve High code-review findings (Driver.TwinCAT-001, -002, -007, -008, -013)
Driver.TwinCAT-001 — InitializeAsync/ReinitializeAsync ignored driverConfigJson. Extracted the DTO-to-options parse into a shared TwinCATDriverFactoryExtensions.ParseOptions; InitializeAsync now re-parses driverConfigJson into a mutable _options field, so a config generation pushed via ReinitializeAsync (added/removed devices, tags, probe settings) is actually applied at runtime. Driver.TwinCAT-002 — LInt/ULInt narrowed to Int32. ToDriverDataType now maps LInt to Int64, ULInt to UInt64, UDInt to UInt32, UInt/USInt to UInt16, Int/SInt to Int16, and the IEC TIME/DATE/DT/TOD types to UInt32 (their raw UDINT counter). Removed the stale "Int64 gap" comment — no truncation or sign flips at the OPC UA encode layer. Driver.TwinCAT-007 — EnsureConnectedAsync was not thread-safe. Connect/reconnect is now serialized per device by a SemaphoreSlim (DeviceState.ConnectGate) with a double-checked connect, mirroring the S7 driver. Concurrent read/write/probe callers can no longer leak a client or race a create-vs-dispose. Driver.TwinCAT-008 — native ADS notification callbacks ran driver logic on the AMS router thread. AdsTwinCATClient now enqueues AdsNotificationEx callbacks onto a bounded Channel drained by a dedicated managed task; the router-thread callback only does a non-blocking TryWrite, so a slow consumer cannot stall ADS notification delivery process-wide. Driver.TwinCAT-013 — TwinCATDriver did not implement IRediscoverable. The driver now implements IRediscoverable; AdsTwinCATClient detects ADS 0x0702 (symbol-version-changed) on read/write paths and raises OnSymbolVersionChanged, which the driver forwards as OnRediscoveryNeeded so Core rebuilds the address space after a PLC program re-download. Adds TwinCATHighFindingsRegressionTests covering all five fixes; updates the data-type mapping assertion in TwinCATDriverTests. TwinCAT driver builds clean; 119 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Channels;
|
||||
using TwinCAT;
|
||||
using TwinCAT.Ads;
|
||||
using TwinCAT.Ads.TypeSystem;
|
||||
@@ -21,16 +22,50 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
/// </remarks>
|
||||
internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
{
|
||||
// Bounded so a slow downstream consumer cannot back the AMS router thread up — the
|
||||
// router thread enqueues and returns immediately (Driver.TwinCAT-008). 50k matches the
|
||||
// Galaxy EventPump default; ~500 notifications/connection is the ADS ceiling so this is
|
||||
// generous headroom against bursty change storms.
|
||||
private const int NotificationQueueCapacity = 50_000;
|
||||
|
||||
private readonly AdsClient _client = new();
|
||||
private readonly ConcurrentDictionary<uint, NotificationRegistration> _notifications = new();
|
||||
|
||||
// Marshals native ADS notifications off the AMS router thread onto a dedicated managed
|
||||
// task. The router-thread callback (OnAdsNotificationEx) only enqueues; DispatchLoopAsync
|
||||
// drains and invokes the per-tag OnChange delegates. Blocking the router thread would
|
||||
// stall every ADS notification across the whole process (docs/v2/driver-specs.md §6).
|
||||
private readonly Channel<PendingNotification> _notificationQueue =
|
||||
Channel.CreateBounded<PendingNotification>(new BoundedChannelOptions(NotificationQueueCapacity)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.DropWrite,
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
});
|
||||
private readonly CancellationTokenSource _dispatchCts = new();
|
||||
private readonly Task _dispatchLoop;
|
||||
private int _disposed;
|
||||
|
||||
public AdsTwinCATClient()
|
||||
{
|
||||
_client.AdsNotificationEx += OnAdsNotificationEx;
|
||||
_dispatchLoop = Task.Run(() => DispatchLoopAsync(_dispatchCts.Token));
|
||||
}
|
||||
|
||||
private readonly record struct PendingNotification(NotificationRegistration Registration, object? Value);
|
||||
|
||||
public bool IsConnected => _client.IsConnected;
|
||||
|
||||
public event EventHandler? OnSymbolVersionChanged;
|
||||
|
||||
/// <summary>Raise <see cref="OnSymbolVersionChanged"/> when <paramref name="adsError"/> is 0x0702.</summary>
|
||||
private uint MapAndSignal(uint adsError)
|
||||
{
|
||||
if (TwinCATStatusMapper.IsSymbolVersionChanged(adsError))
|
||||
OnSymbolVersionChanged?.Invoke(this, EventArgs.Empty);
|
||||
return TwinCATStatusMapper.MapAdsError(adsError);
|
||||
}
|
||||
|
||||
public Task ConnectAsync(TwinCATAmsAddress address, TimeSpan timeout, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_client.IsConnected) return Task.CompletedTask;
|
||||
@@ -60,7 +95,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
var parentResult = await _client.ReadValueAsync(parent, typeof(uint), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (parentResult.ErrorCode != AdsErrorCode.NoError)
|
||||
return (null, TwinCATStatusMapper.MapAdsError((uint)parentResult.ErrorCode));
|
||||
return (null, MapAndSignal((uint)parentResult.ErrorCode));
|
||||
return (ExtractBit(parentResult.Value, bit), TwinCATStatusMapper.Good);
|
||||
}
|
||||
|
||||
@@ -69,13 +104,13 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (result.ErrorCode != AdsErrorCode.NoError)
|
||||
return (null, TwinCATStatusMapper.MapAdsError((uint)result.ErrorCode));
|
||||
return (null, MapAndSignal((uint)result.ErrorCode));
|
||||
|
||||
return (result.Value, TwinCATStatusMapper.Good);
|
||||
}
|
||||
catch (AdsErrorException ex)
|
||||
{
|
||||
return (null, TwinCATStatusMapper.MapAdsError((uint)ex.ErrorCode));
|
||||
return (null, MapAndSignal((uint)ex.ErrorCode));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,11 +141,11 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
.ConfigureAwait(false);
|
||||
return result.ErrorCode == AdsErrorCode.NoError
|
||||
? TwinCATStatusMapper.Good
|
||||
: TwinCATStatusMapper.MapAdsError((uint)result.ErrorCode);
|
||||
: MapAndSignal((uint)result.ErrorCode);
|
||||
}
|
||||
catch (AdsErrorException ex)
|
||||
{
|
||||
return TwinCATStatusMapper.MapAdsError((uint)ex.ErrorCode);
|
||||
return MapAndSignal((uint)ex.ErrorCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,13 +194,35 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
return reg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs on the <see cref="AdsClient"/> AMS router thread. Does the cheap bit-extraction
|
||||
/// decode then enqueues — no driver logic, no consumer callbacks — so a slow consumer
|
||||
/// can never stall ADS notification delivery for the rest of the process
|
||||
/// (Driver.TwinCAT-008). Drops the notification (DropWrite) if the queue is saturated.
|
||||
/// </summary>
|
||||
private void OnAdsNotificationEx(object? sender, AdsNotificationExEventArgs args)
|
||||
{
|
||||
if (!_notifications.TryGetValue(args.Handle, out var reg)) return;
|
||||
var value = args.Value;
|
||||
if (reg.BitIndex is int bit && reg.Type == TwinCATDataType.Bool && value is not bool)
|
||||
value = ExtractBit(value, bit);
|
||||
try { reg.OnChange(reg.SymbolPath, value); } catch { /* consumer-side errors don't crash the ADS thread */ }
|
||||
_notificationQueue.Writer.TryWrite(new PendingNotification(reg, value));
|
||||
}
|
||||
|
||||
private async Task DispatchLoopAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var pending in _notificationQueue.Reader.ReadAllAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
try { pending.Registration.OnChange(pending.Registration.SymbolPath, pending.Value); }
|
||||
catch { /* consumer-side errors stay on this managed task, not the ADS thread */ }
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
// Clean shutdown.
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task DeleteNotificationAsync(uint handle, CancellationToken cancellationToken)
|
||||
@@ -236,8 +293,17 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
|
||||
_client.AdsNotificationEx -= OnAdsNotificationEx;
|
||||
_notifications.Clear();
|
||||
|
||||
// Stop the dispatch loop: complete the queue so the reader drains + exits, then
|
||||
// cancel as a backstop. Best-effort wait so a wedged consumer can't hang teardown.
|
||||
_notificationQueue.Writer.TryComplete();
|
||||
_dispatchCts.Cancel();
|
||||
try { _dispatchLoop.Wait(TimeSpan.FromSeconds(2)); } catch { /* shutdown */ }
|
||||
_dispatchCts.Dispose();
|
||||
|
||||
_client.Dispose();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user