diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs
index 1befff77..c4d5d5e6 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs
@@ -315,14 +315,14 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
// Evict the stale handle so the next call re-creates it (mirrors AbCip).
EvictRuntime(device, def.Name);
results[i] = new DataValueSnapshot(null,
- AbLegacyStatusMapper.MapLibplctagStatus(status), null, now);
+ AbLegacyStatusMapper.MapStatus(status), null, now);
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead,
- $"libplctag status {status} reading {reference}");
+ $"transport status {status} reading {reference}");
if (!device.FirstNonZeroStatusLogged)
{
device.FirstNonZeroStatusLogged = true;
_logger.LogWarning(
- "AbLegacy non-zero libplctag status. Driver={DriverInstanceId} Device={DeviceHostAddress} Reference={Reference} Status={Status}",
+ "AbLegacy non-zero transport status. Driver={DriverInstanceId} Device={DeviceHostAddress} Reference={Reference} Status={Status}",
_driverInstanceId, def.DeviceHostAddress, reference, status);
}
continue;
@@ -416,7 +416,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
{
// Evict the stale handle so the next call re-creates it (mirrors AbCip).
EvictRuntime(device, def.Name);
- results[i] = new WriteResult(AbLegacyStatusMapper.MapLibplctagStatus(status));
+ results[i] = new WriteResult(AbLegacyStatusMapper.MapStatus(status));
}
else
{
@@ -654,7 +654,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
var parentRuntime = await EnsureParentRuntimeAsync(device, parentName, ct).ConfigureAwait(false);
await parentRuntime.ReadAsync(ct).ConfigureAwait(false);
var readStatus = parentRuntime.GetStatus();
- if (readStatus != 0) return AbLegacyStatusMapper.MapLibplctagStatus(readStatus);
+ if (readStatus != 0) return AbLegacyStatusMapper.MapStatus(readStatus);
// Mask the decoded word to its native width: DecodeValue for a 16-bit Int returns a
// sign-extended int, so a word with bit 15 set decodes negative. Masking pins the
@@ -673,7 +673,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
var writeStatus = parentRuntime.GetStatus();
return writeStatus == 0
? AbLegacyStatusMapper.Good
- : AbLegacyStatusMapper.MapLibplctagStatus(writeStatus);
+ : AbLegacyStatusMapper.MapStatus(writeStatus);
}
finally
{
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs
index fadeab3a..62bfe1fa 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs
@@ -2,7 +2,7 @@ using System.Diagnostics;
using System.Net.Sockets;
using System.Text.Json;
using System.Text.Json.Serialization;
-using libplctag;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
@@ -18,23 +18,25 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
/// non-PCCC server) returns Ok = false with a "handshake failed" message instead of
/// a false-positive green tick.
///
-/// Status-code discrimination: libplctag statuses that indicate the controller answered
+/// Status-code discrimination: transport statuses that indicate the controller answered
/// the PCCC session but could not resolve the probe tag
-/// (, ,
-/// ) are treated as "reachable — Ok = true" because
+/// () are treated as "reachable — Ok = true" because
/// a non-existent tag name proves PCCC connectivity. Statuses that indicate a transport or
-/// session failure (, ,
-/// , ,
-/// , ,
-/// , ,
-/// , ,
-/// , ) are treated as
+/// session failure (, ,
+/// , , ) are treated as
/// "handshake failed — Ok = false". All other error statuses default to
/// "handshake failed" (conservative).
///
///
public sealed class AbLegacyDriverProbe : IDriverProbe
{
+ private readonly IAbLegacyTagFactory _tagFactory;
+
+ /// Initializes a new instance of the class.
+ /// The tag factory used for the PCCC handshake; defaults to the libplctag-backed one.
+ public AbLegacyDriverProbe(IAbLegacyTagFactory? tagFactory = null) =>
+ _tagFactory = tagFactory ?? new LibplctagLegacyTagFactory();
+
private static readonly JsonSerializerOptions _opts = new()
{
PropertyNameCaseInsensitive = true,
@@ -104,7 +106,7 @@ public sealed class AbLegacyDriverProbe : IDriverProbe
TagName: tagName,
Timeout: timeout);
- var rt = new LibplctagLegacyTagRuntime(p);
+ var rt = _tagFactory.Create(p);
try
{
await rt.InitializeAsync(cts.Token);
@@ -112,10 +114,10 @@ public sealed class AbLegacyDriverProbe : IDriverProbe
// InitializeAsync completed without throwing — either the tag was found (Ok) or
// libplctag returned a non-exception status. Check via GetStatus().
- var statusCode = (Status)rt.GetStatus();
+ var statusCode = (AbStatus)rt.GetStatus();
if (IsReachableStatus(statusCode))
{
- var msg = statusCode == Status.Ok
+ var msg = statusCode == AbStatus.Ok
? "PCCC session OK"
: "PCCC session OK (controller reachable; probe tag not found)";
return new(true, msg, sw.Elapsed);
@@ -128,10 +130,10 @@ public sealed class AbLegacyDriverProbe : IDriverProbe
{
return new(false, $"Probe timed out after {timeout.TotalSeconds:F0}s.", null);
}
- catch (LibPlcTagException ex)
+ catch (AbTransportException ex)
{
- // LibPlcTagException carries the Status; classify as reachable vs transport failure.
- if (IsReachableException(ex))
+ // AbTransportException carries the Status; classify as reachable vs transport failure.
+ if (IsReachableStatus(ex.Status))
{
sw.Stop();
return new(true, "PCCC session OK (controller reachable; probe tag not found)", sw.Elapsed);
@@ -150,30 +152,13 @@ public sealed class AbLegacyDriverProbe : IDriverProbe
}
///
- /// Returns true for libplctag statuses that indicate the PCCC controller answered
- /// but the probe tag name was not found or did not match — sufficient to confirm PCCC
- /// reachability. Returns true for (tag was found).
+ /// Returns true for statuses that indicate the PCCC controller answered but the
+ /// probe tag was not found — sufficient to confirm PCCC reachability. Also true for
+ /// (tag was found).
///
- private static bool IsReachableStatus(Status s) => s is
- Status.Ok or
- Status.ErrorNotFound or
- Status.ErrorNoMatch or
- Status.ErrorBadDevice;
-
- ///
- /// Inspects a message to decide whether the error is a
- /// "controller answered PCCC but tag not found" condition (reachable) versus a session /
- /// transport failure (unreachable). Falls back to checking the exception message for
- /// known tag-not-found phrases used by the libplctag native library.
- ///
- private static bool IsReachableException(LibPlcTagException ex)
- {
- // libplctag wraps the Status in the message as "STATUS_NOT_FOUND", "STATUS_NO_MATCH", etc.
- var msg = ex.Message ?? string.Empty;
- return msg.Contains("NOT_FOUND", StringComparison.OrdinalIgnoreCase)
- || msg.Contains("NO_MATCH", StringComparison.OrdinalIgnoreCase)
- || msg.Contains("BAD_DEVICE", StringComparison.OrdinalIgnoreCase);
- }
+ /// The transport status.
+ /// Whether the status proves the controller answered.
+ internal static bool IsReachableStatus(AbStatus s) => s is AbStatus.Ok or AbStatus.NotFound;
private static (AbLegacyHostAddress? parsed, AbLegacyDeviceOptions? device) ExtractTarget(AbLegacyDriverOptions opts)
{
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyStatusMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyStatusMapper.cs
index 9402a82e..d5746476 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyStatusMapper.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyStatusMapper.cs
@@ -1,9 +1,9 @@
-using libplctag;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
///
-/// Maps libplctag status codes + PCCC STS/EXT_STS bytes to OPC UA StatusCodes. Mirrors the
+/// Maps transport status codes + PCCC STS/EXT_STS bytes to OPC UA StatusCodes. Mirrors the
/// AbCip mapper — PCCC errors roughly align with CIP general-status in shape but with a
/// different byte vocabulary (PCCC STS nibble-low + EXT_STS on code 0x0F).
///
@@ -22,44 +22,41 @@ public static class AbLegacyStatusMapper
public const uint BadTypeMismatch = 0x80740000u;
///
- /// Map a libplctag return/status code to an OPC UA StatusCode. The integer passed here
- /// is (int)Tag.GetStatus() — the underlying value of the libplctag.NET
- /// enum. Delegates to the strongly-typed overload so the mapping
- /// stays correct regardless of how the wrapper renumbers native PLCTAG_ERR_* constants
- /// in future releases.
+ /// Map the integer form of an (what
+ /// returns) to an OPC UA StatusCode.
///
- /// The integer status code from libplctag.
+ /// The integer transport status.
/// The corresponding OPC UA status code.
- public static uint MapLibplctagStatus(int status) => MapLibplctagStatus((Status)status);
+ public static uint MapStatus(int status) => MapStatus((AbStatus)status);
- ///
- /// Map a libplctag.NET enum value to an OPC UA StatusCode. This is
- /// the canonical core; the int overload exists only for the
- /// seam which boxes the enum as an int.
- ///
- /// The libplctag Status enum value.
+ /// Map a transport-neutral to an OPC UA StatusCode.
+ /// The transport status.
/// The corresponding OPC UA status code.
- public static uint MapLibplctagStatus(Status status) => status switch
+ public static uint MapStatus(AbStatus status) => status switch
{
- Status.Ok => Good,
- Status.Pending => GoodMoreData,
- Status.ErrorTimeout => BadTimeout,
- Status.ErrorNotFound or Status.ErrorNoMatch or Status.ErrorBadDevice => BadNodeIdUnknown,
- Status.ErrorNotAllowed => BadNotWritable,
- Status.ErrorOutOfBounds or Status.ErrorTooLarge or Status.ErrorTooSmall => BadOutOfRange,
- Status.ErrorUnsupported or Status.ErrorNotImplemented => BadNotSupported,
- Status.ErrorBadConnection or Status.ErrorBadGateway or Status.ErrorBadReply
- or Status.ErrorWinsock or Status.ErrorOpen or Status.ErrorClose
- or Status.ErrorRead or Status.ErrorWrite or Status.ErrorRemoteErr
- or Status.ErrorPartial or Status.ErrorAbort => BadCommunicationError,
- _ => BadCommunicationError,
+ AbStatus.Ok => Good,
+ AbStatus.Pending => GoodMoreData,
+ AbStatus.Timeout => BadTimeout,
+ AbStatus.NotFound => BadNodeIdUnknown,
+ AbStatus.NotAllowed => BadNotWritable,
+ AbStatus.OutOfBounds or AbStatus.TooLarge or AbStatus.TooSmall => BadOutOfRange,
+ AbStatus.Unsupported => BadNotSupported,
+ AbStatus.TypeMismatch => BadTypeMismatch,
+ AbStatus.DeviceStateConflict => BadDeviceFailure,
+ _ => BadCommunicationError, // BadConnection, BadReply, Partial, Abort
};
+ /// Map an exception thrown by a transport to an OPC UA StatusCode.
+ /// The exception.
+ /// The mapped status for an , else BadCommunicationError.
+ public static uint MapException(Exception ex) =>
+ ex is AbTransportException ate ? MapStatus(ate.Status) : BadCommunicationError;
+
///
/// Map a PCCC STS (status) byte. Common codes per AB PCCC reference:
/// 0x00 = success, 0x10 = illegal command, 0x20 = bad address, 0x30 = protected,
/// 0x40 = programmer busy, 0x50 = file locked, 0xF0 = extended status follows.
- /// libplctag surfaces only its own enum rather than exposing
+ /// the transports surface only an rather than exposing
/// the raw STS byte, so this method is not wired into the current read/write path.
/// It is retained as the reference mapping for future PCCC-STS inspection.
///
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyStatusTranslation.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyStatusTranslation.cs
new file mode 100644
index 00000000..3d762e84
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyStatusTranslation.cs
@@ -0,0 +1,45 @@
+using libplctag;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
+
+/// The ONLY place libplctag's meets in AbLegacy.
+internal static class LibplctagLegacyStatusTranslation
+{
+ /// Translates a libplctag status into the transport-neutral .
+ /// The libplctag status.
+ /// The corresponding .
+ public static AbStatus ToAbStatus(Status s) => s switch
+ {
+ Status.Ok => AbStatus.Ok,
+ Status.Pending => AbStatus.Pending,
+ Status.ErrorTimeout => AbStatus.Timeout,
+ Status.ErrorNotFound or Status.ErrorNoMatch or Status.ErrorBadDevice => AbStatus.NotFound,
+ Status.ErrorNotAllowed => AbStatus.NotAllowed,
+ Status.ErrorOutOfBounds => AbStatus.OutOfBounds,
+ Status.ErrorTooLarge => AbStatus.TooLarge,
+ Status.ErrorTooSmall => AbStatus.TooSmall,
+ Status.ErrorUnsupported or Status.ErrorNotImplemented => AbStatus.Unsupported,
+ Status.ErrorPartial => AbStatus.Partial,
+ Status.ErrorAbort => AbStatus.Abort,
+ Status.ErrorBadReply => AbStatus.BadReply,
+ _ => AbStatus.BadConnection,
+ };
+
+ /// libplctag builds the exception from Status.ToString(), so the message is the enum name.
+ /// The libplctag exception.
+ /// The corresponding .
+ public static AbStatus ToAbStatus(LibPlcTagException ex) =>
+ Enum.TryParse((ex.Message ?? "").Trim(), ignoreCase: true, out var s) && Enum.IsDefined(s)
+ ? ToAbStatus(s)
+ : AbStatus.BadConnection;
+
+ /// Runs , rethrowing libplctag failures as .
+ /// The libplctag operation.
+ /// A task completing with the operation.
+ public static async Task Wrap(Func op)
+ {
+ try { await op().ConfigureAwait(false); }
+ catch (LibPlcTagException ex) { throw new AbTransportException(ToAbStatus(ex), ex.Message, ex); }
+ }
+}
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs
index fb71ce32..c15605f6 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs
@@ -1,4 +1,5 @@
using libplctag;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
@@ -31,16 +32,19 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
}
///
- public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);
+ public Task InitializeAsync(CancellationToken cancellationToken) =>
+ LibplctagLegacyStatusTranslation.Wrap(() => _tag.InitializeAsync(cancellationToken));
///
- public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken);
+ public Task ReadAsync(CancellationToken cancellationToken) =>
+ LibplctagLegacyStatusTranslation.Wrap(() => _tag.ReadAsync(cancellationToken));
///
- public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken);
+ public Task WriteAsync(CancellationToken cancellationToken) =>
+ LibplctagLegacyStatusTranslation.Wrap(() => _tag.WriteAsync(cancellationToken));
///
- public int GetStatus() => (int)_tag.GetStatus();
+ public int GetStatus() => (int)LibplctagLegacyStatusTranslation.ToAbStatus(_tag.GetStatus());
///
public object? DecodeValue(AbLegacyDataType type, int? bitIndex) => type switch
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyArrayTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyArrayTests.cs
index b01bfbee..9c5e428d 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyArrayTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyArrayTests.cs
@@ -112,7 +112,7 @@ public sealed class AbLegacyArrayTests
var (drv, factory) = NewDriver(
new AbLegacyTagDefinition("Levels", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int, ArrayLength: 5));
await drv.InitializeAsync("{}", CancellationToken.None);
- factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)libplctag.Status.ErrorTimeout };
+ factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.AbStatus.Timeout };
var snapshots = await drv.ReadAsync(["Levels"], CancellationToken.None);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs
index 53824c40..3229fb53 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyBitRmwTests.cs
@@ -229,7 +229,7 @@ public sealed class AbLegacyBitRmwTests
public async Task Bit_write_surfaces_device_rejection_status()
{
// Arrange: the parent tag reads OK (Status = 0) but write returns a timeout error.
- const int errorTimeout = (int)libplctag.Status.ErrorTimeout;
+ const int errorTimeout = (int)ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.AbStatus.Timeout;
var factory = new FakeAbLegacyTagFactory
{
Customise = p => new FakeAbLegacyTag(p)
@@ -252,6 +252,6 @@ public sealed class AbLegacyBitRmwTests
// Assert: the write rejection must NOT be silently swallowed as Good.
var statusCode = results.Single().StatusCode;
statusCode.ShouldNotBe(AbLegacyStatusMapper.Good);
- statusCode.ShouldBe(AbLegacyStatusMapper.MapLibplctagStatus(errorTimeout));
+ statusCode.ShouldBe(AbLegacyStatusMapper.MapStatus(errorTimeout));
}
}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEvictOnFailureTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEvictOnFailureTests.cs
index 428c1144..aebeb1a1 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEvictOnFailureTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyEvictOnFailureTests.cs
@@ -1,4 +1,4 @@
-using libplctag;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
@@ -42,7 +42,7 @@ public sealed class AbLegacyEvictOnFailureTests
{
callCount++;
return callCount == 1
- ? new FakeAbLegacyTag(p) { Status = (int)Status.ErrorBadConnection }
+ ? new FakeAbLegacyTag(p) { Status = (int)AbStatus.BadConnection }
: new FakeAbLegacyTag(p) { Status = 0, Value = 42 };
};
await drv.InitializeAsync("{}", CancellationToken.None);
@@ -94,7 +94,7 @@ public sealed class AbLegacyEvictOnFailureTests
{
callCount++;
return callCount == 1
- ? new FakeAbLegacyTag(p) { Status = (int)Status.ErrorBadConnection }
+ ? new FakeAbLegacyTag(p) { Status = (int)AbStatus.BadConnection }
: new FakeAbLegacyTag(p) { Status = 0 };
};
await drv.InitializeAsync("{}", CancellationToken.None);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyHostAndStatusTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyHostAndStatusTests.cs
index 1af52555..07c5abdb 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyHostAndStatusTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyHostAndStatusTests.cs
@@ -1,6 +1,7 @@
using libplctag;
using Shouldly;
using Xunit;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
@@ -65,42 +66,75 @@ public sealed class AbLegacyHostAndStatusTests
AbLegacyStatusMapper.MapPcccStatus(sts).ShouldBe(expected);
}
- /// Verifies that libplctag Status enum members are mapped to OPC UA status codes correctly.
- /// The libplctag Status enum value to map.
+ /// Verifies that AbStatus members map to OPC UA status codes correctly.
+ /// The AbStatus value to map.
/// The expected OPC UA status code.
- // Driver.AbLegacy-010 — tests use the libplctag.NET Status enum members (what
- // (int)Tag.GetStatus() actually returns) rather than the unverified magic integers
- // that predated this fix (-5/-7/-14/-16/-17 matched neither native PLCTAG_ERR_*
- // constants nor the .NET wrapper enum ordinals reliably).
[Theory]
- [InlineData(Status.Ok, AbLegacyStatusMapper.Good)]
- [InlineData(Status.Pending, AbLegacyStatusMapper.GoodMoreData)]
- [InlineData(Status.ErrorTimeout, AbLegacyStatusMapper.BadTimeout)]
- [InlineData(Status.ErrorNotFound, AbLegacyStatusMapper.BadNodeIdUnknown)]
- [InlineData(Status.ErrorNoMatch, AbLegacyStatusMapper.BadNodeIdUnknown)]
- [InlineData(Status.ErrorNotAllowed, AbLegacyStatusMapper.BadNotWritable)]
- [InlineData(Status.ErrorOutOfBounds, AbLegacyStatusMapper.BadOutOfRange)]
- [InlineData(Status.ErrorTooLarge, AbLegacyStatusMapper.BadOutOfRange)]
- [InlineData(Status.ErrorBadConnection, AbLegacyStatusMapper.BadCommunicationError)]
- [InlineData(Status.ErrorBadGateway, AbLegacyStatusMapper.BadCommunicationError)]
- [InlineData(Status.ErrorUnsupported, AbLegacyStatusMapper.BadNotSupported)]
- [InlineData(Status.ErrorNoMem, AbLegacyStatusMapper.BadCommunicationError)] // unmapped → generic comms
- public void LibplctagStatus_maps_real_enum_members(Status status, uint expected)
+ [InlineData(AbStatus.Ok, AbLegacyStatusMapper.Good)]
+ [InlineData(AbStatus.Pending, AbLegacyStatusMapper.GoodMoreData)]
+ [InlineData(AbStatus.Timeout, AbLegacyStatusMapper.BadTimeout)]
+ [InlineData(AbStatus.NotFound, AbLegacyStatusMapper.BadNodeIdUnknown)]
+ [InlineData(AbStatus.NotAllowed, AbLegacyStatusMapper.BadNotWritable)]
+ [InlineData(AbStatus.OutOfBounds, AbLegacyStatusMapper.BadOutOfRange)]
+ [InlineData(AbStatus.TooLarge, AbLegacyStatusMapper.BadOutOfRange)]
+ [InlineData(AbStatus.TooSmall, AbLegacyStatusMapper.BadOutOfRange)]
+ [InlineData(AbStatus.Unsupported, AbLegacyStatusMapper.BadNotSupported)]
+ [InlineData(AbStatus.TypeMismatch, AbLegacyStatusMapper.BadTypeMismatch)]
+ [InlineData(AbStatus.DeviceStateConflict, AbLegacyStatusMapper.BadDeviceFailure)]
+ [InlineData(AbStatus.BadConnection, AbLegacyStatusMapper.BadCommunicationError)]
+ [InlineData(AbStatus.BadReply, AbLegacyStatusMapper.BadCommunicationError)]
+ [InlineData(AbStatus.Partial, AbLegacyStatusMapper.BadCommunicationError)]
+ [InlineData(AbStatus.Abort, AbLegacyStatusMapper.BadCommunicationError)]
+ public void AbStatus_maps_to_opcua_codes(AbStatus status, uint expected)
{
- AbLegacyStatusMapper.MapLibplctagStatus(status).ShouldBe(expected);
+ AbLegacyStatusMapper.MapStatus(status).ShouldBe(expected);
// The int overload must agree — it is the seam IAbLegacyTagRuntime.GetStatus() drives.
- AbLegacyStatusMapper.MapLibplctagStatus((int)status).ShouldBe(expected);
+ AbLegacyStatusMapper.MapStatus((int)status).ShouldBe(expected);
+ }
+
+ /// MapException reads the status off an AbTransportException, else falls back to comms error.
+ [Fact]
+ public void MapException_uses_transport_status()
+ {
+ AbLegacyStatusMapper.MapException(new AbTransportException(AbStatus.Timeout, "t"))
+ .ShouldBe(AbLegacyStatusMapper.BadTimeout);
+ AbLegacyStatusMapper.MapException(new InvalidOperationException("x"))
+ .ShouldBe(AbLegacyStatusMapper.BadCommunicationError);
+ }
+
+ ///
+ /// The regression oracle: the OPC UA code the pre-seam MapLibplctagStatus(Status) returned,
+ /// copied verbatim from the deleted switch. For every libplctag member the new
+ /// translation + must reproduce it.
+ ///
+ private static uint OldMapLibplctagStatus(Status status) => status switch
+ {
+ Status.Ok => AbLegacyStatusMapper.Good,
+ Status.Pending => AbLegacyStatusMapper.GoodMoreData,
+ Status.ErrorTimeout => AbLegacyStatusMapper.BadTimeout,
+ Status.ErrorNotFound or Status.ErrorNoMatch or Status.ErrorBadDevice => AbLegacyStatusMapper.BadNodeIdUnknown,
+ Status.ErrorNotAllowed => AbLegacyStatusMapper.BadNotWritable,
+ Status.ErrorOutOfBounds or Status.ErrorTooLarge or Status.ErrorTooSmall => AbLegacyStatusMapper.BadOutOfRange,
+ Status.ErrorUnsupported or Status.ErrorNotImplemented => AbLegacyStatusMapper.BadNotSupported,
+ _ => AbLegacyStatusMapper.BadCommunicationError,
+ };
+
+ /// Every libplctag Status member maps to the same OPC UA code as before the seam.
+ [Fact]
+ public void Translation_plus_mapper_matches_the_old_libplctag_switch_for_every_member()
+ {
+ foreach (var s in Enum.GetValues())
+ {
+ AbLegacyStatusMapper.MapStatus(LibplctagLegacyStatusTranslation.ToAbStatus(s))
+ .ShouldBe(OldMapLibplctagStatus(s), $"libplctag Status.{s}");
+ }
}
/// Verifies that timeout errors are distinguished from generic communication errors.
[Fact]
- public void MapLibplctagStatus_distinguishes_timeout_from_generic_comms_error()
+ public void MapStatus_distinguishes_timeout_from_generic_comms_error()
{
- // Regression for Driver.AbLegacy-010: timeout must not fall through to
- // BadCommunicationError the way the old magic-integer switch did.
- AbLegacyStatusMapper.MapLibplctagStatus((int)Status.ErrorTimeout)
- .ShouldBe(AbLegacyStatusMapper.BadTimeout);
- AbLegacyStatusMapper.MapLibplctagStatus((int)Status.ErrorNotFound)
- .ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
+ AbLegacyStatusMapper.MapStatus((int)AbStatus.Timeout).ShouldBe(AbLegacyStatusMapper.BadTimeout);
+ AbLegacyStatusMapper.MapStatus((int)AbStatus.NotFound).ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
}
}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs
index 45d36552..e047a267 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/AbLegacyReadWriteTests.cs
@@ -1,4 +1,4 @@
-using libplctag;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
@@ -70,12 +70,12 @@ public sealed class AbLegacyReadWriteTests
[Fact]
public async Task NonZero_libplctag_status_maps_via_AbLegacyStatusMapper()
{
- // Use the real libplctag.Status enum value rather than a raw integer so the test
+ // Use the real AbStatus enum value rather than a raw integer so the test
// stays correct if the wrapper renumbers its ordinals (Driver.AbLegacy-010).
var (drv, factory) = NewDriver(
new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int));
await drv.InitializeAsync("{}", CancellationToken.None);
- factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)Status.ErrorNotFound };
+ factory.Customise = p => new FakeAbLegacyTag(p) { Status = (int)AbStatus.NotFound };
var snapshots = await drv.ReadAsync(["X"], CancellationToken.None);
snapshots.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadNodeIdUnknown);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/LibplctagLegacyStatusTranslationTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/LibplctagLegacyStatusTranslationTests.cs
new file mode 100644
index 00000000..9d396696
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests/LibplctagLegacyStatusTranslationTests.cs
@@ -0,0 +1,36 @@
+using libplctag;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
+
+public sealed class LibplctagLegacyStatusTranslationTests
+{
+ [Theory]
+ [InlineData(Status.Ok, AbStatus.Ok)]
+ [InlineData(Status.Pending, AbStatus.Pending)]
+ [InlineData(Status.ErrorTimeout, AbStatus.Timeout)]
+ [InlineData(Status.ErrorNotFound, AbStatus.NotFound)]
+ [InlineData(Status.ErrorNoMatch, AbStatus.NotFound)]
+ [InlineData(Status.ErrorBadDevice, AbStatus.NotFound)]
+ [InlineData(Status.ErrorNotAllowed, AbStatus.NotAllowed)]
+ [InlineData(Status.ErrorOutOfBounds, AbStatus.OutOfBounds)]
+ [InlineData(Status.ErrorTooLarge, AbStatus.TooLarge)]
+ [InlineData(Status.ErrorTooSmall, AbStatus.TooSmall)]
+ [InlineData(Status.ErrorUnsupported, AbStatus.Unsupported)]
+ [InlineData(Status.ErrorNotImplemented, AbStatus.Unsupported)]
+ [InlineData(Status.ErrorPartial, AbStatus.Partial)]
+ [InlineData(Status.ErrorAbort, AbStatus.Abort)]
+ [InlineData(Status.ErrorBadReply, AbStatus.BadReply)]
+ [InlineData(Status.ErrorBadGateway, AbStatus.BadConnection)]
+ [InlineData(Status.ErrorWinsock, AbStatus.BadConnection)]
+ public void Translates(Status from, AbStatus to) => LibplctagLegacyStatusTranslation.ToAbStatus(from).ShouldBe(to);
+
+ [Fact]
+ public void Exception_message_is_recovered_as_the_enum_name()
+ {
+ LibplctagLegacyStatusTranslation.ToAbStatus(new LibPlcTagException("ErrorNotFound")).ShouldBe(AbStatus.NotFound);
+ LibplctagLegacyStatusTranslation.ToAbStatus(new LibPlcTagException("gibberish")).ShouldBe(AbStatus.BadConnection);
+ }
+}