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;
}
}