From 10c909cbc227c6938afd0861e7e4caa9160ad0f2 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 7 Oct 2026 15:44:02 -0400 Subject: [PATCH] fix(abcip): translate decode/encode-time libplctag exceptions; doc and probe-test cleanups --- .../AbCipDriver.cs | 4 ++-- .../AbCipDriverProbe.cs | 17 ++++----------- .../IAbCipTagRuntime.cs | 2 +- .../LibplctagStatusTranslation.cs | 18 ++++++++++++++++ .../LibplctagTagRuntime.cs | 17 +++++++++++---- .../AbCipDriverProbeTests.cs | 21 +++++++++++++++++++ .../AbCipDriverReadTests.cs | 20 ++++++++++++++++++ .../AbCipStatusMapperTests.cs | 10 +++++++++ 8 files changed, 89 insertions(+), 20 deletions(-) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs index df97d4a9..fd10e654 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs @@ -834,8 +834,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, } catch (Exception ex) { - // Transport exception — evict so the next read creates a fresh handle. A libplctag - // exception carries its Status, so a tag the controller does not have reads + // Transport exception — evict so the next read creates a fresh handle. An AbTransportException + // carries its AbStatus, so a tag the controller does not have reads // BadNodeIdUnknown and a decode past the buffer BadOutOfRange, not a comms error. EvictRuntime(device, def.Name); results[fb.OriginalIndex] = new DataValueSnapshot(null, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs index 6973cd04..84bf5979 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs @@ -18,19 +18,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// non-CIP server) returns Ok = false with a "handshake failed" message instead of /// a false-positive green tick. /// -/// Status-code discrimination: transport statuses (AbStatus) that indicate the controller answered -/// the CIP session but could not resolve the probe tag -/// (ErrorNotFound, ErrorNoMatch, -/// ErrorBadDevice) are treated as "reachable — Ok = true" because -/// a non-existent tag name proves CIP connectivity. Statuses that indicate a transport or -/// session failure (ErrorTimeout, ErrorBadConnection, -/// ErrorBadGateway, ErrorWinsock, -/// ErrorOpen, ErrorClose, -/// ErrorRead, ErrorWrite, -/// ErrorBadReply, ErrorRemoteErr, -/// ErrorPartial, ErrorAbort) are treated as -/// "handshake failed — Ok = false". All other error statuses default to -/// "handshake failed" (conservative). +/// Status-code discrimination: AbStatus.Ok and AbStatus.NotFound mean the controller +/// answered the CIP session (a non-existent probe tag name still proves CIP connectivity) and are +/// treated as "reachable - Ok = true". Every other status (timeout, bad connection, bad reply, +/// partial, abort, ...) is a transport or session failure: "handshake failed - Ok = false". /// /// public sealed class AbCipDriverProbe : IDriverProbe diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs index 61b6a016..ed0c781d 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs @@ -28,7 +28,7 @@ public interface IAbCipTagRuntime : IDisposable /// Raw transport status code (an AbStatus value) — mapped to an OPC UA StatusCode via /// . Zero on success, negative on error. /// - /// The raw libplctag status code (zero on success, negative on error). + /// The AbStatus value (zero on success, negative on error). int GetStatus(); /// diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagStatusTranslation.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagStatusTranslation.cs index 4e938c5b..3d695bdd 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagStatusTranslation.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagStatusTranslation.cs @@ -55,4 +55,22 @@ internal static class LibplctagStatusTranslation try { return await op().ConfigureAwait(false); } catch (LibPlcTagException ex) { throw new AbTransportException(ToAbStatus(ex), ex.Message, ex); } } + + /// Runs a synchronous libplctag call, rethrowing as . + /// The result type. + /// The libplctag call. + /// The call result. + public static T WrapSync(Func op) + { + try { return op(); } + catch (LibPlcTagException ex) { throw new AbTransportException(ToAbStatus(ex), ex.Message, ex); } + } + + /// Runs a synchronous libplctag call, rethrowing as . + /// The libplctag call. + public static void WrapSync(Action op) + { + try { op(); } + catch (LibPlcTagException ex) { throw new AbTransportException(ToAbStatus(ex), ex.Message, ex); } + } } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs index 62d8a914..3087492a 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs @@ -51,13 +51,16 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime public int GetStatus() => (int)LibplctagStatusTranslation.ToAbStatus(_tag.GetStatus()); /// - public int GetSize() => _tag.GetSize(); + public int GetSize() => LibplctagStatusTranslation.WrapSync(() => _tag.GetSize()); /// public object? DecodeValue(AbCipDataType type, int? bitIndex) => DecodeValueAt(type, 0, bitIndex); /// - public object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex) => type switch + public object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex) => + LibplctagStatusTranslation.WrapSync(() => DecodeValueAtCore(type, offset, bitIndex)); + + private object? DecodeValueAtCore(AbCipDataType type, int offset, int? bitIndex) => type switch { AbCipDataType.Bool => bitIndex is int bit ? _tag.GetBit(bit) @@ -79,7 +82,10 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime }; /// - public object? DecodeArray(AbCipDataType type, int count) + public object? DecodeArray(AbCipDataType type, int count) => + LibplctagStatusTranslation.WrapSync(() => DecodeArrayCore(type, count)); + + private object? DecodeArrayCore(AbCipDataType type, int count) { if (count < 1) count = 1; // libplctag reports the per-element byte size once the tag has been read; it correctly @@ -160,7 +166,10 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime }; /// - public void EncodeValue(AbCipDataType type, int? bitIndex, object? value) + public void EncodeValue(AbCipDataType type, int? bitIndex, object? value) => + LibplctagStatusTranslation.WrapSync(() => EncodeValueCore(type, bitIndex, value)); + + private void EncodeValueCore(AbCipDataType type, int? bitIndex, object? value) { switch (type) { diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverProbeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverProbeTests.cs index 5707aea4..90c4f4cf 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverProbeTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverProbeTests.cs @@ -181,4 +181,25 @@ public sealed class AbCipDriverProbeTests public void AnUnrecognisedExceptionMessageIsNotTreatedAsReachable() => AbCipDriverProbe.IsReachableException( new AbTransportException(AbStatus.BadConnection, "something the wrapper has never said")).ShouldBeFalse(); + + /// A fake runtime reporting NotFound is reachable ("probe tag not found"). + [Fact] + public async Task Injected_factory_NotFound_status_is_reported_reachable() + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + var factory = new FakeAbCipTagFactory + { + Customise = p => new FakeAbCipTag(p) { Status = (int)AbStatus.NotFound }, + }; + + var result = await new AbCipDriverProbe(factory).ProbeAsync( + $"{{\"devices\":[{{\"hostAddress\":\"ab://127.0.0.1:{port}/1,0\"}}]}}", + TimeSpan.FromSeconds(3), + TestContext.Current.CancellationToken); + + result.Ok.ShouldBeTrue(); + result.Message!.ShouldContain("probe tag not found"); + } } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs index fa46b20d..61a78199 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverReadTests.cs @@ -224,4 +224,24 @@ public sealed class AbCipDriverReadTests snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadCommunicationError); factory.Tags["Nope"].Disposed.ShouldBeTrue(); } + + /// A decode that throws OutOfBounds reads BadOutOfRange through the driver read path. + [Fact] + public async Task Decode_OutOfBounds_exception_maps_to_BadOutOfRange() + { + var (drv, factory) = NewDriver( + new AbCipTagDefinition("Over", "ab://10.0.0.5/1,0", "Over", AbCipDataType.DInt)); + await drv.InitializeAsync("{}", CancellationToken.None); + factory.Customise = p => new ThrowingDecodeTag(p); + + var snapshots = await drv.ReadAsync(["Over"], CancellationToken.None); + + snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadOutOfRange); + } + + private sealed class ThrowingDecodeTag(AbCipTagCreateParams p) : FakeAbCipTag(p) + { + public override object? DecodeValue(AbCipDataType type, int? bitIndex) => + throw new AbTransportException(AbStatus.OutOfBounds, "decode past buffer"); + } } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipStatusMapperTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipStatusMapperTests.cs index e8cf140b..8c813c63 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipStatusMapperTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipStatusMapperTests.cs @@ -104,4 +104,14 @@ public sealed class AbCipStatusMapperTests Status.ErrorUnsupported or Status.ErrorNotImplemented => AbCipStatusMapper.BadNotSupported, _ => AbCipStatusMapper.BadCommunicationError, // incl. BadConnection/BadGateway/BadReply/Partial/Abort/... }; + + /// A decode past the buffer end surfaces as BadOutOfRange once the adapter wraps it. + [Fact] + public void Wrapped_decode_OutOfBounds_maps_to_BadOutOfRange() + { + var ex = Should.Throw(() => + LibplctagStatusTranslation.WrapSync(() => throw new LibPlcTagException("ErrorOutOfBounds"))); + + AbCipStatusMapper.MapException(ex).ShouldBe(AbCipStatusMapper.BadOutOfRange); + } }