Merge fix/693-gateway-attime: Gateway AtTime from raw reads + shared evaluator, node type hint (#693)

This commit is contained in:
Joseph Doherty
2026-10-13 01:18:09 -04:00
14 changed files with 1510 additions and 7 deletions
@@ -3399,6 +3399,36 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
return NodeId.Null;
}
/// <summary>
/// Gitea #693: the AtTime value-type hint of a historized variable — its DataType's built-in type as a
/// <see cref="TypeCode"/> when the node is a scalar of a type the historian evaluates (Boolean, the integers, Float,
/// Double, String, DateTime); <c>null</c> for an array, an unknown node, or any other type. A UNS mirror carries its
/// backing raw tag's DataType, so either NodeId gives the same hint.
/// </summary>
/// <param name="nodeId">The HistoryRead request's node id.</param>
/// <returns>The hint, or null.</returns>
internal TypeCode? AtTimeValueType(NodeId nodeId)
{
if (!_variables.TryGetValue(MapKey(nodeId), out var variable) || variable.ValueRank != ValueRanks.Scalar) return null;
return TypeInfo.GetBuiltInType(variable.DataType) switch
{
BuiltInType.Boolean => TypeCode.Boolean,
BuiltInType.SByte => TypeCode.SByte,
BuiltInType.Byte => TypeCode.Byte,
BuiltInType.Int16 => TypeCode.Int16,
BuiltInType.UInt16 => TypeCode.UInt16,
BuiltInType.Int32 => TypeCode.Int32,
BuiltInType.UInt32 => TypeCode.UInt32,
BuiltInType.Int64 => TypeCode.Int64,
BuiltInType.UInt64 => TypeCode.UInt64,
BuiltInType.Float => TypeCode.Single,
BuiltInType.Double => TypeCode.Double,
BuiltInType.String => TypeCode.String,
BuiltInType.DateTime => TypeCode.DateTime,
_ => null,
};
}
/// <summary>Map a Tag.DataType string ("Boolean", "Int32", "Float", "Double", "String",
/// "DateTime") to the OPC UA built-in NodeId. Unknown names fall back to BaseDataType
/// (matches CreateVariable's default for lazy-created nodes).</summary>
@@ -5177,9 +5207,12 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
var work = new List<Func<Task>>(nodesToProcess.Count);
foreach (var handle in nodesToProcess)
{
// #693: the node's scalar value type, for a backend whose stored values carry none (the Gateway).
var valueType = AtTimeValueType(handle.NodeId);
work.Add(() => ServeNodeAsync(handle, results, errors, (source, tagname, token) => source.ReadAtTimeAsync(
tagname,
timestamps,
valueType,
token), ct));
}
@@ -97,6 +97,15 @@ public sealed class ServerHistorianOptions
/// </summary>
public bool HonorReturnBounds { get; init; } = true;
/// <summary>
/// Gitea #693, <c>Gateway</c> backend only: whether HistoryRead-AtTime is served from RAW gateway reads evaluated with
/// the SQL backends' shared AtTime rules (exact stored value at a stored time, linear between usable neighbours with
/// the Interpolated bit, held before a Bad neighbour as Uncertain, null + the outage's Bad code inside an outage,
/// <c>BadNoData</c> before the first sample). Default <c>true</c>. <c>false</c> is the kill switch back to the
/// pre-#693 answer: AVEVA's own interpolation (the gateway's <c>ReadAtTime</c>), passed through unchanged.
/// </summary>
public bool GatewayAtTimeFromRaw { get; init; } = true;
/// <summary>
/// Maximum number of historized nodes served CONCURRENTLY within one HistoryRead batch (arch-review
/// 03/S3). The SDK's <c>HistoryRead*</c> override surface is synchronous, so each arm block-bridges