Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport/AbStatusCodes.cs
T
Joseph Doherty dfc62cef14 fix(ab-transport): map CIP 0x1C/0x20/0xFF-0x2104 as refusals; remap the PCCC STS table
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
2026-10-22 16:43:37 -04:00

82 lines
6.1 KiB
C#

namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
/// <summary>
/// Maps wire statuses (CIP general/extended, PCCC STS/EXT STS) to the transport-neutral <see cref="AbStatus"/> the
/// drivers classify. The class an <see cref="AbStatus"/> lands in is load-bearing for driver health (#527/#737/#738):
/// an ANSWERED refusal (NotFound / NotAllowed / OutOfBounds / TooLarge / TooSmall / TypeMismatch /
/// DeviceStateConflict, and for AbCip Unsupported) proves the device replied; <see cref="AbStatus.BadConnection"/>
/// is the no-reply comms class; <see cref="AbStatus.BadReply"/> / <see cref="AbStatus.Partial"/> are a reply that
/// cannot be classified. Every code not named below is deliberately <see cref="AbStatus.BadReply"/>.
/// </summary>
public static class AbStatusCodes
{
/// <summary>Maps a CIP general status (and optional extended status words) to an <see cref="AbStatus"/>.</summary>
/// <param name="general">The CIP general status byte.</param>
/// <param name="extended">The extended status words, if any.</param>
/// <returns>The mapped status.</returns>
/// <remarks>
/// 0x01 (connection failure, incl. a routing hop's unconnected-send timeout) and 0x07 (connection lost) stay
/// <see cref="AbStatus.BadReply"/> on purpose: a bridge reports a dead END device that way, so they are neither an
/// answer nor proof of no reply (AbLegacy #738 "opaque"). A Forward Open refused with one of the request-level
/// codes (0x1C / 0x20 / 0xFF-0x2104) stays a connection failure — see
/// <c>ConnectionManagerServices.ParseForwardOpenReply</c>.
/// </remarks>
public static AbStatus FromCip(byte general, ReadOnlySpan<ushort> extended) => general switch
{
0x00 or 0x0B => AbStatus.Ok, // success / already in requested mode
0x04 or 0x05 or 0x16 => AbStatus.NotFound, // path segment error / path unknown / object does not exist
0x06 => AbStatus.Partial, // partial transfer (fragmented read continues)
0x08 => AbStatus.Unsupported, // service not supported
0x0A => AbStatus.OutOfBounds, // attribute list error
0x0C or 0x10 => AbStatus.DeviceStateConflict, // object state conflict / device state conflict
0x0E or 0x0F => AbStatus.NotAllowed, // attribute not settable / privilege violation
0x13 => AbStatus.TooSmall, // not enough data
0x15 => AbStatus.TooLarge, // too much data
// Lane K F1 (#737): the controller answered and refused this one request — not a garbled reply, so not a
// comms fault that degrades the driver and evicts the runtime.
0x1C => AbStatus.Unsupported, // missing attribute list entry data
0x20 => AbStatus.Unsupported, // invalid parameter
0xFF when extended.Length > 0 && extended[0] == 0x2104 => AbStatus.OutOfBounds, // offset beyond end of tag
0xFF when extended.Length > 0 && extended[0] == 0x2105 => AbStatus.OutOfBounds, // beyond end of the object
0xFF when extended.Length > 0 && extended[0] == 0x2107 => AbStatus.TypeMismatch, // data type mismatch
_ => AbStatus.BadReply,
};
/// <summary>
/// True for the CIP statuses Lane K F1 moved from <see cref="AbStatus.BadReply"/> to a request-level refusal.
/// A Forward Open refused with one of them is still a connection failure.
/// </summary>
/// <param name="general">The CIP general status byte.</param>
/// <param name="extended">The extended status words, if any.</param>
/// <returns><see langword="true"/> for 0x1C, 0x20 and 0xFF/0x2104.</returns>
internal static bool IsRequestLevelRefusal(byte general, ReadOnlySpan<ushort> extended) =>
general is 0x1C or 0x20 || (general == 0xFF && extended.Length > 0 && extended[0] == 0x2104);
/// <summary>Maps a PCCC status byte (and optional extended status) to an <see cref="AbStatus"/>.</summary>
/// <param name="sts">The PCCC STS byte.</param>
/// <param name="extSts">The extended status byte, when present.</param>
/// <returns>The mapped status.</returns>
/// <remarks>
/// Per the DF1/PCCC STS table (1770-6.5.16). Lane K F3 (AbLegacy F8, review 07 r0 I4): 0x20, 0x30 and 0x90 are
/// reported by the HOST or a BRIDGE (a DHRIO, a 1761-NET-ENI) when the END device cannot be reached, so they are
/// the no-reply comms class. Mapping them to an answered refusal made a dead DH+/DH-485 node behind a bridge count
/// as reachable forever — no fail-fast backoff, no comms-loss watchdog.
/// </remarks>
public static AbStatus FromPccc(byte sts, byte? extSts) => sts switch
{
0x00 => AbStatus.Ok,
0x10 => AbStatus.Unsupported, // illegal command or format
0x20 => AbStatus.BadConnection, // host has a problem and will not communicate
0x30 => AbStatus.BadConnection, // remote node host missing, disconnected or shut down
0x40 => AbStatus.DeviceStateConflict, // host could not complete: hardware fault
0x50 => AbStatus.NotFound, // addressing problem or memory protect rungs
0x60 => AbStatus.NotAllowed, // function not allowed due to command protection
0x70 => AbStatus.DeviceStateConflict, // processor is in Program mode
0x90 => AbStatus.BadConnection, // remote node cannot buffer command
0xB0 => AbStatus.DeviceStateConflict, // remote node problem due to download
0xF0 when extSts is 0x06 => AbStatus.NotFound, // EXT: address does not point to something usable
0xF0 when extSts is 0x07 => AbStatus.OutOfBounds, // EXT: file is wrong size
_ => AbStatus.BadReply, // 0x80, 0xA0, 0xC0, other EXT STS: unclassified
};
}