using System.Collections.Concurrent; using TwinCAT.Ads; namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; /// /// Default backed by Beckhoff's . /// One instance per AMS target; reused across reads / writes / probes. /// /// /// Wire behavior depends on a reachable AMS router — on Windows the router comes /// from TwinCAT XAR; elsewhere from the Beckhoff.TwinCAT.Ads.TcpRouter package /// hosted by the server process. Neither is built-in here; deployment wires one in. /// /// Error mapping — ADS error codes surface through /// and get translated to OPC UA status codes via . /// internal sealed class AdsTwinCATClient : ITwinCATClient { private readonly AdsClient _client = new(); private readonly ConcurrentDictionary _notifications = new(); public AdsTwinCATClient() { _client.AdsNotificationEx += OnAdsNotificationEx; } public bool IsConnected => _client.IsConnected; public Task ConnectAsync(TwinCATAmsAddress address, TimeSpan timeout, CancellationToken cancellationToken) { if (_client.IsConnected) return Task.CompletedTask; _client.Timeout = (int)Math.Max(1_000, timeout.TotalMilliseconds); var netId = AmsNetId.Parse(address.NetId); _client.Connect(netId, address.Port); return Task.CompletedTask; } public async Task<(object? value, uint status)> ReadValueAsync( string symbolPath, TwinCATDataType type, int? bitIndex, CancellationToken cancellationToken) { try { var clrType = MapToClrType(type); var result = await _client.ReadValueAsync(symbolPath, clrType, cancellationToken) .ConfigureAwait(false); if (result.ErrorCode != AdsErrorCode.NoError) return (null, TwinCATStatusMapper.MapAdsError((uint)result.ErrorCode)); var value = result.Value; if (bitIndex is int bit && type == TwinCATDataType.Bool && value is not bool) value = ExtractBit(value, bit); return (value, TwinCATStatusMapper.Good); } catch (AdsErrorException ex) { return (null, TwinCATStatusMapper.MapAdsError((uint)ex.ErrorCode)); } } public async Task WriteValueAsync( string symbolPath, TwinCATDataType type, int? bitIndex, object? value, CancellationToken cancellationToken) { if (bitIndex is int && type == TwinCATDataType.Bool) throw new NotSupportedException( "BOOL-within-word writes require read-modify-write; tracked in task #181."); try { var converted = ConvertForWrite(type, value); var result = await _client.WriteValueAsync(symbolPath, converted, cancellationToken) .ConfigureAwait(false); return result.ErrorCode == AdsErrorCode.NoError ? TwinCATStatusMapper.Good : TwinCATStatusMapper.MapAdsError((uint)result.ErrorCode); } catch (AdsErrorException ex) { return TwinCATStatusMapper.MapAdsError((uint)ex.ErrorCode); } } public async Task ProbeAsync(CancellationToken cancellationToken) { try { var state = await _client.ReadStateAsync(cancellationToken).ConfigureAwait(false); return state.ErrorCode == AdsErrorCode.NoError; } catch { return false; } } public async Task AddNotificationAsync( string symbolPath, TwinCATDataType type, int? bitIndex, TimeSpan cycleTime, Action onChange, CancellationToken cancellationToken) { var clrType = MapToClrType(type); // NotificationSettings takes cycle + max-delay in 100ns units. AdsTransMode.OnChange // fires when the value differs; OnCycle fires every cycle. OnChange is the right default // for OPC UA data-change semantics — the PLC already has the best view of "has this // changed" so we let it decide. var cycleTicks = (uint)Math.Max(1, cycleTime.Ticks / TimeSpan.TicksPerMillisecond * 10_000); var settings = new NotificationSettings(AdsTransMode.OnChange, (int)cycleTicks, 0); // AddDeviceNotificationExAsync returns Task; AdsNotificationEx fires // with the handle as part of the event args so we use the handle as the correlation // key into _notifications. var result = await _client.AddDeviceNotificationExAsync( symbolPath, settings, userData: null, clrType, args: null, cancellationToken) .ConfigureAwait(false); if (result.ErrorCode != AdsErrorCode.NoError) throw new InvalidOperationException( $"AddDeviceNotificationExAsync failed with ADS error {result.ErrorCode} for {symbolPath}"); var reg = new NotificationRegistration(symbolPath, type, bitIndex, onChange, this, result.Handle); _notifications[result.Handle] = reg; return reg; } 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 */ } } internal async Task DeleteNotificationAsync(uint handle, CancellationToken cancellationToken) { _notifications.TryRemove(handle, out _); try { await _client.DeleteDeviceNotificationAsync(handle, cancellationToken).ConfigureAwait(false); } catch { /* best-effort tear-down; target may already be gone */ } } public void Dispose() { _client.AdsNotificationEx -= OnAdsNotificationEx; _notifications.Clear(); _client.Dispose(); } private sealed class NotificationRegistration( string symbolPath, TwinCATDataType type, int? bitIndex, Action onChange, AdsTwinCATClient owner, uint handle) : ITwinCATNotificationHandle { public string SymbolPath { get; } = symbolPath; public TwinCATDataType Type { get; } = type; public int? BitIndex { get; } = bitIndex; public Action OnChange { get; } = onChange; public void Dispose() { // Fire-and-forget AMS call — caller has already committed to the tear-down. _ = owner.DeleteNotificationAsync(handle, CancellationToken.None); } } private static Type MapToClrType(TwinCATDataType type) => type switch { TwinCATDataType.Bool => typeof(bool), TwinCATDataType.SInt => typeof(sbyte), TwinCATDataType.USInt => typeof(byte), TwinCATDataType.Int => typeof(short), TwinCATDataType.UInt => typeof(ushort), TwinCATDataType.DInt => typeof(int), TwinCATDataType.UDInt => typeof(uint), TwinCATDataType.LInt => typeof(long), TwinCATDataType.ULInt => typeof(ulong), TwinCATDataType.Real => typeof(float), TwinCATDataType.LReal => typeof(double), TwinCATDataType.String or TwinCATDataType.WString => typeof(string), TwinCATDataType.Time or TwinCATDataType.Date or TwinCATDataType.DateTime or TwinCATDataType.TimeOfDay => typeof(uint), _ => typeof(int), }; private static object ConvertForWrite(TwinCATDataType type, object? value) => type switch { TwinCATDataType.Bool => Convert.ToBoolean(value), TwinCATDataType.SInt => Convert.ToSByte(value), TwinCATDataType.USInt => Convert.ToByte(value), TwinCATDataType.Int => Convert.ToInt16(value), TwinCATDataType.UInt => Convert.ToUInt16(value), TwinCATDataType.DInt => Convert.ToInt32(value), TwinCATDataType.UDInt => Convert.ToUInt32(value), TwinCATDataType.LInt => Convert.ToInt64(value), TwinCATDataType.ULInt => Convert.ToUInt64(value), TwinCATDataType.Real => Convert.ToSingle(value), TwinCATDataType.LReal => Convert.ToDouble(value), TwinCATDataType.String or TwinCATDataType.WString => Convert.ToString(value) ?? string.Empty, TwinCATDataType.Time or TwinCATDataType.Date or TwinCATDataType.DateTime or TwinCATDataType.TimeOfDay => Convert.ToUInt32(value), _ => throw new NotSupportedException($"TwinCATDataType {type} not writable."), }; private static bool ExtractBit(object? rawWord, int bit) => rawWord switch { short s => (s & (1 << bit)) != 0, ushort us => (us & (1 << bit)) != 0, int i => (i & (1 << bit)) != 0, uint ui => (ui & (1u << bit)) != 0, long l => (l & (1L << bit)) != 0, ulong ul => (ul & (1UL << bit)) != 0, _ => false, }; } /// Default — one per call. internal sealed class AdsTwinCATClientFactory : ITwinCATClientFactory { public ITwinCATClient Create() => new AdsTwinCATClient(); }