fa33e1acf1
Three fixes for the SubscriptionManager/MxAccessClient subscription pipeline: 1. Serialize Subscribe and UnsubscribeClient with a SemaphoreSlim gate to prevent race where old-session unsubscribe removes new-session COM subscriptions. CreateMxAccessSubscriptionsAsync is now awaited instead of fire-and-forget. 2. Fix dual VTQ delivery in MxAccessClient.OnDataChange — each update was delivered twice (once via stored callback, once via OnTagValueChanged property). Now uses stored callback as the single delivery path. 3. Store pending tag addresses when CreateMxAccessSubscriptionsAsync fails (MxAccess down) and retry them on reconnect via NotifyReconnection/RetryPendingSubscriptionsAsync. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
129 lines
5.0 KiB
C#
129 lines
5.0 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 (SubscriptionManager.OnTagValueChanged).
|
|
// This is the single delivery path — OnTagValueChanged property is NOT invoked
|
|
// separately to avoid duplicate VTQ delivery.
|
|
Action<string, Vtq> callback;
|
|
lock (_lock)
|
|
{
|
|
if (!_storedSubscriptions.TryGetValue(address, out callback))
|
|
{
|
|
// Fall back to global handler if no stored callback
|
|
OnTagValueChanged?.Invoke(address, vtq);
|
|
return;
|
|
}
|
|
}
|
|
|
|
callback.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;
|
|
}
|
|
}
|
|
}
|