c019d0fc2b
ServerCapabilities/AggregateFunctions (i=2997) is Part 5's list of every aggregate the server supports, and live AggregateFilter subscriptions still compute all 37 SDK aggregates, so it keeps the SDK's full set; only i=11201 follows the backend. A null SupportedAggregates is treated as the interface default (all) by both the advertisement and HistoryRead-Processed (it used to NRE inside the batch). Docs: i=2997/i=11201 split and the boot window. Co-Authored-By: Claude <noreply@anthropic.com>
92 lines
4.8 KiB
C#
92 lines
4.8 KiB
C#
using System.Reflection;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Server;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
|
|
|
/// <summary>
|
|
/// Gitea #682 — make <c>HistoryServerCapabilities/AggregateFunctions</c> (i=11201) list exactly the aggregates the
|
|
/// wired historian backend serves for HistoryRead (<see cref="IHistorianDataSource.SupportedAggregates"/>), instead of
|
|
/// the SDK's full registered set.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// ⚠️ <c>ServerCapabilities/AggregateFunctions</c> (i=2997) is deliberately NOT trimmed. Part 5 defines it as every
|
|
/// aggregate the SERVER supports, and the server still supports all the SDK's registered aggregates for live
|
|
/// data: a MonitoredItem <c>AggregateFilter</c> is validated and computed by the SDK's <c>AggregateManager</c>,
|
|
/// not the historian. The SDK itself models the split — <c>DiagnosticsNodeManager.AddAggregateFunction</c> always
|
|
/// adds to i=2997 and adds to i=11201 only when the aggregate is historical. i=11201 is the history surface, so it
|
|
/// is the one that follows the backend.
|
|
/// </para>
|
|
/// <para>
|
|
/// Only the folder ⇄ aggregate <c>Organizes</c> reference pair is removed or restored (both directions); the
|
|
/// calculator factories and the aggregate-function object nodes stay. Idempotent and reversible, so re-wiring a
|
|
/// different source re-syncs the folder.
|
|
/// </para>
|
|
/// </remarks>
|
|
internal static class AdvertisedAggregateFunctions
|
|
{
|
|
/// <summary>Every standard aggregate-function NodeId the SDK's nodeset knows (<c>ObjectIds.AggregateFunction_*</c>).</summary>
|
|
private static readonly NodeId[] StandardAggregateIds = typeof(ObjectIds)
|
|
.GetFields(BindingFlags.Public | BindingFlags.Static)
|
|
.Where(f => f.FieldType == typeof(NodeId) && f.Name.StartsWith("AggregateFunction_", StringComparison.Ordinal))
|
|
.Select(f => (NodeId)f.GetValue(null)!)
|
|
.ToArray();
|
|
|
|
/// <summary>The trimmed folder — the HISTORY aggregate list only (see remarks for why not i=2997).</summary>
|
|
internal static readonly NodeId Folder = ObjectIds.HistoryServerCapabilities_AggregateFunctions;
|
|
|
|
/// <summary>
|
|
/// The set a source serves, treating a <c>null</c> <see cref="IHistorianDataSource.SupportedAggregates"/> (an
|
|
/// out-of-tree source that returns null despite the non-nullable contract) as <see cref="HistoryAggregateTypes.All"/>,
|
|
/// the interface default — the ONE null rule both the advertisement and HistoryRead-Processed use.
|
|
/// </summary>
|
|
/// <param name="source">The historian source.</param>
|
|
/// <returns>Its served set, never null.</returns>
|
|
public static IReadOnlySet<HistoryAggregateType> ServedBy(IHistorianDataSource source) =>
|
|
source.SupportedAggregates ?? HistoryAggregateTypes.All;
|
|
|
|
/// <summary>
|
|
/// Sync i=11201 to <paramref name="served"/>: an aggregate-function node is organized under it iff the server maps
|
|
/// it (<see cref="AggregateFunctionMap"/>) AND <paramref name="served"/> contains it.
|
|
/// </summary>
|
|
/// <param name="server">The running server.</param>
|
|
/// <param name="served">The backend's served aggregates.</param>
|
|
/// <returns>The advertised NodeIds, or <c>null</c> when the diagnostics node manager or folder is not available.</returns>
|
|
public static IReadOnlyList<NodeId>? Apply(IServerInternal server, IReadOnlySet<HistoryAggregateType> served)
|
|
{
|
|
if (server?.DiagnosticsNodeManager is not CustomNodeManager2 diagnostics) return null;
|
|
|
|
var advertised = new List<NodeId>();
|
|
lock (diagnostics.Lock)
|
|
{
|
|
var folder = diagnostics.FindPredefinedNode(Folder, typeof(BaseObjectState));
|
|
if (folder is null) return null;
|
|
|
|
foreach (var aggregateId in StandardAggregateIds)
|
|
{
|
|
var aggregateNode = diagnostics.FindPredefinedNode(aggregateId, null);
|
|
if (aggregateNode is null) continue;
|
|
|
|
var advertise = AggregateFunctionMap.Map(aggregateId) is { } aggregate && served.Contains(aggregate);
|
|
if (advertise) advertised.Add(aggregateId);
|
|
|
|
var linked = folder.ReferenceExists(ReferenceTypeIds.Organizes, false, aggregateId);
|
|
if (advertise && !linked)
|
|
{
|
|
folder.AddReference(ReferenceTypeIds.Organizes, false, aggregateId);
|
|
aggregateNode.AddReference(ReferenceTypeIds.Organizes, true, folder.NodeId);
|
|
}
|
|
else if (!advertise && linked)
|
|
{
|
|
folder.RemoveReference(ReferenceTypeIds.Organizes, false, aggregateId);
|
|
aggregateNode.RemoveReference(ReferenceTypeIds.Organizes, true, folder.NodeId);
|
|
}
|
|
}
|
|
}
|
|
|
|
return advertised;
|
|
}
|
|
}
|