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.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ArchestrA.MxAccess;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.LmxProxy.Host.Domain;
|
||||
@@ -31,6 +29,17 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
{
|
||||
var quality = MapQuality(pwItemQuality);
|
||||
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);
|
||||
|
||||
// Resolve address from handle map
|
||||
@@ -84,9 +93,8 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
var status = ItemStatus[0];
|
||||
if (status.success == 0)
|
||||
{
|
||||
string errorMsg = GetWriteErrorMessage(status.detail);
|
||||
Log.Warning("OnWriteComplete callback: write failed for handle {Handle}: {Error} (Category={Category}, Detail={Detail})",
|
||||
phItemHandle, errorMsg, status.category, status.detail);
|
||||
Log.Warning("OnWriteComplete callback: write failed for handle {Handle}: {Status}",
|
||||
phItemHandle, MxStatusMapper.FormatStatus(status.detail, (int)status.category, (int)status.detectedBy));
|
||||
}
|
||||
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>
|
||||
/// Converts a timestamp object to DateTime in UTC.
|
||||
/// </summary>
|
||||
|
||||
@@ -24,6 +24,16 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
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)
|
||||
{
|
||||
Log.Error(ex, "Failed to write value to {Address}", address);
|
||||
|
||||
Reference in New Issue
Block a user