fix(driver-focas): resolve Low code-review findings (Driver.FOCAS-007,008,009,010,011)
- Driver.FOCAS-007: optional ILogger<FocasDriver> + alarm-projection logger; log Debug around every formerly-empty catch (probe / shutdown / fixed-tree / recycle / alarms-read / projection). - Driver.FOCAS-008: cache the parsed FocasAddress per tag at InitializeAsync; Read/WriteAsync look it up instead of re-parsing on every call. - Driver.FOCAS-009: ProbeLoopAsync now wraps client.ProbeAsync in a linked CTS honouring Probe.Timeout so a hung CNC socket can't block past the configured limit. - Driver.FOCAS-010: FocasOperationModeExtensions.ToText delegates to FocasOpMode.ToText — single canonical op-mode label surface. - Driver.FOCAS-011: FocasAlarmType constants are typed short to match the cnc_rdalmmsg2 wire field and the projection switch arms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
@@ -21,14 +23,16 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable
|
||||
{
|
||||
private readonly FocasDriver _driver;
|
||||
private readonly TimeSpan _pollInterval;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Dictionary<long, Subscription> _subs = new();
|
||||
private readonly Lock _subsLock = new();
|
||||
private long _nextId;
|
||||
|
||||
public FocasAlarmProjection(FocasDriver driver, TimeSpan pollInterval)
|
||||
public FocasAlarmProjection(FocasDriver driver, TimeSpan pollInterval, ILogger? logger = null)
|
||||
{
|
||||
_driver = driver;
|
||||
_pollInterval = pollInterval;
|
||||
_logger = logger ?? NullLogger.Instance;
|
||||
}
|
||||
|
||||
public Task<IAlarmSubscriptionHandle> SubscribeAsync(
|
||||
@@ -58,8 +62,10 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable
|
||||
{
|
||||
if (!_subs.Remove(h.Id, out sub)) return;
|
||||
}
|
||||
try { sub.Cts.Cancel(); } catch { }
|
||||
try { await sub.Loop.ConfigureAwait(false); } catch { }
|
||||
try { sub.Cts.Cancel(); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Cancelling alarm-subscription CTS failed"); }
|
||||
try { await sub.Loop.ConfigureAwait(false); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Awaiting alarm-subscription loop failed during unsubscribe"); }
|
||||
sub.Cts.Dispose();
|
||||
}
|
||||
|
||||
@@ -78,8 +84,10 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable
|
||||
lock (_subsLock) { snap = [.. _subs.Values]; _subs.Clear(); }
|
||||
foreach (var sub in snap)
|
||||
{
|
||||
try { sub.Cts.Cancel(); } catch { }
|
||||
try { await sub.Loop.ConfigureAwait(false); } catch { }
|
||||
try { sub.Cts.Cancel(); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Cancelling alarm-subscription CTS during dispose failed"); }
|
||||
try { await sub.Loop.ConfigureAwait(false); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Awaiting alarm-subscription loop during dispose failed"); }
|
||||
sub.Cts.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -136,7 +144,11 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; }
|
||||
catch { /* per-tick failures are non-fatal — next tick retries */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* per-tick failures are non-fatal — next tick retries */
|
||||
_logger.LogDebug(ex, "FOCAS alarm-projection poll tick failed");
|
||||
}
|
||||
|
||||
try { await Task.Delay(_pollInterval, ct).ConfigureAwait(false); }
|
||||
catch (OperationCanceledException) { break; }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
@@ -22,8 +24,14 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
private readonly string _driverInstanceId;
|
||||
private readonly IFocasClientFactory _clientFactory;
|
||||
private readonly PollGroupEngine _poll;
|
||||
private readonly ILogger<FocasDriver> _logger;
|
||||
private readonly Dictionary<string, DeviceState> _devices = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, FocasTagDefinition> _tagsByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
// Per-tag-name cache of the FocasAddress parsed once at InitializeAsync. ReadAsync /
|
||||
// WriteAsync look up the pre-parsed value instead of re-parsing tag.Address on every hot
|
||||
// call — resolves Driver.FOCAS-008.
|
||||
private readonly Dictionary<string, FocasAddress> _parsedAddressesByTagName =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private FocasAlarmProjection? _alarmProjection;
|
||||
// _health is read/written from multiple threads (ReadAsync, WriteAsync, ProbeLoopAsync).
|
||||
// Volatile.Read/Write ensures every thread sees the latest reference without a lock — the
|
||||
@@ -35,12 +43,14 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
public event EventHandler<AlarmEventArgs>? OnAlarmEvent;
|
||||
|
||||
public FocasDriver(FocasDriverOptions options, string driverInstanceId,
|
||||
IFocasClientFactory? clientFactory = null)
|
||||
IFocasClientFactory? clientFactory = null,
|
||||
ILogger<FocasDriver>? logger = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
_options = options;
|
||||
_driverInstanceId = driverInstanceId;
|
||||
_clientFactory = clientFactory ?? new Wire.WireFocasClientFactory();
|
||||
_logger = logger ?? NullLogger<FocasDriver>.Instance;
|
||||
_poll = new PollGroupEngine(
|
||||
reader: ReadAsync,
|
||||
onChange: (handle, tagRef, snapshot) =>
|
||||
@@ -82,6 +92,11 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
$"FOCAS tag '{tag.Name}' ({tag.Address}) rejected by capability matrix: {reason}");
|
||||
}
|
||||
_tagsByName[tag.Name] = tag;
|
||||
// Cache the parsed FocasAddress so ReadAsync / WriteAsync don't re-parse on every
|
||||
// hot-path call (Driver.FOCAS-008). The address string has already been validated
|
||||
// by FocasAddress.TryParse above; reusing the parsed record avoids per-tick allocs
|
||||
// on subscription pollers.
|
||||
_parsedAddressesByTagName[tag.Name] = parsed;
|
||||
}
|
||||
|
||||
if (_options.Probe.Enabled)
|
||||
@@ -105,7 +120,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
}
|
||||
|
||||
if (_options.AlarmProjection.Enabled)
|
||||
_alarmProjection = new FocasAlarmProjection(this, _options.AlarmProjection.PollInterval);
|
||||
_alarmProjection = new FocasAlarmProjection(this, _options.AlarmProjection.PollInterval, _logger);
|
||||
|
||||
if (_options.FixedTree.Enabled)
|
||||
{
|
||||
@@ -143,19 +158,26 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
}
|
||||
foreach (var state in _devices.Values)
|
||||
{
|
||||
try { state.ProbeCts?.Cancel(); } catch { }
|
||||
// Cancel-then-dispose can race in tight shutdown loops; swallowing is intentional
|
||||
// but we now log the cause so a noisy shutdown leaves a Debug trace
|
||||
// (Driver.FOCAS-007).
|
||||
try { state.ProbeCts?.Cancel(); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Cancelling probe CTS for {Host} failed", state.Options.HostAddress); }
|
||||
state.ProbeCts?.Dispose();
|
||||
state.ProbeCts = null;
|
||||
try { state.RecycleCts?.Cancel(); } catch { }
|
||||
try { state.RecycleCts?.Cancel(); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Cancelling recycle CTS for {Host} failed", state.Options.HostAddress); }
|
||||
state.RecycleCts?.Dispose();
|
||||
state.RecycleCts = null;
|
||||
try { state.FixedTreeCts?.Cancel(); } catch { }
|
||||
try { state.FixedTreeCts?.Cancel(); }
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Cancelling fixed-tree CTS for {Host} failed", state.Options.HostAddress); }
|
||||
state.FixedTreeCts?.Dispose();
|
||||
state.FixedTreeCts = null;
|
||||
state.DisposeClient();
|
||||
}
|
||||
_devices.Clear();
|
||||
_tagsByName.Clear();
|
||||
_parsedAddressesByTagName.Clear();
|
||||
Volatile.Write(ref _health, new DriverHealth(DriverState.Unknown, Volatile.Read(ref _health).LastSuccessfulRead, null));
|
||||
}
|
||||
|
||||
@@ -206,8 +228,14 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
try
|
||||
{
|
||||
var client = await EnsureConnectedAsync(device, cancellationToken).ConfigureAwait(false);
|
||||
var parsed = FocasAddress.TryParse(def.Address)
|
||||
?? throw new InvalidOperationException($"FOCAS tag '{def.Name}' has malformed Address '{def.Address}'.");
|
||||
// Parsed at InitializeAsync — defensive fallback re-parse only if the tag was
|
||||
// somehow not seeded (shouldn't happen, but keeps the call total).
|
||||
if (!_parsedAddressesByTagName.TryGetValue(def.Name, out var parsed))
|
||||
{
|
||||
parsed = FocasAddress.TryParse(def.Address)
|
||||
?? throw new InvalidOperationException(
|
||||
$"FOCAS tag '{def.Name}' has malformed Address '{def.Address}'.");
|
||||
}
|
||||
var (value, status) = await client.ReadAsync(parsed, def.DataType, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
results[i] = new DataValueSnapshot(value, status, now, now);
|
||||
@@ -260,8 +288,12 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
try
|
||||
{
|
||||
var client = await EnsureConnectedAsync(device, cancellationToken).ConfigureAwait(false);
|
||||
var parsed = FocasAddress.TryParse(def.Address)
|
||||
?? throw new InvalidOperationException($"FOCAS tag '{def.Name}' has malformed Address '{def.Address}'.");
|
||||
if (!_parsedAddressesByTagName.TryGetValue(def.Name, out var parsed))
|
||||
{
|
||||
parsed = FocasAddress.TryParse(def.Address)
|
||||
?? throw new InvalidOperationException(
|
||||
$"FOCAS tag '{def.Name}' has malformed Address '{def.Address}'.");
|
||||
}
|
||||
var status = await client.WriteAsync(parsed, def.DataType, w.Value, cancellationToken).ConfigureAwait(false);
|
||||
results[i] = new WriteResult(status);
|
||||
}
|
||||
@@ -489,10 +521,35 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
try
|
||||
{
|
||||
var client = await EnsureConnectedAsync(state, ct).ConfigureAwait(false);
|
||||
success = await client.ProbeAsync(ct).ConfigureAwait(false);
|
||||
// Apply Probe.Timeout so a hung CNC socket gets cancelled at the configured
|
||||
// budget rather than blocking until the OS TCP timeout (Driver.FOCAS-009).
|
||||
// TimeSpan.Zero / negative means "no per-probe timeout" — fall back to the loop
|
||||
// cancellation token unmodified.
|
||||
var probeTimeout = _options.Probe.Timeout;
|
||||
if (probeTimeout > TimeSpan.Zero)
|
||||
{
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
linked.CancelAfter(probeTimeout);
|
||||
success = await client.ProbeAsync(linked.Token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
success = await client.ProbeAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; }
|
||||
catch { /* connect-failure path already disposed + cleared the client */ }
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
// Per-probe timeout fired — the loop is still alive. Treat as a failed probe so
|
||||
// the host state transitions to Stopped, and log so silent timeouts are visible.
|
||||
_logger.LogDebug(ex, "FOCAS probe timed out for {Host} after {Timeout}",
|
||||
state.Options.HostAddress, _options.Probe.Timeout);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* connect-failure path already disposed + cleared the client */
|
||||
_logger.LogDebug(ex, "FOCAS probe failed for {Host}", state.Options.HostAddress);
|
||||
}
|
||||
|
||||
TransitionDeviceState(state, success ? HostState.Running : HostState.Stopped);
|
||||
|
||||
@@ -542,8 +599,10 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
sys, [.. axes], [.. spindles], [.. spindleMaxRpms], caps);
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { return; }
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "FOCAS fixed-tree bootstrap failed for {Host} — retrying",
|
||||
state.Options.HostAddress);
|
||||
try { await Task.Delay(TimeSpan.FromSeconds(2), ct).ConfigureAwait(false); }
|
||||
catch (OperationCanceledException) { return; }
|
||||
}
|
||||
@@ -559,7 +618,13 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
var loads = await client2.GetSpindleLoadsAsync(ct).ConfigureAwait(false);
|
||||
for (var i = 0; i < loads.Count; i++) state.LastSpindleLoads[i] = loads[i];
|
||||
}
|
||||
catch { /* first-tick poll will retry */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* first-tick poll will retry */
|
||||
_logger.LogDebug(ex,
|
||||
"FOCAS bootstrap spindle-loads prime failed for {Host} — first poll tick will retry",
|
||||
state.Options.HostAddress);
|
||||
}
|
||||
}
|
||||
|
||||
var programPollDue = DateTime.MinValue;
|
||||
@@ -591,7 +656,12 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
var loads = await client.GetServoLoadsAsync(ct).ConfigureAwait(false);
|
||||
PublishServoLoads(state, loads);
|
||||
}
|
||||
catch { /* transient — next tick retries */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* transient — next tick retries */
|
||||
_logger.LogDebug(ex, "FOCAS servo-loads poll failed for {Host}",
|
||||
state.Options.HostAddress);
|
||||
}
|
||||
}
|
||||
if (cache.Capabilities.SpindleLoad)
|
||||
{
|
||||
@@ -600,7 +670,12 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
var loads = await client.GetSpindleLoadsAsync(ct).ConfigureAwait(false);
|
||||
for (var i = 0; i < loads.Count; i++) state.LastSpindleLoads[i] = loads[i];
|
||||
}
|
||||
catch { /* transient */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* transient */
|
||||
_logger.LogDebug(ex, "FOCAS spindle-loads poll failed for {Host}",
|
||||
state.Options.HostAddress);
|
||||
}
|
||||
}
|
||||
|
||||
// Program-info poll runs on its own cadence — much slower than the axis
|
||||
@@ -615,7 +690,12 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
state.LastProgramInfo = program;
|
||||
if (firstAxisSnap is { } s) state.LastProgramAxisRef = s;
|
||||
}
|
||||
catch { /* transient — next tick retries */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* transient — next tick retries */
|
||||
_logger.LogDebug(ex, "FOCAS program-info poll failed for {Host}",
|
||||
state.Options.HostAddress);
|
||||
}
|
||||
programPollDue = DateTime.UtcNow + programInterval;
|
||||
}
|
||||
|
||||
@@ -631,13 +711,23 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
var t = await client.GetTimerAsync(kind, ct).ConfigureAwait(false);
|
||||
state.LastTimers[kind] = t;
|
||||
}
|
||||
catch { /* per-kind failures are non-fatal */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* per-kind failures are non-fatal */
|
||||
_logger.LogDebug(ex, "FOCAS timer poll failed for {Host} kind={Kind}",
|
||||
state.Options.HostAddress, kind);
|
||||
}
|
||||
}
|
||||
timerPollDue = DateTime.UtcNow + timerInterval;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; }
|
||||
catch { /* next tick retries — transient blips are expected */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* next tick retries — transient blips are expected */
|
||||
_logger.LogDebug(ex, "FOCAS fixed-tree poll tick failed for {Host}",
|
||||
state.Options.HostAddress);
|
||||
}
|
||||
|
||||
try { await Task.Delay(_options.FixedTree.PollInterval, ct).ConfigureAwait(false); }
|
||||
catch (OperationCanceledException) { break; }
|
||||
@@ -801,7 +891,11 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
// reconnect because the goal is just to release the FWLIB handle slot; a
|
||||
// readable tick one probe cycle later is an acceptable cost.
|
||||
try { state.DisposeClient(); }
|
||||
catch { /* already disposed or race — next EnsureConnected recovers */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* already disposed or race — next EnsureConnected recovers */
|
||||
_logger.LogDebug(ex, "FOCAS handle-recycle dispose failed for {Host}", state.Options.HostAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -858,7 +952,11 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
result.Add((state.Options.HostAddress, alarms));
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; }
|
||||
catch { /* surface a device-local fault on the next tick */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
/* surface a device-local fault on the next tick */
|
||||
_logger.LogDebug(ex, "FOCAS alarm poll failed for {Host}", state.Options.HostAddress);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -272,16 +272,22 @@ public sealed class UnimplementedFocasClientFactory : IFocasClientFactory
|
||||
/// Well-known FOCAS alarm types from <c>fwlib32.h</c> <c>ALM_TYPE_*</c>. Narrow subset —
|
||||
/// the full list is ~15 types per model; these cover the universally-present categories.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Constants are typed <see cref="short"/> so they match the wire field width on
|
||||
/// <c>cnc_rdalmmsg2</c> (and so <see cref="FocasAlarmProjection"/>'s <c>switch (short)</c>
|
||||
/// statements compile against a matching type rather than relying on implicit int→short
|
||||
/// narrowing on the constants).
|
||||
/// </remarks>
|
||||
public static class FocasAlarmType
|
||||
{
|
||||
/// <summary>Pass to <see cref="IFocasClient.ReadAlarmsAsync"/>-equivalent to mean "any type".</summary>
|
||||
public const int All = -1;
|
||||
public const int Parameter = 0; // ALM_P
|
||||
public const int PulseCode = 1; // ALM_Y (servo)
|
||||
public const int Overtravel = 2; // ALM_O
|
||||
public const int Overheat = 3; // ALM_H
|
||||
public const int Servo = 4; // ALM_S
|
||||
public const int DataIo = 5; // ALM_T
|
||||
public const int MemoryCheck = 6; // ALM_M
|
||||
public const int MacroAlarm = 13; // ALM_MC — used by #3006 etc.
|
||||
public const short All = -1;
|
||||
public const short Parameter = 0; // ALM_P
|
||||
public const short PulseCode = 1; // ALM_Y (servo)
|
||||
public const short Overtravel = 2; // ALM_O
|
||||
public const short Overheat = 3; // ALM_H
|
||||
public const short Servo = 4; // ALM_S
|
||||
public const short DataIo = 5; // ALM_T
|
||||
public const short MemoryCheck = 6; // ALM_M
|
||||
public const short MacroAlarm = 13; // ALM_MC — used by #3006 etc.
|
||||
}
|
||||
|
||||
@@ -58,23 +58,13 @@ public static class FocasOperationModeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonical operator-facing label for an operation mode (e.g. <c>"AUTO"</c>,
|
||||
/// <c>"EDIT"</c>). Unknown codes fall back to the raw numeric value as a string
|
||||
/// so the UI still shows something interpretable.
|
||||
/// <c>"EDIT"</c>). Delegates to <see cref="FocasOpMode.ToText"/> so the wire layer
|
||||
/// and the fixed-tree projection render identical labels — historically these two
|
||||
/// surfaces diverged ("TJOG" vs "T-JOG", "TEACH_IN_HANDLE" vs "TEACH-IN-HANDLE",
|
||||
/// and different unknown-code fallbacks). Resolved by Driver.FOCAS-010.
|
||||
/// </summary>
|
||||
public static string ToText(this FocasOperationMode mode) => mode switch
|
||||
{
|
||||
FocasOperationMode.Mdi => "MDI",
|
||||
FocasOperationMode.Auto => "AUTO",
|
||||
FocasOperationMode.TJog => "T-JOG",
|
||||
FocasOperationMode.Edit => "EDIT",
|
||||
FocasOperationMode.Handle => "HANDLE",
|
||||
FocasOperationMode.Jog => "JOG",
|
||||
FocasOperationMode.TeachInHandle => "TEACH-IN-HANDLE",
|
||||
FocasOperationMode.Reference => "REFERENCE",
|
||||
FocasOperationMode.Remote => "REMOTE",
|
||||
FocasOperationMode.Test => "TEST",
|
||||
_ => ((short)mode).ToString(),
|
||||
};
|
||||
public static string ToText(this FocasOperationMode mode) =>
|
||||
FocasOpMode.ToText((short)mode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire;
|
||||
|
||||
/// <summary>
|
||||
@@ -14,9 +16,25 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire;
|
||||
/// </remarks>
|
||||
public sealed class WireFocasClient : IFocasClient
|
||||
{
|
||||
private readonly FocasWireClient _wire = new();
|
||||
private readonly FocasWireClient _wire;
|
||||
private FocasHostAddress? _address;
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor — wire client without logger. Selected by the legacy
|
||||
/// no-arg <see cref="WireFocasClientFactory.Create"/> path.
|
||||
/// </summary>
|
||||
public WireFocasClient() : this(logger: null) { }
|
||||
|
||||
/// <summary>
|
||||
/// Construct with an optional logger. Threaded through to
|
||||
/// <see cref="FocasWireClient"/> so the per-response Debug entries actually reach
|
||||
/// the host's logging pipeline (Driver.FOCAS-007).
|
||||
/// </summary>
|
||||
public WireFocasClient(ILogger<FocasWireClient>? logger)
|
||||
{
|
||||
_wire = new FocasWireClient(logger);
|
||||
}
|
||||
|
||||
public bool IsConnected => _wire.IsConnected;
|
||||
|
||||
public async Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken cancellationToken)
|
||||
@@ -339,5 +357,20 @@ public sealed class WireFocasClient : IFocasClient
|
||||
/// <summary>Factory producing <see cref="WireFocasClient"/> instances — one per configured device.</summary>
|
||||
public sealed class WireFocasClientFactory : IFocasClientFactory
|
||||
{
|
||||
public IFocasClient Create() => new WireFocasClient();
|
||||
private readonly ILogger<FocasWireClient>? _logger;
|
||||
|
||||
public WireFocasClientFactory() : this(logger: null) { }
|
||||
|
||||
/// <summary>
|
||||
/// Construct the factory with a logger that every created <see cref="WireFocasClient"/>
|
||||
/// forwards to its <see cref="FocasWireClient"/>. Resolves Driver.FOCAS-007 — the wire
|
||||
/// client already emits Debug entries per FOCAS response, but the previous no-arg
|
||||
/// factory path discarded them.
|
||||
/// </summary>
|
||||
public WireFocasClientFactory(ILogger<FocasWireClient>? logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IFocasClient Create() => new WireFocasClient(_logger);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user