feat(opcua): advertise the historian raw cap as MaxReturnDataValues on source sync
Lane H1 follow-through: SyncAdvertisedAggregates now also calls HistoryPaging.AdvertiseMaxReturnDataValues(Server, source .MaxRawValuesPerRead) so HistoryServerCapabilities/MaxReturnDataValues (i=11273) follows the historian source on every assignment (the Null default advertises 0 = no limit); a false return logs a Warning (the cap is enforced by HistoryRead regardless). Test: NodeManagerMaxReturnDataValuesSyncTests reads i=11273 back through the SDK master node manager after setting a capped source (7000, 250) and after resetting to the Null source (0). Refs #732 #736 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -458,6 +458,15 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
advertised.Count, source.GetType().Name,
|
||||
string.Join(", ", advertised.Select(id => AggregateFunctionMap.Map(id)?.ToString() ?? id.ToString())));
|
||||
}
|
||||
|
||||
// #736: HistoryServerCapabilities/MaxReturnDataValues (i=11273) carries the source's raw-page cap
|
||||
// (0 = unlimited), re-synced on every source assignment like the aggregate set above.
|
||||
if (!HistoryPaging.AdvertiseMaxReturnDataValues(Server, source.MaxRawValuesPerRead))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"OtOpcUaNodeManager: could not advertise HistoryServerCapabilities/MaxReturnDataValues ({Cap}) for historian {Source}; the cap is still enforced by HistoryRead",
|
||||
source.MaxRawValuesPerRead, source.GetType().Name);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Server;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using HistorianRead = ZB.MOM.WW.OtOpcUa.Core.Abstractions.HistoryReadResult;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// #736 — assigning <see cref="OtOpcUaNodeManager.HistorianDataSource"/> re-syncs
|
||||
/// <c>HistoryServerCapabilities/MaxReturnDataValues</c> (i=11273) to the source's
|
||||
/// <see cref="IHistorianDataSource.MaxRawValuesPerRead"/>, read back the way a client would. The Null
|
||||
/// source (the historian-disabled default) advertises Part 11's "no limit".
|
||||
/// </summary>
|
||||
public sealed class NodeManagerMaxReturnDataValuesSyncTests(BoundsServerFixture fx) : IClassFixture<BoundsServerFixture>
|
||||
{
|
||||
[Fact]
|
||||
public async Task Setting_the_historian_source_advertises_its_raw_cap()
|
||||
{
|
||||
await fx.Gate.WaitAsync(TestContext.Current.CancellationToken);
|
||||
try
|
||||
{
|
||||
fx.Nm.HistorianDataSource = new CappedSource(7_000);
|
||||
(await ReadAdvertisedAsync()).ShouldBe(7_000u);
|
||||
|
||||
fx.Nm.HistorianDataSource = new CappedSource(250);
|
||||
(await ReadAdvertisedAsync()).ShouldBe(250u);
|
||||
|
||||
fx.Nm.HistorianDataSource = null!; // the Null default: no cap
|
||||
(await ReadAdvertisedAsync()).ShouldBe(0u);
|
||||
}
|
||||
finally
|
||||
{
|
||||
fx.Nm.HistorianDataSource = null!;
|
||||
fx.Gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<uint> ReadAdvertisedAsync()
|
||||
{
|
||||
var context = new OperationContext(new RequestHeader(), secureChannelContext: null, RequestType.Read, identity: null);
|
||||
var nodesToRead = new ReadValueIdCollection
|
||||
{
|
||||
new ReadValueId { NodeId = VariableIds.HistoryServerCapabilities_MaxReturnDataValues, AttributeId = Attributes.Value },
|
||||
};
|
||||
var (values, _) = await fx.Nm.Server.NodeManager.ReadAsync(context, 0, TimestampsToReturn.Neither, nodesToRead, TestContext.Current.CancellationToken);
|
||||
values.Count.ShouldBe(1);
|
||||
StatusCode.IsGood(values[0].StatusCode).ShouldBeTrue(values[0].StatusCode.ToString());
|
||||
return (uint)values[0].Value;
|
||||
}
|
||||
|
||||
private sealed class CappedSource(int cap) : IHistorianDataSource
|
||||
{
|
||||
public int MaxRawValuesPerRead => cap;
|
||||
public Task<HistorianRead> ReadRawAsync(string r, DateTime s, DateTime e, uint m, CancellationToken ct) =>
|
||||
NullHistorianDataSource.Instance.ReadRawAsync(r, s, e, m, ct);
|
||||
public Task<HistorianRead> ReadProcessedAsync(string r, DateTime s, DateTime e, TimeSpan i, HistoryAggregateType a, CancellationToken ct) =>
|
||||
NullHistorianDataSource.Instance.ReadProcessedAsync(r, s, e, i, a, ct);
|
||||
public Task<HistorianRead> ReadAtTimeAsync(string r, IReadOnlyList<DateTime> t, CancellationToken ct) =>
|
||||
NullHistorianDataSource.Instance.ReadAtTimeAsync(r, t, ct);
|
||||
public Task<HistoricalEventsResult> ReadEventsAsync(string? n, DateTime s, DateTime e, int m, CancellationToken ct) =>
|
||||
NullHistorianDataSource.Instance.ReadEventsAsync(n, s, e, m, ct);
|
||||
public HistorianHealthSnapshot GetHealthSnapshot() => NullHistorianDataSource.Instance.GetHealthSnapshot();
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user