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:
Joseph Doherty
2026-05-23 07:45:38 -04:00
parent f7e3e9885e
commit 6575c6e5f6
8 changed files with 522 additions and 64 deletions
@@ -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; }