fix(opcua): raw open-ended/end-only reads per Part 11; extreme-bound conformance (#669)

This commit is contained in:
Joseph Doherty
2026-10-11 08:37:24 -04:00
parent acce99de9a
commit 50b504d293
3 changed files with 88 additions and 1 deletions
@@ -5174,7 +5174,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
// to an open upper bound, or the cursor would store MinValue and a backend would swap the reversed
// resume window. A resumed read is always forward (the cursor is only ever stored for a forward read),
// so its direction comes from the cursor, not from the resume request's details.
if (!resumed && endUtc == DateTime.MinValue && startUtc != DateTime.MinValue) endUtc = DateTime.MaxValue;
if (!resumed && endUtc == DateTime.MinValue && startUtc != DateTime.MinValue)
endUtc = new DateTime(DateTime.MaxValue.Ticks, DateTimeKind.Utc); // Utc kind: a proto Timestamp conversion rejects Unspecified
// Reverse event reads (start > end) are not paged: the cursor below is forward-only.
var forward = resumed || startUtc <= endUtc;
@@ -5966,6 +5967,17 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
tagname = resolved;
startUtc = startTimeUtc;
// Part 11: with NumValuesPerNode > 0 one bound may be left unset. Only a StartTime ⇒ page FORWARD from
// it to an open upper bound; only an EndTime ⇒ page BACKWARD from it (a reverse read, start > end).
// Without this an unset EndTime (MinValue) looked like a reverse read to a lower bound of "now".
if (numValuesPerNode > 0)
{
if (endUtc == DateTime.MinValue && startUtc != DateTime.MinValue)
endUtc = new DateTime(DateTime.MaxValue.Ticks, DateTimeKind.Utc);
else if (startUtc == DateTime.MinValue && endUtc != DateTime.MinValue)
(startUtc, endUtc) = (endUtc, new DateTime(0, DateTimeKind.Utc));
}
}
// HistoryRead is NOT under the node-manager Lock — awaiting the async source is safe.