Provides technical documentation covering OPC UA server, address space, Galaxy repository, MXAccess bridge, data types, read/write, subscriptions, alarms, historian, incremental sync, configuration, dashboard, service hosting, and CLI tool. Updates README with component documentation table. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7.0 KiB
Incremental Sync
When a Galaxy redeployment is detected, the OPC UA address space must be updated to reflect the new hierarchy and attributes. Rather than tearing down the entire address space and rebuilding from scratch (which disconnects all clients and drops all subscriptions), LmxNodeManager performs an incremental sync that identifies changed objects and rebuilds only the affected subtrees.
Cached State
LmxNodeManager retains shallow copies of the last-published hierarchy and attributes:
private List<GalaxyObjectInfo>? _lastHierarchy;
private List<GalaxyAttributeInfo>? _lastAttributes;
These are updated at the end of every BuildAddressSpace or SyncAddressSpace call via new List<T>(source) to create independent copies. The copies serve as the baseline for the next diff comparison.
On the first call (when _lastHierarchy is null), SyncAddressSpace falls through to a full BuildAddressSpace since there is no baseline to diff against.
AddressSpaceDiff
AddressSpaceDiff is a static helper class that computes the set of changed Galaxy object IDs between two snapshots.
FindChangedGobjectIds
This method compares old and new hierarchy+attributes and returns a HashSet<int> of gobject IDs that have any difference. It detects three categories of changes:
Added objects -- Present in new hierarchy but not in old:
foreach (var id in newObjects.Keys)
if (!oldObjects.ContainsKey(id))
changed.Add(id);
Removed objects -- Present in old hierarchy but not in new:
foreach (var id in oldObjects.Keys)
if (!newObjects.ContainsKey(id))
changed.Add(id);
Modified objects -- Present in both but with different properties. ObjectsEqual compares TagName, BrowseName, ContainedName, ParentGobjectId, and IsArea.
Attribute set changes -- For objects that exist in both snapshots, attributes are grouped by GobjectId and compared pairwise. AttributeSetsEqual sorts both lists by FullTagReference and PrimitiveName, then checks each pair via AttributesEqual, which compares AttributeName, FullTagReference, MxDataType, IsArray, ArrayDimension, PrimitiveName, SecurityClassification, IsHistorized, and IsAlarm. A difference in count or any field mismatch marks the owning gobject as changed.
Objects already marked as changed by hierarchy comparison are skipped during attribute comparison to avoid redundant work.
ExpandToSubtrees
When a Galaxy object changes, its children must also be rebuilt because they may reference the parent's node or have inherited attribute changes. ExpandToSubtrees performs a BFS traversal from each changed ID, adding all descendants:
public static HashSet<int> ExpandToSubtrees(HashSet<int> changed,
List<GalaxyObjectInfo> hierarchy)
{
var childrenByParent = hierarchy.GroupBy(h => h.ParentGobjectId)
.ToDictionary(g => g.Key, g => g.Select(h => h.GobjectId).ToList());
var expanded = new HashSet<int>(changed);
var queue = new Queue<int>(changed);
while (queue.Count > 0)
{
var id = queue.Dequeue();
if (childrenByParent.TryGetValue(id, out var children))
foreach (var childId in children)
if (expanded.Add(childId))
queue.Enqueue(childId);
}
return expanded;
}
The expansion runs against both the old and new hierarchy. This is necessary because a removed parent's children appear in the old hierarchy (for teardown) while an added parent's children appear in the new hierarchy (for construction).
SyncAddressSpace Flow
SyncAddressSpace orchestrates the incremental update inside the OPC UA framework Lock:
-
Diff -- Call
FindChangedGobjectIdswith the cached and new snapshots. If no changes are detected, update the cached snapshots and return early. -
Expand -- Call
ExpandToSubtreeson both old and new hierarchies to include descendant objects. -
Snapshot subscriptions -- Before teardown, iterate
_gobjectToTagRefsfor each changed gobject ID and record the current MXAccess subscription ref-counts. These are needed to restore subscriptions after rebuild. -
Teardown -- Call
TearDownGobjectsto remove the old nodes and clean up tracking state. -
Rebuild -- Filter the new hierarchy and attributes to only the changed gobject IDs, then call
BuildSubtreeto create the replacement nodes. -
Restore subscriptions -- For each previously subscribed tag reference that still exists in
_tagToVariableNodeafter rebuild, re-open the MXAccess subscription and restore the original ref-count. -
Update cache -- Replace
_lastHierarchyand_lastAttributeswith shallow copies of the new data.
TearDownGobjects
TearDownGobjects removes all OPC UA nodes and tracking state for a set of gobject IDs:
For each gobject ID, it processes the associated tag references from _gobjectToTagRefs:
-
Unsubscribe -- If the tag has an active MXAccess subscription (entry in
_subscriptionRefCounts), callUnsubscribeAsyncand remove the ref-count entry. -
Remove alarm tracking -- Find any
_alarmInAlarmTagsentries whoseSourceTagReferencematches the tag. For each, unsubscribe the InAlarm, Priority, and DescAttrName tags, then remove the alarm entry. -
Delete variable node -- Call
DeleteNodeon the variable'sNodeId, remove from_tagToVariableNode, clean up_nodeIdToTagReferenceand_tagMetadata, and decrementVariableNodeCount. -
Delete object/folder node -- Remove the gobject's entry from
_nodeMapand callDeleteNode. Non-folder nodes decrementObjectNodeCount.
All MXAccess calls and DeleteNode calls are wrapped in try/catch with ignored exceptions, since teardown must complete even if individual cleanup steps fail.
BuildSubtree
BuildSubtree creates OPC UA nodes for a subset of the Galaxy hierarchy, reusing existing parent nodes from _nodeMap.
The method first topologically sorts the input hierarchy (same TopologicalSort used by BuildAddressSpace) to ensure parents are created before children. For each object:
-
Find parent -- Look up
ParentGobjectIdin_nodeMap. If the parent was not part of the changed set, it already exists from the previous build. If no parent is found, fall back to the rootZBfolder. This is the key difference fromBuildAddressSpace-- subtree builds reuse the existing node tree rather than starting from the root. -
Create node -- Areas become
FolderStatewithOrganizesreference; non-areas becomeBaseObjectStatewithHasComponentreference. The node is added to_nodeMap. -
Create variable nodes -- Attributes are processed with the same primitive-grouping logic as
BuildAddressSpace, creatingBaseDataVariableStatenodes viaCreateAttributeVariable. -
Alarm tracking -- If
_alarmTrackingEnabledis set, alarm attributes are detected andAlarmConditionStatenodes are created using the same logic as the full build. EventNotifier flags are set on parent nodes, and alarm tags are auto-subscribed.