@page "/clusters/{ClusterId}/drivers/new/focas" @attribute [Microsoft.AspNetCore.Authorization.Authorize] @rendermode RenderMode.InteractiveServer @using Microsoft.AspNetCore.Components.Forms @using Microsoft.EntityFrameworkCore @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @using ZB.MOM.WW.OtOpcUa.Driver.FOCAS @inject IDbContextFactory DbFactory @inject NavigationManager Nav

@(IsNew ? "New Fanuc FOCAS driver" : "Edit Fanuc FOCAS driver") · @ClusterId

Cancel
@if (!_loaded) {

Loading…

} else if (!IsNew && _existing is null) {
Driver instance @DriverInstanceId was not found in cluster @ClusterId.
} else { @* Connection *@
Connection
Per-operation timeout. Default 2 s.
@* Probe *@
Connectivity probe
Test Connect timeout (1–60 s).
@* Alarm projection *@
Alarm projection
Surfaces FOCAS alarms via IAlarmSource.
One cnc_rdalmmsg2 call per device per tick. Default 2 s.
@* Handle recycle *@
Handle recycle
Proactive FWLIB session recycle to prevent handle pool exhaustion. Default off.
Typical: 30 min (shared pool) or 360 min (single client).
@* Fixed tree *@
Fixed-node tree
Exposes Identity/, Axes/, etc. from cnc_sysinfo/cnc_rdaxisname/cnc_rddynamic2. Default off.
cnc_rddynamic2 cadence per axis. Default 250 ms.
Program/mode info cadence. 0 = disabled. Default 1 s.
Power-on/cutting/cycle timer cadence. 0 = disabled. Default 30 s.
@* Devices — read-only JSON view *@
Devices
Each device represents one CNC. Device list editor (with CNC series selector) coming in a follow-up phase. Format: [{"hostAddress":"192.168.0.10:8193","deviceName":"CNC1","series":"Thirty_i"}]
@if (_form.DevicesJson is not null) {
@_form.DevicesJson
} else {

No devices configured.

}
@* Tags — read-only JSON view *@
Tags
Tag list editor coming in a follow-up phase. Tags reference device host addresses and FOCAS address strings (e.g. X0.0, R100, PARAM:1815/0, MACRO:500).
@if (_form.TagsJson is not null) {
@_form.TagsJson
} else {

No tags configured.

}
} @code { [Parameter] public string ClusterId { get; set; } = ""; [Parameter] public string? DriverInstanceId { get; set; } private const string DriverTypeKey = "Focas"; 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, _busy; private string? _error; 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() { DriverType = DriverTypeKey, NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true }; _form = FormModel.FromOptions(new FocasDriverOptions()); } 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 FocasDriverOptions(); _form = FormModel.FromOptions(opts); _form.ResilienceConfig = _existing.ResilienceConfig; _form.RowVersion = _existing.RowVersion; } } _loaded = true; } private async Task SubmitAsync() { _busy = true; _error = null; try { var opts = _form.ToOptions(); var configJson = System.Text.Json.JsonSerializer.Serialize(opts, _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 static FocasDriverOptions? TryDeserialize(string json) { try { return System.Text.Json.JsonSerializer.Deserialize(json, _jsonOpts); } catch { return null; } } public sealed class FormModel { // Connection public int TimeoutSeconds { get; set; } = 2; // Probe public bool ProbeEnabled { get; set; } = true; public int ProbeIntervalSeconds { get; set; } = 5; public int ProbeTimeoutSeconds { get; set; } = 2; public int AdminProbeTimeoutSeconds { get; set; } = 10; // Alarm projection public bool AlarmProjectionEnabled { get; set; } = false; public int AlarmProjectionPollIntervalSeconds { get; set; } = 2; // Handle recycle public bool HandleRecycleEnabled { get; set; } = false; public int HandleRecycleIntervalMinutes { get; set; } = 60; // Fixed tree public bool FixedTreeEnabled { get; set; } = false; public int FixedTreePollIntervalMs { get; set; } = 250; public int FixedTreeProgramPollIntervalSeconds { get; set; } = 1; public int FixedTreeTimerPollIntervalSeconds { get; set; } = 30; // Collections JSON view (read-only) public string? DevicesJson { get; set; } public string? TagsJson { get; set; } // Preserved originals (round-tripped unchanged) private IReadOnlyList _devices = []; private IReadOnlyList _tags = []; // Common public string? ResilienceConfig { get; set; } public byte[] RowVersion { get; set; } = []; private static readonly System.Text.Json.JsonSerializerOptions _displayOpts = new() { WriteIndented = true, PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase, }; public static FormModel FromOptions(FocasDriverOptions o) { var m = new FormModel { TimeoutSeconds = (int)o.Timeout.TotalSeconds, ProbeEnabled = o.Probe.Enabled, ProbeIntervalSeconds = (int)o.Probe.Interval.TotalSeconds, ProbeTimeoutSeconds = (int)o.Probe.Timeout.TotalSeconds, AdminProbeTimeoutSeconds = o.ProbeTimeoutSeconds, AlarmProjectionEnabled = o.AlarmProjection.Enabled, AlarmProjectionPollIntervalSeconds = (int)o.AlarmProjection.PollInterval.TotalSeconds, HandleRecycleEnabled = o.HandleRecycle.Enabled, HandleRecycleIntervalMinutes = (int)o.HandleRecycle.Interval.TotalMinutes, FixedTreeEnabled = o.FixedTree.Enabled, FixedTreePollIntervalMs = (int)o.FixedTree.PollInterval.TotalMilliseconds, FixedTreeProgramPollIntervalSeconds = (int)o.FixedTree.ProgramPollInterval.TotalSeconds, FixedTreeTimerPollIntervalSeconds = (int)o.FixedTree.TimerPollInterval.TotalSeconds, _devices = o.Devices, _tags = o.Tags, }; m.DevicesJson = o.Devices.Count == 0 ? null : System.Text.Json.JsonSerializer.Serialize(o.Devices, _displayOpts); m.TagsJson = o.Tags.Count == 0 ? null : System.Text.Json.JsonSerializer.Serialize(o.Tags, _displayOpts); return m; } public FocasDriverOptions ToOptions() => new() { Timeout = TimeSpan.FromSeconds(TimeoutSeconds), Probe = new FocasProbeOptions { Enabled = ProbeEnabled, Interval = TimeSpan.FromSeconds(ProbeIntervalSeconds), Timeout = TimeSpan.FromSeconds(ProbeTimeoutSeconds), }, ProbeTimeoutSeconds = AdminProbeTimeoutSeconds, AlarmProjection = new FocasAlarmProjectionOptions { Enabled = AlarmProjectionEnabled, PollInterval = TimeSpan.FromSeconds(AlarmProjectionPollIntervalSeconds), }, HandleRecycle = new FocasHandleRecycleOptions { Enabled = HandleRecycleEnabled, Interval = TimeSpan.FromMinutes(HandleRecycleIntervalMinutes), }, FixedTree = new FocasFixedTreeOptions { Enabled = FixedTreeEnabled, PollInterval = TimeSpan.FromMilliseconds(FixedTreePollIntervalMs), ProgramPollInterval = TimeSpan.FromSeconds(FixedTreeProgramPollIntervalSeconds), TimerPollInterval = TimeSpan.FromSeconds(FixedTreeTimerPollIntervalSeconds), }, Devices = _devices, Tags = _tags, }; } }