diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/GatewayQualityMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/GatewayQualityMapper.cs
new file mode 100644
index 00000000..a4829da2
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/GatewayQualityMapper.cs
@@ -0,0 +1,47 @@
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping;
+
+///
+/// Maps a raw OPC DA quality byte (the gateway's opc_quality field) to an OPC UA StatusCode
+/// uint.
+///
+///
+/// Byte-identical port of
+/// ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Internal.QualityMapper.Map (itself a
+/// port of the sidecar's HistorianQualityMapper.Map). The table is duplicated rather than
+/// shared because the projects do not share an assembly; a change to the quality table must be
+/// applied in every copy and is kept in parity by the per-byte tests.
+///
+internal static class GatewayQualityMapper
+{
+ /// Maps an OPC DA quality byte to an OPC UA StatusCode.
+ /// The OPC DA quality byte value.
+ /// An OPC UA StatusCode as a uint.
+ public static uint Map(byte q) => q switch
+ {
+ // Good family (192+)
+ 192 => 0x00000000u, // Good
+ 216 => 0x00D80000u, // Good_LocalOverride
+
+ // Uncertain family (64-191)
+ 64 => 0x40000000u, // Uncertain
+ 68 => 0x40900000u, // Uncertain_LastUsableValue
+ 80 => 0x40930000u, // Uncertain_SensorNotAccurate
+ 84 => 0x40940000u, // Uncertain_EngineeringUnitsExceeded
+ 88 => 0x40950000u, // Uncertain_SubNormal
+
+ // Bad family (0-63)
+ 0 => 0x80000000u, // Bad
+ 4 => 0x80890000u, // Bad_ConfigurationError
+ 8 => 0x808A0000u, // Bad_NotConnected
+ 12 => 0x808B0000u, // Bad_DeviceFailure
+ 16 => 0x808C0000u, // Bad_SensorFailure
+ 20 => 0x80050000u, // Bad_CommunicationError
+ 24 => 0x808D0000u, // Bad_OutOfService
+ 32 => 0x80320000u, // Bad_WaitingForInitialData
+
+ // Unknown — fall back to category bucket so callers still get something usable.
+ _ when q >= 192 => 0x00000000u,
+ _ when q >= 64 => 0x40000000u,
+ _ => 0x80000000u,
+ };
+}
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/SampleMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/SampleMapper.cs
new file mode 100644
index 00000000..fcc92adb
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/SampleMapper.cs
@@ -0,0 +1,87 @@
+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;
+ }
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/GatewayQualityMapperTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/GatewayQualityMapperTests.cs
new file mode 100644
index 00000000..15c7fced
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/GatewayQualityMapperTests.cs
@@ -0,0 +1,19 @@
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Mapping;
+
+public sealed class GatewayQualityMapperTests
+{
+ [Theory]
+ [InlineData(192, 0x00000000u)] // Good
+ [InlineData(216, 0x00D80000u)] // Good_LocalOverride
+ [InlineData(64, 0x40000000u)] // Uncertain
+ [InlineData(0, 0x80000000u)] // Bad
+ [InlineData(8, 0x808A0000u)] // Bad_NotConnected
+ [InlineData(255, 0x00000000u)] // >=192 bucket
+ [InlineData(100, 0x40000000u)] // >=64 bucket
+ [InlineData(1, 0x80000000u)] // bad bucket
+ public void Maps_opc_quality_byte(byte q, uint expected)
+ => Assert.Equal(expected, GatewayQualityMapper.Map(q));
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/SampleMapperTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/SampleMapperTests.cs
new file mode 100644
index 00000000..969b9764
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/SampleMapperTests.cs
@@ -0,0 +1,56 @@
+using Google.Protobuf.WellKnownTypes;
+using Xunit;
+using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
+using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Mapping;
+
+public sealed class SampleMapperTests
+{
+ [Fact]
+ public void Numeric_sample_maps_value_quality_and_timestamps()
+ {
+ var s = new HistorianSample { Tag = "T", NumericValue = 12.5,
+ Quality = 192, OpcQuality = 192, Timestamp = Ts(2026, 1, 1, 0, 0, 0) };
+ var snap = SampleMapper.ToSnapshot(s);
+ Assert.Equal(12.5, Assert.IsType(snap.Value));
+ Assert.Equal(0x00000000u, snap.StatusCode);
+ Assert.Equal(DateTimeKind.Utc, snap.SourceTimestampUtc!.Value.Kind);
+ }
+
+ [Fact]
+ public void String_sample_carries_string_value()
+ {
+ var s = new HistorianSample { Tag = "T", StringValue = "abc", OpcQuality = 192, Timestamp = Ts(2026, 1, 1, 0, 0, 0) };
+ Assert.Equal("abc", SampleMapper.ToSnapshot(s).Value);
+ }
+
+ [Fact]
+ public void Bad_quality_sample_maps_to_bad_status()
+ {
+ var s = new HistorianSample { Tag = "T", NumericValue = 1.0, OpcQuality = 0, Timestamp = Ts(2026, 1, 1, 0, 0, 0) };
+ Assert.Equal(0x80000000u, SampleMapper.ToSnapshot(s).StatusCode);
+ }
+
+ [Fact]
+ public void Aggregate_null_value_is_BadNoData()
+ {
+ var a = new HistorianAggregateSample { Tag = "T", /* Value unset, no Good quality */ EndTime = Ts(2026, 1, 1, 0, 0, 0) };
+ var snap = SampleMapper.ToAggregateSnapshot(a);
+ Assert.Equal(0x800E0000u, snap.StatusCode); // BadNoData
+ Assert.Null(snap.Value);
+ }
+
+ [Fact]
+ public void Aggregate_good_bucket_carries_value()
+ {
+ var a = new HistorianAggregateSample { Tag = "T", Value = 42.0, OpcQuality = 192, EndTime = Ts(2026, 1, 1, 0, 0, 0) };
+ var snap = SampleMapper.ToAggregateSnapshot(a);
+ Assert.Equal(0x00000000u, snap.StatusCode); // Good
+ Assert.Equal(42.0, Assert.IsType(snap.Value));
+ }
+
+ // Ts(...) builds a Google.Protobuf.WellKnownTypes.Timestamp from UTC parts.
+ private static Timestamp Ts(int y, int mo, int d, int h, int mi, int s)
+ => Timestamp.FromDateTime(new DateTime(y, mo, d, h, mi, s, DateTimeKind.Utc));
+}