docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes project bookkeeping IDs (task/tracking refs) from shipped code comments, so the docs read cleanly and CommentChecker is quiet except for known false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc). Doc/comment-only; no logic changed; solution builds clean.
This commit is contained in:
@@ -20,7 +20,7 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
/// tear-down + rebuild.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// This is the side-effecting layer Task 47 deferred to F14. It stays pure-of-SDK so
|
||||
/// This is the side-effecting layer deferred to F14. It stays pure-of-SDK so
|
||||
/// production binds a real SDK sink, dev/Mac binds <see cref="NullOpcUaAddressSpaceSink"/>,
|
||||
/// and tests can capture every call.
|
||||
/// </summary>
|
||||
@@ -101,7 +101,7 @@ public sealed class AddressSpaceApplier
|
||||
plan.ChangedEquipment.Count + plan.ChangedDrivers.Count + plan.ChangedAlarms.Count +
|
||||
plan.ChangedEquipmentTags.Count +
|
||||
plan.ChangedEquipmentVirtualTags.Count +
|
||||
// OpcUaServer-001: a UNS Area/Line rename is an in-place change to an existing folder node.
|
||||
// A UNS Area/Line rename is an in-place change to an existing folder node.
|
||||
plan.RenamedFolders.Count;
|
||||
var addedCount =
|
||||
plan.AddedEquipment.Count + plan.AddedDrivers.Count + plan.AddedAlarms.Count +
|
||||
@@ -122,7 +122,7 @@ public sealed class AddressSpaceApplier
|
||||
// respawn (DriverHostActor → ApplyVirtualTags), so it skips the rebuild and PRESERVES every
|
||||
// client's server-wide subscriptions. Any structural / node-affecting vtag change (Name/
|
||||
// FolderPath/DataType) — or any non-vtag change anywhere — still forces a full rebuild.
|
||||
// F10b + FB-7 (surgical tag write): a CHANGED equipment tag whose ONLY differences are Writable /
|
||||
// F10b (surgical tag write): a CHANGED equipment tag whose ONLY differences are Writable /
|
||||
// IsHistorized / HistorianTagname / DataType / IsArray / ArrayLength (a plain value variable — no
|
||||
// alarm condition node) can be updated IN PLACE on the existing node via
|
||||
// ISurgicalAddressSpaceSink.UpdateTagAttributes (see TagDeltaIsSurgicalEligible), avoiding the full
|
||||
@@ -140,7 +140,7 @@ public sealed class AddressSpaceApplier
|
||||
plan.ChangedEquipmentVirtualTags.Any(d => !VtagDeltaIsNodeIrrelevant(d));
|
||||
|
||||
var surgicalTagDeltas = plan.ChangedEquipmentTags.Where(TagDeltaIsSurgicalEligible).ToList();
|
||||
// OpcUaServer-001 — UNS Area / Line renames are surgically applicable (in-place DisplayName swap)
|
||||
// UNS Area / Line renames are surgically applicable (in-place DisplayName swap)
|
||||
// when no structural rebuild fires. When a rebuild DOES fire (any add/remove/structural change),
|
||||
// MaterialiseHierarchy re-creates every folder with the new display names, so the renames are
|
||||
// covered for free and need no separate surgical pass.
|
||||
@@ -383,7 +383,7 @@ public sealed class AddressSpaceApplier
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #85 — build the UNS Area/Line/Equipment folder hierarchy in the address space from a
|
||||
/// Build the UNS Area/Line/Equipment folder hierarchy in the address space from a
|
||||
/// composition snapshot. Called by <c>OpcUaPublishActor</c> after a rebuild so OPC UA
|
||||
/// clients browsing the server see proper folder structure instead of flat tag ids.
|
||||
/// Idempotent: each <c>EnsureFolder</c> call returns the existing folder if already
|
||||
@@ -420,19 +420,13 @@ public sealed class AddressSpaceApplier
|
||||
/// ensure its optional <c>FolderPath</c> sub-folder under the existing equipment folder, then
|
||||
/// ensure a Variable (NodeId = <c>FullName</c>, the driver-side ref) inside it. Variables
|
||||
/// start BadWaitingForInitialData; the driver fills live values in a later milestone.
|
||||
/// Idempotent.
|
||||
/// <para>
|
||||
/// <b>Task 0 architecture decisions (recorded per the equipment-namespace-structure
|
||||
/// plan).</b> Decision #1 = <b>A</b> — a sink-based pass, NOT a reuse of
|
||||
/// <c>EquipmentNodeWalker</c>: no sink-backed <c>IAddressSpaceBuilder</c> adapter exists
|
||||
/// (<c>GenericDriverNodeManager.CapturingBuilder</c> decorates another builder, not the
|
||||
/// sink), and the walker re-creates the whole Area/Line/Equipment tree with browse-path
|
||||
/// NodeIds — incompatible with this path's logical-Id NodeIds (decision #3) and the
|
||||
/// already-materialised equipment folders (decision #4). Decision #4 = this pass adds
|
||||
/// ONLY variables (and any per-tag sub-folder); <see cref="MaterialiseHierarchy"/> owns
|
||||
/// the equipment folders and this pass never re-creates them. The sink's
|
||||
/// <c>EnsureVariable</c> takes a plain <c>string dataType</c> (not a DriverAttributeInfo).
|
||||
/// </para>
|
||||
/// Idempotent. This is a sink-based pass, NOT a reuse of <c>EquipmentNodeWalker</c>: no
|
||||
/// sink-backed <c>IAddressSpaceBuilder</c> adapter exists, and the walker re-creates the
|
||||
/// whole Area/Line/Equipment tree with browse-path NodeIds, incompatible with this path's
|
||||
/// logical-Id NodeIds and the already-materialised equipment folders. This pass adds ONLY
|
||||
/// variables (and any per-tag sub-folder); <see cref="MaterialiseHierarchy"/> owns the
|
||||
/// equipment folders and this pass never re-creates them. The sink's <c>EnsureVariable</c>
|
||||
/// takes a plain <c>string dataType</c> (not a DriverAttributeInfo).
|
||||
/// </summary>
|
||||
/// <param name="composition">The composition result containing the equipment tags to materialise.</param>
|
||||
public void MaterialiseEquipmentTags(AddressSpaceComposition composition)
|
||||
@@ -442,7 +436,7 @@ public sealed class AddressSpaceApplier
|
||||
|
||||
// Sub-folders first — a tag's FolderPath becomes one folder UNDER its equipment folder
|
||||
// (deduped per distinct equipment+path). Tags with no FolderPath hang directly under the
|
||||
// equipment folder, which MaterialiseHierarchy already created (decision #4: never re-create
|
||||
// equipment folder, which MaterialiseHierarchy already created (never re-create
|
||||
// the equipment folder here).
|
||||
var foldersCreated = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var tag in composition.EquipmentTags)
|
||||
@@ -641,7 +635,7 @@ public sealed class AddressSpaceApplier
|
||||
Historize = d.Current.Historize,
|
||||
}).Equals(d.Current);
|
||||
|
||||
// F10b + FB-7: a CHANGED equipment tag whose ONLY differences are Writable / IsHistorized /
|
||||
// F10b: a CHANGED equipment tag whose ONLY differences are Writable / IsHistorized /
|
||||
// HistorianTagname / DataType / IsArray / ArrayLength (a plain value variable — no alarm condition node)
|
||||
// can be updated IN PLACE on the existing node via ISurgicalAddressSpaceSink.UpdateTagAttributes,
|
||||
// avoiding a full rebuild (preserving subscriptions). The presentation-shape fields (DataType / IsArray /
|
||||
@@ -650,7 +644,7 @@ public sealed class AddressSpaceApplier
|
||||
// differences still fall through to a rebuild — FullName/DriverInstanceId re-route the node to a different
|
||||
// driver point, Name re-derives the NodeId, and an alarm flip turns the node into a Part 9 condition. The
|
||||
// override-unequal default also covers any future field.
|
||||
// REACH (live-verified FB-7): the shape path only fires for drivers whose TagConfig carries a stable
|
||||
// REACH (live-verified): the shape path only fires for drivers whose TagConfig carries a stable
|
||||
// top-level "FullName" (Galaxy = tag_name.AttributeName; OpcUaClient = the node id) — there a DataType/array
|
||||
// edit leaves FullName untouched ⇒ surgical. For structured-TagConfig protocol drivers (Modbus/S7/AbCip/…)
|
||||
// AddressSpaceComposer.ExtractTagFullName falls back to the RAW TagConfig blob as FullName, so a DataType/
|
||||
|
||||
@@ -171,11 +171,11 @@ public sealed record EquipmentVirtualTagPlan(
|
||||
IReadOnlyList<string> DependencyRefs,
|
||||
bool Historize = false)
|
||||
{
|
||||
/// <summary>Structural equality: the auto-generated record equality would compare
|
||||
/// <see cref="DependencyRefs"/> (an interface-typed list) BY REFERENCE, flagging every
|
||||
/// VirtualTag as "changed" on every parse (fresh list instances). Compare it element-wise
|
||||
/// so a no-op redeploy diffs empty. <see cref="Historize"/> is included so a Historize-only
|
||||
/// toggle is detected as a change.</summary>
|
||||
/// <inheritdoc />
|
||||
// Structural equality: the auto-generated record equality would compare DependencyRefs (an
|
||||
// interface-typed list) BY REFERENCE, flagging every VirtualTag as "changed" on every parse
|
||||
// (fresh list instances). Compare it element-wise so a no-op redeploy diffs empty. Historize
|
||||
// is included so a Historize-only toggle is detected as a change.
|
||||
public bool Equals(EquipmentVirtualTagPlan? other) =>
|
||||
other is not null &&
|
||||
VirtualTagId == other.VirtualTagId &&
|
||||
@@ -187,6 +187,7 @@ public sealed record EquipmentVirtualTagPlan(
|
||||
Historize == other.Historize &&
|
||||
DependencyRefs.SequenceEqual(other.DependencyRefs, StringComparer.Ordinal);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = new HashCode();
|
||||
@@ -238,17 +239,15 @@ public sealed record EquipmentScriptedAlarmPlan(
|
||||
bool Retain,
|
||||
bool Enabled)
|
||||
{
|
||||
/// <summary>Structural equality: the auto-generated record equality would compare
|
||||
/// <see cref="DependencyRefs"/> (an interface-typed list) BY REFERENCE, flagging every alarm as
|
||||
/// "changed" on every parse (fresh list instances). Compare it element-wise so a no-op redeploy
|
||||
/// diffs empty (mirrors <see cref="EquipmentVirtualTagPlan"/>).</summary>
|
||||
/// <remarks>
|
||||
/// <b>DependencyRefs equality is order-sensitive</b> (SequenceEqual).
|
||||
/// <see cref="EquipmentScriptPaths.ExtractAlarmDependencyRefs"/> is the canonical, deterministic
|
||||
/// producer of that order (predicate <c>ctx.GetTag</c> reads first, then first-seen message
|
||||
/// template tokens). Downstream byte-parity between the live composer and the artifact-decode
|
||||
/// mirror depends on both sides calling <c>ExtractAlarmDependencyRefs</c> with identical inputs.
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
// Structural equality: the auto-generated record equality would compare DependencyRefs (an
|
||||
// interface-typed list) BY REFERENCE, flagging every alarm as "changed" on every parse (fresh
|
||||
// list instances). Compare it element-wise so a no-op redeploy diffs empty (mirrors
|
||||
// EquipmentVirtualTagPlan). DependencyRefs equality is order-sensitive (SequenceEqual).
|
||||
// EquipmentScriptPaths.ExtractAlarmDependencyRefs is the canonical, deterministic producer of
|
||||
// that order (predicate ctx.GetTag reads first, then first-seen message template tokens).
|
||||
// Downstream byte-parity between the live composer and the artifact-decode mirror depends on
|
||||
// both sides calling ExtractAlarmDependencyRefs with identical inputs.
|
||||
public bool Equals(EquipmentScriptedAlarmPlan? other) =>
|
||||
other is not null &&
|
||||
ScriptedAlarmId == other.ScriptedAlarmId &&
|
||||
@@ -280,9 +279,9 @@ public sealed record EquipmentScriptedAlarmPlan(
|
||||
/// <summary>
|
||||
/// Pure composer that flattens the live-edit DB tables into the address-space build plan a
|
||||
/// driver-role host needs. Same inputs → same outputs, no logging, no DB writes. The driver-role
|
||||
/// startup (Task 53) consumes the result and hands it to the node-manager factory.
|
||||
/// startup consumes the result and hands it to the node-manager factory.
|
||||
///
|
||||
/// #85 — the composer now carries UNS topology (<see cref="UnsAreaProjection"/> +
|
||||
/// The composer also carries UNS topology (<see cref="UnsAreaProjection"/> +
|
||||
/// <see cref="UnsLineProjection"/>) so <c>AddressSpaceApplier</c> can build the
|
||||
/// <c>Area/Line/Equipment</c> folder hierarchy in the SDK's address space. The legacy
|
||||
/// <c>EquipmentNodeWalker</c> integration that did this server-side is fully replaced by the
|
||||
@@ -597,6 +596,8 @@ public static class AddressSpaceComposer
|
||||
/// <summary>Parses the optional <c>alarm</c> object from a tag's <c>TagConfig</c> JSON. Returns null
|
||||
/// when absent, non-object, or non-JSON (the tag is then a plain variable). Never throws. The
|
||||
/// artifact-decode side (<c>DeploymentArtifact.ExtractTagAlarm</c>) MUST parse identically (byte-parity).</summary>
|
||||
/// <param name="tagConfig">The tag's raw <c>TagConfig</c> JSON (nullable/blank tolerated).</param>
|
||||
/// <returns>The parsed native-alarm intent, or <see langword="null"/> when the tag has no alarm object.</returns>
|
||||
internal static EquipmentTagAlarmInfo? ExtractTagAlarm(string? tagConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return null;
|
||||
@@ -627,6 +628,8 @@ public static class AddressSpaceComposer
|
||||
/// resolved later). The raw string value is used — not trimmed — matching <c>ExtractTagFullName</c> /
|
||||
/// <c>ExtractTagAlarm</c>. Never throws. The artifact-decode side
|
||||
/// (<c>DeploymentArtifact.ExtractTagHistorize</c>) MUST parse identically (byte-parity).</summary>
|
||||
/// <param name="tagConfig">The tag's raw <c>TagConfig</c> JSON (nullable/blank tolerated).</param>
|
||||
/// <returns>The parsed historize flag and optional historian tagname override.</returns>
|
||||
internal static (bool IsHistorized, string? HistorianTagname) ExtractTagHistorize(string? tagConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null);
|
||||
@@ -657,6 +660,8 @@ public static class AddressSpaceComposer
|
||||
/// <see cref="ExtractTagHistorize"/> exactly in structure + null/blank/non-object/malformed-JSON
|
||||
/// tolerance. Never throws. The artifact-decode side
|
||||
/// (<c>DeploymentArtifact.ExtractTagArray</c>) MUST parse identically (byte-parity).</summary>
|
||||
/// <param name="tagConfig">The tag's raw <c>TagConfig</c> JSON (nullable/blank tolerated).</param>
|
||||
/// <returns>The parsed array flag and optional array length.</returns>
|
||||
internal static (bool IsArray, uint? ArrayLength) ExtractTagArray(string? tagConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null);
|
||||
|
||||
@@ -53,7 +53,7 @@ public sealed record AddressSpacePlan(
|
||||
public IReadOnlyList<EquipmentVirtualTagDelta> ChangedEquipmentVirtualTags { get; init; } = Array.Empty<EquipmentVirtualTagDelta>();
|
||||
|
||||
/// <summary>
|
||||
/// OpcUaServer-001 — UNS Area / Line folder renames: a folder whose stable id is unchanged but
|
||||
/// UNS Area / Line folder renames: a folder whose stable id is unchanged but
|
||||
/// whose <c>DisplayName</c> differs between the previous + next composition. A deploy whose ONLY
|
||||
/// change is an Area or Line rename produces NO Equipment / Driver / Alarm / Tag / VirtualTag
|
||||
/// delta, so without this set the plan would be <see cref="IsEmpty"/> and
|
||||
@@ -84,7 +84,7 @@ public sealed record AddressSpacePlan(
|
||||
public sealed record EquipmentTagDelta(EquipmentTagPlan Previous, EquipmentTagPlan Current);
|
||||
public sealed record EquipmentVirtualTagDelta(EquipmentVirtualTagPlan Previous, EquipmentVirtualTagPlan Current);
|
||||
|
||||
/// <summary>OpcUaServer-001 — one renamed UNS Area / Line folder: the stable folder
|
||||
/// <summary>One renamed UNS Area / Line folder: the stable folder
|
||||
/// <paramref name="FolderNodeId"/> (the area's <c>UnsAreaId</c> or line's <c>UnsLineId</c>, the exact
|
||||
/// NodeId <c>MaterialiseHierarchy</c> placed the folder at) and the <paramref name="NewDisplayName"/>
|
||||
/// to apply in place.</summary>
|
||||
@@ -103,6 +103,7 @@ public static class AddressSpacePlanner
|
||||
/// </summary>
|
||||
/// <param name="previous">The previous composition result.</param>
|
||||
/// <param name="next">The new composition result.</param>
|
||||
/// <returns>The diff plan between the two compositions.</returns>
|
||||
public static AddressSpacePlan Compute(AddressSpaceComposition previous, AddressSpaceComposition next)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(previous);
|
||||
@@ -138,7 +139,7 @@ public static class AddressSpacePlanner
|
||||
t => t.VirtualTagId,
|
||||
(a, b) => new AddressSpacePlan.EquipmentVirtualTagDelta(a, b));
|
||||
|
||||
// OpcUaServer-001 — UNS Area / Line renames: a folder whose stable id is unchanged but whose
|
||||
// UNS Area / Line renames: a folder whose stable id is unchanged but whose
|
||||
// DisplayName differs. Diffed by stable id (UnsAreaId / UnsLineId) so an Area/Line whose ONLY
|
||||
// change is its friendly name is no longer a silent no-op at the IsEmpty gate. The folder NodeId
|
||||
// IS the area/line id (the exact scheme MaterialiseHierarchy uses), so the rename carries it
|
||||
@@ -163,7 +164,7 @@ public static class AddressSpacePlanner
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OpcUaServer-001 — emit a <see cref="AddressSpacePlan.FolderRename"/> for every folder present in
|
||||
/// Emit a <see cref="AddressSpacePlan.FolderRename"/> for every folder present in
|
||||
/// BOTH snapshots (matched by stable <paramref name="identity"/>) whose <paramref name="displayName"/>
|
||||
/// differs (ordinal). Added/removed folders are NOT renames — they're handled by the equipment /
|
||||
/// hierarchy rebuild path — so this pass only flags an in-place display-name change on a surviving
|
||||
|
||||
@@ -122,6 +122,7 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
||||
/// <summary>Starts the OPC UA application and server.</summary>
|
||||
/// <param name="server">The standard server instance to start.</param>
|
||||
/// <param name="cancellationToken">A cancellation token for the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task StartAsync(StandardServer server, CancellationToken cancellationToken)
|
||||
{
|
||||
_server = server;
|
||||
@@ -377,6 +378,7 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
||||
/// listening endpoints — the misconfiguration is logged and very visible.
|
||||
/// </summary>
|
||||
/// <param name="profiles">The security profiles to build policies for.</param>
|
||||
/// <returns>The SDK security policies for the requested profiles.</returns>
|
||||
internal static IEnumerable<ServerSecurityPolicy> BuildSecurityPolicies(IEnumerable<OpcUaSecurityProfile> profiles)
|
||||
{
|
||||
var seen = new HashSet<OpcUaSecurityProfile>();
|
||||
@@ -424,6 +426,7 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
||||
/// project supplies the LDAP adapter, and <see cref="NullOpcUaUserAuthenticator"/> denies all
|
||||
/// UserName logins when none is registered.
|
||||
/// </summary>
|
||||
/// <returns>The Anonymous and UserName token policies.</returns>
|
||||
internal static IEnumerable<UserTokenPolicy> BuildUserTokenPolicies()
|
||||
{
|
||||
yield return new UserTokenPolicy(UserTokenType.Anonymous)
|
||||
@@ -439,6 +442,7 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
|
||||
}
|
||||
|
||||
/// <summary>Disposes the application host and cleans up resources.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_impersonateHandler is not null && _server?.CurrentInstance?.SessionManager is { } sessionManager)
|
||||
|
||||
@@ -58,7 +58,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// Keyed by NodeId → the actual <see cref="FolderState"/> so <see cref="RebuildAddressSpace"/> can
|
||||
/// pass the folder to <c>RemoveRootNotifier</c> on teardown.</summary>
|
||||
private readonly Dictionary<NodeId, FolderState> _notifierFolders = new();
|
||||
/// <summary>Phase C (Task 4): event-notifier folder NodeId-identifier → the event-history source
|
||||
/// <summary>Phase C: event-notifier folder NodeId-identifier → the event-history source
|
||||
/// name passed to <see cref="IHistorianDataSource.ReadEventsAsync"/>. The equipment-folder NodeId
|
||||
/// identifier IS the equipment id, which IS the sourceName, so key and value are the same string;
|
||||
/// the map's presence (not its value) is what makes a folder an event-history source. Populated by
|
||||
@@ -168,7 +168,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// this source — so a single registered historian (e.g. Wonderware) serves many drivers' nodes,
|
||||
/// independent of any driver's lifecycle.
|
||||
/// <para>
|
||||
/// Set by the Host at <c>StartAsync</c> (Task 5). The <see cref="NullHistorianDataSource"/>
|
||||
/// Set by the Host at <c>StartAsync</c>. The <see cref="NullHistorianDataSource"/>
|
||||
/// default (assigning <c>null</c> restores it) means "no historian wired" → every read
|
||||
/// returns empty, so a historized node's HistoryRead surfaces <c>GoodNoData</c> rather than
|
||||
/// faulting. Backed by a <c>volatile</c> field (auto-properties can't be volatile) to make
|
||||
@@ -261,7 +261,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(nodeId);
|
||||
EnsureAddressSpaceCreated(); // OpcUaServer-004: fail legibly if called before the server started.
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
@@ -297,7 +297,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(alarmNodeId);
|
||||
ArgumentNullException.ThrowIfNull(state);
|
||||
EnsureAddressSpaceCreated(); // OpcUaServer-004: fail legibly if called before the server started.
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
// Look up + project under a SINGLE Lock so a concurrent RebuildAddressSpace can't clear
|
||||
// _alarmConditions / detach the condition node between the lookup and the Set* calls.
|
||||
@@ -573,6 +573,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <param name="displayName">Human-readable condition name (BrowseName / DisplayName / Message / ConditionName).</param>
|
||||
/// <param name="alarmType">Domain alarm type — maps to the SDK condition subtype (see remarks).</param>
|
||||
/// <param name="severity">Domain severity (treated as an OPC UA 1..1000 severity); mapped to <see cref="EventSeverity"/>.</param>
|
||||
/// <param name="isNative">True when the condition is a NATIVE (driver-fed, e.g. Galaxy) alarm rather than a scripted one.</param>
|
||||
/// <remarks>
|
||||
/// <para><b>AlarmType → SDK subtype mapping.</b> Script-driven alarms have no OPC limit /
|
||||
/// setpoint values, so any limit-style subtype would have unset limit children. We therefore
|
||||
@@ -586,7 +587,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(alarmNodeId);
|
||||
ArgumentException.ThrowIfNullOrEmpty(displayName);
|
||||
EnsureAddressSpaceCreated(); // OpcUaServer-004: fail legibly if called before the server started.
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
@@ -693,7 +694,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// (ScriptedAlarmEngine.EnableAsync/DisableAsync, dispatched by ScriptedAlarmHostActor on the
|
||||
// "Enable"/"Disable" AlarmCommand operations), so a scripted condition routes through the same
|
||||
// AlarmAck-gated HandleAlarmCommand as the other handlers. NATIVE (driver-fed) conditions have no
|
||||
// engine enable/disable surface (Phase 3 decision #2) — they short-circuit to BadNotSupported.
|
||||
// engine enable/disable surface — they short-circuit to BadNotSupported.
|
||||
alarm.OnEnableDisable = (context, condition, enabling) =>
|
||||
{
|
||||
if (IsNativeAlarmNode(alarmNodeId))
|
||||
@@ -720,6 +721,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// (driver-fed) alarm rather than a scripted one. A later task uses this to route a native condition's
|
||||
/// inbound Acknowledge to the driver instead of the scripted engine.</summary>
|
||||
/// <param name="alarmNodeId">The alarm condition node id.</param>
|
||||
/// <returns>True when the condition is native (driver-fed); false when it is scripted or not found.</returns>
|
||||
internal bool IsNativeAlarmNode(string alarmNodeId)
|
||||
{
|
||||
// _nativeAlarmNodeIds is a plain HashSet mutated only under Lock (in MaterialiseAlarmCondition /
|
||||
@@ -811,7 +813,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="NodeValueEventHandler"/> attached to a writable equipment-tag variable by
|
||||
/// <see cref="EnsureVariable"/> (Task 11). The OPC UA SDK invokes it when a client writes the
|
||||
/// <see cref="EnsureVariable"/>. The OPC UA SDK invokes it when a client writes the
|
||||
/// node's Value. It resolves the calling principal off the SDK <paramref name="context"/> the
|
||||
/// SAME way <see cref="HandleAlarmCommand"/> does, gates on the
|
||||
/// <see cref="OpcUaDataPlaneRoles.WriteOperate"/> role + the gateway being wired
|
||||
@@ -1230,7 +1232,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// register it as a root notifier (idempotent — guarded by <see cref="_notifierFolders"/>) so the
|
||||
/// alarm condition has a notifier path to the Server object for T16's event propagation.</summary>
|
||||
/// <remarks>
|
||||
/// Phase C (Task 4): when a real historian is wired at promotion time (the source is NOT the
|
||||
/// Phase C: when a real historian is wired at promotion time (the source is NOT the
|
||||
/// <see cref="NullHistorianDataSource"/>), the folder ALSO gets the
|
||||
/// <see cref="EventNotifiers.HistoryRead"/> bit OR-ed in (keeping SubscribeToEvents) and registers
|
||||
/// its NodeId identifier as an event-history source so the <see cref="HistoryReadEvents"/> override
|
||||
@@ -1272,7 +1274,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <summary>
|
||||
/// Ensure a folder node exists at <paramref name="folderNodeId"/> with the given display
|
||||
/// name, parented under <paramref name="parentNodeId"/> (or the namespace root when null).
|
||||
/// #85 — used by <see cref="AddressSpaceApplier"/> to materialise the UNS Area/Line/Equipment
|
||||
/// Used by <see cref="AddressSpaceApplier"/> to materialise the UNS Area/Line/Equipment
|
||||
/// folder hierarchy. Idempotent: the second call with the same id returns the cached
|
||||
/// folder so adding child variables under it still works.
|
||||
/// </summary>
|
||||
@@ -1283,7 +1285,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(folderNodeId);
|
||||
ArgumentException.ThrowIfNullOrEmpty(displayName);
|
||||
EnsureAddressSpaceCreated(); // OpcUaServer-004: fail legibly if called before the server started.
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
if (_folders.ContainsKey(folderNodeId)) return;
|
||||
|
||||
@@ -1310,7 +1312,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <summary>
|
||||
/// Update an EXISTING folder node's <see cref="FolderState.DisplayName"/> in place and notify
|
||||
/// subscribers, WITHOUT a rebuild — the in-place counterpart of <see cref="EnsureFolder"/> for a
|
||||
/// UNS Area / Line rename (OpcUaServer-001). <see cref="EnsureFolder"/> early-returns for an
|
||||
/// UNS Area / Line rename. <see cref="EnsureFolder"/> early-returns for an
|
||||
/// already-present folder id and never touches an existing folder's display name, so a
|
||||
/// rename-only deploy would otherwise be invisible until a full <see cref="RebuildAddressSpace"/>
|
||||
/// cleared <c>_folders</c>. The NodeId + BrowseName are unchanged (a rename only touches the
|
||||
@@ -1351,7 +1353,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <param name="dataType">The OPC UA data type name (e.g., "Boolean", "Int32", "String").</param>
|
||||
/// <param name="writable">When true the node is created <c>CurrentReadWrite</c> (an authored
|
||||
/// ReadWrite equipment tag) and the inbound-write handler <see cref="OnEquipmentTagWrite"/> is attached
|
||||
/// to its <c>OnWriteValue</c> (Task 11) so a client write gates on the <c>WriteOperate</c> role + routes
|
||||
/// to its <c>OnWriteValue</c> so a client write gates on the <c>WriteOperate</c> role + routes
|
||||
/// to the backing driver; when false it stays <c>CurrentRead</c> (read-only) with no write handler.</param>
|
||||
/// <param name="historianTagname">Phase C: null ⇒ the node is NOT historized (Historizing=false, no
|
||||
/// HistoryRead bit, not registered). Non-null ⇒ the node is created <c>Historizing</c> with the
|
||||
@@ -1368,7 +1370,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(variableNodeId);
|
||||
ArgumentException.ThrowIfNullOrEmpty(displayName);
|
||||
EnsureAddressSpaceCreated(); // OpcUaServer-004: fail legibly if called before the server started.
|
||||
EnsureAddressSpaceCreated();
|
||||
|
||||
// If already present, leave it alone (idempotent re-applies).
|
||||
if (_variables.ContainsKey(variableNodeId)) return;
|
||||
@@ -1401,7 +1403,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
StatusCode = StatusCodes.BadWaitingForInitialData,
|
||||
Timestamp = DateTime.MinValue,
|
||||
};
|
||||
// Task 11: a writable equipment tag owns an inbound-write handler. The SDK invokes
|
||||
// A writable equipment tag owns an inbound-write handler. The SDK invokes
|
||||
// OnWriteValue on a client write; it gates on the WriteOperate role and routes to the backing
|
||||
// driver via NodeWriteGateway. Read-only nodes leave the handler unattached so a write is
|
||||
// rejected by the SDK's own AccessLevel check before it ever reaches a handler.
|
||||
@@ -1437,7 +1439,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// in place and notify subscribers, WITHOUT a rebuild — so client MonitoredItems on the node survive.
|
||||
/// Returns false when the node id is unknown (caller falls back to a full rebuild).
|
||||
/// <para>
|
||||
/// FB-7: when the shape (DataType / ValueRank / ArrayDimensions) ACTUALLY changes, two extra things
|
||||
/// When the shape (DataType / ValueRank / ArrayDimensions) ACTUALLY changes, two extra things
|
||||
/// happen. (1) The node's value is reset to <c>BadWaitingForInitialData</c> (Value=null) so no value
|
||||
/// still typed to the OLD DataType is exposed between this update and the driver's next publish — the
|
||||
/// same fresh-node state <see cref="EnsureVariable"/> creates, which closes the "brief value-type
|
||||
@@ -1476,7 +1478,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
if (historized) _historizedTagnames[variableNodeId] = historianTagname!;
|
||||
else _historizedTagnames.TryRemove(variableNodeId, out _);
|
||||
|
||||
// FB-7: swap DataType + ValueRank + ArrayDimensions in place, but only when they actually differ
|
||||
// Swap DataType + ValueRank + ArrayDimensions in place, but only when they actually differ
|
||||
// from the live node — a Writable/Historizing-only change leaves the shape untouched (no value
|
||||
// reset, no model-change event), preserving the original surgical behaviour byte-for-byte.
|
||||
var newDataType = ResolveBuiltInDataType(dataType);
|
||||
@@ -1727,7 +1729,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// Drop the notifier-folder guard so re-materialised alarms re-promote their (rebuilt)
|
||||
// equipment folders to event notifiers.
|
||||
_notifierFolders.Clear();
|
||||
// Phase C (Task 4): drop the event-history source registrations alongside the notifier folders
|
||||
// Phase C: drop the event-history source registrations alongside the notifier folders
|
||||
// they map; re-materialised alarms re-register them (with the HistoryRead bit) on re-promotion.
|
||||
_eventNotifierSources.Clear();
|
||||
}
|
||||
@@ -1740,7 +1742,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
return _folders.TryGetValue(parentNodeId, out var existing) ? existing : _root!;
|
||||
}
|
||||
|
||||
/// <summary>OpcUaServer-004: guard the address-space mutators against a too-early call. <c>_root</c>
|
||||
/// <summary>Guard the address-space mutators against a too-early call. <c>_root</c>
|
||||
/// is only assigned in <see cref="CreateAddressSpace"/>, which the SDK invokes during
|
||||
/// <c>StandardServer</c> start; every public mutator (<see cref="WriteValue"/>,
|
||||
/// <see cref="WriteAlarmCondition"/>, <see cref="EnsureFolder"/>, <see cref="EnsureVariable"/>,
|
||||
@@ -1766,7 +1768,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// that carry the HistoryRead access bit), validates the timestamp args, handles
|
||||
// `releaseContinuationPoints`, and dispatches by `details` runtime type to the per-details
|
||||
// protected virtuals below. We override all four arms: the three variable-history virtuals
|
||||
// (Raw/Processed/AtTime) and the event-history arm (HistoryReadEvents, Task 4). Each override
|
||||
// (Raw/Processed/AtTime) and the event-history arm (HistoryReadEvents). Each override
|
||||
// receives the pre-filtered handles and fills results[handle.Index] / errors[handle.Index] —
|
||||
// handle.Index is the original index into the service-level results/errors lists, seeded by the
|
||||
// base. The base pre-seeds every handle's error to BadHistoryOperationUnsupported, so a handle
|
||||
@@ -1778,18 +1780,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
// backend throw) never throws out of the batch.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Serve a HistoryRead-Raw request over the pre-filtered historized variable handles, dispatching
|
||||
/// each to <see cref="IHistorianDataSource.ReadRawAsync"/>. Modified-history reads
|
||||
/// (<c>IsReadModified</c>) are unsupported — we don't serve a modified-value history surface.
|
||||
/// <para>
|
||||
/// Raw is the only arm that pages server-side: <c>ReadRawModifiedDetails</c> carries a client
|
||||
/// count cap (<c>NumValuesPerNode</c>), so a page that returns exactly that many samples MAY
|
||||
/// have more behind it ⇒ a time-based continuation point is emitted (see
|
||||
/// <see cref="ServeRawPaged"/>). An inbound continuation point on a node resumes its stored
|
||||
/// read. <c>NumValuesPerNode == 0</c> ("all values") never pages.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
protected override void HistoryReadRawModified(
|
||||
ServerSystemContext context,
|
||||
ReadRawModifiedDetails details,
|
||||
@@ -1816,15 +1807,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serve a HistoryRead-Processed request, mapping each node's per-node aggregate NodeId (from the
|
||||
/// parallel <c>AggregateType</c> collection — the base guarantees it is the same length as
|
||||
/// <c>nodesToRead</c>) to a <see cref="HistoryAggregateType"/> and dispatching to
|
||||
/// <see cref="IHistorianDataSource.ReadProcessedAsync"/>. An unknown aggregate yields
|
||||
/// <c>BadAggregateNotSupported</c> for that node. Single-shot (no continuation point):
|
||||
/// <c>ReadProcessedDetails</c> carries no client count cap — the bucket count is deterministic
|
||||
/// (window / interval) — so there is no "full page" signal to page on.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
protected override void HistoryReadProcessed(
|
||||
ServerSystemContext context,
|
||||
ReadProcessedDetails details,
|
||||
@@ -1864,12 +1847,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serve a HistoryRead-AtTime request, dispatching the requested timestamps to
|
||||
/// <see cref="IHistorianDataSource.ReadAtTimeAsync"/>. Single-shot (no continuation point):
|
||||
/// AtTime carries no client count cap — the request IS the timestamp list and the result is
|
||||
/// exactly one sample per requested timestamp — so there is no "full page" signal to page on.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
protected override void HistoryReadAtTime(
|
||||
ServerSystemContext context,
|
||||
ReadAtTimeDetails details,
|
||||
@@ -1891,18 +1869,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serve a HistoryRead-Events request over the equipment-folder event-notifier nodes (the folders
|
||||
/// that own alarm conditions). Each handle's NodeId identifier is resolved against
|
||||
/// <see cref="_eventNotifierSources"/>: a miss ⇒ <c>BadHistoryOperationUnsupported</c> (a node we own
|
||||
/// that isn't a registered event-history source — e.g. a plain folder, or one promoted while no
|
||||
/// historian was wired); a hit block-bridges to <see cref="IHistorianDataSource.ReadEventsAsync"/>
|
||||
/// for the folder's source name and projects each <see cref="HistoricalEvent"/> into a
|
||||
/// <see cref="HistoryEventFieldList"/> per the request's event filter. Like the Raw/Processed/AtTime
|
||||
/// arms this is NOT invoked under the node-manager <c>Lock</c>, so the block-bridge is safe; each
|
||||
/// handle is served under try/catch so a backend throw becomes a Bad status for THAT node only and
|
||||
/// never throws out of the batch.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
protected override void HistoryReadEvents(
|
||||
ServerSystemContext context,
|
||||
ReadEventDetails details,
|
||||
@@ -1936,7 +1903,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
sourceName,
|
||||
details.StartTime,
|
||||
details.EndTime,
|
||||
// OpcUaServer-002: NumValuesPerNode==0 means "no limit — return ALL values" per OPC UA
|
||||
// NumValuesPerNode==0 means "no limit — return ALL values" per OPC UA
|
||||
// Part 4/11, so translate it to UNBOUNDED (int.MaxValue) here. Passing the int<=0
|
||||
// backend-default-cap sentinel instead would silently truncate a "give me everything"
|
||||
// events read at the backend default. A positive cap passes through (clamped).
|
||||
@@ -1974,7 +1941,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <returns>The clamped non-negative int.</returns>
|
||||
private static int ClampToInt(uint value) => value > int.MaxValue ? int.MaxValue : (int)value;
|
||||
|
||||
/// <summary>OpcUaServer-002: map a HistoryRead-Events <c>NumValuesPerNode</c> request cap onto the
|
||||
/// <summary>Map a HistoryRead-Events <c>NumValuesPerNode</c> request cap onto the
|
||||
/// <see cref="IHistorianDataSource.ReadEventsAsync"/> <c>maxEvents</c> argument, honouring the OPC UA
|
||||
/// Part 4/11 semantics that <c>NumValuesPerNode == 0</c> means "no limit — return ALL values".
|
||||
/// We translate 0 to UNBOUNDED (<see cref="int.MaxValue"/>) — a very large positive cap — rather than
|
||||
@@ -2315,14 +2282,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drop the resume state for any continuation points the client asked to release
|
||||
/// (<c>releaseContinuationPoints == true</c>) and return WITHOUT reading data, per OPC UA Part 4.
|
||||
/// The base dispatcher routes a release-only HistoryRead here (it never reaches the per-details
|
||||
/// arms), so this is the single place that must free Raw's stored cursors. Each handle's released
|
||||
/// point is <c>nodesToRead[handle.Index].ContinuationPoint</c>; releasing an unknown / null point
|
||||
/// is a harmless no-op. Errors are left Good (the base pre-seeds them) — a release does not fail.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
protected override void HistoryReleaseContinuationPoints(
|
||||
ServerSystemContext context,
|
||||
IList<HistoryReadValueId> nodesToRead,
|
||||
|
||||
@@ -20,76 +20,37 @@ public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgicalAddre
|
||||
_nodeManager = nodeManager;
|
||||
}
|
||||
|
||||
/// <summary>Writes a value to the OPC UA address space.</summary>
|
||||
/// <param name="nodeId">The OPC UA node identifier.</param>
|
||||
/// <param name="value">The value being written.</param>
|
||||
/// <param name="quality">The OPC UA quality status.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
/// <inheritdoc />
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
|
||||
=> _nodeManager.WriteValue(nodeId, value, quality, sourceTimestampUtc);
|
||||
|
||||
/// <summary>Writes the full Part 9 alarm-condition state to the OPC UA address space.</summary>
|
||||
/// <param name="alarmNodeId">The alarm node identifier.</param>
|
||||
/// <param name="state">The full condition state to project onto the node.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
/// <inheritdoc />
|
||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc)
|
||||
=> _nodeManager.WriteAlarmCondition(alarmNodeId, state, sourceTimestampUtc);
|
||||
|
||||
/// <summary>Materialises a real Part 9 alarm-condition node in the address space.</summary>
|
||||
/// <param name="alarmNodeId">The alarm node identifier (== ScriptedAlarmId).</param>
|
||||
/// <param name="equipmentNodeId">The equipment folder node identifier the condition parents under.</param>
|
||||
/// <param name="displayName">The human-readable condition name.</param>
|
||||
/// <param name="alarmType">The domain alarm type.</param>
|
||||
/// <param name="severity">The domain severity.</param>
|
||||
/// <param name="isNative">True for a driver-fed (native) equipment-tag alarm; false (default) for a scripted alarm.</param>
|
||||
/// <inheritdoc />
|
||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
||||
=> _nodeManager.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative);
|
||||
|
||||
/// <summary>Ensures a folder node exists in the address space.</summary>
|
||||
/// <param name="folderNodeId">The folder node identifier.</param>
|
||||
/// <param name="parentNodeId">The parent folder node identifier.</param>
|
||||
/// <param name="displayName">The display name for the folder.</param>
|
||||
/// <inheritdoc />
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
|
||||
=> _nodeManager.EnsureFolder(folderNodeId, parentNodeId, displayName);
|
||||
|
||||
/// <summary>Ensures a variable node exists in the address space.</summary>
|
||||
/// <param name="variableNodeId">The variable node identifier.</param>
|
||||
/// <param name="parentFolderNodeId">The parent folder node identifier.</param>
|
||||
/// <param name="displayName">The display name for the variable.</param>
|
||||
/// <param name="dataType">The OPC UA data type.</param>
|
||||
/// <param name="writable">When true the node is created read/write; otherwise read-only.</param>
|
||||
/// <param name="historianTagname">null ⇒ not historized; non-null ⇒ create Historizing with the
|
||||
/// HistoryRead access bit and register the historian tagname.</param>
|
||||
/// <param name="isArray">When true the node is created as a 1-D array; when false (default) scalar.</param>
|
||||
/// <param name="arrayLength">The declared length of the 1-D array when <paramref name="isArray"/> is true.</param>
|
||||
/// <inheritdoc />
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
|
||||
=> _nodeManager.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength);
|
||||
|
||||
/// <summary>F10b: surgically update an existing variable node's Writable + Historizing + presentation
|
||||
/// shape (DataType / array-ness) in place (no rebuild). Returns false when the node does not exist
|
||||
/// (caller falls back to a full rebuild).</summary>
|
||||
/// <param name="variableNodeId">The variable node identifier.</param>
|
||||
/// <param name="writable">When true the node becomes read/write with the inbound-write handler; otherwise read-only.</param>
|
||||
/// <param name="historianTagname">null ⇒ not historized; non-null ⇒ Historizing with the HistoryRead bit and tagname binding.</param>
|
||||
/// <param name="dataType">The OPC UA built-in data type name to apply in place.</param>
|
||||
/// <param name="isArray">When true the node becomes a 1-D array; when false scalar.</param>
|
||||
/// <param name="arrayLength">The declared length of the 1-D array when <paramref name="isArray"/> is true.</param>
|
||||
/// <inheritdoc />
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength)
|
||||
=> _nodeManager.UpdateTagAttributes(variableNodeId, writable, historianTagname, dataType, isArray, arrayLength);
|
||||
|
||||
/// <summary>OpcUaServer-001: surgically update an existing folder node's display name in place (no
|
||||
/// rebuild) for a UNS Area / Line rename. Returns false when the folder does not exist (caller falls
|
||||
/// back to a full rebuild).</summary>
|
||||
/// <param name="folderNodeId">The folder node identifier whose display name to update in place.</param>
|
||||
/// <param name="displayName">The new display name to apply.</param>
|
||||
/// <returns>True when the in-place update was applied; false when the folder is missing.</returns>
|
||||
/// <inheritdoc />
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName)
|
||||
=> _nodeManager.UpdateFolderDisplayName(folderNodeId, displayName);
|
||||
|
||||
/// <summary>Rebuilds the entire OPC UA address space.</summary>
|
||||
/// <inheritdoc />
|
||||
public void RebuildAddressSpace() => _nodeManager.RebuildAddressSpace();
|
||||
|
||||
/// <summary>Announces a runtime NodeAdded model-change (discovered-node injection) to subscribed clients.</summary>
|
||||
/// <param name="affectedNodeId">The node under which discovered nodes were added.</param>
|
||||
/// <inheritdoc />
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId) => _nodeManager.RaiseNodesAddedModelChange(affectedNodeId);
|
||||
}
|
||||
|
||||
@@ -32,8 +32,7 @@ public sealed class SdkServiceLevelPublisher : IServiceLevelPublisher
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>Publishes the service level to the OPC UA Server object.</summary>
|
||||
/// <param name="serviceLevel">The service level value to publish.</param>
|
||||
/// <inheritdoc />
|
||||
public void Publish(byte serviceLevel)
|
||||
{
|
||||
var node = _serverInternal.ServerObject?.ServiceLevel;
|
||||
|
||||
@@ -55,11 +55,7 @@ public sealed class NullOpcUaUserAuthenticator : IOpcUaUserAuthenticator
|
||||
public static readonly NullOpcUaUserAuthenticator Instance = new();
|
||||
private NullOpcUaUserAuthenticator() { }
|
||||
|
||||
/// <summary>Authenticates a username (always denies in this null implementation).</summary>
|
||||
/// <param name="username">The username to authenticate.</param>
|
||||
/// <param name="password">The cleartext password to authenticate.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that returns a denial result indicating no authenticator is configured.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task<OpcUaUserAuthResult> AuthenticateUserNameAsync(string username, string password, CancellationToken ct) =>
|
||||
Task.FromResult(OpcUaUserAuthResult.Deny("No UserName authenticator is configured on this server."));
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public static class OpcUaDataPlaneRoles
|
||||
|
||||
/// <summary>The role that grants authority to write a writable equipment-tag variable node
|
||||
/// (FreeAccess / Operate attributes). A session must carry this role for the inbound
|
||||
/// <c>OnWriteValue</c> handler (Task 11) to route the value to the backing driver; absent it the
|
||||
/// <c>OnWriteValue</c> handler to route the value to the backing driver; absent it the
|
||||
/// write is denied with <c>BadUserAccessDenied</c> before any driver call.</summary>
|
||||
public const string WriteOperate = "WriteOperate";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user