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);