feat(opcuaclient): add BuildBaseEventFilter + MapHistoryEvents pure cores
This commit is contained in:
@@ -1675,6 +1675,91 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(aggregate), aggregate, null),
|
||||
};
|
||||
|
||||
// Canonical BaseEventType select-clause order — MapHistoryEvents maps by these indices.
|
||||
private static readonly string[] EventFieldBrowseNames =
|
||||
[
|
||||
BrowseNames.EventId, // 0
|
||||
BrowseNames.SourceName, // 1
|
||||
BrowseNames.Time, // 2
|
||||
BrowseNames.ReceiveTime, // 3
|
||||
BrowseNames.Message, // 4
|
||||
BrowseNames.Severity, // 5
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Builds the fixed canonical EventFilter the driver sends upstream for HistoryReadEvents —
|
||||
/// the six BaseEventType fields the OtOpcUa server projects (<see cref="HistoricalEvent"/>).
|
||||
/// The clause order is load-bearing: <see cref="MapHistoryEvents"/> reads results by index.
|
||||
/// </summary>
|
||||
/// <returns>An EventFilter with six SimpleAttributeOperand value clauses.</returns>
|
||||
internal static EventFilter BuildBaseEventFilter()
|
||||
{
|
||||
var filter = new EventFilter();
|
||||
foreach (var browseName in EventFieldBrowseNames)
|
||||
{
|
||||
filter.SelectClauses.Add(new SimpleAttributeOperand
|
||||
{
|
||||
TypeDefinitionId = ObjectTypeIds.BaseEventType,
|
||||
BrowsePath = new QualifiedNameCollection { new QualifiedName(browseName) },
|
||||
AttributeId = Attributes.Value,
|
||||
});
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an upstream <see cref="HistoryEvent"/> (field arrays ordered to match
|
||||
/// <see cref="BuildBaseEventFilter"/>) onto <see cref="HistoricalEvent"/> records. Defensive:
|
||||
/// short / null / wrong-typed fields degrade to null/default rather than throwing.
|
||||
/// </summary>
|
||||
/// <param name="historyEvent">The upstream history-event payload.</param>
|
||||
/// <returns>The mapped historical events in upstream order.</returns>
|
||||
internal static IReadOnlyList<Core.Abstractions.HistoricalEvent> MapHistoryEvents(HistoryEvent historyEvent)
|
||||
{
|
||||
if (historyEvent?.Events is not { Count: > 0 } rows) return [];
|
||||
var result = new List<Core.Abstractions.HistoricalEvent>(rows.Count);
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var fields = row?.EventFields;
|
||||
result.Add(new Core.Abstractions.HistoricalEvent(
|
||||
EventId: CoerceEventId(FieldAt(fields, 0)),
|
||||
SourceName: CoerceString(FieldAt(fields, 1)),
|
||||
EventTimeUtc: CoerceDateTime(FieldAt(fields, 2)),
|
||||
ReceivedTimeUtc: CoerceDateTime(FieldAt(fields, 3)),
|
||||
Message: CoerceString(FieldAt(fields, 4)),
|
||||
Severity: CoerceSeverity(FieldAt(fields, 5))));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static object? FieldAt(VariantCollection? fields, int index)
|
||||
=> fields is not null && index < fields.Count ? fields[index].Value : null;
|
||||
|
||||
private static string CoerceEventId(object? value) => value switch
|
||||
{
|
||||
byte[] bytes => Convert.ToBase64String(bytes),
|
||||
string s => s,
|
||||
null => string.Empty,
|
||||
_ => value.ToString() ?? string.Empty,
|
||||
};
|
||||
|
||||
private static string? CoerceString(object? value) => value switch
|
||||
{
|
||||
LocalizedText lt => lt.Text,
|
||||
string s => s,
|
||||
null => null,
|
||||
_ => value.ToString(),
|
||||
};
|
||||
|
||||
private static DateTime CoerceDateTime(object? value)
|
||||
=> value is DateTime dt ? dt : DateTime.MinValue;
|
||||
|
||||
private static ushort CoerceSeverity(object? value)
|
||||
{
|
||||
try { return value is null ? (ushort)0 : Convert.ToUInt16(value); }
|
||||
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { return 0; }
|
||||
}
|
||||
|
||||
// ReadEventsAsync stays at the interface default (throws NotSupportedException) per
|
||||
// IHistoryProvider contract -- the OPC UA Client driver CAN forward HistoryReadEvents,
|
||||
// but the call-site needs an EventFilter SelectClauses surface which the interface
|
||||
|
||||
Reference in New Issue
Block a user