diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Modbus/ModbusAddressEditor.razor b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Modbus/ModbusAddressEditor.razor
new file mode 100644
index 0000000..4eba086
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Modbus/ModbusAddressEditor.razor
@@ -0,0 +1,79 @@
+@using ZB.MOM.WW.OtOpcUa.Driver.Modbus
+
+@*
+ #145 — Live address-string parser preview for Modbus tag editing. Bound to a string
+ AddressString; on every input keystroke the parser runs and surfaces the resolved
+ breakdown (Region, PduOffset, DataType, Bit, ByteOrder, ArrayCount, StringLength) or
+ the parse error. Family flag drives the parser's family-native branch (#144).
+
+ Re-uses the same ModbusAddressParser the wire driver uses, so grammar drift is
+ impossible by construction. Internal-namespace component called from the larger
+ DriverInstance editor.
+*@
+
+
+
Address string
+
+ @if (IsValid && _parsed is not null)
+ {
+
+ Parsed:
+ Region=@_parsed.Region
+ Offset=@_parsed.Offset
+ Type=@_parsed.DataType
+ @if (_parsed.Bit.HasValue) { Bit=@_parsed.Bit }
+ @if (_parsed.ByteOrder != ModbusByteOrder.BigEndian) { Order=@_parsed.ByteOrder }
+ @if (_parsed.ArrayCount.HasValue) { Array[@_parsed.ArrayCount] }
+ @if (_parsed.StringLength > 0) { StrLen=@_parsed.StringLength }
+
+ }
+ else if (Diagnostic is not null)
+ {
+
@Diagnostic
+ }
+
+
+@code {
+ [Parameter] public string? AddressString { get; set; }
+ [Parameter] public EventCallback AddressStringChanged { get; set; }
+ [Parameter] public ModbusFamily Family { get; set; } = ModbusFamily.Generic;
+ [Parameter] public MelsecFamily MelsecSubFamily { get; set; } = MelsecFamily.Q_L_iQR;
+ [Parameter] public EventCallback ParsedChanged { get; set; }
+
+ private ParsedModbusAddress? _parsed;
+ private string? Diagnostic;
+ private bool IsValid => _parsed is not null && Diagnostic is null;
+
+ protected override void OnParametersSet() => Reparse();
+
+ private async Task OnInputChanged(ChangeEventArgs e)
+ {
+ AddressString = e.Value as string;
+ await AddressStringChanged.InvokeAsync(AddressString);
+ Reparse();
+ await ParsedChanged.InvokeAsync(_parsed);
+ }
+
+ private void Reparse()
+ {
+ if (string.IsNullOrWhiteSpace(AddressString))
+ {
+ _parsed = null;
+ Diagnostic = null;
+ return;
+ }
+ if (ModbusAddressParser.TryParse(AddressString, Family, MelsecSubFamily, out var parsed, out var err))
+ {
+ _parsed = parsed;
+ Diagnostic = null;
+ }
+ else
+ {
+ _parsed = null;
+ Diagnostic = err;
+ }
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Modbus/ModbusOptionsEditor.razor b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Modbus/ModbusOptionsEditor.razor
new file mode 100644
index 0000000..66ddf2c
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Modbus/ModbusOptionsEditor.razor
@@ -0,0 +1,169 @@
+@using ZB.MOM.WW.OtOpcUa.Driver.Modbus
+
+@*
+ #145 — Driver-instance options panel for the Modbus driver. Surfaces every option group
+ added by #136-#144 so users can configure the driver via the UI rather than hand-editing
+ DriverConfig JSON. Bound to a ModbusOptionsViewModel; the parent page round-trips that
+ model to the DriverConfig.json column on save.
+*@
+
+
+
+
Connection
+
+
+
Family (#144)
+
+
+ PLC family
+
+ @foreach (var f in Enum.GetValues())
+ {
+ @f
+ }
+
+
+ @if (Model.Family == ModbusFamily.MELSEC)
+ {
+
+ MELSEC sub-family
+
+ @foreach (var f in Enum.GetValues())
+ {
+ @f
+ }
+
+
+ }
+
+
+
Keep-alive (#139)
+
+
+
Reconnect (#139)
+
+
+
Protocol (#140)
+
+
+
+
+
+
+ Use FC15 for single coil
+
+
+
+
+
+ Use FC16 for single reg
+
+
+
+
+
+ Write-on-change only (#141)
+
+
+
+
+
+
+@code {
+ [Parameter, EditorRequired] public ModbusOptionsViewModel Model { get; set; } = default!;
+
+ ///
+ /// UI binding model. Maps 1:1 onto the JSON DTO the driver factory accepts; serialised
+ /// to DriverConfig.json by the calling save handler. Defaults match
+ /// ModbusDriverOptions defaults so unedited rows produce the historical wire
+ /// output verbatim.
+ ///
+ public sealed class ModbusOptionsViewModel
+ {
+ public string Host { get; set; } = "127.0.0.1";
+ public int Port { get; set; } = 502;
+ public byte UnitId { get; set; } = 1;
+ public ModbusFamily Family { get; set; } = ModbusFamily.Generic;
+ public MelsecFamily MelsecSubFamily { get; set; } = MelsecFamily.Q_L_iQR;
+
+ public bool KeepAliveEnabled { get; set; } = true;
+ public int KeepAliveTimeSec { get; set; } = 30;
+ public int KeepAliveIntervalSec { get; set; } = 10;
+ public int KeepAliveRetryCount { get; set; } = 3;
+
+ public int ReconnectInitialDelayMs { get; set; } = 0;
+ public int ReconnectMaxDelayMs { get; set; } = 30000;
+ public double ReconnectBackoffMultiplier { get; set; } = 2.0;
+
+ public int MaxRegistersPerRead { get; set; } = 125;
+ public int MaxRegistersPerWrite { get; set; } = 123;
+ public int MaxCoilsPerRead { get; set; } = 2000;
+ public int MaxReadGap { get; set; } = 0;
+
+ public bool UseFC15ForSingleCoilWrites { get; set; } = false;
+ public bool UseFC16ForSingleRegisterWrites { get; set; } = false;
+ public bool WriteOnChangeOnly { get; set; } = false;
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/ZB.MOM.WW.OtOpcUa.Admin.csproj b/src/ZB.MOM.WW.OtOpcUa.Admin/ZB.MOM.WW.OtOpcUa.Admin.csproj
index 0ee2b33..73755d8 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Admin/ZB.MOM.WW.OtOpcUa.Admin.csproj
+++ b/src/ZB.MOM.WW.OtOpcUa.Admin/ZB.MOM.WW.OtOpcUa.Admin.csproj
@@ -25,6 +25,7 @@
+
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/ModbusOptionsViewModelTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/ModbusOptionsViewModelTests.cs
new file mode 100644
index 0000000..1079b61
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/ModbusOptionsViewModelTests.cs
@@ -0,0 +1,46 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Admin.Components.Pages.Modbus;
+using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
+
+namespace ZB.MOM.WW.OtOpcUa.Admin.Tests;
+
+///
+/// #145 Admin UI: smoke coverage for the ModbusOptionsEditor view model. The Blazor
+/// component itself is exercised in browser-runtime tests; this fixture pins the default
+/// values the form initialises to so a regression that flips an unedited row to a
+/// non-default value gets caught.
+///
+[Trait("Category", "Unit")]
+public sealed class ModbusOptionsViewModelTests
+{
+ [Fact]
+ public void Defaults_Match_DriverOption_Defaults()
+ {
+ var vm = new ModbusOptionsEditor.ModbusOptionsViewModel();
+ var driverDefault = new ModbusDriverOptions();
+
+ vm.Host.ShouldBe(driverDefault.Host);
+ vm.Port.ShouldBe(driverDefault.Port);
+ vm.UnitId.ShouldBe(driverDefault.UnitId);
+ vm.Family.ShouldBe(driverDefault.Family);
+ vm.MelsecSubFamily.ShouldBe(driverDefault.MelsecSubFamily);
+
+ vm.KeepAliveEnabled.ShouldBe(driverDefault.KeepAlive.Enabled);
+ vm.KeepAliveTimeSec.ShouldBe((int)driverDefault.KeepAlive.Time.TotalSeconds);
+ vm.KeepAliveIntervalSec.ShouldBe((int)driverDefault.KeepAlive.Interval.TotalSeconds);
+ vm.KeepAliveRetryCount.ShouldBe(driverDefault.KeepAlive.RetryCount);
+
+ vm.ReconnectInitialDelayMs.ShouldBe((int)driverDefault.Reconnect.InitialDelay.TotalMilliseconds);
+ vm.ReconnectMaxDelayMs.ShouldBe((int)driverDefault.Reconnect.MaxDelay.TotalMilliseconds);
+ vm.ReconnectBackoffMultiplier.ShouldBe(driverDefault.Reconnect.BackoffMultiplier);
+
+ vm.MaxRegistersPerRead.ShouldBe(driverDefault.MaxRegistersPerRead);
+ vm.MaxRegistersPerWrite.ShouldBe(driverDefault.MaxRegistersPerWrite);
+ vm.MaxCoilsPerRead.ShouldBe(driverDefault.MaxCoilsPerRead);
+ vm.MaxReadGap.ShouldBe(driverDefault.MaxReadGap);
+ vm.UseFC15ForSingleCoilWrites.ShouldBe(driverDefault.UseFC15ForSingleCoilWrites);
+ vm.UseFC16ForSingleRegisterWrites.ShouldBe(driverDefault.UseFC16ForSingleRegisterWrites);
+ vm.WriteOnChangeOnly.ShouldBe(driverDefault.WriteOnChangeOnly);
+ }
+}
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/ZB.MOM.WW.OtOpcUa.Admin.Tests.csproj b/tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/ZB.MOM.WW.OtOpcUa.Admin.Tests.csproj
index 05b7bd1..b842b85 100644
--- a/tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/ZB.MOM.WW.OtOpcUa.Admin.Tests.csproj
+++ b/tests/ZB.MOM.WW.OtOpcUa.Admin.Tests/ZB.MOM.WW.OtOpcUa.Admin.Tests.csproj
@@ -21,6 +21,7 @@
+