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:
@@ -163,8 +163,8 @@ public sealed record AbCipDeviceOptions(
|
||||
/// threaded in from <see cref="RawTagEntry.DeviceName"/> by the driver at table-build time (the mapper
|
||||
/// leaves it empty; device placement is no longer in the TagConfig blob). Resolved against the driver's
|
||||
/// devices by matching <see cref="AbCipDeviceOptions.DeviceName"/> or <see cref="AbCipDeviceOptions.HostAddress"/>.
|
||||
/// TODO(v3 WaveC): once the connection host moves to the Device row's DeviceConfig this is a pure logical
|
||||
/// device name; the name still reads <c>DeviceHostAddress</c> to avoid churn across the driver + CLI.</param>
|
||||
/// The host-address match is deliberate (the Client CLI passes the host as the device name); the name
|
||||
/// reads <c>DeviceHostAddress</c> to avoid churn across the driver + CLI.</param>
|
||||
/// <param name="TagPath">Logix symbolic path (controller or program scope).</param>
|
||||
/// <param name="DataType">Logix atomic type, or <see cref="AbCipDataType.Structure"/> for UDT-typed tags.</param>
|
||||
/// <param name="Writable">When <c>true</c> and the tag's ExternalAccess permits writes, IWritable routes writes here.</param>
|
||||
|
||||
@@ -101,8 +101,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
private readonly Dictionary<string, DeviceState> _devices = new(StringComparer.OrdinalIgnoreCase);
|
||||
// Tag-routing index: a tag's device routing key (DeviceName ?? HostAddress, plus the HostAddress alias)
|
||||
// → its DeviceState. A tag resolves to its device by DeviceName-or-host so multi-device routing keys on
|
||||
// the RawPath's device segment. TODO(v3 WaveC): collapses to a pure DeviceName index once the connection
|
||||
// host moves into the Device row's DeviceConfig.
|
||||
// the RawPath's device segment. The HostAddress alias is deliberate (Client CLI + tests route by host);
|
||||
// _options.Devices is already the merged per-device DeviceConfig.
|
||||
private readonly Dictionary<string, DeviceState> _devicesByRoutingKey = new(StringComparer.OrdinalIgnoreCase);
|
||||
// v3 authored RawPath → definition table (parents + fanned-out UDT members), keyed case-sensitive
|
||||
// ordinal. Built in InitializeAsync from _options.RawTags via the pure mapper.
|
||||
@@ -1716,8 +1716,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
private static bool IsArrayTag(AbCipTagDefinition tag) => tag.IsArray || tag.ElementCount > 1;
|
||||
|
||||
// A tag routes to a device when its device routing key (DeviceHostAddress, threaded from the
|
||||
// RawTagEntry.DeviceName) matches the device's DeviceName or its host address. TODO(v3 WaveC):
|
||||
// collapses to a DeviceName-only match once the connection host lives on the Device row's DeviceConfig.
|
||||
// RawTagEntry.DeviceName) matches the device's DeviceName or its host address (the host alias is
|
||||
// deliberate: the Client CLI routes by host).
|
||||
private static bool RoutesToDevice(AbCipTagDefinition tag, AbCipDeviceOptions device) =>
|
||||
string.Equals(tag.DeviceHostAddress, device.HostAddress, StringComparison.OrdinalIgnoreCase)
|
||||
|| (!string.IsNullOrEmpty(device.DeviceName)
|
||||
|
||||
@@ -637,12 +637,11 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
/// read/write surfaces <c>BadNodeIdUnknown</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TODO(v3 WaveC): the live device host address must ultimately come from the owning
|
||||
/// <c>Device</c> row's <c>DeviceConfig</c> (resolved by the deploy artifact / bootstrapper) rather
|
||||
/// than from a configured <see cref="AbLegacyDriverOptions.Devices"/> entry. Until Wave C threads
|
||||
/// the Device row through, the best available option is to route by matching the delivered
|
||||
/// <see cref="RawTagEntry.DeviceName"/> against <c>_options.Devices</c> and reusing that entry's
|
||||
/// <c>HostAddress</c> as the connection key.
|
||||
/// <see cref="AbLegacyDriverOptions.Devices"/> is already the per-device <c>DeviceConfig</c>
|
||||
/// (<c>DriverDeviceConfigMerger</c> rebuilds it at deploy time), so routing matches the delivered
|
||||
/// <see cref="RawTagEntry.DeviceName"/> against <c>_options.Devices</c> and reuses that entry's
|
||||
/// <c>HostAddress</c> as the connection key. The HostAddress fallback is deliberate: the Client CLI
|
||||
/// passes the host as the DeviceName.
|
||||
/// </remarks>
|
||||
/// <param name="deviceName">The owning device name delivered on the <see cref="RawTagEntry"/>.</param>
|
||||
/// <returns>The matched device's <c>HostAddress</c>; empty when unroutable.</returns>
|
||||
|
||||
@@ -224,9 +224,9 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
_driverInstanceId, entry.RawPath);
|
||||
continue;
|
||||
}
|
||||
// TODO(v3 WaveC): the live device host must come from the tag's Device row's DeviceConfig
|
||||
// (keyed by the RawPath's device segment). Until Wave C threads DeviceConfig through, recover
|
||||
// the host by matching the entry's DeviceName against the configured Devices list.
|
||||
// options.Devices already IS the per-device DeviceConfig (DriverDeviceConfigMerger rebuilds it at
|
||||
// deploy time, one object per Device row with DeviceName = the RawPath device segment), so the
|
||||
// host is recovered by DeviceName against that list.
|
||||
var deviceHost = ResolveDeviceHost(entry.DeviceName);
|
||||
if (deviceHost is null)
|
||||
throw new InvalidOperationException(
|
||||
@@ -382,11 +382,10 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
internal bool IsParsedAddressCached(string reference) =>
|
||||
_parsedAddressesByTagName.ContainsKey(reference);
|
||||
|
||||
// TODO(v3 WaveC): the live device host must come from the tag's Device row's DeviceConfig (keyed by
|
||||
// the RawPath's device segment). Until Wave C threads DeviceConfig through, recover the host by matching
|
||||
// the RawTagEntry's DeviceName against the configured Devices list (by DeviceName, falling back to
|
||||
// HostAddress; and, for a single-device driver, an empty DeviceName routes to the sole device). Returns
|
||||
// null when no device matches — the caller fails the tag fast (Driver.FOCAS-003).
|
||||
// Recovers the host by matching the RawTagEntry's DeviceName against the configured Devices list (which
|
||||
// DriverDeviceConfigMerger fills from each Device row's DeviceConfig). By DeviceName, falling back to
|
||||
// HostAddress (the Client CLI passes the host as the DeviceName); for a single-device driver an empty
|
||||
// DeviceName routes to the sole device. Returns null when no device matches — the caller fails the tag fast (Driver.FOCAS-003).
|
||||
private string? ResolveDeviceHost(string deviceName)
|
||||
{
|
||||
foreach (var d in _options.Devices)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire;
|
||||
@@ -34,10 +35,12 @@ public static class FocasDriverFactoryExtensions
|
||||
/// Throws if 'FOCAS' is already registered — single-instance per process.
|
||||
/// </summary>
|
||||
/// <param name="registry">The driver factory registry to register with.</param>
|
||||
public static void Register(DriverFactoryRegistry registry)
|
||||
/// <param name="loggerFactory">Optional logger factory; without it the driver and its wire client run on
|
||||
/// the null logger and none of their diagnostics reach the node log (CONV-3).</param>
|
||||
public static void Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(registry);
|
||||
registry.Register(DriverTypeName, CreateInstance);
|
||||
registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -46,18 +49,21 @@ public static class FocasDriverFactoryExtensions
|
||||
/// <param name="driverInstanceId">The unique driver instance identifier.</param>
|
||||
/// <param name="driverConfigJson">The driver configuration JSON string.</param>
|
||||
/// <returns>A configured <see cref="FocasDriver"/> instance.</returns>
|
||||
internal static FocasDriver CreateInstance(string driverInstanceId, string driverConfigJson)
|
||||
/// <param name="loggerFactory">Optional logger factory for the driver and its wire client.</param>
|
||||
internal static FocasDriver CreateInstance(string driverInstanceId, string driverConfigJson,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
var binding = ParseBinding(driverInstanceId, driverConfigJson);
|
||||
var binding = ParseBinding(driverInstanceId, driverConfigJson, loggerFactory);
|
||||
return new FocasDriver(
|
||||
binding.Options,
|
||||
driverInstanceId,
|
||||
binding.ClientFactory,
|
||||
loggerFactory?.CreateLogger<FocasDriver>(),
|
||||
// Hand the driver the SAME parse it was built with, so InitializeAsync can re-derive EVERY
|
||||
// config-derived dependency on a reinitialize — options AND the backend client factory
|
||||
// together (#516). Re-parsing options alone would run a new device/tag set against the old
|
||||
// backend, which is worse than not re-parsing at all.
|
||||
rebind: json => ParseBinding(driverInstanceId, json));
|
||||
rebind: json => ParseBinding(driverInstanceId, json, loggerFactory));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -68,8 +74,10 @@ public static class FocasDriverFactoryExtensions
|
||||
/// </summary>
|
||||
/// <param name="driverInstanceId">The unique driver instance identifier.</param>
|
||||
/// <param name="driverConfigJson">The driver configuration JSON string.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory handed to the wire backend.</param>
|
||||
/// <returns>The parsed options + backend factory.</returns>
|
||||
internal static FocasDriverBinding ParseBinding(string driverInstanceId, string driverConfigJson)
|
||||
internal static FocasDriverBinding ParseBinding(string driverInstanceId, string driverConfigJson,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
||||
@@ -109,7 +117,7 @@ public static class FocasDriverFactoryExtensions
|
||||
HandleRecycle = BuildHandleRecycle(dto.HandleRecycle),
|
||||
};
|
||||
|
||||
return new FocasDriverBinding(options, BuildClientFactory(dto, driverInstanceId));
|
||||
return new FocasDriverBinding(options, BuildClientFactory(dto, driverInstanceId, loggerFactory));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -131,14 +139,15 @@ public static class FocasDriverFactoryExtensions
|
||||
/// </summary>
|
||||
/// <param name="dto">The driver configuration DTO.</param>
|
||||
/// <param name="driverInstanceId">The driver instance identifier for error reporting.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory for the wire client.</param>
|
||||
/// <returns>A configured <see cref="IFocasClientFactory"/> instance.</returns>
|
||||
internal static IFocasClientFactory BuildClientFactory(
|
||||
FocasDriverConfigDto dto, string driverInstanceId)
|
||||
FocasDriverConfigDto dto, string driverInstanceId, ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
var backend = (dto.Backend ?? "wire").Trim().ToLowerInvariant();
|
||||
return backend switch
|
||||
{
|
||||
"wire" => new WireFocasClientFactory(),
|
||||
"wire" => new WireFocasClientFactory(loggerFactory?.CreateLogger<FocasWireClient>()),
|
||||
"unimplemented" or "none" or "stub" => new UnimplementedFocasClientFactory(),
|
||||
_ => throw new InvalidOperationException(
|
||||
$"FOCAS driver config for '{driverInstanceId}' has unknown Backend '{dto.Backend}'. " +
|
||||
|
||||
@@ -819,9 +819,9 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
/// <summary>
|
||||
/// Resolve a <see cref="RawTagEntry.DeviceName"/> to the device's live host address
|
||||
/// (<see cref="TwinCATTagDefinition.DeviceHostAddress"/>) used to route reads/writes/subscriptions
|
||||
/// to the right connection. <b>TODO(v3 WaveC):</b> the live device host must come from the
|
||||
/// <c>Device</c> row's <c>DeviceConfig</c> (the deploy artifact will carry it); today we resolve
|
||||
/// the DeviceName against this driver's own <see cref="TwinCATDriverOptions.Devices"/> list —
|
||||
/// to the right connection. The deploy-time
|
||||
/// <c>DriverDeviceConfigMerger</c> already fills <see cref="TwinCATDriverOptions.Devices"/> from each
|
||||
/// <c>Device</c> row's <c>DeviceConfig</c>, so we resolve the DeviceName against that list —
|
||||
/// first by <see cref="TwinCATDeviceOptions.DeviceName"/>, then by
|
||||
/// <see cref="TwinCATDeviceOptions.HostAddress"/> (callers that route by host, e.g. the Client
|
||||
/// CLI, pass the host string as the DeviceName). An empty DeviceName routes to the sole device
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
|
||||
@@ -16,21 +17,25 @@ public static class TwinCATDriverFactoryExtensions
|
||||
|
||||
/// <summary>Registers the TwinCAT driver factory with the provided registry.</summary>
|
||||
/// <param name="registry">The driver factory registry to register with.</param>
|
||||
public static void Register(DriverFactoryRegistry registry)
|
||||
/// <param name="loggerFactory">Optional logger factory; without it the driver runs on the null logger
|
||||
/// and none of its diagnostics reach the node log (CONV-3).</param>
|
||||
public static void Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(registry);
|
||||
registry.Register(DriverTypeName, CreateInstance);
|
||||
registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory));
|
||||
}
|
||||
|
||||
/// <summary>Creates a TwinCAT driver instance from the provided configuration.</summary>
|
||||
/// <param name="driverInstanceId">The driver instance identifier.</param>
|
||||
/// <param name="driverConfigJson">The driver configuration as JSON.</param>
|
||||
/// <returns>The constructed <see cref="TwinCATDriver"/> instance.</returns>
|
||||
internal static TwinCATDriver CreateInstance(string driverInstanceId, string driverConfigJson)
|
||||
/// <param name="loggerFactory">Optional logger factory for the driver instance.</param>
|
||||
internal static TwinCATDriver CreateInstance(string driverInstanceId, string driverConfigJson,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
||||
var options = ParseOptions(driverConfigJson, driverInstanceId);
|
||||
return new TwinCATDriver(options, driverInstanceId);
|
||||
return new TwinCATDriver(options, driverInstanceId, logger: loggerFactory?.CreateLogger<TwinCATDriver>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user