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:
@@ -1,5 +1,9 @@
|
||||
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;
|
||||
|
||||
@@ -149,6 +153,56 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
catch { /* best-effort tear-down; target may already be gone */ }
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<TwinCATDiscoveredSymbol> 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;
|
||||
|
||||
Reference in New Issue
Block a user