Merge #608/#602/#596: stale WaveC TODOs resolved, FOCAS/TwinCAT factory logging, Modbus health asymmetry documented, AbCip browsePageDelayMs field, Device1 by-design

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 06:53:59 -04:00
17 changed files with 192 additions and 50 deletions
@@ -46,6 +46,11 @@
</InputSelect>
<div class="form-text">Which protocol stack talks to the controller. Default omits the setting so the node-level <code>Drivers:AbCip:DefaultTransport</code> picks (Native unless an operator pinned it); takes effect on the next deploy.</div>
</div>
<div class="col-md-3">
<label class="form-label" for="abcipBrowsePageDelay">Browse page delay (ms)</label>
<InputNumber @bind-Value="_form.BrowsePageDelayMs" @bind-Value:after="EmitAsync" class="form-control form-control-sm" id="abcipBrowsePageDelay" />
<div class="form-text">Pause between controller symbol-browse pages (<code>browsePageDelayMs</code>). Default 5 ms; 0 disables the pause.</div>
</div>
</div>
</div>
</section>
@@ -147,7 +152,7 @@
/// <summary>The factory binds integer-millisecond keys, not the TimeSpan strings the typed options serialise (#576).</summary>
internal static readonly (string TimeSpanPath, string MsPath)[] TimeSpanToMs =
[
("timeout", "timeoutMs"), ("alarmPollInterval", "alarmPollIntervalMs"),
("timeout", "timeoutMs"), ("alarmPollInterval", "alarmPollIntervalMs"), ("browsePageDelay", "browsePageDelayMs"),
("probe.interval", "probe.intervalMs"), ("probe.timeout", "probe.timeoutMs"),
];
@@ -215,6 +220,8 @@
public int MaxConcurrentReads { get; set; } = 8;
public bool EnableControllerBrowse { get; set; } = false;
public bool EnableDeclarationOnlyUdtGrouping { get; set; } = false;
// Milliseconds between controller symbol-browse pages (factory key browsePageDelayMs; default 5).
public int BrowsePageDelayMs { get; set; } = 5;
// Alarm projection
public bool EnableAlarmProjection { get; set; } = false;
@@ -241,6 +248,7 @@
f._loadedAlarmPoll = Ms(json, "alarmPollIntervalMs") ?? f._loadedAlarmPoll;
f._loadedProbeInterval = Ms(json, "probe.intervalMs") ?? f._loadedProbeInterval;
f._loadedProbeTimeout = Ms(json, "probe.timeoutMs") ?? f._loadedProbeTimeout;
f.BrowsePageDelayMs = DriverConfigMs.Read(json, "browsePageDelayMs") ?? f.BrowsePageDelayMs;
f.TimeoutSeconds = DriverConfigMs.ToSeconds(f._loadedTimeout ?? o.Timeout);
f.AlarmPollIntervalSeconds = DriverConfigMs.ToSeconds(f._loadedAlarmPoll ?? o.AlarmPollInterval);
f.ProbeIntervalSeconds = DriverConfigMs.ToSeconds(f._loadedProbeInterval ?? o.Probe.Interval);
@@ -259,6 +267,7 @@
MaxConcurrentReads = o.MaxConcurrentReads,
EnableControllerBrowse = o.EnableControllerBrowse,
EnableDeclarationOnlyUdtGrouping = o.EnableDeclarationOnlyUdtGrouping,
BrowsePageDelayMs = (int)o.BrowsePageDelay.TotalMilliseconds,
EnableAlarmProjection = o.EnableAlarmProjection,
AlarmPollIntervalSeconds = (int)o.AlarmPollInterval.TotalSeconds,
ProbeEnabled = o.Probe.Enabled,
@@ -286,6 +295,7 @@
EnableAlarmProjection = EnableAlarmProjection,
AlarmPollInterval = DriverConfigMs.Save(AlarmPollIntervalSeconds, _loadedAlarmPoll),
EnableDeclarationOnlyUdtGrouping = EnableDeclarationOnlyUdtGrouping,
BrowsePageDelay = TimeSpan.FromMilliseconds(Math.Max(0, BrowsePageDelayMs)),
ProbeTimeoutSeconds = AdminProbeTimeoutSeconds,
};
}
@@ -12,9 +12,9 @@
directly (New folder/driver/group, Rename, Delete, Toggle) via the small dialogs
built alongside — RawNameDialog / RawConfirmDialog / RawDriverTypeDialog — and
refreshes the affected subtree afterwards (reload the container for create-under
ops, the parent for item-level ops). Only Browse device stays a placeholder (Wave C
owns it). The former RawStubModal is repurposed here as the shared message surface
for delete-blockers, rename warnings, and the Browse "coming soon" note. This
ops, the parent for item-level ops). Browse device opens the discovery modal
(OnBrowseDevice). The former RawStubModal is repurposed here as the shared message
surface for delete-blockers and rename warnings. This
component is a single instance that recurses via the RenderNode RenderFragment, so it
owns one set of modals + one injected service for the whole tree. *@
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
@@ -294,8 +294,8 @@
}
// ---------------------------------------------------------------------------
// Per-kind context menus. Every OnClick opens a placeholder in this wave; the
// named handlers below are the seam Wave B/C replaces with the real modals.
// Per-kind context menus. Every OnClick dispatches to a named handler below,
// each of which opens the real modal or calls the service.
// ---------------------------------------------------------------------------
private IReadOnlyList<ContextMenuItem> MenuFor(RawNode node)
@@ -422,8 +422,8 @@
// ---------------------------------------------------------------------------
// Named action handlers — each opens the real modal/dialog or calls the service
// directly, then refreshes the affected subtree. Only OnBrowseDevice is a
// placeholder (Wave C owns device browse).
// directly, then refreshes the affected subtree. OnBrowseDevice opens the
// discovery modal.
// ---------------------------------------------------------------------------
// --- Folder / Cluster ---
@@ -511,8 +511,9 @@ public sealed class RawTreeService(IDbContextFactory<OtOpcUaConfigDbContext> dbF
DriverConfig = string.IsNullOrWhiteSpace(driverConfigJson) ? "{}" : driverConfigJson,
});
// Auto-create the default device so a freshly-created driver has a child to show. Wave B (WP3)
// refines device/driver config authoring; here the default device carries an empty config.
// Auto-create the default device so a freshly-created driver has a child to show. By design
// (#596): every driver needs at least one device for browse + tags, and DriverDeviceConfigMerger treats
// the sole device's DeviceConfig as the endpoint. It is renamable/deletable and carries an empty config.
db.Devices.Add(new Device
{
DeviceId = NewId("DEV"),
@@ -214,7 +214,7 @@ public static class DriverFactoryBootstrap
Driver.AbCip.AbCipDriverFactoryExtensions.Register(registry, loggerFactory, abCipDefaultTransport);
Driver.AbLegacy.AbLegacyDriverFactoryExtensions.Register(registry, loggerFactory);
Driver.Calculation.CalculationDriverFactoryExtensions.Register(registry, loggerFactory, scriptRoot);
Driver.FOCAS.FocasDriverFactoryExtensions.Register(registry);
Driver.FOCAS.FocasDriverFactoryExtensions.Register(registry, loggerFactory);
Driver.Galaxy.GalaxyDriverFactoryExtensions.Register(registry, secretResolver, loggerFactory);
Driver.Modbus.ModbusDriverFactoryExtensions.Register(registry, loggerFactory);
Driver.Mqtt.MqttDriverFactoryExtensions.Register(registry, loggerFactory, primaryRoleSource);
@@ -228,6 +228,6 @@ public static class DriverFactoryBootstrap
Driver.OpcUaClient.OpcUaClientDriverFactoryExtensions.Register(registry, loggerFactory, secretResolver);
Driver.S7.S7DriverFactoryExtensions.Register(registry);
Driver.Sql.SqlDriverFactoryExtensions.Register(registry, loggerFactory);
Driver.TwinCAT.TwinCATDriverFactoryExtensions.Register(registry);
Driver.TwinCAT.TwinCATDriverFactoryExtensions.Register(registry, loggerFactory);
}
}