diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbCipDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbCipDriverPage.razor index d7bd17e5..3b3ee667 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbCipDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbCipDriverPage.razor @@ -5,6 +5,7 @@ @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.AdminUI.Clients @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Driver.AbCip @@ -46,8 +47,20 @@ else + + + + + @* Operation timeout *@
Operation settings
@@ -183,6 +196,12 @@ else private bool _busy; private string? _error; + // Address picker state + private bool _showPicker; + private string _pickedAddress = ""; + + private void OnAddressPicked(string address) => _pickedAddress = address; + // Collections are preserved through round-trip and shown as read-only JSON. private IReadOnlyList _devices = []; private IReadOnlyList _tags = []; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbLegacyDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbLegacyDriverPage.razor index 99aa96e6..de836c4f 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbLegacyDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/AbLegacyDriverPage.razor @@ -5,6 +5,7 @@ @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.AdminUI.Clients @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy @@ -47,8 +48,20 @@ else + + + + + @* Operation settings *@
Operation settings
@@ -152,6 +165,12 @@ else private bool _busy; private string? _error; + // Address picker state + private bool _showPicker; + private string _pickedAddress = ""; + + private void OnAddressPicked(string address) => _pickedAddress = address; + // Collections are preserved through round-trip and shown as read-only JSON. private IReadOnlyList _devices = []; private IReadOnlyList _tags = []; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/FocasDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/FocasDriverPage.razor index 3a4e11f8..4b1b1d01 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/FocasDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/FocasDriverPage.razor @@ -5,6 +5,7 @@ @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.AdminUI.Clients @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Driver.FOCAS @@ -46,8 +47,20 @@ else + + + + + @* Connection *@
Connection
@@ -241,6 +254,12 @@ else private bool _loaded, _busy; private string? _error; + // Address picker state + private bool _showPicker; + private string _pickedAddress = ""; + + private void OnAddressPicked(string address) => _pickedAddress = address; + protected override async Task OnInitializedAsync() { await using var db = await DbFactory.CreateDbContextAsync(); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor index dd3dc70b..826fb765 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/GalaxyDriverPage.razor @@ -5,6 +5,7 @@ @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.AdminUI.Clients @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config @@ -46,8 +47,20 @@ else + + + + + @* mxaccessgw connection *@
mxaccessgw connection
@@ -213,6 +226,12 @@ else private bool _busy; private string? _error; + // Address picker state + private bool _showPicker; + private string _pickedAddress = ""; + + private void OnAddressPicked(string address) => _pickedAddress = address; + protected override async Task OnInitializedAsync() { await using var db = await DbFactory.CreateDbContextAsync(); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/HistorianWonderwareDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/HistorianWonderwareDriverPage.razor index a2dad941..8916dcc4 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/HistorianWonderwareDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/HistorianWonderwareDriverPage.razor @@ -5,6 +5,7 @@ @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.AdminUI.Clients @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client @@ -46,8 +47,20 @@ else + + + + + @* Connection *@
Connection
@@ -145,6 +158,12 @@ else private bool _busy; private string? _error; + // Address picker state + private bool _showPicker; + private string _pickedAddress = ""; + + private void OnAddressPicked(string address) => _pickedAddress = address; + protected override async Task OnInitializedAsync() { await using var db = await DbFactory.CreateDbContextAsync(); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/ModbusDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/ModbusDriverPage.razor index 6ba38b3a..bc852c06 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/ModbusDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/ModbusDriverPage.razor @@ -5,6 +5,7 @@ @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.AdminUI.Clients @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Driver.Modbus @@ -46,8 +47,20 @@ else + + + + + @* Transport *@
Transport
@@ -298,6 +311,13 @@ else private bool _loaded; private bool _busy; private string? _error; + + // Address picker state + private bool _showPicker; + private string _pickedAddress = ""; + + private void OnAddressPicked(string address) => _pickedAddress = address; + // Held separately because Tags is a collection — rendered as read-only JSON. private IReadOnlyList _tags = []; private string _tagsJson = "[]"; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor index 4d516334..16b68beb 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/OpcUaClientDriverPage.razor @@ -5,6 +5,7 @@ @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.AdminUI.Clients @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient @@ -46,8 +47,20 @@ else + + + + + @* Endpoint *@
Endpoint
@@ -261,6 +274,12 @@ else private bool _busy; private string? _error; + // Address picker state + private bool _showPicker; + private string _pickedAddress = ""; + + private void OnAddressPicked(string address) => _pickedAddress = address; + // Read-only JSON snippets for collections that have no list editor yet. private string _endpointUrlsJson = "[]"; private string _unsMappingTableJson = "{}"; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/S7DriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/S7DriverPage.razor index 58867fd0..3cccc933 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/S7DriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/S7DriverPage.razor @@ -5,6 +5,7 @@ @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.AdminUI.Clients @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Driver.S7 @@ -46,8 +47,20 @@ else + + + + + @* Connection *@
Connection
@@ -177,6 +190,12 @@ else private bool _loaded, _busy; private string? _error; + // Address picker state + private bool _showPicker; + private string _pickedAddress = ""; + + private void OnAddressPicked(string address) => _pickedAddress = address; + protected override async Task OnInitializedAsync() { await using var db = await DbFactory.CreateDbContextAsync(); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/TwinCATDriverPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/TwinCATDriverPage.razor index 3fe62517..1aea1c31 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/TwinCATDriverPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/Drivers/TwinCATDriverPage.razor @@ -5,6 +5,7 @@ @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.AdminUI.Clients @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT @@ -46,8 +47,20 @@ else + + + + + @* Options *@
Options
@@ -183,6 +196,12 @@ else private bool _loaded, _busy; private string? _error; + // Address picker state + private bool _showPicker; + private string _pickedAddress = ""; + + private void OnAddressPicked(string address) => _pickedAddress = address; + protected override async Task OnInitializedAsync() { await using var db = await DbFactory.CreateDbContextAsync(); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverTagPicker.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverTagPicker.razor new file mode 100644 index 00000000..b5922e43 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/DriverTagPicker.razor @@ -0,0 +1,52 @@ +@* Shared modal shell for per-driver address pickers. Parent page toggles `Visible`; + the child fragment renders the per-driver picker body. The shell handles modal + chrome, the "Use this address" button, and dismisses on close. *@ + +@if (Visible) +{ + + +} + +@code { + [Parameter] public bool Visible { get; set; } + [Parameter] public EventCallback VisibleChanged { get; set; } + [Parameter] public string Title { get; set; } = "Address builder"; + [Parameter] public string CurrentAddress { get; set; } = ""; + [Parameter] public EventCallback OnPickAddress { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + + private async Task OnCloseAsync() + { + await VisibleChanged.InvokeAsync(false); + } + + private async Task OnUseAsync() + { + await OnPickAddress.InvokeAsync(CurrentAddress); + await VisibleChanged.InvokeAsync(false); + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/AbCipAddressPickerBody.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/AbCipAddressPickerBody.razor new file mode 100644 index 00000000..0492921e --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/AbCipAddressPickerBody.razor @@ -0,0 +1,57 @@ +@* Static AB CIP address builder: tag name + optional element index → tag[idx] or tag *@ + +
+
+ + +
Use dot notation for nested tags or UDT members.
+
+
+ + +
Leave 0 for non-array tags (no index appended).
+
+
+
+ + +
+
+
+ +
+ Result: + @_built +
+ +@code { + [Parameter] public string CurrentAddress { get; set; } = ""; + [Parameter] public EventCallback CurrentAddressChanged { get; set; } + + private string _tagName = ""; + private int _elementIndex = 0; + private bool _useIndex = false; + private string _built = ""; + + protected override void OnInitialized() + { + _built = Build(); + _ = CurrentAddressChanged.InvokeAsync(_built); + } + + private async Task OnChangedAsync() + { + _built = Build(); + await CurrentAddressChanged.InvokeAsync(_built); + } + + private string Build() + { + if (string.IsNullOrWhiteSpace(_tagName)) + return ""; + return _useIndex ? $"{_tagName}[{_elementIndex}]" : _tagName; + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/AbLegacyAddressBuilder.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/AbLegacyAddressBuilder.cs new file mode 100644 index 00000000..03137eaf --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/AbLegacyAddressBuilder.cs @@ -0,0 +1,12 @@ +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; + +/// +/// Pure static helper that converts AB Legacy PLC file-type + file-number + element +/// into the canonical address string (e.g. N7:0). +/// Extracted so unit tests can call it without bUnit. +/// +public static class AbLegacyAddressBuilder +{ + public static string Build(string fileType, int fileNumber, int element) + => $"{fileType}{fileNumber}:{element}"; +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/AbLegacyAddressPickerBody.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/AbLegacyAddressPickerBody.razor new file mode 100644 index 00000000..95447a55 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/AbLegacyAddressPickerBody.razor @@ -0,0 +1,58 @@ +@* Static AB Legacy address builder: file type + file number + element → N7:0 *@ +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers + +
+
+ + +
+
+ + +
e.g. 7 for N7
+
+
+ + +
e.g. 0 for N7:0
+
+
+ +
+ Result: + @_built +
+ +@code { + [Parameter] public string CurrentAddress { get; set; } = ""; + [Parameter] public EventCallback CurrentAddressChanged { get; set; } + + private string _fileType = "N"; + private int _fileNumber = 7; + private int _element = 0; + private string _built = ""; + + protected override void OnInitialized() + { + _built = AbLegacyAddressBuilder.Build(_fileType, _fileNumber, _element); + _ = CurrentAddressChanged.InvokeAsync(_built); + } + + private async Task OnChangedAsync() + { + _built = AbLegacyAddressBuilder.Build(_fileType, _fileNumber, _element); + await CurrentAddressChanged.InvokeAsync(_built); + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/FOCASAddressPickerBody.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/FOCASAddressPickerBody.razor new file mode 100644 index 00000000..81c64ff3 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/FOCASAddressPickerBody.razor @@ -0,0 +1,45 @@ +@* Static FOCAS address builder: parameter group + parameter ID → axis:5 *@ +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers + +
+
+ + +
+
+ + +
+
+ +
+ Result: + @_built +
+ +@code { + [Parameter] public string CurrentAddress { get; set; } = ""; + [Parameter] public EventCallback CurrentAddressChanged { get; set; } + + private string _group = "axis"; + private int _parameterId = 0; + private string _built = ""; + + protected override void OnInitialized() + { + _built = FocasAddressBuilder.Build(_group, _parameterId); + _ = CurrentAddressChanged.InvokeAsync(_built); + } + + private async Task OnChangedAsync() + { + _built = FocasAddressBuilder.Build(_group, _parameterId); + await CurrentAddressChanged.InvokeAsync(_built); + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/FocasAddressBuilder.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/FocasAddressBuilder.cs new file mode 100644 index 00000000..b5ebd94c --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/FocasAddressBuilder.cs @@ -0,0 +1,12 @@ +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; + +/// +/// Pure static helper that converts a FOCAS parameter group + parameter ID +/// into the canonical address string (e.g. axis:5). +/// Extracted so unit tests can call it without bUnit. +/// +public static class FocasAddressBuilder +{ + public static string Build(string group, int parameterId) + => $"{group}:{parameterId}"; +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/GalaxyAddressPickerBody.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/GalaxyAddressPickerBody.razor new file mode 100644 index 00000000..151dc6a5 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/GalaxyAddressPickerBody.razor @@ -0,0 +1,57 @@ +@* Static Galaxy address builder: tag_name.AttributeName free text → verbatim. + Live Galaxy browse deferred to a follow-up phase. *@ + +
+ Note: Live Galaxy browse is deferred to a follow-up phase. + Enter the tag and attribute name manually below. +
+ +
+
+ + +
Globally unique system (tag) name.
+
+
+ + +
MXAccess attribute name.
+
+
+ +
+ Result: + @_built +
+ +@code { + [Parameter] public string CurrentAddress { get; set; } = ""; + [Parameter] public EventCallback CurrentAddressChanged { get; set; } + + private string _tagName = ""; + private string _attributeName = ""; + private string _built = ""; + + protected override void OnInitialized() + { + _built = Build(); + _ = CurrentAddressChanged.InvokeAsync(_built); + } + + private async Task OnChangedAsync() + { + _built = Build(); + await CurrentAddressChanged.InvokeAsync(_built); + } + + private string Build() + { + if (string.IsNullOrWhiteSpace(_tagName)) + return ""; + if (string.IsNullOrWhiteSpace(_attributeName)) + return _tagName; + return $"{_tagName}.{_attributeName}"; + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/HistorianWonderwareAddressBuilder.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/HistorianWonderwareAddressBuilder.cs new file mode 100644 index 00000000..2fc5b467 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/HistorianWonderwareAddressBuilder.cs @@ -0,0 +1,12 @@ +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; + +/// +/// Pure static helper that converts a Wonderware Historian tag name + retrieval mode +/// + interval into the canonical address query string (e.g. MyTag?mode=Cyclic&interval=60). +/// Extracted so unit tests can call it without bUnit. +/// +public static class HistorianWonderwareAddressBuilder +{ + public static string Build(string tagName, string mode, int interval) + => $"{tagName}?mode={mode}&interval={interval}"; +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/HistorianWonderwareAddressPickerBody.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/HistorianWonderwareAddressPickerBody.razor new file mode 100644 index 00000000..ce1fc254 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/HistorianWonderwareAddressPickerBody.razor @@ -0,0 +1,52 @@ +@* Static Wonderware Historian address builder: tag name + retrieval mode + interval + → MyTag?mode=Cyclic&interval=60 *@ +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers + +
+
+ + +
+
+ + +
+
+ + +
Polling/retrieval interval.
+
+
+ +
+ Result: + @_built +
+ +@code { + [Parameter] public string CurrentAddress { get; set; } = ""; + [Parameter] public EventCallback CurrentAddressChanged { get; set; } + + private string _tagName = ""; + private string _mode = "Cyclic"; + private int _interval = 60; + private string _built = ""; + + protected override void OnInitialized() + { + _built = string.IsNullOrWhiteSpace(_tagName) ? "" : HistorianWonderwareAddressBuilder.Build(_tagName, _mode, _interval); + _ = CurrentAddressChanged.InvokeAsync(_built); + } + + private async Task OnChangedAsync() + { + _built = string.IsNullOrWhiteSpace(_tagName) ? "" : HistorianWonderwareAddressBuilder.Build(_tagName, _mode, _interval); + await CurrentAddressChanged.InvokeAsync(_built); + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/ModbusAddressBuilder.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/ModbusAddressBuilder.cs new file mode 100644 index 00000000..13b90164 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/ModbusAddressBuilder.cs @@ -0,0 +1,22 @@ +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; + +/// +/// Pure static helper that converts Modbus register-type + offset + length into +/// the canonical address string used by the Modbus driver (e.g. 4x00001-1). +/// Extracted so unit tests can call it without bUnit. +/// +public static class ModbusAddressBuilder +{ + public static string Build(string regType, int offset, int length) + { + var prefix = regType switch + { + "Coil" => "0x", + "DiscreteInput" => "1x", + "Input" => "3x", + "Holding" => "4x", + _ => "4x", + }; + return $"{prefix}{offset:00000}-{length}"; + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/ModbusAddressPickerBody.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/ModbusAddressPickerBody.razor new file mode 100644 index 00000000..1cfda81d --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/ModbusAddressPickerBody.razor @@ -0,0 +1,51 @@ +@* Static Modbus address builder: register type + offset + length → 4x00001-4 *@ +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ Result: + @_built +
+ +@code { + [Parameter] public string CurrentAddress { get; set; } = ""; + [Parameter] public EventCallback CurrentAddressChanged { get; set; } + + private string _regType = "Holding"; + private int _offset = 1; + private int _length = 1; + private string _built = ""; + + protected override void OnInitialized() + { + _built = ModbusAddressBuilder.Build(_regType, _offset, _length); + _ = CurrentAddressChanged.InvokeAsync(_built); + } + + private async Task OnChangedAsync() + { + _built = ModbusAddressBuilder.Build(_regType, _offset, _length); + await CurrentAddressChanged.InvokeAsync(_built); + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/OpcUaClientAddressPickerBody.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/OpcUaClientAddressPickerBody.razor new file mode 100644 index 00000000..3e5d7334 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/OpcUaClientAddressPickerBody.razor @@ -0,0 +1,43 @@ +@* Static OPC UA Client address builder: NodeId free text → verbatim. + Live browse deferred to a follow-up phase. *@ + +
+ Note: Live OPC UA node browse is deferred to a follow-up phase. + Enter the NodeId string manually below. +
+ +
+
+ + +
+ OPC UA NodeId string, e.g. ns=2;s=Channel.Device.Tag or i=1001. +
+
+
+ +
+ Result: + @_built +
+ +@code { + [Parameter] public string CurrentAddress { get; set; } = ""; + [Parameter] public EventCallback CurrentAddressChanged { get; set; } + + private string _nodeId = ""; + private string _built = ""; + + protected override void OnInitialized() + { + _built = _nodeId; + _ = CurrentAddressChanged.InvokeAsync(_built); + } + + private async Task OnChangedAsync() + { + _built = _nodeId; + await CurrentAddressChanged.InvokeAsync(_built); + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/S7AddressBuilder.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/S7AddressBuilder.cs new file mode 100644 index 00000000..6280681d --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/S7AddressBuilder.cs @@ -0,0 +1,37 @@ +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; + +/// +/// Pure static helper that converts S7 area + db-number + offset + data-type +/// into the canonical S7 address string (e.g. DB10.DBD20:REAL, M0.0:X). +/// Extracted so unit tests can call it without bUnit. +/// +public static class S7AddressBuilder +{ + /// DB / M / I / Q + /// Only relevant when area == "DB". + /// Byte offset (decimal). + /// X / B / W / D / REAL + public static string Build(string area, int dbNumber, int offset, string s7Type) + { + if (area == "DB") + { + // e.g. DB10.DBD20:REAL / DB1.DBX0.0:X / DB5.DBW4:W + var qualifier = s7Type switch + { + "X" => $"DBX{offset}.0", + "B" => $"DBB{offset}", + "W" => $"DBW{offset}", + "D" => $"DBD{offset}", + "REAL" => $"DBD{offset}", + _ => $"DBD{offset}", + }; + return $"DB{dbNumber}.{qualifier}:{s7Type}"; + } + else + { + // e.g. M0.0:X / I4:B / Q2:W + var offsetStr = s7Type == "X" ? $"{offset}.0" : $"{offset}"; + return $"{area}{offsetStr}:{s7Type}"; + } + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/S7AddressPickerBody.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/S7AddressPickerBody.razor new file mode 100644 index 00000000..7fcddbe1 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/S7AddressPickerBody.razor @@ -0,0 +1,65 @@ +@* Static S7 address builder: area + db-number + offset + type → DB10.DBD20:REAL / M0.0:X *@ +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers + +
+
+ + +
+ @if (_area == "DB") + { +
+ + +
+ } +
+ + +
+
+ + +
+
+ +
+ Result: + @_built +
+ +@code { + [Parameter] public string CurrentAddress { get; set; } = ""; + [Parameter] public EventCallback CurrentAddressChanged { get; set; } + + private string _area = "DB"; + private int _dbNumber = 1; + private int _offset = 0; + private string _s7Type = "REAL"; + private string _built = ""; + + protected override void OnInitialized() + { + _built = S7AddressBuilder.Build(_area, _dbNumber, _offset, _s7Type); + _ = CurrentAddressChanged.InvokeAsync(_built); + } + + private async Task OnChangedAsync() + { + _built = S7AddressBuilder.Build(_area, _dbNumber, _offset, _s7Type); + await CurrentAddressChanged.InvokeAsync(_built); + } +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/TwinCATAddressPickerBody.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/TwinCATAddressPickerBody.razor new file mode 100644 index 00000000..c2e0e187 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/TwinCATAddressPickerBody.razor @@ -0,0 +1,37 @@ +@* Static TwinCAT address builder: ADS variable name (free text) → verbatim *@ + +
+
+ + +
+ Full ADS symbol path, e.g. MAIN.fValue or GVL.iCounter. +
+
+
+ +
+ Result: + @_built +
+ +@code { + [Parameter] public string CurrentAddress { get; set; } = ""; + [Parameter] public EventCallback CurrentAddressChanged { get; set; } + + private string _varName = ""; + private string _built = ""; + + protected override void OnInitialized() + { + _built = _varName; + _ = CurrentAddressChanged.InvokeAsync(_built); + } + + private async Task OnChangedAsync() + { + _built = _varName; + await CurrentAddressChanged.InvokeAsync(_built); + } +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/AbLegacyAddressBuilderTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/AbLegacyAddressBuilderTests.cs new file mode 100644 index 00000000..b4452b18 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/AbLegacyAddressBuilderTests.cs @@ -0,0 +1,17 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Pickers; + +public sealed class AbLegacyAddressBuilderTests +{ + [Theory] + [InlineData("N", 7, 0, "N7:0")] + [InlineData("B", 3, 1, "B3:1")] + [InlineData("F", 8, 12, "F8:12")] + [InlineData("T", 4, 0, "T4:0")] + [InlineData("C", 5, 2, "C5:2")] + public void Build_Canonical(string fileType, int fileNumber, int element, string expected) + => AbLegacyAddressBuilder.Build(fileType, fileNumber, element).ShouldBe(expected); +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/FocasAddressBuilderTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/FocasAddressBuilderTests.cs new file mode 100644 index 00000000..d95d057a --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/FocasAddressBuilderTests.cs @@ -0,0 +1,16 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Pickers; + +public sealed class FocasAddressBuilderTests +{ + [Theory] + [InlineData("axis", 5, "axis:5")] + [InlineData("spindle", 0, "spindle:0")] + [InlineData("program", 100, "program:100")] + [InlineData("status", 1, "status:1")] + public void Build_Canonical(string group, int parameterId, string expected) + => FocasAddressBuilder.Build(group, parameterId).ShouldBe(expected); +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/HistorianWonderwareAddressBuilderTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/HistorianWonderwareAddressBuilderTests.cs new file mode 100644 index 00000000..816e550f --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/HistorianWonderwareAddressBuilderTests.cs @@ -0,0 +1,15 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Pickers; + +public sealed class HistorianWonderwareAddressBuilderTests +{ + [Theory] + [InlineData("SysTimeHour", "Cyclic", 60, "SysTimeHour?mode=Cyclic&interval=60")] + [InlineData("ReactorTemp", "Last", 1, "ReactorTemp?mode=Last&interval=1")] + [InlineData("FlowRate", "Delta", 30, "FlowRate?mode=Delta&interval=30")] + public void Build_Canonical(string tag, string mode, int interval, string expected) + => HistorianWonderwareAddressBuilder.Build(tag, mode, interval).ShouldBe(expected); +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/ModbusAddressBuilderTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/ModbusAddressBuilderTests.cs new file mode 100644 index 00000000..1a25886a --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/ModbusAddressBuilderTests.cs @@ -0,0 +1,21 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Pickers; + +public sealed class ModbusAddressBuilderTests +{ + [Theory] + [InlineData("Holding", 1, 1, "4x00001-1")] + [InlineData("Coil", 0, 1, "0x00000-1")] + [InlineData("Holding", 123, 4, "4x00123-4")] + [InlineData("DiscreteInput", 5, 1, "1x00005-1")] + [InlineData("Input", 99999, 125, "3x99999-125")] + public void Build_Canonical(string type, int offset, int length, string expected) + => ModbusAddressBuilder.Build(type, offset, length).ShouldBe(expected); + + [Fact] + public void Build_UnknownType_FallsBackToHolding() + => ModbusAddressBuilder.Build("Unknown", 1, 1).ShouldBe("4x00001-1"); +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/S7AddressBuilderTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/S7AddressBuilderTests.cs new file mode 100644 index 00000000..38f102df --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Pickers/S7AddressBuilderTests.cs @@ -0,0 +1,20 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Pickers; + +public sealed class S7AddressBuilderTests +{ + [Theory] + [InlineData("DB", 10, 20, "REAL", "DB10.DBD20:REAL")] + [InlineData("DB", 1, 0, "X", "DB1.DBX0.0:X")] + [InlineData("DB", 5, 4, "W", "DB5.DBW4:W")] + [InlineData("DB", 2, 6, "B", "DB2.DBB6:B")] + [InlineData("M", 1, 0, "X", "M0.0:X")] + [InlineData("M", 1, 4, "W", "M4:W")] + [InlineData("I", 1, 0, "B", "I0:B")] + [InlineData("Q", 1, 2, "W", "Q2:W")] + public void Build_Canonical(string area, int dbNumber, int offset, string s7Type, string expected) + => S7AddressBuilder.Build(area, dbNumber, offset, s7Type).ShouldBe(expected); +}