diff --git a/ZB.MOM.WW.OtOpcUa.slnx b/ZB.MOM.WW.OtOpcUa.slnx
index 253e325..b022300 100644
--- a/ZB.MOM.WW.OtOpcUa.slnx
+++ b/ZB.MOM.WW.OtOpcUa.slnx
@@ -8,6 +8,7 @@
+
@@ -22,6 +23,7 @@
+
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/IModbusTransport.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/IModbusTransport.cs
new file mode 100644
index 0000000..cd979f3
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/IModbusTransport.cs
@@ -0,0 +1,25 @@
+namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
+
+///
+/// Abstraction over the Modbus TCP socket. Takes a PDU (function code + data, excluding
+/// the 7-byte MBAP header) and returns the response PDU — the transport owns transaction-id
+/// pairing, framing, and socket I/O. Tests supply in-memory fakes.
+///
+public interface IModbusTransport : IAsyncDisposable
+{
+ Task ConnectAsync(CancellationToken ct);
+
+ ///
+ /// Send a Modbus PDU (function code + function-specific data) and read the response PDU.
+ /// Throws when the server returns an exception PDU
+ /// (function code + 0x80 + exception code).
+ ///
+ Task SendAsync(byte unitId, byte[] pdu, CancellationToken ct);
+}
+
+public sealed class ModbusException(byte functionCode, byte exceptionCode, string message)
+ : Exception(message)
+{
+ public byte FunctionCode { get; } = functionCode;
+ public byte ExceptionCode { get; } = exceptionCode;
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs b/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs
new file mode 100644
index 0000000..783ec76
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs
@@ -0,0 +1,308 @@
+using System.Buffers.Binary;
+using System.Text.Json;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;
+
+///
+/// Modbus TCP implementation of + +
+/// + . First native-protocol greenfield
+/// driver for the v2 stack — validates the driver-agnostic IAddressSpaceBuilder +
+/// IReadable/IWritable abstractions generalize beyond Galaxy.
+///
+///
+/// Scope limits: synchronous Read/Write only, no subscriptions (Modbus has no push model;
+/// subscriptions would need a polling loop over the declared tags — additive PR). Historian
+/// + alarm capabilities are out of scope (the protocol doesn't express them).
+///
+public sealed class ModbusDriver(ModbusDriverOptions options, string driverInstanceId,
+ Func? transportFactory = null)
+ : IDriver, ITagDiscovery, IReadable, IWritable, IDisposable, IAsyncDisposable
+{
+ private readonly ModbusDriverOptions _options = options;
+ private readonly Func _transportFactory =
+ transportFactory ?? (o => new ModbusTcpTransport(o.Host, o.Port, o.Timeout));
+
+ private IModbusTransport? _transport;
+ private DriverHealth _health = new(DriverState.Unknown, null, null);
+ private readonly Dictionary _tagsByName = new(StringComparer.OrdinalIgnoreCase);
+
+ public string DriverInstanceId => driverInstanceId;
+ public string DriverType => "Modbus";
+
+ public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
+ {
+ _health = new DriverHealth(DriverState.Initializing, null, null);
+ try
+ {
+ _transport = _transportFactory(_options);
+ await _transport.ConnectAsync(cancellationToken).ConfigureAwait(false);
+ foreach (var t in _options.Tags) _tagsByName[t.Name] = t;
+ _health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null);
+ }
+ catch (Exception ex)
+ {
+ _health = new DriverHealth(DriverState.Faulted, null, ex.Message);
+ throw;
+ }
+ }
+
+ public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
+ {
+ await ShutdownAsync(cancellationToken);
+ await InitializeAsync(driverConfigJson, cancellationToken);
+ }
+
+ public async Task ShutdownAsync(CancellationToken cancellationToken)
+ {
+ if (_transport is not null) await _transport.DisposeAsync().ConfigureAwait(false);
+ _transport = null;
+ _health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
+ }
+
+ public DriverHealth GetHealth() => _health;
+ public long GetMemoryFootprint() => 0;
+ public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ // ---- ITagDiscovery ----
+
+ public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ var folder = builder.Folder("Modbus", "Modbus");
+ foreach (var t in _options.Tags)
+ {
+ folder.Variable(t.Name, t.Name, new DriverAttributeInfo(
+ FullName: t.Name,
+ DriverDataType: MapDataType(t.DataType),
+ IsArray: false,
+ ArrayDim: null,
+ SecurityClass: t.Writable ? SecurityClassification.Operate : SecurityClassification.ViewOnly,
+ IsHistorized: false,
+ IsAlarm: false));
+ }
+ return Task.CompletedTask;
+ }
+
+ // ---- IReadable ----
+
+ public async Task> ReadAsync(
+ IReadOnlyList fullReferences, CancellationToken cancellationToken)
+ {
+ var transport = RequireTransport();
+ var now = DateTime.UtcNow;
+ var results = new DataValueSnapshot[fullReferences.Count];
+ for (var i = 0; i < fullReferences.Count; i++)
+ {
+ if (!_tagsByName.TryGetValue(fullReferences[i], out var tag))
+ {
+ results[i] = new DataValueSnapshot(null, StatusBadNodeIdUnknown, null, now);
+ continue;
+ }
+ try
+ {
+ var value = await ReadOneAsync(transport, tag, cancellationToken).ConfigureAwait(false);
+ results[i] = new DataValueSnapshot(value, 0u, now, now);
+ _health = new DriverHealth(DriverState.Healthy, now, null);
+ }
+ catch (Exception ex)
+ {
+ results[i] = new DataValueSnapshot(null, StatusBadInternalError, null, now);
+ _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
+ }
+ }
+ return results;
+ }
+
+ private async Task