diff --git a/samples/DI/PumpDeviceIntegrationServer/OpenUsdComposition.cs b/samples/DI/PumpDeviceIntegrationServer/OpenUsdComposition.cs
index 7aad5bec11..c1c30cf8c5 100644
--- a/samples/DI/PumpDeviceIntegrationServer/OpenUsdComposition.cs
+++ b/samples/DI/PumpDeviceIntegrationServer/OpenUsdComposition.cs
@@ -36,6 +36,7 @@ using Opc.Ua.OpenUsd;
using Opc.Ua.OpenUsd.Server;
using Opc.Ua.Pumps;
using Opc.Ua.Server;
+using Opc.Ua.Server.Fluent;
using Opc.Ua.Server.NodeManager;
namespace Pumps
@@ -94,8 +95,7 @@ namespace Pumps
/// needles that make each machine read as its own.
///
///
- private async ValueTask MaterialisePlantAggregationAsync(
- CancellationToken cancellationToken)
+ private void MaterialisePlantAggregation(INodeManagerBuilder builder)
{
if (m_plantStage == null)
{
@@ -131,15 +131,14 @@ namespace Pumps
assetReference: "@pump.usda@",
componentTypeDefinition: pumpTypeId);
- SystemContext.AssignInstanceChildNodeIds(plantRep);
- await AddPredefinedNodeAsync(SystemContext, plantRep, cancellationToken)
- .ConfigureAwait(false);
+ // Staging assigns the whole subtree its final NodeIds - the same
+ // pass AssignInstanceChildNodeIds used to run here - and leaves
+ // the registration to CreateAddressSpaceAsync.
+ builder.Add(plantRep, deviceSet.NodeId);
- FolderState? registry = m_openUsdRoot?.Representations;
- if (registry != null)
+ if (m_openUsdRoot?.Representations is FolderState registry)
{
- registry.AddReference(ReferenceTypeIds.Organizes, false, plantRep.NodeId);
- plantRep.AddReference(ReferenceTypeIds.Organizes, true, registry.NodeId);
+ plantRep.RegisterInDiscovery(registry);
}
m_logger.MaterialisedPlantAggregation(m_twins.Count);
@@ -156,7 +155,12 @@ namespace Pumps
// connected server simulates and nothing else. A line pump is a static
// topology entry, not a machine anyone is driving; rendering it put
// phantom pumps in the twin that no client could account for.
- private async ValueTask MaterialiseProductionLineAsync(CancellationToken cancellationToken)
+ // The subtree is staged on the builder rather than registered here: the
+ // manager's CreateAddressSpaceAsync registers everything staged during
+ // Configure and runs the reverse-reference pass over it, and staging
+ // mints the NodeIds through the same factory this method used to call
+ // by hand.
+ private void MaterialiseProductionLine(INodeManagerBuilder builder)
{
if (m_plantStage == null)
{
@@ -172,35 +176,18 @@ namespace Pumps
return;
}
- var line = new BaseObjectState(deviceSet)
- {
- SymbolicName = "ProductionLine",
- BrowseName = new QualifiedName("ProductionLine", ns),
- DisplayName = new LocalizedText("ProductionLine"),
- ReferenceTypeId = ReferenceTypeIds.HasComponent,
- TypeDefinitionId = Opc.Ua.ObjectTypeIds.BaseObjectType
- };
- deviceSet.AddChild(line);
- line.NodeId = SystemContext.NodeIdFactory.New(SystemContext, line);
+ BaseObjectState line = builder
+ .AddObject(new QualifiedName("ProductionLine", ns), deviceSet.NodeId)
+ .Node;
- var pumps = new FolderState(line)
- {
- SymbolicName = "Pumps",
- BrowseName = new QualifiedName("Pumps", ns),
- DisplayName = new LocalizedText("Pumps"),
- ReferenceTypeId = ReferenceTypeIds.Organizes,
- TypeDefinitionId = Opc.Ua.ObjectTypeIds.FolderType
- };
- line.AddChild(pumps);
- pumps.NodeId = SystemContext.NodeIdFactory.New(SystemContext, pumps);
+ FolderState pumps = builder
+ .AddFolder(new QualifiedName("Pumps", ns), line.NodeId)
+ .Node;
m_linePumps = pumps;
// Two static aggregated entries (1..n baseline).
- CreateAggregatedPump(pumps, "P-201", ns);
- CreateAggregatedPump(pumps, "P-202", ns);
-
- SystemContext.AssignInstanceChildNodeIds(line);
- await AddPredefinedNodeAsync(SystemContext, line, cancellationToken).ConfigureAwait(false);
+ CreateAggregatedPump(builder, pumps.NodeId, "P-201", ns);
+ CreateAggregatedPump(builder, pumps.NodeId, "P-202", ns);
// Dynamic composition: emit model-change events on runtime add/remove.
ModelChangeEmissionEnabled = true;
@@ -217,18 +204,13 @@ namespace Pumps
// An aggregated line entry: a plain topology Object with no
// representation, because it is not a machine the server simulates.
- private void CreateAggregatedPump(NodeState parent, string name, ushort ns)
+ private static void CreateAggregatedPump(
+ INodeManagerBuilder builder, NodeId parentId, string name, ushort ns)
{
- var obj = new BaseObjectState(parent)
- {
- SymbolicName = name,
- BrowseName = new QualifiedName(name, ns),
- DisplayName = new LocalizedText(name),
- ReferenceTypeId = ReferenceTypeIds.Organizes,
- TypeDefinitionId = Opc.Ua.ObjectTypeIds.BaseObjectType
- };
- parent.AddChild(obj);
- obj.NodeId = SystemContext.NodeIdFactory.New(SystemContext, obj);
+ BaseObjectState entry = builder
+ .AddObject(new QualifiedName(name, ns), parentId)
+ .Node;
+ entry.ReferenceTypeId = ReferenceTypeIds.Organizes;
}
// Dynamic demo (ยง5.13): repeatedly add a pump (emits a GeneralModelChange),
diff --git a/samples/DI/PumpDeviceIntegrationServer/PumpNodeManager.cs b/samples/DI/PumpDeviceIntegrationServer/PumpNodeManager.cs
index 6bd91e96e4..f00a5902e4 100644
--- a/samples/DI/PumpDeviceIntegrationServer/PumpNodeManager.cs
+++ b/samples/DI/PumpDeviceIntegrationServer/PumpNodeManager.cs
@@ -186,7 +186,7 @@ namespace Pumps
// predefined instances that Configure(builder) will wire.
// Mirrors the synchronous fluent Configure(builder) but
// runs first so the builder has typed nodes available.
- await ConfigureInstancesAsync(cancellationToken)
+ await ConfigureInstancesAsync(builder, cancellationToken)
.ConfigureAwait(false);
// Configuration phase 2 (sync): wire fluent callbacks
@@ -223,6 +223,7 @@ namespace Pumps
/// directly.
///
private async ValueTask ConfigureInstancesAsync(
+ INodeManagerBuilder builder,
CancellationToken cancellationToken)
{
// OpenUSD facility first so the pump representation can reference the stage.
@@ -247,12 +248,12 @@ namespace Pumps
// Plant-level aggregation: composes one full-fidelity pump prim per
// configured pump, so the rendered scene scales with --pumps N.
- await MaterialisePlantAggregationAsync(cancellationToken).ConfigureAwait(false);
+ MaterialisePlantAggregation(builder);
// Composition demo: a ProductionLine aggregating 1..n pumps (Many), with a
// dynamically added/removed pump (model-change events) and a cross-server
// component (federation). See OpenUsdComposition.cs.
- await MaterialiseProductionLineAsync(cancellationToken).ConfigureAwait(false);
+ MaterialiseProductionLine(builder);
}
///
diff --git a/samples/Redundancy/RedundantServer/HaSampleNodeManager.cs b/samples/Redundancy/RedundantServer/HaSampleNodeManager.cs
index 105efc9d37..4ce5bbcc4d 100644
--- a/samples/Redundancy/RedundantServer/HaSampleNodeManager.cs
+++ b/samples/Redundancy/RedundantServer/HaSampleNodeManager.cs
@@ -37,6 +37,7 @@ using Opc.Ua;
using Opc.Ua.Redundancy;
using Opc.Ua.Redundancy.Server;
using Opc.Ua.Server;
+using Opc.Ua.Server.Fluent;
using Opc.Ua.Server.Historian;
using Opc.Ua.Server.Hosting;
@@ -147,9 +148,9 @@ namespace RedundantServer
}
///
- /// Minimal address space that participates in distributed replication.
+ /// Minimal address space that participates in distributed replication.
///
- public sealed class HaSampleNodeManager : AsyncCustomNodeManager
+ public sealed class HaSampleNodeManager : FluentNodeManagerBase
{
private readonly ILeaderElection m_leaderElection;
private readonly HaSampleReplicaInfo m_replicaInfo;
@@ -158,8 +159,10 @@ namespace RedundantServer
private readonly CancellationTokenSource m_simulationCts = new();
private readonly Lock m_updateLock = new();
private static readonly TimeSpan s_valueFreshness = TimeSpan.FromSeconds(10);
- private BaseDataVariableState? m_counter;
- private BaseDataVariableState? m_activeReplica;
+ private const string kSampleFolderName = "HighAvailability";
+ private FolderState? m_folder;
+ private BaseVariableState? m_counter;
+ private BaseVariableState? m_activeReplica;
private BaseObjectState? m_historyEvents;
private Task? m_simulationTask;
private int m_counterValue;
@@ -193,22 +196,85 @@ namespace RedundantServer
m_historian = historian;
}
+ ///
+ /// Mints the browse name as the identifier, for the handful of nodes
+ /// this sample publishes by name.
+ ///
+ ///
+ ///
+ /// Those identifiers are part of the sample's contract - the redundant
+ /// client addresses ns=N;s=Counter directly - so they cannot be
+ /// left to the derived form the default factory mints.
+ ///
+ ///
+ /// Every other node keeps that derived form, and the narrowness
+ /// matters: a browse name is only unique among its siblings, and the
+ /// historian hangs an HA Configuration object off each node it
+ /// historizes, so a blanket rule would hand the Counter's and the
+ /// HistoryEvents' one the same identifier. An identifier a caller
+ /// already chose here is kept, on the default factory's own terms.
+ ///
+ ///
+ public override NodeId New(ISystemContext context, NodeState node)
+ {
+ if (node == null)
+ {
+ return base.New(context, node!);
+ }
+
+ ushort namespaceIndex = NamespaceIndexes[0];
+ if (!node.NodeId.IsNull &&
+ (node.NodeId.NamespaceIndex == namespaceIndex ||
+ node is not BaseInstanceState { Parent: not null }))
+ {
+ return node.NodeId;
+ }
+
+ return IsPublishedByName(node)
+ ? new NodeId(node.BrowseName.Name!, namespaceIndex)
+ : base.New(context, node);
+ }
+
+ ///
+ /// Whether is the sample folder or one of the
+ /// nodes directly beneath it - the ones a client spells out.
+ ///
+ ///
+ /// The folder reaches this before it has been staged, so it has no
+ /// parent yet and is recognised by its browse name instead.
+ ///
+ private bool IsPublishedByName(NodeState node)
+ {
+ if (string.IsNullOrEmpty(node.BrowseName.Name))
+ {
+ return false;
+ }
+
+ NodeState? parent = (node as BaseInstanceState)?.Parent;
+ return parent == null
+ ? node.BrowseName.Name == kSampleFolderName
+ : ReferenceEquals(parent, m_folder);
+ }
+
///
public override async ValueTask CreateAddressSpaceAsync(
IDictionary> externalReferences,
CancellationToken cancellationToken = default)
{
- if (!externalReferences.TryGetValue(ObjectIds.ObjectsFolder, out IList? references))
- {
- externalReferences[ObjectIds.ObjectsFolder] = references = [];
- }
+ NodeManagerBuilder builder = CreateFluentBuilder(NamespaceIndexes[0]);
- ushort namespaceIndex = NamespaceIndexes[0];
- FolderState folder = CreateFolder(null, namespaceIndex, "HighAvailability", "High Availability");
- folder.AddReference(ReferenceTypeIds.Organizes, true, ObjectIds.ObjectsFolder);
- references.Add(new NodeStateReference(ReferenceTypeIds.Organizes, false, folder.NodeId));
+ // Unparented, so the staged folder organises itself under the
+ // Objects folder; the manager used to add that reference and its
+ // externalReferences counterpart by hand.
+ INodeBuilder folder = builder.AddFolder(kSampleFolderName);
+ folder.Node.DisplayName = new LocalizedText("en", "High Availability");
+ NodeId folderId = folder.Node.NodeId;
- m_counter = CreateVariable(folder, namespaceIndex, "Counter", DataTypeIds.Int32, Variant.From(0));
+ // New() names the folder's direct children after their browse
+ // name, so it has to know which node the folder is.
+ m_folder = folder.Node;
+
+ m_counter = AddSampleVariable(builder, folderId, "Counter", Variant.From(0));
if (m_valueCache != null)
{
// Opt the Counter's read/write callbacks into the distributed
@@ -224,27 +290,24 @@ namespace RedundantServer
_ => new ValueTask(ReadLocalCounter()));
}
- m_activeReplica = CreateVariable(
- folder,
- namespaceIndex,
+ m_activeReplica = AddSampleVariable(
+ builder,
+ folderId,
"ActiveReplica",
- DataTypeIds.String,
Variant.From("unknown"));
- m_historyEvents = new BaseObjectState(folder)
- {
- SymbolicName = "HistoryEvents",
- ReferenceTypeId = ReferenceTypeIds.Organizes,
- TypeDefinitionId = ObjectTypeIds.BaseObjectType,
- NodeId = new NodeId("HistoryEvents", namespaceIndex),
- BrowseName = new QualifiedName("HistoryEvents", namespaceIndex),
- DisplayName = new LocalizedText("en", "History Events"),
- WriteMask = AttributeWriteMask.None,
- UserWriteMask = AttributeWriteMask.None,
- EventNotifier = EventNotifiers.SubscribeToEvents
- };
- folder.AddChild(m_historyEvents);
- AddFactoryAssignedNodes(folder, namespaceIndex);
+ m_historyEvents = builder.AddObject("HistoryEvents", folderId).Node;
+ m_historyEvents.ReferenceTypeId = ReferenceTypeIds.Organizes;
+ m_historyEvents.DisplayName = new LocalizedText("en", "History Events");
+ m_historyEvents.EventNotifier = EventNotifiers.SubscribeToEvents;
+
+ // Deliberately not staged: this subtree exists to show what a
+ // factory-assigned identity looks like next to the named ones
+ // above, so it keeps minting through the NodeIdFactory itself
+ // rather than through New. Attaching it once the folder has been
+ // staged leaves those identifiers untouched, and the subtree is
+ // still registered as part of the folder's.
+ AddFactoryAssignedNodes(folder.Node, NamespaceIndexes[0]);
if (m_historian != null)
{
@@ -268,7 +331,13 @@ namespace RedundantServer
cancellationToken: cancellationToken).ConfigureAwait(false);
}
- await AddPredefinedNodeAsync(SystemContext, folder, cancellationToken).ConfigureAwait(false);
+ // Registering the staged subtree and re-running the reverse
+ // reference pass is what publishes the folder's inverse Organizes
+ // reference into externalReferences[ObjectsFolder].
+ await RegisterAuthoredNodesAsync(builder, cancellationToken).ConfigureAwait(false);
+ await CompleteConfigureAsync(externalReferences, cancellationToken)
+ .ConfigureAwait(false);
+ await SealConfigurationAsync(builder, cancellationToken).ConfigureAwait(false);
}
private void AddFactoryAssignedNodes(FolderState parent, ushort namespaceIndex)
@@ -331,53 +400,36 @@ namespace RedundantServer
return m_historian ?? base.GetHistorianProvider(node);
}
- private static FolderState CreateFolder(NodeState? parent, ushort namespaceIndex, string path, string name)
- {
- var folder = new FolderState(parent)
- {
- SymbolicName = name,
- ReferenceTypeId = ReferenceTypeIds.Organizes,
- TypeDefinitionId = ObjectTypeIds.FolderType,
- NodeId = new NodeId(path, namespaceIndex),
- BrowseName = new QualifiedName(path, namespaceIndex),
- DisplayName = new LocalizedText("en", name),
- WriteMask = AttributeWriteMask.None,
- UserWriteMask = AttributeWriteMask.None,
- EventNotifier = EventNotifiers.None
- };
-
- parent?.AddChild(folder);
- return folder;
- }
-
- private static BaseDataVariableState CreateVariable(
- NodeState parent,
- ushort namespaceIndex,
+ ///
+ /// Stages one writable sample variable under the sample folder.
+ ///
+ ///
+ /// CLR type the variable carries; the staged node takes its DataType
+ /// and ValueRank from it.
+ ///
+ /// The fluent builder staging the subtree.
+ /// The folder the variable hangs off.
+ /// Browse name of the variable.
+ /// The value a client reads before the
+ /// simulation has produced one.
+ private static BaseVariableState AddSampleVariable(
+ NodeManagerBuilder builder,
+ NodeId parentId,
string name,
- NodeId dataType,
Variant initialValue)
{
- var variable = new BaseDataVariableState(parent)
- {
- SymbolicName = name,
- ReferenceTypeId = ReferenceTypeIds.Organizes,
- TypeDefinitionId = VariableTypeIds.BaseDataVariableType,
- NodeId = new NodeId(name, namespaceIndex),
- BrowseName = new QualifiedName(name, namespaceIndex),
- DisplayName = new LocalizedText("en", name),
- WriteMask = AttributeWriteMask.None,
- UserWriteMask = AttributeWriteMask.None,
- DataType = dataType,
- ValueRank = ValueRanks.Scalar,
- AccessLevel = AccessLevels.CurrentReadOrWrite,
- UserAccessLevel = AccessLevels.CurrentReadOrWrite,
- Historizing = false,
- Value = initialValue,
- StatusCode = StatusCodes.Good,
- Timestamp = DateTime.UtcNow
- };
+ BaseVariableState variable = builder
+ .AddVariable(name, parentId)
+ .Writable()
+ .Node;
- parent.AddChild(variable);
+ // The sample organises its variables rather than componentising
+ // them, which is the shape the redundant client browses.
+ variable.ReferenceTypeId = ReferenceTypeIds.Organizes;
+ variable.DisplayName = new LocalizedText("en", name);
+ variable.Value = initialValue;
+ variable.StatusCode = StatusCodes.Good;
+ variable.Timestamp = DateTime.UtcNow;
return variable;
}
@@ -446,7 +498,7 @@ namespace RedundantServer
private async Task UpdateCounterAsync(CancellationToken cancellationToken)
{
- BaseDataVariableState? counter = m_counter;
+ BaseVariableState? counter = m_counter;
if (counter == null)
{
return;
@@ -519,7 +571,7 @@ namespace RedundantServer
}
private async Task ArchiveCounterAsync(
- BaseDataVariableState counter,
+ BaseVariableState counter,
DataValue sample,
CancellationToken cancellationToken)
{
@@ -572,7 +624,7 @@ namespace RedundantServer
private async Task SeedCounterFromCacheAsync(CancellationToken cancellationToken)
{
- BaseDataVariableState? counter = m_counter;
+ BaseVariableState? counter = m_counter;
if (m_valueCache == null || counter == null)
{
return;
@@ -611,7 +663,7 @@ namespace RedundantServer
private void UpdateActiveReplica(string value)
{
- BaseDataVariableState? activeReplica = m_activeReplica;
+ BaseVariableState? activeReplica = m_activeReplica;
if (activeReplica == null)
{
return;
diff --git a/samples/WotCon/FlatTagServer/FlatTagNodeManager.cs b/samples/WotCon/FlatTagServer/FlatTagNodeManager.cs
index 242560b707..9056fae708 100644
--- a/samples/WotCon/FlatTagServer/FlatTagNodeManager.cs
+++ b/samples/WotCon/FlatTagServer/FlatTagNodeManager.cs
@@ -33,6 +33,7 @@ using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.Server;
+using Opc.Ua.Server.Fluent;
namespace FlatTagServer
{
@@ -75,7 +76,7 @@ namespace FlatTagServer
///
/// Minimal async address space exposing one half of the aggregate Pump tags.
///
- public sealed class FlatTagNodeManager : AsyncCustomNodeManager
+ public sealed class FlatTagNodeManager : FluentNodeManagerBase
{
///
/// Initializes the node manager.
@@ -88,6 +89,53 @@ namespace FlatTagServer
m_options = options ?? throw new ArgumentNullException(nameof(options));
}
+ ///
+ /// Mints the dotted tag paths this sample is addressed by.
+ ///
+ ///
+ /// The identifiers are part of the sample's contract rather than an
+ /// implementation detail: the aggregating client's Thing Descriptions
+ /// spell out s=Pump1.Identification.Manufacturer, so a staged
+ /// node keeps its browse path as its identifier instead of taking the
+ /// hashed one the default factory would mint.
+ ///
+ public override NodeId New(ISystemContext context, NodeState node)
+ {
+ ushort namespaceIndex = NamespaceIndexes[0];
+
+ // An identifier the caller has already chosen here is kept, on the
+ // same terms as the default factory: the supervision signals name
+ // their tag and their condition explicitly, and NodeState.Create
+ // runs its assignment pass over a subtree that already carries
+ // them. A staged node arrives with its NodeId cleared, so it falls
+ // through to the browse path below.
+ if (node is not null &&
+ !node.NodeId.IsNull &&
+ (node.NodeId.NamespaceIndex == namespaceIndex ||
+ node is not BaseInstanceState { Parent: not null }))
+ {
+ return node.NodeId;
+ }
+
+ string? browseName = node?.BrowseName.Name;
+ if (string.IsNullOrEmpty(browseName))
+ {
+ return base.New(context, node!);
+ }
+
+ if (node is BaseInstanceState instance &&
+ instance.Parent is NodeState parent &&
+ parent.NodeId.NamespaceIndex == namespaceIndex &&
+ parent.NodeId.IdType == IdType.String)
+ {
+ return new NodeId(
+ parent.NodeId.IdentifierAsString + "." + browseName,
+ namespaceIndex);
+ }
+
+ return new NodeId(browseName!, namespaceIndex);
+ }
+
///
public override async ValueTask CreateAddressSpaceAsync(
IDictionary> externalReferences,
@@ -98,161 +146,97 @@ namespace FlatTagServer
throw new ArgumentNullException(nameof(externalReferences));
}
- if (!externalReferences.TryGetValue(
- ObjectIds.ObjectsFolder,
- out IList? references))
- {
- externalReferences[ObjectIds.ObjectsFolder] = references = [];
- }
+ NodeManagerBuilder builder = CreateFluentBuilder(NamespaceIndexes[0]);
+ await AddPumpAsync(builder, "Pump1", m_options.Values, cancellationToken)
+ .ConfigureAwait(false);
+ await AddPumpAsync(builder, "Pump2", m_options.Pump2Values, cancellationToken)
+ .ConfigureAwait(false);
- ushort namespaceIndex = NamespaceIndexes[0];
- await AddPumpAsync(
- references,
- namespaceIndex,
- "Pump1",
- m_options.Values,
- cancellationToken).ConfigureAwait(false);
- await AddPumpAsync(
- references,
- namespaceIndex,
- "Pump2",
- m_options.Pump2Values,
- cancellationToken).ConfigureAwait(false);
+ // Registering the staged pumps and then re-running the reverse
+ // reference pass is what puts each pump's inverse Organizes
+ // reference into externalReferences[ObjectsFolder]; the manager used
+ // to maintain that entry by hand.
+ await RegisterAuthoredNodesAsync(builder, cancellationToken).ConfigureAwait(false);
+ await CompleteConfigureAsync(externalReferences, cancellationToken)
+ .ConfigureAwait(false);
+ await SealConfigurationAsync(builder, cancellationToken).ConfigureAwait(false);
}
private async ValueTask AddPumpAsync(
- IList externalReferences,
- ushort namespaceIndex,
- string pumpNodeId,
+ INodeManagerBuilder builder,
+ string pumpName,
FlatTagValues values,
CancellationToken cancellationToken)
{
- BaseObjectState pump = CreateObject(
- null,
- namespaceIndex,
- pumpNodeId,
- pumpNodeId,
- ReferenceTypeIds.Organizes);
- pump.AddReference(ReferenceTypeIds.Organizes, true, ObjectIds.ObjectsFolder);
- externalReferences.Add(new NodeStateReference(
- ReferenceTypeIds.Organizes,
- false,
- pump.NodeId));
+ // Unparented, so the staged node organises itself under the Objects
+ // folder and its NodeId comes out as the bare pump name.
+ INodeBuilder pump = AddGroup(builder, pumpName);
+ NodeId pumpId = pump.Node.NodeId;
- BaseObjectState identification = CreateObject(
- pump, namespaceIndex, pumpNodeId + ".Identification", "Identification");
- CreateVariable(
- identification, namespaceIndex, pumpNodeId + ".Identification.Manufacturer", "Manufacturer",
- DataTypeIds.LocalizedText, Variant.From(new LocalizedText(values.Manufacturer)), property: true);
- CreateVariable(
- identification, namespaceIndex, pumpNodeId + ".Identification.SerialNumber", "SerialNumber",
- DataTypeIds.String, Variant.From(values.SerialNumber), property: true);
- CreateVariable(
- identification, namespaceIndex, pumpNodeId + ".Identification.ProductInstanceUri", "ProductInstanceUri",
- DataTypeIds.String, Variant.From(values.ProductInstanceUri), property: true);
+ NodeId identification = AddGroup(builder, "Identification", pumpId).Node.NodeId;
+ AddTag(
+ builder, identification, "Manufacturer",
+ Variant.From(new LocalizedText(values.Manufacturer)), property: true);
+ AddTag(
+ builder, identification, "SerialNumber",
+ Variant.From(values.SerialNumber), property: true);
+ AddTag(
+ builder, identification, "ProductInstanceUri",
+ Variant.From(values.ProductInstanceUri), property: true);
- BaseObjectState operational = CreateObject(
- pump,
- namespaceIndex,
- pumpNodeId + ".Operational",
- "Operational");
- BaseObjectState measurements = CreateObject(
- operational,
- namespaceIndex,
- pumpNodeId + ".Operational.Measurements",
- "Measurements");
- BaseObjectState events = CreateObject(
- pump,
- namespaceIndex,
- pumpNodeId + ".Events",
- "Events");
+ NodeId operational = AddGroup(builder, "Operational", pumpId).Node.NodeId;
+ NodeId measurements = AddGroup(builder, "Measurements", operational).Node.NodeId;
+ INodeBuilder events = AddGroup(builder, "Events", pumpId);
// The pump is the notifier an aggregating server subscribes to. Its
// conditions live under the supervision Objects below it, and OPC
// 10000-3 only delivers their events to a client that can reach a
// notifier, so the pump both carries the bit and is registered as a
// root notifier with the server.
- pump.EventNotifier = EventNotifiers.SubscribeToEvents;
- events.EventNotifier = EventNotifiers.SubscribeToEvents;
- await AddRootNotifierAsync(pump, cancellationToken).ConfigureAwait(false);
+ pump.Node.EventNotifier = EventNotifiers.SubscribeToEvents;
+ events.Node.EventNotifier = EventNotifiers.SubscribeToEvents;
+ await AddRootNotifierAsync(pump.Node, cancellationToken).ConfigureAwait(false);
var signals = new List();
- AddManagementMethods(namespaceIndex, pump, pumpNodeId, signals);
+ AddManagementMethods(builder, pumpId, signals);
if (m_options.SourceNamespaceUri == FlatTagServerOptions.SourceANamespaceUri)
{
- AddSourceAVariables(
- namespaceIndex,
- pumpNodeId,
- measurements,
- events,
- values,
- signals);
+ AddSourceAVariables(builder, measurements, events.Node.NodeId, values, signals);
}
else
{
- AddSourceBVariables(
- namespaceIndex,
- pumpNodeId,
- measurements,
- events,
- values,
- signals);
+ AddSourceBVariables(builder, measurements, events.Node.NodeId, values, signals);
}
-
- await AddPredefinedNodeAsync(SystemContext, pump, cancellationToken)
- .ConfigureAwait(false);
}
private void AddSourceAVariables(
- ushort namespaceIndex,
- string pumpNodeId,
- BaseObjectState measurements,
- BaseObjectState events,
+ INodeManagerBuilder builder,
+ NodeId measurements,
+ NodeId events,
FlatTagValues values,
List signals)
{
- CreateVariable(
- measurements,
- namespaceIndex,
- pumpNodeId + ".Operational.Measurements.DifferentialPressure",
- "DifferentialPressure",
- DataTypeIds.Double,
+ AddTag(
+ builder, measurements, "DifferentialPressure",
Variant.From(values.DifferentialPressure));
- CreateVariable(
- measurements,
- namespaceIndex,
- pumpNodeId + ".Operational.Measurements.FluidTemperature",
- "FluidTemperature",
- DataTypeIds.Double,
+ AddTag(
+ builder, measurements, "FluidTemperature",
Variant.From(values.FluidTemperature));
- CreateVariable(
- measurements,
- namespaceIndex,
- pumpNodeId + ".Operational.Measurements.MassFlow",
- "MassFlow",
- DataTypeIds.Double,
+ AddTag(
+ builder, measurements, "MassFlow",
Variant.From(values.MassFlow));
- CreateVariable(
- measurements,
- namespaceIndex,
- pumpNodeId + ".Operational.Measurements.Level",
- "Level",
- DataTypeIds.Double,
+ AddTag(
+ builder, measurements, "Level",
Variant.From(values.Level));
- BaseObjectState supervision = CreateObject(
- events,
- namespaceIndex,
- pumpNodeId + ".Events.SupervisionProcessFluid",
- "SupervisionProcessFluid");
+ BaseObjectState supervision = AddGroup(
+ builder, "SupervisionProcessFluid", events).Node;
supervision.EventNotifier = EventNotifiers.SubscribeToEvents;
signals.Add(new SupervisionSignal(
SystemContext,
Server.Telemetry,
supervision,
- namespaceIndex,
- pumpNodeId + ".Events.SupervisionProcessFluid.Cavitation",
"Cavitation",
"CavitationAlarm",
severity: 700,
@@ -260,54 +244,32 @@ namespace FlatTagServer
}
private void AddSourceBVariables(
- ushort namespaceIndex,
- string pumpNodeId,
- BaseObjectState measurements,
- BaseObjectState events,
+ INodeManagerBuilder builder,
+ NodeId measurements,
+ NodeId events,
FlatTagValues values,
List signals)
{
- CreateVariable(
- measurements,
- namespaceIndex,
- pumpNodeId + ".Operational.Measurements.BearingTemperature",
- "BearingTemperature",
- DataTypeIds.Double,
+ AddTag(
+ builder, measurements, "BearingTemperature",
Variant.From(values.BearingTemperature));
- CreateVariable(
- measurements,
- namespaceIndex,
- pumpNodeId + ".Operational.Measurements.PumpPowerInput",
- "PumpPowerInput",
- DataTypeIds.Double,
+ AddTag(
+ builder, measurements, "PumpPowerInput",
Variant.From(values.PumpPowerInput));
- CreateVariable(
- measurements,
- namespaceIndex,
- pumpNodeId + ".Operational.Measurements.PumpEfficiency",
- "PumpEfficiency",
- DataTypeIds.Double,
+ AddTag(
+ builder, measurements, "PumpEfficiency",
Variant.From(values.PumpEfficiency));
- CreateVariable(
- measurements,
- namespaceIndex,
- pumpNodeId + ".Operational.Measurements.NumberOfStarts",
- "NumberOfStarts",
- DataTypeIds.UInt32,
+ AddTag(
+ builder, measurements, "NumberOfStarts",
Variant.From(values.NumberOfStarts));
- BaseObjectState supervision = CreateObject(
- events,
- namespaceIndex,
- pumpNodeId + ".Events.SupervisionPumpOperation",
- "SupervisionPumpOperation");
+ BaseObjectState supervision = AddGroup(
+ builder, "SupervisionPumpOperation", events).Node;
supervision.EventNotifier = EventNotifiers.SubscribeToEvents;
signals.Add(new SupervisionSignal(
SystemContext,
Server.Telemetry,
supervision,
- namespaceIndex,
- pumpNodeId + ".Events.SupervisionPumpOperation.MotorOverheat",
"MotorOverheat",
"MotorOverheatAlarm",
severity: 800,
@@ -320,54 +282,34 @@ namespace FlatTagServer
/// also what returns a tripped supervision signal to normal.
///
private static void AddManagementMethods(
- ushort namespaceIndex,
- BaseObjectState pump,
- string pumpNodeId,
+ INodeManagerBuilder builder,
+ NodeId pumpId,
List signals)
{
- var running = new BaseDataVariableState(pump)
- {
- SymbolicName = "Running",
- ReferenceTypeId = ReferenceTypeIds.HasComponent,
- TypeDefinitionId = VariableTypeIds.BaseDataVariableType,
- NodeId = new NodeId(pumpNodeId + ".Running", namespaceIndex),
- BrowseName = new QualifiedName("Running", namespaceIndex),
- DisplayName = new LocalizedText("en", "Running"),
- DataType = DataTypeIds.Boolean,
- ValueRank = ValueRanks.Scalar,
- AccessLevel = AccessLevels.CurrentRead,
- UserAccessLevel = AccessLevels.CurrentRead,
- Historizing = false,
- Value = Variant.From(true),
- StatusCode = StatusCodes.Good,
- Timestamp = DateTime.UtcNow
- };
- pump.AddChild(running);
+ BaseVariableState running = AddTag(
+ builder, pumpId, "Running", Variant.From(true), readBack: false);
- CreateMethod(
- pump,
- namespaceIndex,
- pumpNodeId + ".Start",
+ AddCommand(
+ builder,
+ pumpId,
"Start",
(context, _, _, _, _, _) =>
{
SetRunning(context, running, value: true);
return new ValueTask(ServiceResult.Good);
});
- CreateMethod(
- pump,
- namespaceIndex,
- pumpNodeId + ".Stop",
+ AddCommand(
+ builder,
+ pumpId,
"Stop",
(context, _, _, _, _, _) =>
{
SetRunning(context, running, value: false);
return new ValueTask(ServiceResult.Good);
});
- CreateMethod(
- pump,
- namespaceIndex,
- pumpNodeId + ".Reset",
+ AddCommand(
+ builder,
+ pumpId,
"Reset",
(context, _, _, _, _, _) =>
{
@@ -384,7 +326,7 @@ namespace FlatTagServer
private static void SetRunning(
ISystemContext context,
- BaseDataVariableState running,
+ BaseVariableState running,
bool value)
{
running.Value = Variant.From(value);
@@ -392,84 +334,85 @@ namespace FlatTagServer
running.ClearChangeMasks(context, includeChildren: false);
}
- private static void CreateMethod(
- NodeState parent,
- ushort namespaceIndex,
- string nodeId,
+ ///
+ /// Stages one of the grouping Objects a pump is browsed through.
+ ///
+ ///
+ /// Only the display name is set here: the staged node already carries
+ /// the browse name, the type definition and - through
+ /// - the dotted NodeId this sample publishes. The
+ /// sample tags its display names with a locale, which the fluent
+ /// default does not.
+ ///
+ private static INodeBuilder AddGroup(
+ INodeManagerBuilder builder,
string browseName,
- GenericMethodCalledEventHandler2Async onCall)
+ NodeId parentId = default)
{
- var method = new MethodState(parent)
- {
- SymbolicName = browseName,
- ReferenceTypeId = ReferenceTypeIds.HasComponent,
- NodeId = new NodeId(nodeId, namespaceIndex),
- BrowseName = new QualifiedName(browseName, namespaceIndex),
- DisplayName = new LocalizedText("en", browseName),
- WriteMask = AttributeWriteMask.None,
- UserWriteMask = AttributeWriteMask.None,
- Executable = true,
- UserExecutable = true,
- OnCallMethod2Async = onCall
- };
- parent.AddChild(method);
- }
-
- private static BaseObjectState CreateObject(
- NodeState? parent,
- ushort namespaceIndex,
- string nodeId,
- string browseName,
- NodeId referenceTypeId = default)
- {
- var node = new BaseObjectState(parent)
- {
- SymbolicName = browseName,
- ReferenceTypeId = referenceTypeId.IsNull
- ? ReferenceTypeIds.HasComponent
- : referenceTypeId,
- TypeDefinitionId = ObjectTypeIds.BaseObjectType,
- NodeId = new NodeId(nodeId, namespaceIndex),
- BrowseName = new QualifiedName(browseName, namespaceIndex),
- DisplayName = new LocalizedText("en", browseName),
- WriteMask = AttributeWriteMask.None,
- UserWriteMask = AttributeWriteMask.None,
- EventNotifier = EventNotifiers.None
- };
- parent?.AddChild(node);
+ INodeBuilder node = builder.AddObject(browseName, parentId);
+ node.Node.DisplayName = new LocalizedText("en", browseName);
return node;
}
- private static void CreateVariable(
- NodeState parent,
- ushort namespaceIndex,
- string nodeId,
+ ///
+ /// Stages one flat tag holding a constant value.
+ ///
+ ///
+ /// CLR type the tag carries; the staged variable takes its DataType
+ /// and ValueRank from it.
+ ///
+ /// The fluent builder staging the pump.
+ /// The Object the tag hangs off.
+ /// Browse name of the tag.
+ /// The value the tag reports.
+ ///
+ /// Whether the tag is a Property rather than a data variable.
+ ///
+ ///
+ /// Whether reads are answered from through an
+ /// async callback. Tags the server itself drives - the pump's
+ /// Running flag - pass false so a write from the
+ /// simulation is what a client sees.
+ ///
+ private static BaseVariableState AddTag(
+ INodeManagerBuilder builder,
+ NodeId parentId,
string browseName,
- NodeId dataType,
Variant value,
- bool property = false)
+ bool property = false,
+ bool readBack = true)
{
- var variable = new BaseDataVariableState(parent)
+ IVariableBuilder tag = builder.AddVariable(browseName, parentId);
+ BaseVariableState node = tag.Node;
+ node.DisplayName = new LocalizedText("en", browseName);
+ if (property)
{
- SymbolicName = browseName,
- ReferenceTypeId = property ? ReferenceTypeIds.HasProperty : ReferenceTypeIds.HasComponent,
- TypeDefinitionId = property ? VariableTypeIds.PropertyType : VariableTypeIds.BaseDataVariableType,
- NodeId = new NodeId(nodeId, namespaceIndex),
- BrowseName = new QualifiedName(browseName, namespaceIndex),
- DisplayName = new LocalizedText("en", browseName),
- WriteMask = AttributeWriteMask.None,
- UserWriteMask = AttributeWriteMask.None,
- DataType = dataType,
- ValueRank = ValueRanks.Scalar,
- AccessLevel = AccessLevels.CurrentRead,
- UserAccessLevel = AccessLevels.CurrentRead,
- Historizing = false,
- Value = value,
- StatusCode = StatusCodes.Good,
- Timestamp = DateTime.UtcNow,
- OnSimpleReadValueAsync = (_, _, ct) => ReadValueAsync(value, ct)
- };
- parent.AddChild(variable);
+ node.ReferenceTypeId = ReferenceTypeIds.HasProperty;
+ node.TypeDefinitionId = VariableTypeIds.PropertyType;
+ }
+ node.Value = value;
+ node.StatusCode = StatusCodes.Good;
+ node.Timestamp = DateTime.UtcNow;
+ if (readBack)
+ {
+ tag.OnRead((_, _, ct) => ReadValueAsync(value, ct));
+ }
+ return node;
+ }
+
+ ///
+ /// Stages one of the argument-less Methods an operator manages the
+ /// pump with.
+ ///
+ private static void AddCommand(
+ INodeManagerBuilder builder,
+ NodeId parentId,
+ string browseName,
+ GenericMethodCalledEventHandler2Async onCall)
+ {
+ INodeBuilder method = builder.AddMethod(browseName, parentId);
+ method.Node.DisplayName = new LocalizedText("en", browseName);
+ method.OnCall(onCall);
}
private static async ValueTask ReadValueAsync(
diff --git a/samples/WotCon/FlatTagServer/SupervisionSignal.cs b/samples/WotCon/FlatTagServer/SupervisionSignal.cs
index 69df0bf607..be1a8ed11d 100644
--- a/samples/WotCon/FlatTagServer/SupervisionSignal.cs
+++ b/samples/WotCon/FlatTagServer/SupervisionSignal.cs
@@ -60,9 +60,11 @@ namespace FlatTagServer
///
/// The system context used to create the nodes.
/// The telemetry context of the owning server.
- /// The supervision Object that owns the signal.
- /// The namespace the nodes belong to.
- /// The node identifier of the boolean tag.
+ ///
+ /// The supervision Object that owns the signal. Both nodes take their
+ /// namespace and the leading part of their identifier from it, so the
+ /// signal follows wherever the supervision Object was staged.
+ ///
/// The browse name of the boolean tag.
/// The browse name of the alarm condition.
/// The severity reported while the alarm is active.
@@ -74,8 +76,6 @@ namespace FlatTagServer
ISystemContext context,
ITelemetryContext telemetry,
BaseObjectState parent,
- ushort namespaceIndex,
- string tagPath,
string name,
string conditionName,
ushort severity,
@@ -86,6 +86,9 @@ namespace FlatTagServer
throw new ArgumentNullException(nameof(parent));
}
+ ushort namespaceIndex = parent.NodeId.NamespaceIndex;
+ string tagPath = parent.NodeId.IdentifierAsString + "." + name;
+
m_severity = severity;
m_active = initiallyActive;