From 1d6015bc87cc40881d0bac23a943d944a2af8e2d Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 19 Apr 2026 19:59:37 -0400 Subject: [PATCH] =?UTF-8?q?FOCAS=20PR=203=20=E2=80=94=20ITagDiscovery=20+?= =?UTF-8?q?=20ISubscribable=20+=20IHostConnectivityProbe=20+=20IPerCallHos?= =?UTF-8?q?tResolver.=20Completes=20the=20FOCAS=20driver=20=E2=80=94=207-i?= =?UTF-8?q?nterface=20capability=20set=20matching=20AbCip/AbLegacy/TwinCAT?= =?UTF-8?q?=20(minus=20IAlarmSource=20=E2=80=94=20Fanuc=20CNC=20alarms=20l?= =?UTF-8?q?ive=20in=20a=20different=20API=20surface,=20tracked=20as=20a=20?= =?UTF-8?q?future-phase=20concern).=20ITagDiscovery=20emits=20pre-declared?= =?UTF-8?q?=20tags=20under=20a=20FOCAS=20root=20+=20per-device=20sub-folde?= =?UTF-8?q?r=20keyed=20on=20the=20canonical=20focas://host:port=20string?= =?UTF-8?q?=20with=20DeviceName=20fallback.=20Writable=20=E2=86=92=20Opera?= =?UTF-8?q?te,=20non-writable=20=E2=86=92=20ViewOnly.=20No=20native=20FOCA?= =?UTF-8?q?S=20symbol=20browsing=20=E2=80=94=20CNCs=20don't=20expose=20a?= =?UTF-8?q?=20tag=20catalogue=20the=20way=20Logix=20or=20TwinCAT=20do;=20o?= =?UTF-8?q?perators=20declare=20addresses=20explicitly.=20ISubscribable=20?= =?UTF-8?q?consumes=20the=20shared=20PollGroupEngine=20=E2=80=94=205th=20c?= =?UTF-8?q?onsumer=20of=20the=20engine=20after=20Modbus=20+=20AbCip=20+=20?= =?UTF-8?q?AbLegacy=20+=20TwinCAT-poll-mode.=20100ms=20interval=20floor=20?= =?UTF-8?q?inherited.=20FOCAS=20has=20no=20native=20notification/subscript?= =?UTF-8?q?ion=20protocol=20(unlike=20TwinCAT=20ADS),=20so=20polling=20is?= =?UTF-8?q?=20the=20only=20option=20=E2=80=94=20every=20subscribed=20tag?= =?UTF-8?q?=20round-trips=20through=20cnc=5Frdpmcrng=20/=20cnc=5Frdparam?= =?UTF-8?q?=20/=20cnc=5Frdmacro=20on=20each=20tick.=20IHostConnectivityPro?= =?UTF-8?q?be=20uses=20the=20existing=20IFocasClient.ProbeAsync=20which=20?= =?UTF-8?q?in=20the=20real=20FwlibFocasClient=20calls=20cnc=5Fstatinfo=20(?= =?UTF-8?q?cheap=20handshake=20returning=20ODBST=20with=20tmmode/aut/run/m?= =?UTF-8?q?otion/alarm=20state).=20Probe=20loop=20runs=20when=20Enabled=3D?= =?UTF-8?q?true,=20catches=20OperationCanceledException=20during=20shutdow?= =?UTF-8?q?n,=20falls=20through=20to=20Stopped=20on=20exceptions,=20emits?= =?UTF-8?q?=20Running/Stopped=20transitions=20via=20OnHostStatusChanged=20?= =?UTF-8?q?with=20the=20canonical=20focas://host:port=20as=20the=20host-na?= =?UTF-8?q?me=20key.=20Same-state=20spurious-event=20guard=20under=20per-d?= =?UTF-8?q?evice=20lock.=20IPerCallHostResolver=20maps=20tag=20full-ref=20?= =?UTF-8?q?to=20DeviceHostAddress=20for=20Phase=206.1=20bulkhead/breaker?= =?UTF-8?q?=20keying=20per=20plan=20decision=20#144=20=E2=80=94=20unknown?= =?UTF-8?q?=20refs=20fall=20back=20to=20first=20device,=20no=20devices=20?= =?UTF-8?q?=E2=86=92=20DriverInstanceId.=20ShutdownAsync=20now=20disposes?= =?UTF-8?q?=20PollGroupEngine=20+=20cancels/disposes=20per-device=20probe?= =?UTF-8?q?=20CTS=20+=20disposes=20cached=20clients.=20DeviceState=20gains?= =?UTF-8?q?=20ProbeLock=20/=20HostState=20/=20HostStateChangedUtc=20/=20Pr?= =?UTF-8?q?obeCts=20matching=20the=20shape=20used=20by=20AbCip/AbLegacy/Tw?= =?UTF-8?q?inCAT.=209=20new=20unit=20tests=20in=20FocasCapabilityTests=20?= =?UTF-8?q?=E2=80=94=20discovery=20tag=20emission=20with=20correct=20Secur?= =?UTF-8?q?ityClassification,=20subscription=20initial=20poll=20raises=20O?= =?UTF-8?q?nDataChange,=20shutdown=20cancels=20subscriptions,=20GetHostSta?= =?UTF-8?q?tuses=20entry-per-device,=20probe=20Running=20/=20Stopped=20tra?= =?UTF-8?q?nsitions,=20ResolveHost=20for=20known=20/=20unknown=20/=20no-de?= =?UTF-8?q?vices=20paths.=20FocasScaffoldingTests=20updated=20with=20Probe?= =?UTF-8?q?.Enabled=3Dfalse=20where=20the=20default=20factory=20would=20ot?= =?UTF-8?q?herwise=20try=20to=20load=20Fwlib32.dll=20during=20the=20probe-?= =?UTF-8?q?loop=20spinup.=20Total=20FOCAS=20unit=20tests=20now=20115/115?= =?UTF-8?q?=20passing=20(+9=20from=20PR=202's=20106);=20full=20solution=20?= =?UTF-8?q?builds=200=20errors;=20Modbus=20/=20AbCip=20/=20AbLegacy=20/=20?= =?UTF-8?q?TwinCAT=20/=20other=20drivers=20untouched.=20FOCAS=20driver=20i?= =?UTF-8?q?s=20real-wire-capable=20end-to-end=20=E2=80=94=20read=20/=20wri?= =?UTF-8?q?te=20/=20discover=20/=20subscribe=20/=20probe=20/=20host-resolv?= =?UTF-8?q?e=20for=20Fanuc=20FS=200i/16i/18i/21i/30i/31i/32i/Series=2035i/?= =?UTF-8?q?Power=20Mate=20i=20controllers=20once=20deployment=20drops=20Fw?= =?UTF-8?q?lib32.dll=20beside=20the=20server.=20Closes=20task=20#120=20sub?= =?UTF-8?q?task=20FOCAS.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../FocasDriver.cs | 128 +++++++++- .../FocasCapabilityTests.cs | 239 ++++++++++++++++++ .../FocasScaffoldingTests.cs | 1 + 3 files changed, 364 insertions(+), 4 deletions(-) create mode 100644 tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasCapabilityTests.cs diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs index 90b53da..be0e986 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs @@ -15,15 +15,20 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS; /// + the default makes misconfigured servers /// fail fast. /// -public sealed class FocasDriver : IDriver, IReadable, IWritable, IDisposable, IAsyncDisposable +public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, + IHostConnectivityProbe, IPerCallHostResolver, IDisposable, IAsyncDisposable { private readonly FocasDriverOptions _options; private readonly string _driverInstanceId; private readonly IFocasClientFactory _clientFactory; + private readonly PollGroupEngine _poll; private readonly Dictionary _devices = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _tagsByName = new(StringComparer.OrdinalIgnoreCase); private DriverHealth _health = new(DriverState.Unknown, null, null); + public event EventHandler? OnDataChange; + public event EventHandler? OnHostStatusChanged; + public FocasDriver(FocasDriverOptions options, string driverInstanceId, IFocasClientFactory? clientFactory = null) { @@ -31,6 +36,10 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, IDisposable, IA _options = options; _driverInstanceId = driverInstanceId; _clientFactory = clientFactory ?? new FwlibFocasClientFactory(); + _poll = new PollGroupEngine( + reader: ReadAsync, + onChange: (handle, tagRef, snapshot) => + OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot))); } public string DriverInstanceId => _driverInstanceId; @@ -49,6 +58,16 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, IDisposable, IA _devices[device.HostAddress] = new DeviceState(addr, device); } foreach (var tag in _options.Tags) _tagsByName[tag.Name] = tag; + + if (_options.Probe.Enabled) + { + foreach (var state in _devices.Values) + { + state.ProbeCts = new CancellationTokenSource(); + var ct = state.ProbeCts.Token; + _ = Task.Run(() => ProbeLoopAsync(state, ct), ct); + } + } _health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null); } catch (Exception ex) @@ -65,13 +84,19 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, IDisposable, IA await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false); } - public Task ShutdownAsync(CancellationToken cancellationToken) + public async Task ShutdownAsync(CancellationToken cancellationToken) { - foreach (var state in _devices.Values) state.DisposeClient(); + await _poll.DisposeAsync().ConfigureAwait(false); + foreach (var state in _devices.Values) + { + try { state.ProbeCts?.Cancel(); } catch { } + state.ProbeCts?.Dispose(); + state.ProbeCts = null; + state.DisposeClient(); + } _devices.Clear(); _tagsByName.Clear(); _health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null); - return Task.CompletedTask; } public DriverHealth GetHealth() => _health; @@ -189,6 +214,96 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, IDisposable, IA return results; } + // ---- ITagDiscovery ---- + + public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(builder); + var root = builder.Folder("FOCAS", "FOCAS"); + foreach (var device in _options.Devices) + { + var label = device.DeviceName ?? device.HostAddress; + var deviceFolder = root.Folder(device.HostAddress, label); + var tagsForDevice = _options.Tags.Where(t => + string.Equals(t.DeviceHostAddress, device.HostAddress, StringComparison.OrdinalIgnoreCase)); + foreach (var tag in tagsForDevice) + { + deviceFolder.Variable(tag.Name, tag.Name, new DriverAttributeInfo( + FullName: tag.Name, + DriverDataType: tag.DataType.ToDriverDataType(), + IsArray: false, + ArrayDim: null, + SecurityClass: tag.Writable + ? SecurityClassification.Operate + : SecurityClassification.ViewOnly, + IsHistorized: false, + IsAlarm: false, + WriteIdempotent: tag.WriteIdempotent)); + } + } + return Task.CompletedTask; + } + + // ---- ISubscribable (polling overlay via shared engine) ---- + + public Task SubscribeAsync( + IReadOnlyList fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) => + Task.FromResult(_poll.Subscribe(fullReferences, publishingInterval)); + + public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) + { + _poll.Unsubscribe(handle); + return Task.CompletedTask; + } + + // ---- IHostConnectivityProbe ---- + + public IReadOnlyList GetHostStatuses() => + [.. _devices.Values.Select(s => new HostConnectivityStatus(s.Options.HostAddress, s.HostState, s.HostStateChangedUtc))]; + + private async Task ProbeLoopAsync(DeviceState state, CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + var success = false; + try + { + var client = await EnsureConnectedAsync(state, ct).ConfigureAwait(false); + success = await client.ProbeAsync(ct).ConfigureAwait(false); + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; } + catch { /* connect-failure path already disposed + cleared the client */ } + + TransitionDeviceState(state, success ? HostState.Running : HostState.Stopped); + + try { await Task.Delay(_options.Probe.Interval, ct).ConfigureAwait(false); } + catch (OperationCanceledException) { break; } + } + } + + private void TransitionDeviceState(DeviceState state, HostState newState) + { + HostState old; + lock (state.ProbeLock) + { + old = state.HostState; + if (old == newState) return; + state.HostState = newState; + state.HostStateChangedUtc = DateTime.UtcNow; + } + OnHostStatusChanged?.Invoke(this, + new HostStatusChangedEventArgs(state.Options.HostAddress, old, newState)); + } + + // ---- IPerCallHostResolver ---- + + public string ResolveHost(string fullReference) + { + if (_tagsByName.TryGetValue(fullReference, out var def)) + return def.DeviceHostAddress; + return _options.Devices.FirstOrDefault()?.HostAddress ?? DriverInstanceId; + } + private async Task EnsureConnectedAsync(DeviceState device, CancellationToken ct) { if (device.Client is { IsConnected: true } c) return c; @@ -215,6 +330,11 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, IDisposable, IA public FocasDeviceOptions Options { get; } = options; public IFocasClient? Client { get; set; } + public object ProbeLock { get; } = new(); + public HostState HostState { get; set; } = HostState.Unknown; + public DateTime HostStateChangedUtc { get; set; } = DateTime.UtcNow; + public CancellationTokenSource? ProbeCts { get; set; } + public void DisposeClient() { Client?.Dispose(); diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasCapabilityTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasCapabilityTests.cs new file mode 100644 index 0000000..2c002a6 --- /dev/null +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasCapabilityTests.cs @@ -0,0 +1,239 @@ +using System.Collections.Concurrent; +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; +using ZB.MOM.WW.OtOpcUa.Driver.FOCAS; + +namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; + +[Trait("Category", "Unit")] +public sealed class FocasCapabilityTests +{ + // ---- ITagDiscovery ---- + + [Fact] + public async Task DiscoverAsync_emits_pre_declared_tags() + { + var builder = new RecordingBuilder(); + var drv = new FocasDriver(new FocasDriverOptions + { + Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193", DeviceName: "Lathe-1")], + Tags = + [ + new FocasTagDefinition("Run", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte), + new FocasTagDefinition("Alarm", "focas://10.0.0.5:8193", "R200", FocasDataType.Byte, Writable: false), + ], + Probe = new FocasProbeOptions { Enabled = false }, + }, "drv-1", new FakeFocasClientFactory()); + await drv.InitializeAsync("{}", CancellationToken.None); + + await drv.DiscoverAsync(builder, CancellationToken.None); + + builder.Folders.ShouldContain(f => f.BrowseName == "FOCAS"); + builder.Folders.ShouldContain(f => f.BrowseName == "focas://10.0.0.5:8193" && f.DisplayName == "Lathe-1"); + builder.Variables.Single(v => v.BrowseName == "Run").Info.SecurityClass.ShouldBe(SecurityClassification.Operate); + builder.Variables.Single(v => v.BrowseName == "Alarm").Info.SecurityClass.ShouldBe(SecurityClassification.ViewOnly); + } + + // ---- ISubscribable ---- + + [Fact] + public async Task Subscribe_initial_poll_raises_OnDataChange() + { + var factory = new FakeFocasClientFactory + { + Customise = () => new FakeFocasClient { Values = { ["R100"] = (sbyte)42 } }, + }; + var drv = new FocasDriver(new FocasDriverOptions + { + Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")], + Tags = [new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte)], + Probe = new FocasProbeOptions { Enabled = false }, + }, "drv-1", factory); + await drv.InitializeAsync("{}", CancellationToken.None); + + var events = new ConcurrentQueue(); + drv.OnDataChange += (_, e) => events.Enqueue(e); + + var handle = await drv.SubscribeAsync(["X"], TimeSpan.FromMilliseconds(200), CancellationToken.None); + await WaitForAsync(() => events.Count >= 1, TimeSpan.FromSeconds(2)); + + events.First().Snapshot.Value.ShouldBe((sbyte)42); + await drv.UnsubscribeAsync(handle, CancellationToken.None); + } + + [Fact] + public async Task ShutdownAsync_cancels_active_subscriptions() + { + var factory = new FakeFocasClientFactory + { + Customise = () => new FakeFocasClient { Values = { ["R100"] = (sbyte)1 } }, + }; + var drv = new FocasDriver(new FocasDriverOptions + { + Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")], + Tags = [new FocasTagDefinition("X", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte)], + Probe = new FocasProbeOptions { Enabled = false }, + }, "drv-1", factory); + await drv.InitializeAsync("{}", CancellationToken.None); + + var events = new ConcurrentQueue(); + drv.OnDataChange += (_, e) => events.Enqueue(e); + + _ = await drv.SubscribeAsync(["X"], TimeSpan.FromMilliseconds(100), CancellationToken.None); + await WaitForAsync(() => events.Count >= 1, TimeSpan.FromSeconds(1)); + await drv.ShutdownAsync(CancellationToken.None); + + var afterShutdown = events.Count; + await Task.Delay(200); + events.Count.ShouldBe(afterShutdown); + } + + // ---- IHostConnectivityProbe ---- + + [Fact] + public async Task GetHostStatuses_returns_entry_per_device() + { + var drv = new FocasDriver(new FocasDriverOptions + { + Devices = + [ + new FocasDeviceOptions("focas://10.0.0.5:8193"), + new FocasDeviceOptions("focas://10.0.0.6:8193"), + ], + Probe = new FocasProbeOptions { Enabled = false }, + }, "drv-1", new FakeFocasClientFactory()); + await drv.InitializeAsync("{}", CancellationToken.None); + + drv.GetHostStatuses().Count.ShouldBe(2); + } + + [Fact] + public async Task Probe_transitions_to_Running_on_success() + { + var factory = new FakeFocasClientFactory + { + Customise = () => new FakeFocasClient { ProbeResult = true }, + }; + var transitions = new ConcurrentQueue(); + var drv = new FocasDriver(new FocasDriverOptions + { + Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")], + Probe = new FocasProbeOptions + { + Enabled = true, Interval = TimeSpan.FromMilliseconds(100), + Timeout = TimeSpan.FromMilliseconds(50), + }, + }, "drv-1", factory); + drv.OnHostStatusChanged += (_, e) => transitions.Enqueue(e); + + await drv.InitializeAsync("{}", CancellationToken.None); + await WaitForAsync(() => transitions.Any(t => t.NewState == HostState.Running), TimeSpan.FromSeconds(2)); + + drv.GetHostStatuses().Single().State.ShouldBe(HostState.Running); + await drv.ShutdownAsync(CancellationToken.None); + } + + [Fact] + public async Task Probe_transitions_to_Stopped_on_failure() + { + var factory = new FakeFocasClientFactory + { + Customise = () => new FakeFocasClient { ProbeResult = false }, + }; + var transitions = new ConcurrentQueue(); + var drv = new FocasDriver(new FocasDriverOptions + { + Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")], + Probe = new FocasProbeOptions + { + Enabled = true, Interval = TimeSpan.FromMilliseconds(100), + Timeout = TimeSpan.FromMilliseconds(50), + }, + }, "drv-1", factory); + drv.OnHostStatusChanged += (_, e) => transitions.Enqueue(e); + + await drv.InitializeAsync("{}", CancellationToken.None); + await WaitForAsync(() => transitions.Any(t => t.NewState == HostState.Stopped), TimeSpan.FromSeconds(2)); + + drv.GetHostStatuses().Single().State.ShouldBe(HostState.Stopped); + await drv.ShutdownAsync(CancellationToken.None); + } + + // ---- IPerCallHostResolver ---- + + [Fact] + public async Task ResolveHost_returns_declared_device_for_known_tag() + { + var drv = new FocasDriver(new FocasDriverOptions + { + Devices = + [ + new FocasDeviceOptions("focas://10.0.0.5:8193"), + new FocasDeviceOptions("focas://10.0.0.6:8193"), + ], + Tags = + [ + new FocasTagDefinition("A", "focas://10.0.0.5:8193", "R100", FocasDataType.Byte), + new FocasTagDefinition("B", "focas://10.0.0.6:8193", "R100", FocasDataType.Byte), + ], + Probe = new FocasProbeOptions { Enabled = false }, + }, "drv-1", new FakeFocasClientFactory()); + await drv.InitializeAsync("{}", CancellationToken.None); + + drv.ResolveHost("A").ShouldBe("focas://10.0.0.5:8193"); + drv.ResolveHost("B").ShouldBe("focas://10.0.0.6:8193"); + } + + [Fact] + public async Task ResolveHost_falls_back_to_first_device_for_unknown() + { + var drv = new FocasDriver(new FocasDriverOptions + { + Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")], + Probe = new FocasProbeOptions { Enabled = false }, + }, "drv-1", new FakeFocasClientFactory()); + await drv.InitializeAsync("{}", CancellationToken.None); + + drv.ResolveHost("missing").ShouldBe("focas://10.0.0.5:8193"); + } + + [Fact] + public async Task ResolveHost_falls_back_to_DriverInstanceId_when_no_devices() + { + var drv = new FocasDriver(new FocasDriverOptions(), "drv-1", new FakeFocasClientFactory()); + await drv.InitializeAsync("{}", CancellationToken.None); + + drv.ResolveHost("anything").ShouldBe("drv-1"); + } + + // ---- helpers ---- + + private static async Task WaitForAsync(Func condition, TimeSpan timeout) + { + var deadline = DateTime.UtcNow + timeout; + while (!condition() && DateTime.UtcNow < deadline) + await Task.Delay(20); + } + + private sealed class RecordingBuilder : IAddressSpaceBuilder + { + public List<(string BrowseName, string DisplayName)> Folders { get; } = new(); + public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = new(); + + public IAddressSpaceBuilder Folder(string browseName, string displayName) + { Folders.Add((browseName, displayName)); return this; } + + public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info) + { Variables.Add((browseName, info)); return new Handle(info.FullName); } + + public void AddProperty(string _, DriverDataType __, object? ___) { } + + private sealed class Handle(string fullRef) : IVariableHandle + { + public string FullReference => fullRef; + public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink(); + } + private sealed class NullSink : IAlarmConditionSink { public void OnTransition(AlarmEventArgs args) { } } + } +} diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasScaffoldingTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasScaffoldingTests.cs index a8f6c94..58044eb 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasScaffoldingTests.cs +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasScaffoldingTests.cs @@ -207,6 +207,7 @@ public sealed class FocasScaffoldingTests var drv = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")], + Probe = new FocasProbeOptions { Enabled = false }, }, "drv-1"); await drv.InitializeAsync("{}", CancellationToken.None); -- 2.49.1