feat(lmxproxy): add MxAccess status detail mapping for richer error messages
- MxStatusMapper: maps all 40+ MxStatusDetail codes, MxStatusCategory, and MxStatusSource to human-readable names and client messages - OnDataChange: checks MXSTATUS_PROXY.success and overrides quality with specific OPC UA code when MxAccess reports a failure (e.g., CommFailure, ConfigError, WaitingForInitialData) - OnWriteComplete: uses MxStatusMapper.FormatStatus for structured logging - Write errors: catches COMException separately with HRESULT in message - Read errors: distinguishes COM, timeout, and generic failures in logging Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
186
lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/Domain/MxStatusMapper.cs
Normal file
186
lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/Domain/MxStatusMapper.cs
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.LmxProxy.Host.Domain
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Maps MxAccess MXSTATUS_PROXY fields (detail, category, source) to
|
||||||
|
/// human-readable messages and OPC UA quality codes.
|
||||||
|
/// </summary>
|
||||||
|
public static class MxStatusMapper
|
||||||
|
{
|
||||||
|
// ── MxStatusDetail (short) → name + client message ──────────
|
||||||
|
|
||||||
|
private static readonly Dictionary<int, (string Name, string Message)> DetailCodes =
|
||||||
|
new Dictionary<int, (string, string)>
|
||||||
|
{
|
||||||
|
{ 0, ("MX_S_Success", "Success") },
|
||||||
|
{ 1, ("MX_E_RequestTimedOut", "Request to AVEVA System Platform timed out") },
|
||||||
|
{ 2, ("MX_E_PlatformCommunicationError", "Communication error with System Platform") },
|
||||||
|
{ 3, ("MX_E_InvalidPlatformId", "Invalid platform identifier") },
|
||||||
|
{ 4, ("MX_E_InvalidEngineId", "Invalid engine identifier") },
|
||||||
|
{ 5, ("MX_E_EngineCommunicationError", "Communication error with automation engine") },
|
||||||
|
{ 6, ("MX_E_InvalidReference", "Tag reference is invalid or could not be resolved") },
|
||||||
|
{ 7, ("MX_E_NoGalaxyRepository", "Galaxy repository is not available") },
|
||||||
|
{ 8, ("MX_E_InvalidObjectId", "Invalid object identifier") },
|
||||||
|
{ 9, ("MX_E_ObjectSignatureMismatch", "Object signature mismatch") },
|
||||||
|
{ 10, ("MX_E_AttributeSignatureMismatch", "Attribute signature mismatch") },
|
||||||
|
{ 11, ("MX_E_ResolvingAttribute", "Attribute is still being resolved") },
|
||||||
|
{ 12, ("MX_E_ResolvingObject", "Object is still being resolved") },
|
||||||
|
{ 13, ("MX_E_WrongDataType", "Value type does not match attribute data type") },
|
||||||
|
{ 14, ("MX_E_WrongNumberOfDimensions", "Wrong number of array dimensions") },
|
||||||
|
{ 15, ("MX_E_InvalidIndex", "Invalid array index") },
|
||||||
|
{ 16, ("MX_E_IndexOutOfOrder", "Array index out of order") },
|
||||||
|
{ 17, ("MX_E_DimensionDoesNotExist", "Array dimension does not exist") },
|
||||||
|
{ 18, ("MX_E_ConversionNotSupported", "Data type conversion not supported") },
|
||||||
|
{ 19, ("MX_E_UnableToConvertString", "Unable to convert string to target type") },
|
||||||
|
{ 20, ("MX_E_Overflow", "Numeric overflow during conversion") },
|
||||||
|
{ 21, ("MX_E_NmxVersionMismatch", "NMX version mismatch") },
|
||||||
|
{ 22, ("MX_E_NmxInvalidCommand", "NMX invalid command") },
|
||||||
|
{ 23, ("MX_E_LmxVersionMismatch", "LMX version mismatch") },
|
||||||
|
{ 24, ("MX_E_LmxInvalidCommand", "LMX invalid command") },
|
||||||
|
{ 25, ("MX_E_GalaxyRepositoryBusy", "Galaxy repository is busy") },
|
||||||
|
{ 26, ("MX_E_EngineOverloaded", "Automation engine is overloaded") },
|
||||||
|
{ 1000, ("MX_E_InvalidPrimitiveId", "Invalid primitive identifier") },
|
||||||
|
{ 1001, ("MX_E_InvalidAttributeId", "Invalid attribute identifier") },
|
||||||
|
{ 1002, ("MX_E_InvalidPropertyId", "Invalid property identifier") },
|
||||||
|
{ 1003, ("MX_E_IndexOutOfRange", "Array index out of range") },
|
||||||
|
{ 1004, ("MX_E_DataOutOfRange", "Data value out of range") },
|
||||||
|
{ 1005, ("MX_E_IncorrectDataType", "Incorrect data type for this attribute") },
|
||||||
|
{ 1006, ("MX_E_NotReadable", "Attribute is not readable") },
|
||||||
|
{ 1007, ("MX_E_NotWriteable", "Attribute is not writable") },
|
||||||
|
{ 1008, ("MX_E_WriteAccessDenied", "Write access denied — insufficient security") },
|
||||||
|
{ 1009, ("MX_E_UnknownError", "Unknown MxAccess error") },
|
||||||
|
{ 1010, ("MX_E_ObjectInitializing", "Object is still initializing") },
|
||||||
|
{ 1011, ("MX_E_EngineInitializing", "Automation engine is still initializing") },
|
||||||
|
{ 1012, ("MX_E_SecuredWrite", "Attribute requires secured write authentication") },
|
||||||
|
{ 1013, ("MX_E_VerifiedWrite", "Attribute requires verified write (two-user)") },
|
||||||
|
{ 1014, ("MX_E_NoAlarmAckPrivilege", "No alarm acknowledgment privilege") },
|
||||||
|
{ 8000, ("MX_E_AutomationObjectSpecificError", "Automation object specific error") },
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── MxStatusCategory (int) → name ──────────
|
||||||
|
|
||||||
|
private static readonly Dictionary<int, string> CategoryNames = new Dictionary<int, string>
|
||||||
|
{
|
||||||
|
{ -1, "Unknown" },
|
||||||
|
{ 0, "Ok" },
|
||||||
|
{ 1, "Pending" },
|
||||||
|
{ 2, "Warning" },
|
||||||
|
{ 3, "CommunicationError" },
|
||||||
|
{ 4, "ConfigurationError" },
|
||||||
|
{ 5, "OperationalError" },
|
||||||
|
{ 6, "SecurityError" },
|
||||||
|
{ 7, "SoftwareError" },
|
||||||
|
{ 8, "OtherError" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── MxStatusSource (int) → name ──────────
|
||||||
|
|
||||||
|
private static readonly Dictionary<int, string> SourceNames = new Dictionary<int, string>
|
||||||
|
{
|
||||||
|
{ -1, "Unknown" },
|
||||||
|
{ 0, "RequestingLmx" },
|
||||||
|
{ 1, "RespondingLmx" },
|
||||||
|
{ 2, "RequestingNmx" },
|
||||||
|
{ 3, "RespondingNmx" },
|
||||||
|
{ 4, "RequestingAutomationObject" },
|
||||||
|
{ 5, "RespondingAutomationObject" },
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the symbolic name for an MxStatusDetail code (e.g., "MX_E_WrongDataType").
|
||||||
|
/// </summary>
|
||||||
|
public static string GetDetailName(int detailCode)
|
||||||
|
{
|
||||||
|
return DetailCodes.TryGetValue(detailCode, out var entry) ? entry.Name : string.Format("MX_E_Unknown({0})", detailCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a human-readable client message for an MxStatusDetail code.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetDetailMessage(int detailCode)
|
||||||
|
{
|
||||||
|
return DetailCodes.TryGetValue(detailCode, out var entry) ? entry.Message : string.Format("MxAccess error code {0}", detailCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the symbolic name for an MxStatusCategory value.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetCategoryName(int category)
|
||||||
|
{
|
||||||
|
return CategoryNames.TryGetValue(category, out var name) ? name : string.Format("Unknown({0})", category);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the symbolic name for an MxStatusSource value.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetSourceName(int source)
|
||||||
|
{
|
||||||
|
return SourceNames.TryGetValue(source, out var name) ? name : string.Format("Unknown({0})", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a detailed error string from all MXSTATUS_PROXY fields.
|
||||||
|
/// Format: "MX_E_WrongDataType: Value type does not match attribute data type [Category=OperationalError, Source=RespondingAutomationObject]"
|
||||||
|
/// </summary>
|
||||||
|
public static string FormatStatus(int detail, int category, int source)
|
||||||
|
{
|
||||||
|
return string.Format("{0}: {1} [Category={2}, Source={3}]",
|
||||||
|
GetDetailName(detail),
|
||||||
|
GetDetailMessage(detail),
|
||||||
|
GetCategoryName(category),
|
||||||
|
GetSourceName(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps an MxStatusCategory to the most appropriate OPC UA QualityCode.
|
||||||
|
/// Used when MXSTATUS_PROXY.success is false in an OnDataChange callback
|
||||||
|
/// to override the raw OPC DA quality byte.
|
||||||
|
/// </summary>
|
||||||
|
public static Quality CategoryToQuality(int category, int detail)
|
||||||
|
{
|
||||||
|
// Specific detail codes take priority
|
||||||
|
switch (detail)
|
||||||
|
{
|
||||||
|
case 6: // MX_E_InvalidReference
|
||||||
|
case 1001: // MX_E_InvalidAttributeId
|
||||||
|
return Quality.Bad_ConfigError;
|
||||||
|
case 2: // MX_E_PlatformCommunicationError
|
||||||
|
case 5: // MX_E_EngineCommunicationError
|
||||||
|
return Quality.Bad_CommFailure;
|
||||||
|
case 11: // MX_E_ResolvingAttribute
|
||||||
|
case 12: // MX_E_ResolvingObject
|
||||||
|
case 1010: // MX_E_ObjectInitializing
|
||||||
|
case 1011: // MX_E_EngineInitializing
|
||||||
|
return Quality.Bad_WaitingForInitialData;
|
||||||
|
case 1006: // MX_E_NotReadable
|
||||||
|
return Quality.Bad_OutOfService;
|
||||||
|
case 1: // MX_E_RequestTimedOut
|
||||||
|
return Quality.Bad_CommFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to category
|
||||||
|
switch (category)
|
||||||
|
{
|
||||||
|
case 0: // MxCategoryOk
|
||||||
|
return Quality.Good;
|
||||||
|
case 1: // MxCategoryPending
|
||||||
|
return Quality.Uncertain;
|
||||||
|
case 2: // MxCategoryWarning
|
||||||
|
return Quality.Uncertain;
|
||||||
|
case 3: // MxCategoryCommunicationError
|
||||||
|
return Quality.Bad_CommFailure;
|
||||||
|
case 4: // MxCategoryConfigurationError
|
||||||
|
return Quality.Bad_ConfigError;
|
||||||
|
case 5: // MxCategoryOperationalError
|
||||||
|
return Quality.Bad;
|
||||||
|
case 6: // MxCategorySecurityError
|
||||||
|
return Quality.Bad;
|
||||||
|
case 7: // MxCategorySoftwareError
|
||||||
|
return Quality.Bad;
|
||||||
|
default:
|
||||||
|
return Quality.Bad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using ArchestrA.MxAccess;
|
using ArchestrA.MxAccess;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using ZB.MOM.WW.LmxProxy.Host.Domain;
|
using ZB.MOM.WW.LmxProxy.Host.Domain;
|
||||||
@@ -31,6 +29,17 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
{
|
{
|
||||||
var quality = MapQuality(pwItemQuality);
|
var quality = MapQuality(pwItemQuality);
|
||||||
var timestamp = ConvertTimestamp(pftItemTimeStamp);
|
var timestamp = ConvertTimestamp(pftItemTimeStamp);
|
||||||
|
|
||||||
|
// Check MXSTATUS_PROXY — if success is false, override quality
|
||||||
|
// with a more specific code derived from the MxAccess status fields
|
||||||
|
if (ItemStatus != null && ItemStatus.Length > 0 && ItemStatus[0].success == 0)
|
||||||
|
{
|
||||||
|
var status = ItemStatus[0];
|
||||||
|
quality = MxStatusMapper.CategoryToQuality((int)status.category, status.detail);
|
||||||
|
Log.Debug("OnDataChange status failure for handle {Handle}: {Status}",
|
||||||
|
phItemHandle, MxStatusMapper.FormatStatus(status.detail, (int)status.category, (int)status.detectedBy));
|
||||||
|
}
|
||||||
|
|
||||||
var vtq = new Vtq(pvItemValue, timestamp, quality);
|
var vtq = new Vtq(pvItemValue, timestamp, quality);
|
||||||
|
|
||||||
// Resolve address from handle map
|
// Resolve address from handle map
|
||||||
@@ -84,9 +93,8 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
var status = ItemStatus[0];
|
var status = ItemStatus[0];
|
||||||
if (status.success == 0)
|
if (status.success == 0)
|
||||||
{
|
{
|
||||||
string errorMsg = GetWriteErrorMessage(status.detail);
|
Log.Warning("OnWriteComplete callback: write failed for handle {Handle}: {Status}",
|
||||||
Log.Warning("OnWriteComplete callback: write failed for handle {Handle}: {Error} (Category={Category}, Detail={Detail})",
|
phItemHandle, MxStatusMapper.FormatStatus(status.detail, (int)status.category, (int)status.detectedBy));
|
||||||
phItemHandle, errorMsg, status.category, status.detail);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -104,20 +112,6 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a human-readable error message for a write error code.
|
|
||||||
/// </summary>
|
|
||||||
private static string GetWriteErrorMessage(int errorCode)
|
|
||||||
{
|
|
||||||
switch (errorCode)
|
|
||||||
{
|
|
||||||
case 1008: return "User lacks proper security for write operation";
|
|
||||||
case 1012: return "Secured write required";
|
|
||||||
case 1013: return "Verified write required";
|
|
||||||
default: return string.Format("Unknown error code: {0}", errorCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a timestamp object to DateTime in UTC.
|
/// Converts a timestamp object to DateTime in UTC.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -24,6 +24,16 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
{
|
{
|
||||||
return await ReadSingleValueAsync(address, ct);
|
return await ReadSingleValueAsync(address, ct);
|
||||||
}
|
}
|
||||||
|
catch (System.Runtime.InteropServices.COMException comEx)
|
||||||
|
{
|
||||||
|
Log.Error(comEx, "COM read error for tag {Address}: HRESULT=0x{ErrorCode:X8}", address, comEx.ErrorCode);
|
||||||
|
return Vtq.New(null, Quality.Bad_CommFailure);
|
||||||
|
}
|
||||||
|
catch (TimeoutException)
|
||||||
|
{
|
||||||
|
Log.Warning("Read timed out for tag {Address}", address);
|
||||||
|
return Vtq.New(null, Quality.Bad_CommFailure);
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, "ReadAsync failed for tag {Address}", address);
|
Log.Error(ex, "ReadAsync failed for tag {Address}", address);
|
||||||
@@ -203,6 +213,14 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|||||||
|
|
||||||
Log.Debug("Write completed synchronously for {Address} (handle={Handle})", address, itemHandle);
|
Log.Debug("Write completed synchronously for {Address} (handle={Handle})", address, itemHandle);
|
||||||
}
|
}
|
||||||
|
catch (System.Runtime.InteropServices.COMException comEx)
|
||||||
|
{
|
||||||
|
string enriched = string.Format("Write failed for '{0}': COM error 0x{1:X8} — {2}",
|
||||||
|
address, comEx.ErrorCode, comEx.Message);
|
||||||
|
Log.Error(comEx, "COM write error for {Address}: HRESULT=0x{ErrorCode:X8}",
|
||||||
|
address, comEx.ErrorCode);
|
||||||
|
throw new InvalidOperationException(enriched, comEx);
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, "Failed to write value to {Address}", address);
|
Log.Error(ex, "Failed to write value to {Address}", address);
|
||||||
|
|||||||
Reference in New Issue
Block a user