fix(twincat): publish BadNodeIdUnknown for unresolvable native-subscribe references (U12)
The native subscribe path silently skipped a reference it could not resolve (unknown RawPath, or a tag routed to an unconfigured device), leaving the node on whatever status the server seeded. It now publishes one BadNodeIdUnknown snapshot, matching the poll path. Refs #739 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>Raises one data change for a native subscription.</summary>
|
||||
private void RaiseDataChange(ISubscriptionHandle handle, string reference, DataValueSnapshot snapshot) =>
|
||||
OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, reference, snapshot));
|
||||
|
||||
/// <summary>
|
||||
/// Registers one native notification and records its replayable intent on the device — both
|
||||
/// under the device <see cref="DeviceState.ConnectGate"/> so the register can't race a
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Native-subscribe isolation (#739): an unresolvable reference publishes <c>BadNodeIdUnknown</c>
|
||||
/// (U12) instead of keeping whatever status the server seeded, and one failing registration does not
|
||||
/// take down the rest of the subscription (H11).
|
||||
/// </summary>
|
||||
[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);
|
||||
|
||||
/// <summary>
|
||||
/// U12: an unknown reference and a tag routed to an unconfigured device each publish one
|
||||
/// <c>BadNodeIdUnknown</c> snapshot; the resolvable tag still registers and delivers Good (positive control).
|
||||
/// </summary>
|
||||
[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<DataChangeEventArgs>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user