dfc62cef14
K-F1 (AbCip #737 F1): FromCip maps general 0x1C and 0x20 to Unsupported and 0xFF/0x2104 to OutOfBounds, so a controller refusal is per-tag Bad instead of a BadReply comms fault. A Forward Open refused with one of them is still BadConnection. Every CIP code is now listed with its meaning; 0x01/0x07 stay opaque on purpose. K-F3 (AbLegacy F8, review 07 r0 I4): FromPccc follows the DF1 STS table: 0x20/0x30/0x90 -> BadConnection (a bridge reporting an unreachable END node), 0x50 and EXT 0x06 -> NotFound, EXT 0x07 -> OutOfBounds, 0x60 -> NotAllowed, 0x70/0xB0 -> DeviceStateConflict. Golden ParseReply tests per STS, and an end-to-end AbLegacy test: STS 0x30 degrades with LastSuccessfulRead preserved; the 0x60 positive control stays Healthy and advances it. Both were red before. Driver edits are doc remarks and the two runtime tests the old table pinned. Refs #747 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
92 lines
4.2 KiB
C#
92 lines
4.2 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
|
|
|
/// <summary>
|
|
/// Lane K F3 (AbLegacy F8, review 07 r0 I4), end to end over a real socket with the PRODUCTION
|
|
/// <see cref="NativeAbLegacyTagFactory"/>: a bridge (DHRIO / 1761-NET-ENI) in front of a dead DH+ node answers every
|
|
/// PCCC request with STS 0x30 "remote node host missing". That is NOT the device answering, so the driver must treat
|
|
/// it as a comms fault — Degraded, <c>LastSuccessfulRead</c> not advanced — or the comms-loss watchdog never fires
|
|
/// for it. The positive control is a live device refusing a tag (STS 0x60, command protection): per-tag Bad, Healthy,
|
|
/// <c>LastSuccessfulRead</c> advanced.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class NativePcccBridgeStatusHealthTests
|
|
{
|
|
private static readonly byte[] FixedId = [0x07, 0x3D, 0xF3, 0x45, 0x43, 0x50, 0x21];
|
|
|
|
/// <summary>A PCCC fake that answers every request with one fixed STS.</summary>
|
|
private static FakeEipServer StsServer(byte sts)
|
|
{
|
|
var server = new FakeEipServer { ZeroReplyContext = true };
|
|
server.Handler = req =>
|
|
{
|
|
if (FakeEipServer.Command(req) != Ab.Transport.Eip.EncapCommand.SendRRData) return null;
|
|
var cip = FakeEipServer.RequestCip(req);
|
|
byte[] head = [0xCB, 0x00, 0x00, 0x00, .. FixedId, 0x4F];
|
|
return FakeEipServer.RRReply(req, [.. head, sts, cip[15], cip[16]]);
|
|
};
|
|
return server;
|
|
}
|
|
|
|
#pragma warning disable OTOPCUA0001 // the driver is called directly to pin its OWN classification (no resilience wrap)
|
|
private static async Task<AbLegacyDriver> DriverAsync(FakeEipServer server)
|
|
{
|
|
var host = $"ab://127.0.0.1:{server.Port}/1,0";
|
|
var driver = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions(host, AbLegacyPlcFamily.Slc500)],
|
|
RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("T", host, "N7:0", AbLegacyDataType.Int)]),
|
|
Timeout = TimeSpan.FromSeconds(5),
|
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
|
}, "drv-pccc-bridge", new NativeAbLegacyTagFactory(new DeviceSessionPool()));
|
|
await driver.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
|
return driver;
|
|
}
|
|
|
|
/// <summary>STS 0x30 from a bridge: a comms fault, never evidence the device answered.</summary>
|
|
[Fact]
|
|
public async Task A_dead_node_behind_a_bridge_degrades_and_does_not_advance_LastSuccessfulRead()
|
|
{
|
|
var ct = TestContext.Current.CancellationToken;
|
|
await using var server = StsServer(0x30);
|
|
var driver = await DriverAsync(server);
|
|
var before = driver.GetHealth().LastSuccessfulRead;
|
|
|
|
var result = (await driver.ReadAsync(["T"], ct)).Single();
|
|
|
|
result.StatusCode.ShouldBe(AbLegacyStatusMapper.BadCommunicationError);
|
|
var health = driver.GetHealth();
|
|
health.State.ShouldBe(DriverState.Degraded);
|
|
health.LastSuccessfulRead.ShouldBe(before);
|
|
await driver.ShutdownAsync(ct);
|
|
server.HandlerErrors.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>Positive control: STS 0x60 is the live device refusing the tag — answered, so Healthy and advanced.</summary>
|
|
[Fact]
|
|
public async Task A_live_device_refusing_the_tag_stays_Healthy_and_advances_LastSuccessfulRead()
|
|
{
|
|
var ct = TestContext.Current.CancellationToken;
|
|
await using var server = StsServer(0x60);
|
|
var driver = await DriverAsync(server);
|
|
var before = driver.GetHealth().LastSuccessfulRead;
|
|
|
|
var result = (await driver.ReadAsync(["T"], ct)).Single();
|
|
|
|
result.StatusCode.ShouldBe(AbOpcUaStatusMap.BadNotWritable);
|
|
var health = driver.GetHealth();
|
|
health.State.ShouldBe(DriverState.Healthy);
|
|
health.LastSuccessfulRead.ShouldNotBeNull();
|
|
health.LastSuccessfulRead.ShouldNotBe(before);
|
|
await driver.ShutdownAsync(ct);
|
|
server.HandlerErrors.ShouldBeEmpty();
|
|
}
|
|
#pragma warning restore OTOPCUA0001
|
|
}
|