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;