diff --git a/lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/Domain/MxStatusMapper.cs b/lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/Domain/MxStatusMapper.cs
new file mode 100644
index 0000000..c970b17
--- /dev/null
+++ b/lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/Domain/MxStatusMapper.cs
@@ -0,0 +1,186 @@
+using System.Collections.Generic;
+
+namespace ZB.MOM.WW.LmxProxy.Host.Domain
+{
+ ///
+ /// Maps MxAccess MXSTATUS_PROXY fields (detail, category, source) to
+ /// human-readable messages and OPC UA quality codes.
+ ///
+ public static class MxStatusMapper
+ {
+ // ── MxStatusDetail (short) → name + client message ──────────
+
+ private static readonly Dictionary DetailCodes =
+ new Dictionary
+ {
+ { 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 CategoryNames = new Dictionary
+ {
+ { -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 SourceNames = new Dictionary
+ {
+ { -1, "Unknown" },
+ { 0, "RequestingLmx" },
+ { 1, "RespondingLmx" },
+ { 2, "RequestingNmx" },
+ { 3, "RespondingNmx" },
+ { 4, "RequestingAutomationObject" },
+ { 5, "RespondingAutomationObject" },
+ };
+
+ ///
+ /// Gets the symbolic name for an MxStatusDetail code (e.g., "MX_E_WrongDataType").
+ ///
+ public static string GetDetailName(int detailCode)
+ {
+ return DetailCodes.TryGetValue(detailCode, out var entry) ? entry.Name : string.Format("MX_E_Unknown({0})", detailCode);
+ }
+
+ ///
+ /// Gets a human-readable client message for an MxStatusDetail code.
+ ///
+ public static string GetDetailMessage(int detailCode)
+ {
+ return DetailCodes.TryGetValue(detailCode, out var entry) ? entry.Message : string.Format("MxAccess error code {0}", detailCode);
+ }
+
+ ///
+ /// Gets the symbolic name for an MxStatusCategory value.
+ ///
+ public static string GetCategoryName(int category)
+ {
+ return CategoryNames.TryGetValue(category, out var name) ? name : string.Format("Unknown({0})", category);
+ }
+
+ ///
+ /// Gets the symbolic name for an MxStatusSource value.
+ ///
+ public static string GetSourceName(int source)
+ {
+ return SourceNames.TryGetValue(source, out var name) ? name : string.Format("Unknown({0})", source);
+ }
+
+ ///
+ /// 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]"
+ ///
+ 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));
+ }
+
+ ///
+ /// 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.
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/MxAccess/MxAccessClient.EventHandlers.cs b/lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/MxAccess/MxAccessClient.EventHandlers.cs
index d29a6a3..198409d 100644
--- a/lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/MxAccess/MxAccessClient.EventHandlers.cs
+++ b/lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/MxAccess/MxAccessClient.EventHandlers.cs
@@ -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
}
}
- ///
- /// Gets a human-readable error message for a write error code.
- ///
- 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);
- }
- }
-
///
/// Converts a timestamp object to DateTime in UTC.
///
diff --git a/lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/MxAccess/MxAccessClient.ReadWrite.cs b/lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/MxAccess/MxAccessClient.ReadWrite.cs
index c561874..5cfb06e 100644
--- a/lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/MxAccess/MxAccessClient.ReadWrite.cs
+++ b/lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/MxAccess/MxAccessClient.ReadWrite.cs
@@ -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);