Files
scadalink-design/lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/MxAccess/MxAccessClient.EventHandlers.cs
Joseph Doherty 5a9574fb95 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>
2026-03-22 05:10:50 -04:00

129 lines
4.9 KiB
C#

using System;
using ArchestrA.MxAccess;
using Serilog;
using ZB.MOM.WW.LmxProxy.Host.Domain;
namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
{
public sealed partial class MxAccessClient
{
/// <summary>
/// Callback invoked by the SubscriptionManager when it needs to deliver
/// data change events. Set by the SubscriptionManager during initialization.
/// </summary>
public Action<string, Vtq>? OnTagValueChanged { get; set; }
/// <summary>
/// COM event handler for MxAccess OnDataChange events.
/// Signature matches the ArchestrA.MxAccess ILMXProxyServerEvents interface.
/// </summary>
private void OnDataChange(
int hLMXServerHandle,
int phItemHandle,
object pvItemValue,
int pwItemQuality,
object pftItemTimeStamp,
ref MXSTATUS_PROXY[] ItemStatus)
{
try
{
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
string address;
lock (_lock)
{
if (!_handleToAddress.TryGetValue(phItemHandle, out address))
{
Log.Debug("OnDataChange for unknown handle {Handle}, ignoring", phItemHandle);
return;
}
}
// Invoke the stored subscription callback
Action<string, Vtq> callback;
lock (_lock)
{
if (!_storedSubscriptions.TryGetValue(address, out callback))
{
Log.Debug("OnDataChange for {Address} but no callback registered", address);
return;
}
}
callback.Invoke(address, vtq);
// Also route to the SubscriptionManager's global handler
OnTagValueChanged?.Invoke(address, vtq);
}
catch (Exception ex)
{
Log.Error(ex, "Error processing OnDataChange event for handle {Handle}", phItemHandle);
}
}
/// <summary>
/// COM event handler for MxAccess OnWriteComplete events.
/// Signature matches the ArchestrA.MxAccess ILMXProxyServerEvents interface.
/// Kept wired for diagnostic logging only — writes are resolved synchronously
/// when the Write() COM call returns without throwing.
/// </summary>
private void OnWriteComplete(
int hLMXServerHandle,
int phItemHandle,
ref MXSTATUS_PROXY[] ItemStatus)
{
try
{
if (ItemStatus != null && ItemStatus.Length > 0)
{
var status = ItemStatus[0];
if (status.success == 0)
{
Log.Warning("OnWriteComplete callback: write failed for handle {Handle}: {Status}",
phItemHandle, MxStatusMapper.FormatStatus(status.detail, (int)status.category, (int)status.detectedBy));
}
else
{
Log.Debug("OnWriteComplete callback: write succeeded for handle {Handle}", phItemHandle);
}
}
else
{
Log.Debug("OnWriteComplete callback: no status for handle {Handle}", phItemHandle);
}
}
catch (Exception ex)
{
Log.Error(ex, "Error processing OnWriteComplete event for handle {Handle}", phItemHandle);
}
}
/// <summary>
/// Converts a timestamp object to DateTime in UTC.
/// </summary>
private static DateTime ConvertTimestamp(object timestamp)
{
if (timestamp is DateTime dt)
{
return dt.Kind == DateTimeKind.Utc ? dt : dt.ToUniversalTime();
}
return DateTime.UtcNow;
}
}
}