using ZB.MOM.WW.HistorianGateway.Contracts.Grpc; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping; /// /// Maps gateway wire samples ( / ) /// onto the driver-agnostic , mirroring the legacy Wonderware client's /// ToSnapshots / ToAggregateSnapshots conventions. /// internal static class SampleMapper { private const uint StatusGood = 0x00000000u; private const uint StatusBadNoData = 0x800E0000u; /// OPC DA "Good" family floor — a quality byte at/above this carries usable data. private const byte GoodQualityFloor = 192; /// Maps a single raw sample to a value snapshot. /// The gateway raw sample. /// The driver-agnostic snapshot. public static DataValueSnapshot ToSnapshot(HistorianSample sample) { // proto3 explicit presence: prefer the numeric value, else the string value, else null. object? value; if (sample.HasNumericValue) value = sample.NumericValue; // boxes as System.Double else if (sample.HasStringValue) value = sample.StringValue; else value = null; // Prefer the OPC DA quality byte (opc_quality); the gateway populates it directly from the // SDK's OpcQuality, so it is the authoritative byte for GatewayQualityMapper. Fall back to // the historian quality field only when opc_quality is unset (0). byte qualityByte = sample.OpcQuality != 0 ? (byte)sample.OpcQuality : (byte)sample.Quality; return new DataValueSnapshot( Value: value, StatusCode: GatewayQualityMapper.Map(qualityByte), SourceTimestampUtc: sample.Timestamp?.ToDateTime(), // Utc kind ServerTimestampUtc: DateTime.UtcNow); } /// Maps a batch of raw samples to value snapshots, in order. /// The gateway raw samples. /// The driver-agnostic snapshots. public static IReadOnlyList ToSnapshots(IEnumerable samples) { var result = new List(); foreach (var sample in samples) result.Add(ToSnapshot(sample)); return result; } /// Maps a single aggregate bucket to a value snapshot. /// The gateway aggregate sample. /// The driver-agnostic snapshot. /// /// Unlike the legacy Wonderware DTO (a nullable double?), the gateway proto carries a /// non-optional double value, so an unavailable (no-data) bucket cannot be signalled by a /// null value. Instead it is signalled by a non-Good opc_quality: a Good bucket /// (opc_quality >= 192) yields its value with a Good status, anything else maps to /// BadNoData with a null value — preserving the Wonderware aggregate contract (binary /// Good-with-value / BadNoData-null). /// public static DataValueSnapshot ToAggregateSnapshot(HistorianAggregateSample aggregate) { bool hasData = aggregate.OpcQuality >= GoodQualityFloor; return new DataValueSnapshot( Value: hasData ? aggregate.Value : null, // boxes as System.Double when present StatusCode: hasData ? StatusGood : StatusBadNoData, SourceTimestampUtc: (aggregate.EndTime ?? aggregate.StartTime)?.ToDateTime(), // bucket timestamp ServerTimestampUtc: DateTime.UtcNow); } /// Maps a batch of aggregate buckets to value snapshots, in order. /// The gateway aggregate samples. /// The driver-agnostic snapshots. public static IReadOnlyList ToAggregateSnapshots(IEnumerable aggregates) { var result = new List(); foreach (var aggregate in aggregates) result.Add(ToAggregateSnapshot(aggregate)); return result; } }