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 8afb0a38..df97d4a9 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
@@ -1,4 +1,4 @@
-using libplctag;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.OtOpcUa.Commons.Observability;
@@ -789,12 +789,12 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// mirrors the probe loop's recreate-on-failure behaviour.
EvictRuntime(device, def.Name);
results[fb.OriginalIndex] = new DataValueSnapshot(null,
- AbCipStatusMapper.MapLibplctagStatus(status), null, now);
+ AbCipStatusMapper.MapStatus(status), null, now);
_logger.LogWarning(
- "AbCip read returned non-zero libplctag status {LibplctagStatus} for tag {Tag} on device {Device}; " +
+ "AbCip read returned non-zero transport status {Status} for tag {Tag} on device {Device}; " +
"evicting cached runtime so next call re-creates it",
status, reference, def.DeviceHostAddress);
- return ReadOutcome.Degraded($"libplctag status {status} reading {reference}");
+ return ReadOutcome.Degraded($"transport status {status} reading {reference}");
}
var tagPath = AbCipTagPath.TryParse(def.TagPath);
@@ -850,7 +850,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
///
/// Perform one whole-UDT read on the parent tag, then decode each
/// grouped member from the runtime's buffer at its computed byte offset. A per-group
- /// failure (parent read raised, non-zero libplctag status, or missing device) stamps
+ /// failure (parent read raised, non-zero transport status, or missing device) stamps
/// the mapped fault across every grouped member only — sibling groups + the
/// per-tag fallback list are unaffected.
///
@@ -881,13 +881,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
if (status != 0)
{
EvictRuntime(device, parent.Name);
- var mapped = AbCipStatusMapper.MapLibplctagStatus(status);
+ var mapped = AbCipStatusMapper.MapStatus(status);
StampGroupStatus(group, results, now, mapped);
_logger.LogWarning(
- "AbCip whole-UDT read returned non-zero libplctag status {LibplctagStatus} for parent {Parent} " +
+ "AbCip whole-UDT read returned non-zero transport status {Status} for parent {Parent} " +
"on device {Device}; {MemberCount} member values stamped with mapped status",
status, group.ParentName, parent.DeviceHostAddress, group.Members.Count);
- return ReadOutcome.Degraded($"libplctag status {status} reading UDT {group.ParentName}");
+ return ReadOutcome.Degraded($"transport status {status} reading UDT {group.ParentName}");
}
foreach (var member in group.Members)
@@ -992,9 +992,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
if (status != 0)
{
EvictRuntime(device, def.Name);
- results[i] = new WriteResult(AbCipStatusMapper.MapLibplctagStatus(status));
+ results[i] = new WriteResult(AbCipStatusMapper.MapStatus(status));
_logger.LogWarning(
- "AbCip write returned non-zero libplctag status {LibplctagStatus} for tag {Tag} on device {Device}; " +
+ "AbCip write returned non-zero transport status {Status} for tag {Tag} on device {Device}; " +
"evicting cached runtime so next call re-creates it",
status, w.FullReference, def.DeviceHostAddress);
}
@@ -1076,7 +1076,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
var parentRuntime = await EnsureParentRuntimeAsync(device, parentName, ct).ConfigureAwait(false);
await parentRuntime.ReadAsync(ct).ConfigureAwait(false);
var readStatus = parentRuntime.GetStatus();
- if (readStatus != 0) return AbCipStatusMapper.MapLibplctagStatus(readStatus);
+ if (readStatus != 0) return AbCipStatusMapper.MapStatus(readStatus);
var current = Convert.ToInt32(parentRuntime.DecodeValue(AbCipDataType.DInt, bitIndex: null) ?? 0);
var updated = Convert.ToBoolean(value)
@@ -1088,7 +1088,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
var writeStatus = parentRuntime.GetStatus();
return writeStatus == 0
? AbCipStatusMapper.Good
- : AbCipStatusMapper.MapLibplctagStatus(writeStatus);
+ : AbCipStatusMapper.MapStatus(writeStatus);
}
finally
{
@@ -1183,10 +1183,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// not answer. "Tag not found" means the controller replied over a working session; it
// is a config error on this one tag, and counting it as a failed Forward-Open throttled
// every sibling tag on the device (seen live: one typo blacked out the whole device).
- if (ex is LibPlcTagException lpe
- && AbCipStatusMapper.TryGetStatus(lpe, out var status)
- && status != Status.Ok
- && AbCipDriverProbe.IsReachableStatus(status))
+ if (ex is AbTransportException ate
+ && ate.Status != AbStatus.Ok
+ && AbCipDriverProbe.IsReachableStatus(ate.Status))
device.Backoff.RecordSuccess();
else
device.Backoff.RecordFailure(DateTime.UtcNow);
@@ -1211,7 +1210,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// Evict the runtime for from the device's cache and dispose
/// it so the next read/write call re-creates and re-initializes a fresh handle.
/// Called from , , and
- /// after a non-zero libplctag status or transport exception —
+ /// after a non-zero transport status or transport exception —
/// mirroring the probe loop's recreate-on-failure behaviour.
///
private static void EvictRuntime(DeviceState device, string tagName)
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 ccfa04f4..6973cd04 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.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.AbCip.PlcFamilies;
@@ -11,24 +11,24 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
///
/// Two-phase Test-Connect probe for the -shaped driver config.
/// Phase 1: bare TCP connect to the first device's gateway host + EtherNet/IP port to
-/// quickly reject unreachable targets. Phase 2: initialises a libplctag
+/// quickly reject unreachable targets. Phase 2: initialises a tag runtime
/// against the same device to open a real EIP session and perform a CIP Forward Open —
/// confirming the remote endpoint actually speaks EtherNet/IP CIP, not just accepts TCP.
/// A device that accepts the TCP connection but is not a CIP controller (wrong CIP path,
/// non-CIP 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 (AbStatus) that indicate the controller answered
/// the CIP session but could not resolve the probe tag
-/// (, ,
-/// ) are treated as "reachable — Ok = true" because
+/// (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 (, ,
-/// , ,
-/// , ,
-/// , ,
-/// , ,
-/// , ) are treated as
+/// 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).
///
@@ -42,6 +42,12 @@ public sealed class AbCipDriverProbe : IDriverProbe
Converters = { new JsonStringEnumConverter() },
};
+ private readonly IAbCipTagFactory _tagFactory;
+
+ /// Initializes a new instance of the class.
+ /// Factory for the probe's tag runtime; defaults to the libplctag-backed one.
+ public AbCipDriverProbe(IAbCipTagFactory? tagFactory = null) => _tagFactory = tagFactory ?? new LibplctagTagFactory();
+
///
public string DriverType => "AbCip";
@@ -124,7 +130,7 @@ public sealed class AbCipDriverProbe : IDriverProbe
AllowPacking: firstDevice?.AllowPacking ?? profile.SupportsRequestPacking,
ConnectionSize: firstDevice?.ConnectionSize ?? profile.DefaultConnectionSize);
- var rt = new LibplctagTagRuntime(p);
+ var rt = _tagFactory.Create(p);
try
{
await rt.InitializeAsync(cts.Token);
@@ -132,10 +138,10 @@ public sealed class AbCipDriverProbe : 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
? "CIP session OK"
: "CIP session OK (controller reachable; probe tag not found)";
return new(true, msg, sw.Elapsed);
@@ -148,9 +154,9 @@ public sealed class AbCipDriverProbe : 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.
+ // AbTransportException carries the Status; classify as reachable vs transport failure.
if (IsReachableException(ex))
{
sw.Stop();
@@ -170,42 +176,23 @@ public sealed class AbCipDriverProbe : IDriverProbe
}
///
- /// Returns true for libplctag statuses that indicate the CIP controller answered
+ /// Returns true for transport statuses that indicate the CIP controller answered
/// but the probe tag name was not found or did not match — sufficient to confirm CIP
- /// reachability. Returns true for (tag was found).
+ /// reachability. Returns true for Ok (tag was found).
///
- internal static bool IsReachableStatus(Status s) => s is
- Status.Ok or
- Status.ErrorNotFound or
- Status.ErrorNoMatch or
- Status.ErrorBadDevice;
+ internal static bool IsReachableStatus(AbStatus s) => s is AbStatus.Ok or AbStatus.NotFound;
///
- /// Decides whether a is a "controller answered CIP but the
+ /// Decides whether an is a "controller answered CIP but the
/// probe tag did not resolve" condition (reachable) or a session / transport failure
- /// (unreachable), by recovering the the wrapper threw on and deferring
- /// to — one classification, not two that can drift.
+ /// (unreachable), deferring to — one classification, not two.
///
- /// The exception libplctag threw out of InitializeAsync.
+ /// The exception thrown out of InitializeAsync.
/// true when the controller answered; false for a transport/session failure.
- ///
- /// The status arrives as the managed enum NAME.NativeTagWrapper.ThrowIfStatusNotOk
- /// constructs the exception with Status.ToString(), so the message is exactly
- /// "ErrorNotFound" — never the native C spelling "STATUS_NOT_FOUND" this method
- /// used to look for. Those substrings could not match, so the reachable classification was
- /// unreachable on this path — and this IS the path a probe takes whenever its tag does not
- /// resolve, which is every Test Connect against a device with no authored tags. The native
- /// spellings are kept as a secondary match in case a future wrapper reports them.
- ///
- internal static bool IsReachableException(LibPlcTagException ex)
+ internal static bool IsReachableException(AbTransportException ex)
{
- var msg = ex.Message ?? string.Empty;
- if (AbCipStatusMapper.TryGetStatus(ex, out var status))
- return IsReachableStatus(status);
-
- return msg.Contains("NOT_FOUND", StringComparison.OrdinalIgnoreCase)
- || msg.Contains("NO_MATCH", StringComparison.OrdinalIgnoreCase)
- || msg.Contains("BAD_DEVICE", StringComparison.OrdinalIgnoreCase);
+ ArgumentNullException.ThrowIfNull(ex);
+ return IsReachableStatus(ex.Status);
}
private static (AbCipHostAddress? parsed, AbCipDeviceOptions? device) ExtractTarget(AbCipDriverOptions opts)
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipStatusMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipStatusMapper.cs
index bbe48a20..7b026d30 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipStatusMapper.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipStatusMapper.cs
@@ -1,4 +1,4 @@
-using libplctag;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
@@ -26,8 +26,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// writes during download / test-mode transitions).
/// 0x16 object does not exist — BadNodeIdUnknown.
/// 0x1E embedded service error — unwrap to the extended status when possible.
-/// libplctag.NET errors — mapped per-member by
-/// : timeout, not-found, not-allowed, and
+/// Transport errors — mapped per-member by
+/// : timeout, not-found, not-allowed, and
/// out-of-bounds get their specific OPC UA codes; the remaining transport errors
/// fold into BadCommunicationError.
///
@@ -64,66 +64,42 @@ public static class AbCipStatusMapper
};
///
- /// Map a libplctag return/status code to an OPC UA StatusCode. The integer passed here
- /// is (int)Tag.GetStatus() — i.e. the underlying value of the libplctag.NET
- /// enum, NOT a raw native PLCTAG_ERR_* constant. The wrapper
- /// renumbers the native codes into a contiguous enum, so this method switches on the
- /// members directly to stay correct if the wrapper renumbers again.
- /// is success; is an in-flight
- /// operation; every other (negative) member is an error.
+ /// Map a transport status code to an OPC UA StatusCode. The integer is
+ /// , i.e. the underlying value of .
///
- /// The libplctag status code as an integer.
+ /// The as an integer.
/// The mapped OPC UA StatusCode value.
- public static uint MapLibplctagStatus(int status) => MapLibplctagStatus((Status)status);
+ public static uint MapStatus(int status) => MapStatus((AbStatus)status);
///
- /// Recover the libplctag a was thrown on.
- /// The wrapper builds the exception from Status.ToString(), so the message is the enum
- /// name ("ErrorNotFound").
+ /// Map an to an OPC UA StatusCode. Timeout, not-found, not-allowed and
+ /// out-of-bounds get their specific codes; the remaining transport errors fold into
+ /// .
///
- /// The exception.
- /// The recovered status.
- /// true when the message names a defined .
- public static bool TryGetStatus(LibPlcTagException ex, out Status status)
+ /// The transport status.
+ /// The mapped OPC UA StatusCode value.
+ public static uint MapStatus(AbStatus status) => status switch
{
- ArgumentNullException.ThrowIfNull(ex);
- return Enum.TryParse((ex.Message ?? string.Empty).Trim(), ignoreCase: true, out status)
- && Enum.IsDefined(status);
- }
+ 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
+ };
///
- /// Status for an exception thrown while creating / reading a tag: a libplctag exception maps
- /// through its (so a tag the controller does not have reads
+ /// Status for an exception thrown while creating / reading a tag: a transport exception maps
+ /// through its (so a tag the controller does not have reads
/// , not a comms error); anything else is
/// .
///
/// The exception.
/// The OPC UA status code.
public static uint MapException(Exception ex) =>
- ex is LibPlcTagException lpe && TryGetStatus(lpe, out var status)
- ? MapLibplctagStatus(status)
- : BadCommunicationError;
-
- ///
- /// Map a libplctag.NET enum value to an OPC UA StatusCode. This is
- /// the strongly-typed core of the mapper; the int overload exists only for the
- /// seam, which returns the boxed-as-int value.
- ///
- /// The libplctag Status enum value.
- /// The mapped OPC UA StatusCode value.
- public static uint MapLibplctagStatus(Status 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,
- };
+ ex is AbTransportException ate ? MapStatus(ate.Status) : BadCommunicationError;
}
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 d3cbafa8..61b6a016 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs
@@ -25,8 +25,8 @@ public interface IAbCipTagRuntime : IDisposable
Task WriteAsync(CancellationToken cancellationToken);
///
- /// Raw libplctag status code — mapped to an OPC UA StatusCode via
- /// . Zero on success, negative on error.
+ /// 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).
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
new file mode 100644
index 00000000..4e938c5b
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagStatusTranslation.cs
@@ -0,0 +1,58 @@
+using libplctag;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
+
+/// The ONLY place libplctag's meets .
+internal static class LibplctagStatusTranslation
+{
+ /// Translates a libplctag status into the driver-owned .
+ /// The libplctag status.
+ /// The equivalent transport-neutral status.
+ 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,
+ };
+
+ ///
+ /// Recovers the status a was thrown on. libplctag builds the
+ /// exception from Status.ToString(), so the message is the enum name.
+ ///
+ /// The exception.
+ /// The translated status, or when the message names no status.
+ public static AbStatus ToAbStatus(LibPlcTagException ex) =>
+ Enum.TryParse((ex.Message ?? string.Empty).Trim(), ignoreCase: true, out var s) && Enum.IsDefined(s)
+ ? ToAbStatus(s)
+ : AbStatus.BadConnection;
+
+ /// Runs , rethrowing any as .
+ /// The libplctag operation.
+ /// A task completing when the operation does.
+ public static async Task Wrap(Func op)
+ {
+ try { await op().ConfigureAwait(false); }
+ catch (LibPlcTagException ex) { throw new AbTransportException(ToAbStatus(ex), ex.Message, ex); }
+ }
+
+ /// Runs , rethrowing any as .
+ /// The result type.
+ /// The libplctag operation.
+ /// The operation result.
+ public static async Task Wrap(Func> op)
+ {
+ try { return 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.AbCip/LibplctagTagEnumerator.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs
index e2858385..f9626b00 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs
@@ -77,9 +77,12 @@ internal sealed class LibplctagTagEnumerator : IAbCipTagEnumerator
AbCipTagCreateParams deviceParams, string listingName, CancellationToken cancellationToken)
{
using var tag = BuildSymbolListTag(deviceParams, listingName);
- await tag.InitializeAsync(cancellationToken).ConfigureAwait(false);
- await tag.ReadAsync(cancellationToken).ConfigureAwait(false);
- return tag.GetBuffer();
+ return await LibplctagStatusTranslation.Wrap(async () =>
+ {
+ await tag.InitializeAsync(cancellationToken).ConfigureAwait(false);
+ await tag.ReadAsync(cancellationToken).ConfigureAwait(false);
+ return tag.GetBuffer();
+ }).ConfigureAwait(false);
}
///
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 b503dc03..62d8a914 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs
@@ -1,4 +1,5 @@
using libplctag;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
@@ -35,16 +36,19 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
}
///
- public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);
+ public Task InitializeAsync(CancellationToken cancellationToken) =>
+ LibplctagStatusTranslation.Wrap(() => _tag.InitializeAsync(cancellationToken));
///
- public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken);
+ public Task ReadAsync(CancellationToken cancellationToken) =>
+ LibplctagStatusTranslation.Wrap(() => _tag.ReadAsync(cancellationToken));
///
- public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken);
+ public Task WriteAsync(CancellationToken cancellationToken) =>
+ LibplctagStatusTranslation.Wrap(() => _tag.WriteAsync(cancellationToken));
///
- public int GetStatus() => (int)_tag.GetStatus();
+ public int GetStatus() => (int)LibplctagStatusTranslation.ToAbStatus(_tag.GetStatus());
///
public int GetSize() => _tag.GetSize();
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs
index 5af05c01..2fc4d94f 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs
@@ -20,9 +20,13 @@ internal sealed class LibplctagTemplateReader : IAbCipTemplateReader
{
_tag?.Dispose();
_tag = BuildTemplateTag(deviceParams, templateInstanceId);
- await _tag.InitializeAsync(cancellationToken).ConfigureAwait(false);
- await _tag.ReadAsync(cancellationToken).ConfigureAwait(false);
- return _tag.GetBuffer();
+ var tag = _tag;
+ return await LibplctagStatusTranslation.Wrap(async () =>
+ {
+ await tag.InitializeAsync(cancellationToken).ConfigureAwait(false);
+ await tag.ReadAsync(cancellationToken).ConfigureAwait(false);
+ return tag.GetBuffer();
+ }).ConfigureAwait(false);
}
/// Builds the (uninitialized) libplctag tag for the @udt/{id} pseudo.
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverCodeReviewRegressionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverCodeReviewRegressionTests.cs
index dbc39685..f1fabd0f 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverCodeReviewRegressionTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverCodeReviewRegressionTests.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;
@@ -323,7 +323,7 @@ public sealed class AbCipDriverCodeReviewRegressionTests
// First tag creation → returns error; second → returns success.
callCount++;
return callCount == 1
- ? new FakeAbCipTag(p) { Status = (int)libplctag.Status.ErrorBadConnection }
+ ? new FakeAbCipTag(p) { Status = (int)AbStatus.BadConnection }
: new FakeAbCipTag(p) { Status = 0, Value = 42 };
};
var drv = new AbCipDriver(new AbCipDriverOptions
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 ba406b9f..5707aea4 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
@@ -1,6 +1,6 @@
using System.Net;
using System.Net.Sockets;
-using libplctag;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
using Shouldly;
using Xunit;
@@ -151,27 +151,22 @@ public sealed class AbCipDriverProbeTests
// -------------------------------------------------------------------------
///
- /// The exception path classifies by the SAME rule as the status path. libplctag.NET builds
- /// with Status.ToString(), so its message is
- /// "ErrorNotFound" — this method used to look for the native C spelling "STATUS_NOT_FOUND",
- /// which that message can never contain, so a controller that answered "tag does not exist"
- /// was reported as a failed handshake. That is the answer every Test Connect gets on a device
- /// with no authored tags, i.e. every device at the moment an operator first presses the button.
+ /// The exception path classifies by the SAME rule as the status path: a controller that answered
+ /// "tag does not exist" is reachable (the answer every Test Connect gets on a device with no
+ /// authored tags); a session or transport failure is not.
///
- /// The status libplctag threw on.
+ /// The status the transport threw on.
/// Whether the controller should be reported reachable.
[Theory]
- [InlineData(Status.ErrorNotFound, true)]
- [InlineData(Status.ErrorNoMatch, true)]
- [InlineData(Status.ErrorBadDevice, true)]
- [InlineData(Status.ErrorTimeout, false)]
- [InlineData(Status.ErrorBadConnection, false)]
- [InlineData(Status.ErrorBadGateway, false)]
- [InlineData(Status.ErrorBadParam, false)]
- public void ThrownStatus_IsClassifiedTheSameWayAsAReturnedStatus(Status status, bool expectedReachable)
+ [InlineData(AbStatus.NotFound, true)]
+ [InlineData(AbStatus.Timeout, false)]
+ [InlineData(AbStatus.BadConnection, false)]
+ [InlineData(AbStatus.BadReply, false)]
+ [InlineData(AbStatus.Partial, false)]
+ [InlineData(AbStatus.Abort, false)]
+ public void ThrownStatus_IsClassifiedTheSameWayAsAReturnedStatus(AbStatus status, bool expectedReachable)
{
- // Exactly how NativeTagWrapper.ThrowIfStatusNotOk constructs it.
- var ex = new LibPlcTagException(status.ToString());
+ var ex = new AbTransportException(status, status.ToString());
AbCipDriverProbe.IsReachableException(ex).ShouldBe(expectedReachable);
AbCipDriverProbe.IsReachableStatus(status).ShouldBe(expectedReachable);
@@ -185,5 +180,5 @@ public sealed class AbCipDriverProbeTests
[Fact]
public void AnUnrecognisedExceptionMessageIsNotTreatedAsReachable()
=> AbCipDriverProbe.IsReachableException(
- new LibPlcTagException("something the wrapper has never said")).ShouldBeFalse();
+ new AbTransportException(AbStatus.BadConnection, "something the wrapper has never said")).ShouldBeFalse();
}
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 735a9c35..fa46b20d 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
@@ -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;
@@ -87,14 +87,14 @@ public sealed class AbCipDriverReadTests
factory.Tags["Motor1.Speed"].ReadCount.ShouldBe(3);
}
- /// Verifies that non-zero libplctag status is mapped via AbCipStatusMapper.
+ /// Verifies that non-zero transport status is mapped via AbCipStatusMapper.
[Fact]
- public async Task NonZero_libplctag_status_maps_via_AbCipStatusMapper()
+ public async Task NonZero_transport_status_maps_via_AbCipStatusMapper()
{
var (drv, factory) = NewDriver(
new AbCipTagDefinition("Ghost", "ab://10.0.0.5/1,0", "Missing.Tag", AbCipDataType.DInt));
await drv.InitializeAsync("{}", CancellationToken.None);
- factory.Customise = p => new FakeAbCipTag(p) { Status = (int)Status.ErrorNotFound };
+ factory.Customise = p => new FakeAbCipTag(p) { Status = (int)AbStatus.NotFound };
var snapshots = await drv.ReadAsync(["Ghost"], CancellationToken.None);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWholeUdtReadTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWholeUdtReadTests.cs
index b5403ca9..f339b396 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWholeUdtReadTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWholeUdtReadTests.cs
@@ -1,3 +1,4 @@
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
using Shouldly;
using Xunit;
@@ -89,7 +90,7 @@ public sealed class AbCipDriverWholeUdtReadTests
// Prime runtime existence via a first (successful) read so we can flip it to error.
await drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None);
- factory.Tags["Motor"].Status = -3; // libplctag BadTimeout — mapped in AbCipStatusMapper
+ factory.Tags["Motor"].Status = (int)AbStatus.Timeout; // mapped in AbCipStatusMapper
var snapshots = await drv.ReadAsync(["Motor.Speed", "Motor.Torque"], CancellationToken.None);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWriteTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWriteTests.cs
index 2417399b..b70fe900 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWriteTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDriverWriteTests.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;
@@ -84,14 +84,14 @@ public sealed class AbCipDriverWriteTests
results.Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
}
- /// Verifies that non-zero libplctag status after write maps correctly.
+ /// Verifies that non-zero transport status after write maps correctly.
[Fact]
- public async Task Non_zero_libplctag_status_after_write_maps_via_AbCipStatusMapper()
+ public async Task Non_zero_transport_status_after_write_maps_via_AbCipStatusMapper()
{
var (drv, factory) = NewDriver(
new AbCipTagDefinition("Broken", "ab://10.0.0.5/1,0", "Broken", AbCipDataType.DInt));
await drv.InitializeAsync("{}", CancellationToken.None);
- factory.Customise = p => new FakeAbCipTag(p) { Status = (int)Status.ErrorTimeout };
+ factory.Customise = p => new FakeAbCipTag(p) { Status = (int)AbStatus.Timeout };
var results = await drv.WriteAsync(
[new WriteRequest("Broken", 1)], CancellationToken.None);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipLoggingTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipLoggingTests.cs
index 11123518..1704614b 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipLoggingTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipLoggingTests.cs
@@ -1,3 +1,4 @@
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
using Microsoft.Extensions.Logging;
using Shouldly;
using Xunit;
@@ -88,7 +89,7 @@ public sealed class AbCipLoggingTests
var logger = new CapturingLogger();
var factory = new FakeAbCipTagFactory
{
- Customise = p => new FakeAbCipTag(p) { Status = (int)libplctag.Status.ErrorBadConnection },
+ Customise = p => new FakeAbCipTag(p) { Status = (int)AbStatus.BadConnection },
};
var drv = new AbCipDriver(new AbCipDriverOptions
{
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 dc15dcc7..e8cf140b 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
@@ -1,4 +1,5 @@
using libplctag;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
@@ -29,41 +30,78 @@ public sealed class AbCipStatusMapperTests
AbCipStatusMapper.MapCipGeneralStatus(status).ShouldBe(expected);
}
- // Driver.AbCip-002 — the integers here are the underlying values of the libplctag.NET
- // Status enum (what (int)Tag.GetStatus() actually returns), NOT raw native PLCTAG_ERR_*
- // constants. The libplctag.NET wrapper renumbers the native codes into a contiguous enum.
- /// Verifies that libplctag Status enum values are mapped to OPC UA status codes.
- /// The libplctag Status enum value to map.
+ /// Verifies that transport status values are mapped to OPC UA status codes.
+ /// The transport status to map.
/// The expected OPC UA status code.
[Theory]
- [InlineData(Status.Ok, AbCipStatusMapper.Good)]
- [InlineData(Status.Pending, AbCipStatusMapper.GoodMoreData)]
- [InlineData(Status.ErrorTimeout, AbCipStatusMapper.BadTimeout)]
- [InlineData(Status.ErrorNotFound, AbCipStatusMapper.BadNodeIdUnknown)]
- [InlineData(Status.ErrorNotAllowed, AbCipStatusMapper.BadNotWritable)]
- [InlineData(Status.ErrorOutOfBounds, AbCipStatusMapper.BadOutOfRange)]
- [InlineData(Status.ErrorBadConnection, AbCipStatusMapper.BadCommunicationError)]
- [InlineData(Status.ErrorBadGateway, AbCipStatusMapper.BadCommunicationError)]
- [InlineData(Status.ErrorUnsupported, AbCipStatusMapper.BadNotSupported)]
- [InlineData(Status.ErrorNoMem, AbCipStatusMapper.BadCommunicationError)] // unmapped → generic comms
- public void MapLibplctagStatus_maps_real_enum_members(Status status, uint expected)
+ [InlineData(AbStatus.Ok, AbCipStatusMapper.Good)]
+ [InlineData(AbStatus.Pending, AbCipStatusMapper.GoodMoreData)]
+ [InlineData(AbStatus.Timeout, AbCipStatusMapper.BadTimeout)]
+ [InlineData(AbStatus.NotFound, AbCipStatusMapper.BadNodeIdUnknown)]
+ [InlineData(AbStatus.NotAllowed, AbCipStatusMapper.BadNotWritable)]
+ [InlineData(AbStatus.OutOfBounds, AbCipStatusMapper.BadOutOfRange)]
+ [InlineData(AbStatus.TooLarge, AbCipStatusMapper.BadOutOfRange)]
+ [InlineData(AbStatus.TooSmall, AbCipStatusMapper.BadOutOfRange)]
+ [InlineData(AbStatus.Unsupported, AbCipStatusMapper.BadNotSupported)]
+ [InlineData(AbStatus.TypeMismatch, AbCipStatusMapper.BadTypeMismatch)]
+ [InlineData(AbStatus.DeviceStateConflict, AbCipStatusMapper.BadDeviceFailure)]
+ [InlineData(AbStatus.BadConnection, AbCipStatusMapper.BadCommunicationError)]
+ [InlineData(AbStatus.BadReply, AbCipStatusMapper.BadCommunicationError)]
+ [InlineData(AbStatus.Partial, AbCipStatusMapper.BadCommunicationError)]
+ [InlineData(AbStatus.Abort, AbCipStatusMapper.BadCommunicationError)]
+ public void MapStatus_maps_every_AbStatus(AbStatus status, uint expected)
{
- AbCipStatusMapper.MapLibplctagStatus(status).ShouldBe(expected);
- // The int overload must agree — it is the seam IAbCipTagRuntime.GetStatus() drives.
- AbCipStatusMapper.MapLibplctagStatus((int)status).ShouldBe(expected);
+ AbCipStatusMapper.MapStatus(status).ShouldBe(expected);
+ // The int overload must agree - it is the seam IAbCipTagRuntime.GetStatus() drives.
+ AbCipStatusMapper.MapStatus((int)status).ShouldBe(expected);
}
/// Verifies that timeout is distinguished from generic communication error.
[Fact]
- public void MapLibplctagStatus_distinguishes_timeout_from_generic_comms_error()
+ public void MapStatus_distinguishes_timeout_from_generic_comms_error()
{
- // Regression for Driver.AbCip-002: a real timeout used to fall through to
- // BadCommunicationError because the code matched the wrong integer (-5).
- AbCipStatusMapper.MapLibplctagStatus((int)Status.ErrorTimeout)
- .ShouldBe(AbCipStatusMapper.BadTimeout);
- AbCipStatusMapper.MapLibplctagStatus((int)Status.ErrorNotFound)
- .ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
- AbCipStatusMapper.MapLibplctagStatus((int)Status.ErrorNotFound)
- .ShouldNotBe(AbCipStatusMapper.BadCommunicationError);
+ AbCipStatusMapper.MapStatus((int)AbStatus.Timeout).ShouldBe(AbCipStatusMapper.BadTimeout);
+ AbCipStatusMapper.MapStatus((int)AbStatus.NotFound).ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
+ AbCipStatusMapper.MapStatus((int)AbStatus.NotFound).ShouldNotBe(AbCipStatusMapper.BadCommunicationError);
}
+
+ /// Verifies MapException maps transport exceptions by status and everything else to comms error.
+ [Fact]
+ public void MapException_uses_the_transport_status()
+ {
+ AbCipStatusMapper.MapException(new AbTransportException(AbStatus.NotFound, "x"))
+ .ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
+ AbCipStatusMapper.MapException(new InvalidOperationException("x"))
+ .ShouldBe(AbCipStatusMapper.BadCommunicationError);
+ }
+
+ ///
+ /// Regression oracle: the OPC UA code the pre-AbStatus MapLibplctagStatus returned for every
+ /// libplctag member (table copied from the deleted switch) must equal
+ /// MapStatus(ToAbStatus(member)).
+ ///
+ /// A libplctag status.
+ [Theory]
+ [MemberData(nameof(AllLibplctagStatuses))]
+ public void Translation_plus_MapStatus_matches_the_old_libplctag_mapping(Status status)
+ {
+ AbCipStatusMapper.MapStatus(LibplctagStatusTranslation.ToAbStatus(status)).ShouldBe(OldMapLibplctagStatus(status));
+ }
+
+ /// Every defined libplctag member.
+ /// One row per member.
+ public static IEnumerable