using System.Collections.Concurrent; using System.Runtime.CompilerServices; using TwinCAT; using TwinCAT.Ads; using TwinCAT.Ads.TypeSystem; using TwinCAT.TypeSystem; 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 async IAsyncEnumerable BrowseSymbolsAsync( [EnumeratorCancellation] CancellationToken cancellationToken) { // SymbolLoaderFactory downloads the symbol-info blob once then iterates locally — the // async surface on this interface is for our callers, not for the underlying call which // is effectively sync on top of the already-open AdsClient. var settings = new SymbolLoaderSettings(SymbolsLoadMode.Flat); var loader = SymbolLoaderFactory.Create(_client, settings); await Task.Yield(); // honors the async surface; pragmatic given the loader itself is sync foreach (ISymbol symbol in loader.Symbols) { if (cancellationToken.IsCancellationRequested) yield break; var mapped = MapSymbolTypeName(symbol.DataType?.Name); var readOnly = !IsSymbolWritable(symbol); yield return new TwinCATDiscoveredSymbol(symbol.InstancePath, mapped, readOnly); } } private static TwinCATDataType? MapSymbolTypeName(string? typeName) => typeName switch { "BOOL" or "BIT" => TwinCATDataType.Bool, "SINT" or "BYTE" => TwinCATDataType.SInt, "USINT" => TwinCATDataType.USInt, "INT" or "WORD" => TwinCATDataType.Int, "UINT" => TwinCATDataType.UInt, "DINT" or "DWORD" => TwinCATDataType.DInt, "UDINT" => TwinCATDataType.UDInt, "LINT" or "LWORD" => TwinCATDataType.LInt, "ULINT" => TwinCATDataType.ULInt, "REAL" => TwinCATDataType.Real, "LREAL" => TwinCATDataType.LReal, "STRING" => TwinCATDataType.String, "WSTRING" => TwinCATDataType.WString, "TIME" => TwinCATDataType.Time, "DATE" => TwinCATDataType.Date, "DT" or "DATE_AND_TIME" => TwinCATDataType.DateTime, "TOD" or "TIME_OF_DAY" => TwinCATDataType.TimeOfDay, _ => null, // UDTs / FB instances / arrays / pointers — out of atomic scope }; private static bool IsSymbolWritable(ISymbol symbol) { // SymbolAccessRights is a flags enum — the Write bit indicates a writable symbol. // When the symbol implementation doesn't surface it, assume writable + let the PLC // return AccessDenied at write time. if (symbol is Symbol s) return (s.AccessRights & SymbolAccessRights.Write) != 0; return true; } 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(); }