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;
|
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>
|
/// <summary>
|
||||||
/// Diffs the supplied hierarchy against the active probe set, advising new hosts and
|
/// Diffs the supplied hierarchy against the active probe set, advising new hosts and
|
||||||
/// unadvising removed ones. The hierarchy is filtered to runtime host categories
|
/// 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.
|
// worker thread currently inside Read waiting for an MxAccess round-trip.
|
||||||
private readonly ConcurrentQueue<(int GobjectId, bool Stopped)> _pendingHostStateChanges =
|
private readonly ConcurrentQueue<(int GobjectId, bool Stopped)> _pendingHostStateChanges =
|
||||||
new ConcurrentQueue<(int, bool)>();
|
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 AutoResetEvent _dataChangeSignal = new(false);
|
||||||
private readonly Dictionary<int, List<string>> _gobjectToTagRefs = new();
|
private readonly Dictionary<int, List<string>> _gobjectToTagRefs = new();
|
||||||
private readonly HistoryContinuationPointManager _historyContinuations = new();
|
private readonly HistoryContinuationPointManager _historyContinuations = new();
|
||||||
@@ -330,6 +347,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
|||||||
_nodeMap.Clear();
|
_nodeMap.Clear();
|
||||||
_gobjectToTagRefs.Clear();
|
_gobjectToTagRefs.Clear();
|
||||||
_hostedVariables.Clear();
|
_hostedVariables.Clear();
|
||||||
|
_hostIdsByTagRef.Clear();
|
||||||
|
_runtimeStatusNodes.Clear();
|
||||||
VariableNodeCount = 0;
|
VariableNodeCount = 0;
|
||||||
ObjectNodeCount = 0;
|
ObjectNodeCount = 0;
|
||||||
|
|
||||||
@@ -387,6 +406,17 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
|||||||
AddPredefinedNode(SystemContext, node);
|
AddPredefinedNode(SystemContext, node);
|
||||||
_nodeMap[obj.GobjectId] = 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
|
// Create variable nodes for this object's attributes
|
||||||
if (attrsByObject.TryGetValue(obj.GobjectId, out var objAttrs))
|
if (attrsByObject.TryGetValue(obj.GobjectId, out var objAttrs))
|
||||||
{
|
{
|
||||||
@@ -675,6 +705,82 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
|||||||
variables.Count, gobjectId);
|
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>
|
/// <summary>
|
||||||
/// Resets every OPC UA variable hosted by the given Galaxy runtime object to
|
/// 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
|
/// <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);
|
MarkHostVariablesBadQuality(transition.GobjectId);
|
||||||
else
|
else
|
||||||
ClearHostVariablesBadQuality(transition.GobjectId);
|
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();
|
var keys = _pendingDataChanges.Keys.ToList();
|
||||||
|
|||||||
Reference in New Issue
Block a user