@page "/clusters/{ClusterId}/drivers/new/abcip" @attribute [Microsoft.AspNetCore.Authorization.Authorize] @rendermode RenderMode.InteractiveServer @using Microsoft.AspNetCore.Components.Forms @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 @inject IDbContextFactory DbFactory @inject NavigationManager Nav

@(IsNew ? "New AB CIP driver" : "Edit AB CIP driver") · @ClusterId

Cancel
@if (!_loaded) {

Loading…

} else if (!IsNew && _existing is null) {
Driver instance @DriverInstanceId was not found in cluster @ClusterId.
} else { @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId)) { }
@* Operation timeout *@
Operation settings
Default libplctag call timeout. Default 2 s.
Walk the Logix symbol table and surface globals under Discovered/.
Only enable when UDT member declaration order matches controller compiled layout.
@* Alarm projection *@
Alarm projection
Surfaces ALMD tags as alarm conditions via IAlarmSource. Default off.
Default 1 s.
@* Connectivity probe *@
Connectivity probe
Default 5 s.
Default 2 s.
Required when probe is enabled. Leave blank and an operator warning is logged.
Max 60. Used by Test Connect. Default 5.
@* Devices *@ Host addressPLC familyDevice name @d.HostAddress@d.PlcFamily @(string.IsNullOrWhiteSpace(d.DeviceName) ? "—" : d.DeviceName)
@* Tags *@ NameDeviceTag pathTypeWritable @t.Name@t.DeviceHostAddress @t.TagPath@t.DataType@(t.Writable ? "yes" : "no")
} @code { [Parameter] public string ClusterId { get; set; } = ""; [Parameter] public string? DriverInstanceId { get; set; } private const string DriverTypeKey = "AbCip"; private bool IsNew => string.IsNullOrEmpty(DriverInstanceId); private static readonly System.Text.Json.JsonSerializerOptions _jsonOpts = new() { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, UnmappedMemberHandling = System.Text.Json.Serialization.JsonUnmappedMemberHandling.Skip, WriteIndented = false, }; private FormModel _form = new(); private DriverIdentitySection.DriverIdentityModel _identityModel = new() { DriverType = DriverTypeKey }; private DriverInstance? _existing; private List _namespaces = new(); 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 Devices/Tags are collections — edited via the CollectionEditor modal. private List _devices = []; private List _tags = []; protected override async Task OnInitializedAsync() { await using var db = await DbFactory.CreateDbContextAsync(); _namespaces = await db.Namespaces.AsNoTracking() .Where(n => n.ClusterId == ClusterId) .OrderBy(n => n.NamespaceId) .ToListAsync(); if (IsNew) { _identityModel = new() { DriverInstanceId = "", Name = "", DriverType = DriverTypeKey, NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true, }; _form = new FormModel(); } else { _existing = await db.DriverInstances.AsNoTracking() .FirstOrDefaultAsync(d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId); if (_existing is not null) { _identityModel = new() { DriverInstanceId = _existing.DriverInstanceId, Name = _existing.Name, DriverType = _existing.DriverType, NamespaceId = _existing.NamespaceId, Enabled = _existing.Enabled, }; var opts = TryDeserialize(_existing.DriverConfig) ?? new AbCipDriverOptions(); _form = FormModel.FromOptions(opts); _form.ResilienceConfig = _existing.ResilienceConfig; _form.RowVersion = _existing.RowVersion; _devices = opts.Devices.Select(AbCipDeviceRow.FromDefinition).ToList(); _tags = opts.Tags.Select(AbCipTagRow.FromDefinition).ToList(); } } _loaded = true; } private async Task SubmitAsync() { _busy = true; _error = null; try { var configJson = System.Text.Json.JsonSerializer.Serialize( _form.ToOptions( _devices.Select(r => r.ToDefinition()).ToList(), _tags.Select(r => r.ToDefinition()).ToList()), _jsonOpts); await using var db = await DbFactory.CreateDbContextAsync(); if (IsNew) { if (await db.DriverInstances.AnyAsync(d => d.DriverInstanceId == _identityModel.DriverInstanceId)) { _error = $"Driver instance '{_identityModel.DriverInstanceId}' already exists."; return; } db.DriverInstances.Add(new DriverInstance { DriverInstanceId = _identityModel.DriverInstanceId, ClusterId = ClusterId, NamespaceId = _identityModel.NamespaceId, Name = _identityModel.Name, DriverType = DriverTypeKey, Enabled = _identityModel.Enabled, DriverConfig = configJson, ResilienceConfig = string.IsNullOrWhiteSpace(_form.ResilienceConfig) ? null : _form.ResilienceConfig, }); } else { var entity = await db.DriverInstances.FirstOrDefaultAsync( d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId); if (entity is null) { _error = "Row no longer exists."; return; } db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; entity.NamespaceId = _identityModel.NamespaceId; entity.Name = _identityModel.Name; entity.Enabled = _identityModel.Enabled; entity.DriverConfig = configJson; entity.ResilienceConfig = string.IsNullOrWhiteSpace(_form.ResilienceConfig) ? null : _form.ResilienceConfig; } await db.SaveChangesAsync(); Nav.NavigateTo($"/clusters/{ClusterId}/drivers"); } catch (DbUpdateConcurrencyException) { _error = "Another user changed this driver instance while you were editing. Reload to see the latest values, then re-apply your changes."; } catch (Exception ex) { _error = ex.Message; } finally { _busy = false; } } private async Task DeleteAsync() { if (IsNew) return; _busy = true; _error = null; try { await using var db = await DbFactory.CreateDbContextAsync(); var entity = await db.DriverInstances.FirstOrDefaultAsync( d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId); if (entity is null) { Nav.NavigateTo($"/clusters/{ClusterId}/drivers"); return; } db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion; db.DriverInstances.Remove(entity); await db.SaveChangesAsync(); Nav.NavigateTo($"/clusters/{ClusterId}/drivers"); } catch (DbUpdateConcurrencyException) { _error = "Another user changed this driver instance while you were viewing it. Reload before deleting."; } catch (Exception ex) { _error = $"Delete failed: {ex.Message}. (Likely because equipment/tags still reference this driver — remove them first.)"; } finally { _busy = false; } } private string SerializeCurrentConfig() => System.Text.Json.JsonSerializer.Serialize( _form.ToOptions( _devices.Select(r => r.ToDefinition()).ToList(), _tags.Select(r => r.ToDefinition()).ToList()), _jsonOpts); private static AbCipDriverOptions? TryDeserialize(string json) { try { return System.Text.Json.JsonSerializer.Deserialize(json, _jsonOpts); } catch { return null; } } // Mutable VM for the modal editor — AbCipDeviceOptions is an immutable record. public sealed class AbCipDeviceRow { public string HostAddress { get; set; } = ""; public AbCipPlcFamily PlcFamily { get; set; } = AbCipPlcFamily.ControlLogix; public string? DeviceName { get; set; } // Original record (null for newly-added rows). Preserves fields the editor doesn't expose // (AllowPacking, ConnectionSize) across a load→save. private AbCipDeviceOptions? _source; public AbCipDeviceRow Clone() => (AbCipDeviceRow)MemberwiseClone(); // _source is an immutable record ref — safe to share public static AbCipDeviceRow FromDefinition(AbCipDeviceOptions d) => new() { HostAddress = d.HostAddress, PlcFamily = d.PlcFamily, DeviceName = d.DeviceName, _source = d, }; public AbCipDeviceOptions ToDefinition() { var baseDef = _source ?? new AbCipDeviceOptions(HostAddress.Trim(), PlcFamily); return baseDef with { HostAddress = HostAddress.Trim(), PlcFamily = PlcFamily, DeviceName = string.IsNullOrWhiteSpace(DeviceName) ? null : DeviceName.Trim(), }; } public static string? ValidateRow(AbCipDeviceRow row, IReadOnlyList all, int? editIndex) { if (string.IsNullOrWhiteSpace(row.HostAddress)) return "Host address is required."; for (var i = 0; i < all.Count; i++) if (i != editIndex && string.Equals(all[i].HostAddress, row.HostAddress, StringComparison.OrdinalIgnoreCase)) return $"Duplicate device host address '{row.HostAddress}'."; return null; } } // Mutable VM for the modal editor — AbCipTagDefinition is an immutable record. public sealed class AbCipTagRow { public string Name { get; set; } = ""; public string DeviceHostAddress { get; set; } = ""; public string TagPath { get; set; } = ""; public AbCipDataType DataType { get; set; } = AbCipDataType.DInt; public bool Writable { get; set; } = true; // Original record (null for newly-added rows). Preserves fields the editor doesn't expose // (WriteIdempotent, Members, SafetyTag) across a load→save. private AbCipTagDefinition? _source; public AbCipTagRow Clone() => (AbCipTagRow)MemberwiseClone(); // _source is an immutable record ref — safe to share public static AbCipTagRow FromDefinition(AbCipTagDefinition d) => new() { Name = d.Name, DeviceHostAddress = d.DeviceHostAddress, TagPath = d.TagPath, DataType = d.DataType, Writable = d.Writable, _source = d, }; public AbCipTagDefinition ToDefinition() { var baseDef = _source ?? new AbCipTagDefinition(Name.Trim(), DeviceHostAddress.Trim(), TagPath.Trim(), DataType); return baseDef with { Name = Name.Trim(), DeviceHostAddress = DeviceHostAddress.Trim(), TagPath = TagPath.Trim(), DataType = DataType, Writable = Writable, }; } public static string? ValidateRow(AbCipTagRow row, IReadOnlyList all, int? editIndex) { if (string.IsNullOrWhiteSpace(row.Name)) return "Name is required."; for (var i = 0; i < all.Count; i++) if (i != editIndex && string.Equals(all[i].Name, row.Name, StringComparison.OrdinalIgnoreCase)) return $"Duplicate tag name '{row.Name}'."; return null; } } // Flat mutable model — all scalar properties settable for Blazor @bind-Value. // Collections (Devices, Tags) are kept on the component and passed in on ToOptions(). public sealed class FormModel { // Operation public int TimeoutSeconds { get; set; } = 2; public bool EnableControllerBrowse { get; set; } = false; public bool EnableDeclarationOnlyUdtGrouping { get; set; } = false; // Alarm projection public bool EnableAlarmProjection { get; set; } = false; public int AlarmPollIntervalSeconds { get; set; } = 1; // Probe public bool ProbeEnabled { get; set; } = true; public int ProbeIntervalSeconds { get; set; } = 5; public int ProbeTimeoutSeconds { get; set; } = 2; public string? ProbeTagPath { get; set; } // Admin UI probe timeout public int AdminProbeTimeoutSeconds { get; set; } = 5; // Persistence public string? ResilienceConfig { get; set; } public byte[] RowVersion { get; set; } = []; public static FormModel FromOptions(AbCipDriverOptions o) => new() { TimeoutSeconds = (int)o.Timeout.TotalSeconds, EnableControllerBrowse = o.EnableControllerBrowse, EnableDeclarationOnlyUdtGrouping = o.EnableDeclarationOnlyUdtGrouping, EnableAlarmProjection = o.EnableAlarmProjection, AlarmPollIntervalSeconds = (int)o.AlarmPollInterval.TotalSeconds, ProbeEnabled = o.Probe.Enabled, ProbeIntervalSeconds = (int)o.Probe.Interval.TotalSeconds, ProbeTimeoutSeconds = (int)o.Probe.Timeout.TotalSeconds, ProbeTagPath = o.Probe.ProbeTagPath, AdminProbeTimeoutSeconds = o.ProbeTimeoutSeconds, }; public AbCipDriverOptions ToOptions( IReadOnlyList devices, IReadOnlyList tags) => new() { Devices = devices, Tags = tags, Probe = new AbCipProbeOptions { Enabled = ProbeEnabled, Interval = TimeSpan.FromSeconds(ProbeIntervalSeconds), Timeout = TimeSpan.FromSeconds(ProbeTimeoutSeconds), ProbeTagPath = string.IsNullOrWhiteSpace(ProbeTagPath) ? null : ProbeTagPath, }, Timeout = TimeSpan.FromSeconds(TimeoutSeconds), EnableControllerBrowse = EnableControllerBrowse, EnableAlarmProjection = EnableAlarmProjection, AlarmPollInterval = TimeSpan.FromSeconds(AlarmPollIntervalSeconds), EnableDeclarationOnlyUdtGrouping = EnableDeclarationOnlyUdtGrouping, ProbeTimeoutSeconds = AdminProbeTimeoutSeconds, }; } }