fix(abcip): translate decode/encode-time libplctag exceptions; doc and probe-test cleanups
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -18,19 +18,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
/// non-CIP server) returns <c>Ok = false</c> with a "handshake failed" message instead of
|
||||
/// a false-positive green tick.
|
||||
/// <para>
|
||||
/// Status-code discrimination: transport statuses (<c>AbStatus</c>) that indicate the controller answered
|
||||
/// the CIP session but could not resolve the probe tag
|
||||
/// (<c>ErrorNotFound</c>, <c>ErrorNoMatch</c>,
|
||||
/// <c>ErrorBadDevice</c>) are treated as "reachable — Ok = true" because
|
||||
/// a non-existent tag name proves CIP connectivity. Statuses that indicate a transport or
|
||||
/// session failure (<c>ErrorTimeout</c>, <c>ErrorBadConnection</c>,
|
||||
/// <c>ErrorBadGateway</c>, <c>ErrorWinsock</c>,
|
||||
/// <c>ErrorOpen</c>, <c>ErrorClose</c>,
|
||||
/// <c>ErrorRead</c>, <c>ErrorWrite</c>,
|
||||
/// <c>ErrorBadReply</c>, <c>ErrorRemoteErr</c>,
|
||||
/// <c>ErrorPartial</c>, <c>ErrorAbort</c>) are treated as
|
||||
/// "handshake failed — Ok = false". All other error statuses default to
|
||||
/// "handshake failed" (conservative).
|
||||
/// Status-code discrimination: <c>AbStatus.Ok</c> and <c>AbStatus.NotFound</c> 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".
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class AbCipDriverProbe : IDriverProbe
|
||||
|
||||
@@ -28,7 +28,7 @@ public interface IAbCipTagRuntime : IDisposable
|
||||
/// Raw transport status code (an <c>AbStatus</c> value) — mapped to an OPC UA StatusCode via
|
||||
/// <see cref="AbCipStatusMapper.MapStatus(int)"/>. Zero on success, negative on error.
|
||||
/// </summary>
|
||||
/// <returns>The raw libplctag status code (zero on success, negative on error).</returns>
|
||||
/// <returns>The <c>AbStatus</c> value (zero on success, negative on error).</returns>
|
||||
int GetStatus();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
/// <summary>Runs a synchronous libplctag call, rethrowing <see cref="LibPlcTagException"/> as <see cref="AbTransportException"/>.</summary>
|
||||
/// <typeparam name="T">The result type.</typeparam>
|
||||
/// <param name="op">The libplctag call.</param>
|
||||
/// <returns>The call result.</returns>
|
||||
public static T WrapSync<T>(Func<T> op)
|
||||
{
|
||||
try { return op(); }
|
||||
catch (LibPlcTagException ex) { throw new AbTransportException(ToAbStatus(ex), ex.Message, ex); }
|
||||
}
|
||||
|
||||
/// <summary>Runs a synchronous libplctag call, rethrowing <see cref="LibPlcTagException"/> as <see cref="AbTransportException"/>.</summary>
|
||||
/// <param name="op">The libplctag call.</param>
|
||||
public static void WrapSync(Action op)
|
||||
{
|
||||
try { op(); }
|
||||
catch (LibPlcTagException ex) { throw new AbTransportException(ToAbStatus(ex), ex.Message, ex); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,13 +51,16 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
|
||||
public int GetStatus() => (int)LibplctagStatusTranslation.ToAbStatus(_tag.GetStatus());
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetSize() => _tag.GetSize();
|
||||
public int GetSize() => LibplctagStatusTranslation.WrapSync(() => _tag.GetSize());
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? DecodeValue(AbCipDataType type, int? bitIndex) => DecodeValueAt(type, 0, bitIndex);
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -181,4 +181,25 @@ public sealed class AbCipDriverProbeTests
|
||||
public void AnUnrecognisedExceptionMessageIsNotTreatedAsReachable()
|
||||
=> AbCipDriverProbe.IsReachableException(
|
||||
new AbTransportException(AbStatus.BadConnection, "something the wrapper has never said")).ShouldBeFalse();
|
||||
|
||||
/// <summary>A fake runtime reporting NotFound is reachable ("probe tag not found").</summary>
|
||||
[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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,4 +224,24 @@ public sealed class AbCipDriverReadTests
|
||||
snapshots.Single().StatusCode.ShouldBe(AbCipStatusMapper.BadCommunicationError);
|
||||
factory.Tags["Nope"].Disposed.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>A decode that throws OutOfBounds reads BadOutOfRange through the driver read path.</summary>
|
||||
[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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,4 +104,14 @@ public sealed class AbCipStatusMapperTests
|
||||
Status.ErrorUnsupported or Status.ErrorNotImplemented => AbCipStatusMapper.BadNotSupported,
|
||||
_ => AbCipStatusMapper.BadCommunicationError, // incl. BadConnection/BadGateway/BadReply/Partial/Abort/...
|
||||
};
|
||||
|
||||
/// <summary>A decode past the buffer end surfaces as BadOutOfRange once the adapter wraps it.</summary>
|
||||
[Fact]
|
||||
public void Wrapped_decode_OutOfBounds_maps_to_BadOutOfRange()
|
||||
{
|
||||
var ex = Should.Throw<AbTransportException>(() =>
|
||||
LibplctagStatusTranslation.WrapSync<int>(() => throw new LibPlcTagException("ErrorOutOfBounds")));
|
||||
|
||||
AbCipStatusMapper.MapException(ex).ShouldBe(AbCipStatusMapper.BadOutOfRange);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user