TwinCAT follow-up — Symbol browser via AdsClient + SymbolLoaderFactory. Closes task #188. Adds ITwinCATClient.BrowseSymbolsAsync — IAsyncEnumerable yielding TwinCATDiscoveredSymbol (InstancePath + mapped TwinCATDataType + ReadOnly flag) from the target's flat symbol table. AdsTwinCATClient implementation uses SymbolLoaderFactory.Create(_client, new SymbolLoaderSettings(SymbolsLoadMode.Flat)) + iterates loader.Symbols, maps IEC 61131-3 type names (BOOL/SINT/INT/DINT/LINT/REAL/LREAL/STRING/WSTRING/TIME/DATE/DT/TOD + BYTE/WORD/DWORD/LWORD unsigned-word aliases) through MapSymbolTypeName, checks SymbolAccessRights.Write bit for writable vs read-only. Unsupported types (UDTs / function blocks / arrays / pointers) surface with DataType=null so callers can skip or recurse. TwinCATDriverOptions.EnableControllerBrowse — new bool, default false to preserve the strict-config path. When true, DiscoverAsync iterates each device's BrowseSymbolsAsync, filters via TwinCATSystemSymbolFilter (rejects TwinCAT_*, Constants.*, Mc_*, __*, Global_Version* prefixes + anything empty), skips null-DataType symbols, emits surviving symbols under a per-device Discovered/ sub-folder with InstancePath as both FullName + BrowseName + ReadOnly→ViewOnly/writable→Operate. Pre-declared tags from TwinCATDriverOptions.Tags always emit regardless. Browse failure is non-fatal — exception caught + swallowed, pre-declared tags stay in the address space, operators see the failure in driver health on next read. TwinCATSystemSymbolFilter static class mirrors AbCipSystemTagFilter's shape with TwinCAT-specific prefixes. Fake client updated — BrowseResults list for test setup + FireNotification-style single-invocation on each subscribe, ThrowOnBrowse flag for failure testing. 8 new unit tests — strict path emits only pre-declared when EnableControllerBrowse=false, browse enabled adds Discovered/ folder, filter rejects system prefixes, null-DataType symbols skipped, ReadOnly symbols surface ViewOnly, browse failure leaves pre-declared intact, SystemSymbolFilter theory (10 cases). Total TwinCAT unit tests now 110/110 passing (+17 from the native-notification merge's 93); full solution builds 0 errors; other drivers untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-19 20:13:33 -04:00
parent 9ca80fd450
commit c95228391d
7 changed files with 385 additions and 2 deletions

View File

@@ -217,7 +217,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
// ---- ITagDiscovery ----
public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(builder);
var root = builder.Folder("TwinCAT", "TwinCAT");
@@ -225,6 +225,8 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
{
var label = device.DeviceName ?? device.HostAddress;
var deviceFolder = root.Folder(device.HostAddress, label);
// Pre-declared tags — always emitted as the authoritative config path.
var tagsForDevice = _options.Tags.Where(t =>
string.Equals(t.DeviceHostAddress, device.HostAddress, StringComparison.OrdinalIgnoreCase));
foreach (var tag in tagsForDevice)
@@ -241,8 +243,42 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
IsAlarm: false,
WriteIdempotent: tag.WriteIdempotent));
}
// Controller-side symbol browse — opt-in. Falls back to pre-declared-only on any
// client-side error so a flaky symbol-table download doesn't block discovery.
if (_options.EnableControllerBrowse && _devices.TryGetValue(device.HostAddress, out var state))
{
IAddressSpaceBuilder? discoveredFolder = null;
try
{
var client = await EnsureConnectedAsync(state, cancellationToken).ConfigureAwait(false);
await foreach (var sym in client.BrowseSymbolsAsync(cancellationToken).ConfigureAwait(false))
{
if (TwinCATSystemSymbolFilter.IsSystemSymbol(sym.InstancePath)) continue;
if (sym.DataType is not TwinCATDataType dt) continue; // unsupported type
discoveredFolder ??= deviceFolder.Folder("Discovered", "Discovered");
discoveredFolder.Variable(sym.InstancePath, sym.InstancePath, new DriverAttributeInfo(
FullName: sym.InstancePath,
DriverDataType: dt.ToDriverDataType(),
IsArray: false,
ArrayDim: null,
SecurityClass: sym.ReadOnly
? SecurityClassification.ViewOnly
: SecurityClassification.Operate,
IsHistorized: false,
IsAlarm: false,
WriteIdempotent: false));
}
}
catch (OperationCanceledException) { throw; }
catch
{
// Symbol-loader failure is non-fatal to discovery — pre-declared tags already
// shipped + operators see the failure in driver health on next read.
}
}
}
return Task.CompletedTask;
}
// ---- ISubscribable (native ADS notifications with poll fallback) ----