feat(galaxy): per-code OPC UA mapping for the MxStatusDetail vocabulary
Follow-up to 20a816db: mxaccessgw published the full MxStatusDetail enum
(gateway.md @ 75c71ad, "MxStatus Detail Vocabulary", lifted from
Interop.aaMxDataConsumer.MxStatusDetail), confirming the live-captured codes
(1007 = MX_E_NotWriteable, 1008 = MX_E_WriteAccessDenied). FromCategory now
maps the write-relevant band per code before falling back to the category
bucket: 1004 -> BadOutOfRange, 1005 -> BadTypeMismatch, 1006 -> BadNotReadable,
1007 -> BadNotWritable, 1008/1012/1013/1016/1017 -> BadUserAccessDenied,
1010/1011 (initializing, transient) -> BadOutOfService. 8000
(MX_E_AutomationObjectSpecificError) deliberately stays category-level — the
object supplies its own semantics. Codes <= 0xFF are value-ambiguous with the
OPC DA quality-byte space and keep the quality path (still Bad-family).
Four new named constants, all validated by StatusCodeParityTests against the
pinned SDK. Live refusal smoke re-run green (read-only refusal now surfaces
BadNotWritable instead of generic Bad).
This commit is contained in:
@@ -53,6 +53,10 @@ internal static class StatusCodeMap
|
||||
public const uint BadWaitingForInitialData = 0x80320000u;
|
||||
public const uint BadInternalError = 0x80020000u;
|
||||
public const uint BadUserAccessDenied = 0x801F0000u;
|
||||
public const uint BadOutOfRange = 0x803C0000u;
|
||||
public const uint BadTypeMismatch = 0x80740000u;
|
||||
public const uint BadNotReadable = 0x803A0000u;
|
||||
public const uint BadNotWritable = 0x803B0000u;
|
||||
|
||||
/// <summary>
|
||||
/// Fills a still-pending read when the caller's token fires before the gateway answers. Named here
|
||||
@@ -137,13 +141,33 @@ internal static class StatusCodeMap
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map a failure row by its <see cref="MxStatusCategory"/> — used when the detail field is
|
||||
/// an MxStatus detail code rather than an OPC DA quality byte (or contradicts the failure).
|
||||
/// Category-level only: the MxStatus detail vocabulary (1007, 1008, …) is not mapped
|
||||
/// per-code yet, so the log line carries the raw detail for field capture.
|
||||
/// Map a failure row whose detail field is an MxStatus detail code rather than an OPC DA
|
||||
/// quality byte (or contradicts the failure). Per-code first — the <c>MxStatusDetail</c>
|
||||
/// vocabulary is published in mxaccessgw's gateway.md ("MxStatus Detail Vocabulary",
|
||||
/// <c>75c71ad</c>, lifted from <c>Interop.aaMxDataConsumer.MxStatusDetail</c>) — then the
|
||||
/// category bucket. Codes ≤ 0xFF (e.g. 1 = MX_E_RequestTimedOut) never reach here: they are
|
||||
/// value-ambiguous with the quality-byte space and take the quality path, which still lands
|
||||
/// them in the Bad family.
|
||||
/// </summary>
|
||||
private static uint FromCategory(MxStatusProxy status, ILogger? logger)
|
||||
{
|
||||
switch (status.Detail)
|
||||
{
|
||||
case 1004: return BadOutOfRange; // MX_E_DataOutOfRange
|
||||
case 1005: return BadTypeMismatch; // MX_E_IncorrectDataType
|
||||
case 1006: return BadNotReadable; // MX_E_NotReadable
|
||||
case 1007: return BadNotWritable; // MX_E_NotWriteable (live-captured: read-only attr)
|
||||
case 1008: // MX_E_WriteAccessDenied (live-captured: unadvised no-login write)
|
||||
case 1012: // MX_E_SecuredWrite (attribute requires a secured write)
|
||||
case 1013: // MX_E_VerifiedWrite (attribute requires a verified write)
|
||||
case 1016: // MX_E_UserNotHavingAccessRights
|
||||
case 1017: return BadUserAccessDenied; // MX_E_VerifierNotHavingVerifyRights
|
||||
case 1010: // MX_E_ObjectInitializing (transient)
|
||||
case 1011: return BadOutOfService; // MX_E_EngineInitializing (transient)
|
||||
}
|
||||
|
||||
// Everything else — including 8000 MX_E_AutomationObjectSpecificError, deliberately, since
|
||||
// the object supplies its own semantics — falls back to the category bucket.
|
||||
var code = status.Category switch
|
||||
{
|
||||
MxStatusCategory.SecurityError => BadUserAccessDenied,
|
||||
|
||||
@@ -115,32 +115,42 @@ public sealed class StatusCodeMapTests
|
||||
// truncation `(byte)(Detail & 0xFF)` pushed 1007 → 239 and 1008 → 240 into the OPC DA Good band,
|
||||
// mapping a REFUSED write to Good — no node revert, silently-lost write reported committed.
|
||||
|
||||
/// <summary>A refused write (operational error, MxStatus detail code) must map to a Bad-family code.</summary>
|
||||
[Fact]
|
||||
public void FromMxStatus_WriteRefusal_OperationalError_Detail1007_IsBad()
|
||||
{
|
||||
// Live shape: writeRaw to read-only TestMachine_002.Tagname.
|
||||
var s = new MxStatusProxy { Success = 0, Category = MxStatusCategory.OperationalError, Detail = 1007, RawDetectedBy = 5 };
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.Bad);
|
||||
}
|
||||
|
||||
/// <summary>A security-refused write must map to BadUserAccessDenied, not Good.</summary>
|
||||
[Fact]
|
||||
public void FromMxStatus_WriteRefusal_SecurityError_Detail1008_IsUserAccessDenied()
|
||||
{
|
||||
// Live shape: writeRaw through a plain-advised (subscription) handle — no supervisory advise.
|
||||
var s = new MxStatusProxy { Success = 0, Category = MxStatusCategory.SecurityError, Detail = 1008, RawDetectedBy = 5 };
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(StatusCodeMap.BadUserAccessDenied);
|
||||
}
|
||||
|
||||
/// <summary>Category-level routing for large details preserves communication/configuration specificity.</summary>
|
||||
/// <summary>
|
||||
/// Per-code mapping from the MxStatusDetail vocabulary (mxaccessgw gateway.md @ 75c71ad).
|
||||
/// The 1007/1008 rows are the exact live-captured shapes from the 06/S-1 gate.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(MxStatusCategory.CommunicationError, 0x80050000u)] // BadCommunicationError
|
||||
[InlineData(MxStatusCategory.ConfigurationError, 0x80890000u)] // BadConfigurationError
|
||||
[InlineData(MxStatusCategory.SoftwareError, 0x80000000u)] // unmapped category → generic Bad
|
||||
public void FromMxStatus_LargeDetail_RoutesByCategory(MxStatusCategory category, uint expected)
|
||||
[InlineData(1004, MxStatusCategory.OperationalError, 0x803C0000u)] // MX_E_DataOutOfRange → BadOutOfRange
|
||||
[InlineData(1005, MxStatusCategory.OperationalError, 0x80740000u)] // MX_E_IncorrectDataType → BadTypeMismatch
|
||||
[InlineData(1006, MxStatusCategory.OperationalError, 0x803A0000u)] // MX_E_NotReadable → BadNotReadable
|
||||
[InlineData(1007, MxStatusCategory.OperationalError, 0x803B0000u)] // MX_E_NotWriteable → BadNotWritable (live: read-only Tagname)
|
||||
[InlineData(1008, MxStatusCategory.SecurityError, 0x801F0000u)] // MX_E_WriteAccessDenied → BadUserAccessDenied (live: plain-advised write)
|
||||
[InlineData(1012, MxStatusCategory.SecurityError, 0x801F0000u)] // MX_E_SecuredWrite → BadUserAccessDenied
|
||||
[InlineData(1013, MxStatusCategory.SecurityError, 0x801F0000u)] // MX_E_VerifiedWrite → BadUserAccessDenied
|
||||
[InlineData(1016, MxStatusCategory.SecurityError, 0x801F0000u)] // MX_E_UserNotHavingAccessRights → BadUserAccessDenied
|
||||
[InlineData(1017, MxStatusCategory.SecurityError, 0x801F0000u)] // MX_E_VerifierNotHavingVerifyRights → BadUserAccessDenied
|
||||
[InlineData(1010, MxStatusCategory.OperationalError, 0x808D0000u)] // MX_E_ObjectInitializing → BadOutOfService (transient)
|
||||
[InlineData(1011, MxStatusCategory.OperationalError, 0x808D0000u)] // MX_E_EngineInitializing → BadOutOfService (transient)
|
||||
public void FromMxStatus_KnownDetailCodes_MapPerCode(int detail, MxStatusCategory category, uint expected)
|
||||
{
|
||||
var s = new MxStatusProxy { Success = 0, Category = category, Detail = 2001 };
|
||||
var s = new MxStatusProxy { Success = 0, Category = category, Detail = detail, RawDetectedBy = 5 };
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unrecognized large details route by category — including 8000
|
||||
/// MX_E_AutomationObjectSpecificError, which deliberately stays category-level (the object
|
||||
/// supplies its own semantics).
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(2001, MxStatusCategory.CommunicationError, 0x80050000u)] // BadCommunicationError
|
||||
[InlineData(2001, MxStatusCategory.ConfigurationError, 0x80890000u)] // BadConfigurationError
|
||||
[InlineData(2001, MxStatusCategory.SoftwareError, 0x80000000u)] // unmapped category → generic Bad
|
||||
[InlineData(8000, MxStatusCategory.OperationalError, 0x80000000u)] // MX_E_AutomationObjectSpecificError → category-level
|
||||
[InlineData(8000, MxStatusCategory.SecurityError, 0x801F0000u)] // ... which for a security row is BadUserAccessDenied
|
||||
public void FromMxStatus_UnmappedLargeDetail_RoutesByCategory(int detail, MxStatusCategory category, uint expected)
|
||||
{
|
||||
var s = new MxStatusProxy { Success = 0, Category = category, Detail = detail };
|
||||
StatusCodeMap.FromMxStatus(s).ShouldBe(expected);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user