Expose per-host runtime status as synthetic OPC UA variables so clients can observe Platform/Engine ScanState transitions without the status dashboard
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,26 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.MxAccess
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a point-in-time clone of the runtime status for the host identified by
|
||||
/// <paramref name="gobjectId"/>, or <see langword="null"/> when no probe is registered
|
||||
/// for that object. Used by the node manager to populate the synthetic <c>$RuntimeState</c>
|
||||
/// child variables on each host object. Uses the underlying state directly (not the
|
||||
/// transport-gated rewrite), matching <see cref="IsHostStopped"/>.
|
||||
/// </summary>
|
||||
public GalaxyRuntimeStatus? GetHostStatus(int gobjectId)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_probeByGobjectId.TryGetValue(gobjectId, out var probe)
|
||||
&& _byProbe.TryGetValue(probe, out var status))
|
||||
{
|
||||
return Clone(status, forceUnknown: false);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diffs the supplied hierarchy against the active probe set, advising new hosts and
|
||||
/// unadvising removed ones. The hierarchy is filtered to runtime host categories
|
||||
|
||||
@@ -62,6 +62,23 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
// worker thread currently inside Read waiting for an MxAccess round-trip.
|
||||
private readonly ConcurrentQueue<(int GobjectId, bool Stopped)> _pendingHostStateChanges =
|
||||
new ConcurrentQueue<(int, bool)>();
|
||||
|
||||
// Synthetic $-prefixed OPC UA child variables exposed under each $WinPlatform / $AppEngine
|
||||
// object so clients can subscribe to runtime state changes without polling the dashboard.
|
||||
// Populated during BuildAddressSpace and updated from the dispatch-thread queue drain
|
||||
// alongside Mark/Clear, using the same deadlock-safe path.
|
||||
private readonly Dictionary<int, HostRuntimeStatusNodes> _runtimeStatusNodes =
|
||||
new Dictionary<int, HostRuntimeStatusNodes>();
|
||||
|
||||
private sealed class HostRuntimeStatusNodes
|
||||
{
|
||||
public BaseDataVariableState RuntimeState = null!;
|
||||
public BaseDataVariableState LastCallbackTime = null!;
|
||||
public BaseDataVariableState LastScanState = null!;
|
||||
public BaseDataVariableState LastStateChangeTime = null!;
|
||||
public BaseDataVariableState FailureCount = null!;
|
||||
public BaseDataVariableState LastError = null!;
|
||||
}
|
||||
private readonly AutoResetEvent _dataChangeSignal = new(false);
|
||||
private readonly Dictionary<int, List<string>> _gobjectToTagRefs = new();
|
||||
private readonly HistoryContinuationPointManager _historyContinuations = new();
|
||||
@@ -330,6 +347,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
_nodeMap.Clear();
|
||||
_gobjectToTagRefs.Clear();
|
||||
_hostedVariables.Clear();
|
||||
_hostIdsByTagRef.Clear();
|
||||
_runtimeStatusNodes.Clear();
|
||||
VariableNodeCount = 0;
|
||||
ObjectNodeCount = 0;
|
||||
|
||||
@@ -387,6 +406,17 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
AddPredefinedNode(SystemContext, node);
|
||||
_nodeMap[obj.GobjectId] = node;
|
||||
|
||||
// Attach bridge-owned $RuntimeState / $LastCallbackTime / ... synthetic child
|
||||
// variables so OPC UA clients can subscribe to host state changes without
|
||||
// polling the dashboard. Only $WinPlatform (1) and $AppEngine (3) get them.
|
||||
if (_galaxyRuntimeProbeManager != null
|
||||
&& (obj.CategoryId == 1 || obj.CategoryId == 3)
|
||||
&& node is BaseObjectState hostObj)
|
||||
{
|
||||
_runtimeStatusNodes[obj.GobjectId] =
|
||||
CreateHostRuntimeStatusNodes(hostObj, obj.TagName);
|
||||
}
|
||||
|
||||
// Create variable nodes for this object's attributes
|
||||
if (attrsByObject.TryGetValue(obj.GobjectId, out var objAttrs))
|
||||
{
|
||||
@@ -675,6 +705,82 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
variables.Count, gobjectId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the six <c>$</c>-prefixed synthetic child variables on a host object so OPC UA
|
||||
/// clients can subscribe to runtime state changes without polling the dashboard. All
|
||||
/// nodes are read-only and their values are refreshed by <see cref="UpdateHostRuntimeStatusNodes"/>
|
||||
/// from the dispatch-thread queue drain whenever the host transitions.
|
||||
/// </summary>
|
||||
private HostRuntimeStatusNodes CreateHostRuntimeStatusNodes(BaseObjectState hostNode, string hostTagName)
|
||||
{
|
||||
var nodes = new HostRuntimeStatusNodes
|
||||
{
|
||||
RuntimeState = CreateSyntheticVariable(hostNode, hostTagName, "$RuntimeState", DataTypeIds.String, "Unknown"),
|
||||
LastCallbackTime = CreateSyntheticVariable(hostNode, hostTagName, "$LastCallbackTime", DataTypeIds.DateTime, DateTime.MinValue),
|
||||
LastScanState = CreateSyntheticVariable(hostNode, hostTagName, "$LastScanState", DataTypeIds.Boolean, false),
|
||||
LastStateChangeTime = CreateSyntheticVariable(hostNode, hostTagName, "$LastStateChangeTime", DataTypeIds.DateTime, DateTime.MinValue),
|
||||
FailureCount = CreateSyntheticVariable(hostNode, hostTagName, "$FailureCount", DataTypeIds.Int64, 0L),
|
||||
LastError = CreateSyntheticVariable(hostNode, hostTagName, "$LastError", DataTypeIds.String, "")
|
||||
};
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private BaseDataVariableState CreateSyntheticVariable(
|
||||
BaseObjectState parent, string parentTagName, string browseName, NodeId dataType, object initialValue)
|
||||
{
|
||||
var v = CreateVariable(parent, browseName, browseName, dataType, ValueRanks.Scalar);
|
||||
v.NodeId = new NodeId(parentTagName + "." + browseName, NamespaceIndex);
|
||||
v.Value = initialValue;
|
||||
v.StatusCode = StatusCodes.Good;
|
||||
v.Timestamp = DateTime.UtcNow;
|
||||
v.AccessLevel = AccessLevels.CurrentRead;
|
||||
v.UserAccessLevel = AccessLevels.CurrentRead;
|
||||
AddPredefinedNode(SystemContext, v);
|
||||
return v;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the six synthetic child variables on a host from the probe manager's
|
||||
/// current snapshot for that host. Called from the dispatch-thread queue drain after
|
||||
/// Mark/Clear so the state values propagate to subscribed clients in the same publish
|
||||
/// cycle. Takes the node manager <see cref="Lock"/> internally.
|
||||
/// </summary>
|
||||
private void UpdateHostRuntimeStatusNodes(int gobjectId)
|
||||
{
|
||||
if (_galaxyRuntimeProbeManager == null)
|
||||
return;
|
||||
|
||||
HostRuntimeStatusNodes? nodes;
|
||||
lock (Lock)
|
||||
{
|
||||
if (!_runtimeStatusNodes.TryGetValue(gobjectId, out nodes))
|
||||
return;
|
||||
}
|
||||
|
||||
var status = _galaxyRuntimeProbeManager.GetHostStatus(gobjectId);
|
||||
if (status == null)
|
||||
return;
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
SetSynthetic(nodes.RuntimeState, status.State.ToString(), now);
|
||||
SetSynthetic(nodes.LastCallbackTime, status.LastStateCallbackTime ?? DateTime.MinValue, now);
|
||||
SetSynthetic(nodes.LastScanState, status.LastScanState ?? false, now);
|
||||
SetSynthetic(nodes.LastStateChangeTime, status.LastStateChangeTime ?? DateTime.MinValue, now);
|
||||
SetSynthetic(nodes.FailureCount, status.FailureCount, now);
|
||||
SetSynthetic(nodes.LastError, status.LastError ?? "", now);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetSynthetic(BaseDataVariableState variable, object value, DateTime now)
|
||||
{
|
||||
variable.Value = value;
|
||||
variable.StatusCode = StatusCodes.Good;
|
||||
variable.Timestamp = now;
|
||||
variable.ClearChangeMasks(SystemContext, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets every OPC UA variable hosted by the given Galaxy runtime object to
|
||||
/// <see cref="StatusCodes.Good"/>. Invoked by the runtime probe manager's Stopped → Running
|
||||
@@ -2423,6 +2529,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
MarkHostVariablesBadQuality(transition.GobjectId);
|
||||
else
|
||||
ClearHostVariablesBadQuality(transition.GobjectId);
|
||||
|
||||
// Also refresh the synthetic $RuntimeState child nodes on this host so
|
||||
// subscribed OPC UA clients see the state change in the same publish cycle.
|
||||
UpdateHostRuntimeStatusNodes(transition.GobjectId);
|
||||
}
|
||||
|
||||
var keys = _pendingDataChanges.Keys.ToList();
|
||||
|
||||
Reference in New Issue
Block a user