ed05d50795
The frozen 1.5.378.182 StatusAggregateCalculator counts Uncertain regions as Good under TreatUncertainAsBad=false, so live AggregateFilter PercentGood / DurationGood disagreed with HistoryRead (Part 13 §5.4.3.31/§5.4.3.33, Mantis 0006166) whenever Uncertain data was present. Owner-approved 2026-11-11. - New app-owned GoodOnlyStatusAggregateCalculator subclasses the baseline and replaces only the PercentGood/DurationGood computation (Good regions only); CompatibilityAggregateFactory routes exactly those two IDs to it. The copied SDK files are untouched; the other 35 aggregates are byte-identical. - Golden test: the 64 changed vectors (38 PercentGood + 26 DurationGood) are exempted exactly — those whose baseline output differs from the same case's baseline TreatUncertainAsBad=true twin — and pinned to that twin (baseline evidence, not regenerated from the implementation); plus a full Good-only pin over all 352 PercentGood/DurationGood vectors and a hand-computed case. - Live-vs-history parity over Good/Uncertain/Bad mixes (no masks), with a discrimination check against the pre-#809 baseline; the booted-runtime facts test now expects 8000 ms / 40 %. - SdkAggregateOracle: Run/RunLive = the registered factory; RunSdkAsIs = the untouched baseline; its Part13GoodCalculator duplicate is removed. - README (recorded decision), ServerAggregateConfiguration remarks, docs/Historian.md and .claude/rules/historian.md updated. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
81 lines
4.9 KiB
C#
81 lines
4.9 KiB
C#
using Opc.Ua;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
|
|
|
/// <summary>
|
|
/// The Part 13 <see cref="AggregateConfiguration"/> processed reads are evaluated under, and the rule for a
|
|
/// client-supplied one in <c>ReadProcessedDetails</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The aggregate evaluators (<c>AggregateEvaluator</c> / <c>TimeWeightedAggregator</c> in
|
|
/// <c>Driver.Historian.Sql.Common</c>) are written against fixed settings, not parameterised: Uncertain time counts as
|
|
/// Good (<see cref="TreatUncertainAsBad"/> = false) in TimeAverage2 / Total2, while PercentGood / DurationGood count
|
|
/// Good time only and PercentBad / DurationBad Bad time only — Uncertain in neither (Part 13 §5.4.3.31-34, Mantis
|
|
/// 0006166); the per-sample Average / Minimum /
|
|
/// Maximum / Range / *ActualTime / Count are defined by Part 13 over Good values whatever this setting, as the
|
|
/// SDK computes them under it; an interval is Good only when all of
|
|
/// it is (<see cref="PercentDataGood"/> = 100) and Bad only when none of it is non-Bad
|
|
/// (<see cref="PercentDataBad"/> = 100), otherwise <c>UncertainDataSubNormal</c>; the value past the last sample
|
|
/// is held (stepped, <see cref="UseSlopedExtrapolation"/> = false). Part 13's AggregateConfiguration defaults are
|
|
/// TreatUncertainAsBad = false, PercentDataBad = PercentDataGood = 100, UseSlopedExtrapolation = false (from the
|
|
/// spec as recalled, not re-read offline). The SDK matched these until Opc.Ua.Server 1.5.378.182, which flipped
|
|
/// <c>TreatUncertainAsBad</c> to true: that is a DELIBERATE DIVERGENCE we do not follow (consumers such as
|
|
/// ScadaBridge read these aggregates). <c>OtOpcUaSdkServer.CreateAggregateManager</c> pushes these values into
|
|
/// the SDK manager so live AggregateFilter subscriptions agree with history reads; a test pins both sides.
|
|
/// </para>
|
|
/// <para>
|
|
/// The live calculators are the application's frozen 1.5.378.182 copies (<c>Compatibility/Aggregates</c>), whose
|
|
/// <c>ComputeDurationGoodBad</c> counts Uncertain time as Good under <see cref="TreatUncertainAsBad"/> = false. That
|
|
/// made live PercentGood / DurationGood disagree with HistoryRead whenever Uncertain data was present. #809
|
|
/// (owner-approved 2026-11-11) overrides exactly those two (<c>GoodOnlyStatusAggregateCalculator</c>), so live and
|
|
/// HistoryRead now agree on them — pinned by
|
|
/// <c>SdkAggregateParityTests.Live_AggregateFilter_PercentGood_and_DurationGood_equal_HistoryRead_over_Uncertain_samples</c>.
|
|
/// Other known live-vs-HistoryRead differences (e.g. #821, an Uncertain first sample in a late-start interval) are
|
|
/// documented in <c>docs/Historian.md</c>.
|
|
/// </para>
|
|
/// <para>
|
|
/// A request asking for <c>UseServerCapabilitiesDefaults</c>, or spelling out exactly these values, is served;
|
|
/// any other configuration is refused per node with <c>BadAggregateConfigurationRejected</c> rather than silently
|
|
/// answered under settings the client did not ask for (it used to be ignored).
|
|
/// </para>
|
|
/// </remarks>
|
|
public static class ServerAggregateConfiguration
|
|
{
|
|
/// <summary>Uncertain data is not treated as Bad.</summary>
|
|
public const bool TreatUncertainAsBad = false;
|
|
|
|
/// <summary>An interval is Bad only when 100 % of it is Bad.</summary>
|
|
public const byte PercentDataBad = 100;
|
|
|
|
/// <summary>An interval is Good only when 100 % of it is Good.</summary>
|
|
public const byte PercentDataGood = 100;
|
|
|
|
/// <summary>The value past the last sample is held (stepped), not sloped.</summary>
|
|
public const bool UseSlopedExtrapolation = false;
|
|
|
|
/// <summary>The server defaults as an <see cref="AggregateConfiguration"/> (explicit values, not "use defaults").</summary>
|
|
public static AggregateConfiguration Defaults => new()
|
|
{
|
|
UseServerCapabilitiesDefaults = false,
|
|
TreatUncertainAsBad = TreatUncertainAsBad,
|
|
PercentDataBad = PercentDataBad,
|
|
PercentDataGood = PercentDataGood,
|
|
UseSlopedExtrapolation = UseSlopedExtrapolation,
|
|
};
|
|
|
|
/// <summary>
|
|
/// True when a processed read with <paramref name="requested"/> can be served: none supplied,
|
|
/// <c>UseServerCapabilitiesDefaults</c>, or every setting equal to the server's.
|
|
/// </summary>
|
|
/// <param name="requested">The request's configuration (may be null).</param>
|
|
/// <returns>Whether to serve; false ⇒ <c>BadAggregateConfigurationRejected</c>.</returns>
|
|
public static bool IsServable(AggregateConfiguration? requested) =>
|
|
requested is null
|
|
|| requested.UseServerCapabilitiesDefaults
|
|
|| (requested.TreatUncertainAsBad == TreatUncertainAsBad
|
|
&& requested.PercentDataBad == PercentDataBad
|
|
&& requested.PercentDataGood == PercentDataGood
|
|
&& requested.UseSlopedExtrapolation == UseSlopedExtrapolation);
|
|
}
|