diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs
index 66663ef8..1dda4ad7 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs
@@ -666,8 +666,16 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
{
foreach (var reference in fullReferences)
{
- if (!_resolver.TryResolve(reference, out var def)) continue;
- if (!_devices.TryGetValue(def.DeviceHostAddress, out var device)) continue;
+ // An unresolvable reference (unknown RawPath, or a tag routed to an unconfigured device)
+ // publishes BadNodeIdUnknown once — the poll path's status — instead of leaving the node on
+ // whatever status the server seeded it with.
+ if (!_resolver.TryResolve(reference, out var def)
+ || !_devices.TryGetValue(def.DeviceHostAddress, out var device))
+ {
+ RaiseDataChange(handle, reference, new DataValueSnapshot(
+ null, TwinCATStatusMapper.BadNodeIdUnknown, null, DateTime.UtcNow));
+ continue;
+ }
var parsed = TwinCATSymbolPath.TryParse(def.SymbolPath);
var symbolName = parsed?.ToAdsSymbolName() ?? def.SymbolPath;
@@ -724,6 +732,10 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
return handle;
}
+ /// Raises one data change for a native subscription.
+ private void RaiseDataChange(ISubscriptionHandle handle, string reference, DataValueSnapshot snapshot) =>
+ OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, reference, snapshot));
+
///
/// Registers one native notification and records its replayable intent on the device — both
/// under the device so the register can't race a
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATNativeIsolationTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATNativeIsolationTests.cs
new file mode 100644
index 00000000..0d04eaf6
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATNativeIsolationTests.cs
@@ -0,0 +1,59 @@
+using System.Collections.Concurrent;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
+
+///
+/// Native-subscribe isolation (#739): an unresolvable reference publishes BadNodeIdUnknown
+/// (U12) instead of keeping whatever status the server seeded, and one failing registration does not
+/// take down the rest of the subscription (H11).
+///
+[Trait("Category", "Unit")]
+public sealed class TwinCATNativeIsolationTests
+{
+ private const string Host = "ads://5.23.91.23.1.1:851";
+
+ private static TwinCATDriver NewNativeDriver(FakeTwinCATClientFactory factory, params TwinCATTagDefinition[] tags) =>
+ new(new TwinCATDriverOptions
+ {
+ Devices = [new TwinCATDeviceOptions(Host)],
+ RawTags = TwinCATRawTags.Entries(tags),
+ Probe = new TwinCATProbeOptions { Enabled = false },
+ UseNativeNotifications = true,
+ }, "drv-1", factory);
+
+ ///
+ /// U12: an unknown reference and a tag routed to an unconfigured device each publish one
+ /// BadNodeIdUnknown snapshot; the resolvable tag still registers and delivers Good (positive control).
+ ///
+ [Fact]
+ public async Task Native_subscribe_publishes_BadNodeIdUnknown_for_unresolvable_references()
+ {
+ var factory = new FakeTwinCATClientFactory();
+ var drv = NewNativeDriver(factory,
+ new TwinCATTagDefinition("Speed", Host, "MAIN.Speed", TwinCATDataType.DInt),
+ new TwinCATTagDefinition("Orphan", "ads://9.9.9.9.1.1:851", "MAIN.Orphan", TwinCATDataType.DInt));
+ await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
+
+ var events = new ConcurrentQueue();
+ drv.OnDataChange += (_, e) => events.Enqueue(e);
+
+ _ = await drv.SubscribeAsync(["Ghost", "Speed", "Orphan"], TimeSpan.FromMilliseconds(100),
+ TestContext.Current.CancellationToken);
+
+ var ghost = events.Single(e => e.FullReference == "Ghost");
+ ghost.Snapshot.StatusCode.ShouldBe(TwinCATStatusMapper.BadNodeIdUnknown);
+ ghost.Snapshot.Value.ShouldBeNull();
+ events.Single(e => e.FullReference == "Orphan").Snapshot.StatusCode.ShouldBe(TwinCATStatusMapper.BadNodeIdUnknown);
+
+ // Positive control: the resolvable tag registered and its push arrives Good.
+ factory.Clients.ShouldHaveSingleItem().Notifications.ShouldHaveSingleItem().SymbolPath.ShouldBe("MAIN.Speed");
+ factory.Clients[0].FireNotification("MAIN.Speed", 42);
+ var good = events.Single(e => e.FullReference == "Speed");
+ good.Snapshot.StatusCode.ShouldBe(TwinCATStatusMapper.Good);
+ good.Snapshot.Value.ShouldBe(42);
+ }
+}