docs: backfill XML documentation across 756 files
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
This commit is contained in:
Joseph Doherty
2026-05-28 08:10:17 -04:00
parent f9fc7dd2e1
commit 64e3fbe035
756 changed files with 9876 additions and 96 deletions
@@ -19,6 +19,7 @@ public sealed record FocasAddress(
int Number,
int? BitIndex)
{
/// <summary>Gets the canonical string representation of this address.</summary>
public string Canonical => Kind switch
{
FocasAreaKind.Pmc => BitIndex is null
@@ -31,6 +32,9 @@ public sealed record FocasAddress(
_ => $"?{Number}",
};
/// <summary>Attempts to parse a FOCAS address from the given string.</summary>
/// <param name="value">The address string to parse.</param>
/// <returns>A FocasAddress if parsing succeeds; otherwise null.</returns>
public static FocasAddress? TryParse(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
@@ -28,6 +28,10 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable
private readonly Lock _subsLock = new();
private long _nextId;
/// <summary>Initializes a new FOCAS alarm projection.</summary>
/// <param name="driver">FOCAS driver instance.</param>
/// <param name="pollInterval">Polling interval.</param>
/// <param name="logger">Optional logger.</param>
public FocasAlarmProjection(FocasDriver driver, TimeSpan pollInterval, ILogger? logger = null)
{
_driver = driver;
@@ -35,6 +39,10 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable
_logger = logger ?? NullLogger.Instance;
}
/// <summary>Subscribes to alarms from the specified device sources.</summary>
/// <param name="sourceNodeIds">Source node IDs to listen to.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task returning the alarm subscription handle.</returns>
public Task<IAlarmSubscriptionHandle> SubscribeAsync(
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
{
@@ -54,6 +62,10 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable
return Task.FromResult<IAlarmSubscriptionHandle>(handle);
}
/// <summary>Unsubscribes from an alarm subscription.</summary>
/// <param name="handle">Alarm subscription handle.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task UnsubscribeAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
{
if (handle is not FocasAlarmSubscriptionHandle h) return;
@@ -74,10 +86,15 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable
/// condition resolves. Swallow the request so capability negotiation succeeds, rather
/// than surfacing a confusing "not supported" error to the operator.
/// </summary>
/// <param name="acknowledgements">Alarms to acknowledge.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A completed task.</returns>
public Task AcknowledgeAsync(
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken) =>
Task.CompletedTask;
/// <summary>Disposes the alarm projection.</summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async ValueTask DisposeAsync()
{
List<Subscription> snap;
@@ -97,6 +114,9 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable
/// emits raise + clear events. Extracted so tests can drive a tick without spinning up
/// the full Task.Run loop.
/// </summary>
/// <param name="sub">Active subscription.</param>
/// <param name="deviceHostAddress">Device host address.</param>
/// <param name="current">Current alarms from the device.</param>
internal void Tick(Subscription sub, string deviceHostAddress, IReadOnlyList<FocasActiveAlarm> current)
{
var prev = sub.LastByDevice.GetValueOrDefault(deviceHostAddress) ?? [];
@@ -158,6 +178,8 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable
private static string AlarmKey(FocasActiveAlarm a) => $"{a.Type}:{a.AlarmNumber}";
/// <summary>Map FOCAS type to a human-readable category; falls back to the numeric type.</summary>
/// <param name="type">FOCAS alarm type.</param>
/// <returns>The mapped alarm type string.</returns>
internal static string MapAlarmType(short type) => type switch
{
FocasAlarmType.Parameter => "Parameter",
@@ -176,6 +198,8 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable
/// Servo / Emergency-equivalents are Critical; Parameter + Macro are Medium; rest land
/// at High (everything else on a CNC is safety-relevant).
/// </summary>
/// <param name="type">FOCAS alarm type.</param>
/// <returns>The mapped alarm severity.</returns>
internal static AlarmSeverity MapSeverity(short type) => type switch
{
FocasAlarmType.Overtravel => AlarmSeverity.Critical,
@@ -191,10 +215,15 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable
HashSet<string>? deviceFilter,
CancellationTokenSource cts)
{
/// <summary>Gets the subscription handle.</summary>
public FocasAlarmSubscriptionHandle Handle { get; } = handle;
/// <summary>Gets the device filter.</summary>
public HashSet<string>? DeviceFilter { get; } = deviceFilter;
/// <summary>Gets the cancellation token source.</summary>
public CancellationTokenSource Cts { get; } = cts;
/// <summary>Gets or sets the polling loop task.</summary>
public Task Loop { get; set; } = Task.CompletedTask;
/// <summary>Gets the last seen alarms by device.</summary>
public Dictionary<string, IReadOnlyList<FocasActiveAlarm>> LastByDevice { get; } =
new(StringComparer.OrdinalIgnoreCase);
}
@@ -203,5 +232,6 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable
/// <summary>Handle returned by <see cref="FocasAlarmProjection.SubscribeAsync"/>.</summary>
public sealed record FocasAlarmSubscriptionHandle(long Id) : IAlarmSubscriptionHandle
{
/// <summary>Gets the diagnostic identifier for this subscription.</summary>
public string DiagnosticId => $"focas-alarm-sub-{Id}";
}
@@ -20,12 +20,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
public static class FocasCapabilityMatrix
{
/// <summary>
/// Check whether <paramref name="address"/> is accepted by a CNC of
/// <paramref name="series"/>. Returns <c>null</c> on pass + a failure reason
/// on reject — the driver surfaces the reason string verbatim when failing
/// <c>InitializeAsync</c> so operators see the specific out-of-range without
/// guessing.
/// Validates whether an address is accepted by a CNC of the given series.
/// Returns null on pass, or a failure reason on reject.
/// </summary>
/// <param name="series">The CNC series to validate against.</param>
/// <param name="address">The address to validate.</param>
/// <returns>Null if valid, otherwise a failure reason string.</returns>
public static string? Validate(FocasCncSeries series, FocasAddress address)
{
if (series == FocasCncSeries.Unknown) return null;
@@ -39,8 +39,9 @@ public static class FocasCapabilityMatrix
};
}
/// <summary>Macro variable number accepted by a CNC series. Cites
/// <c>cnc_rdmacro</c>/<c>cnc_wrmacro</c> in the Developer Kit.</summary>
/// <summary>Gets the macro variable number range accepted by a CNC series.</summary>
/// <param name="series">The CNC series.</param>
/// <returns>A tuple of (min, max) macro numbers.</returns>
internal static (int min, int max) MacroRange(FocasCncSeries series) => series switch
{
// Common macros 1-33 + 100-199 + 500-999 universally; extended 10000+ only on
@@ -57,8 +58,9 @@ public static class FocasCapabilityMatrix
_ => (0, int.MaxValue),
};
/// <summary>Parameter number accepted; from <c>cnc_rdparam</c>/<c>cnc_wrparam</c>.
/// Ranges reflect the highest-numbered parameter documented per series.</summary>
/// <summary>Gets the parameter number range accepted by a CNC series.</summary>
/// <param name="series">The CNC series.</param>
/// <returns>A tuple of (min, max) parameter numbers.</returns>
internal static (int min, int max) ParameterRange(FocasCncSeries series) => series switch
{
FocasCncSeries.Sixteen_i => (0, 9999),
@@ -73,8 +75,9 @@ public static class FocasCapabilityMatrix
_ => (0, int.MaxValue),
};
/// <summary>PMC letters accepted per series. Legacy controllers omit F/M/C
/// signal groups that 30i-family ladder programs use.</summary>
/// <summary>Gets the PMC letters accepted by a CNC series.</summary>
/// <param name="series">The CNC series.</param>
/// <returns>A set of accepted PMC letter strings.</returns>
internal static IReadOnlySet<string> PmcLetters(FocasCncSeries series) => series switch
{
FocasCncSeries.Sixteen_i => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "R", "D" },
@@ -89,9 +92,9 @@ public static class FocasCapabilityMatrix
_ => new HashSet<string>(StringComparer.OrdinalIgnoreCase),
};
/// <summary>PMC address-number ceiling per series. Multiplied by 8 to get bit
/// count since PMC addresses are byte-addressed on read + bit-addressed on
/// write — FocasAddress carries the bit separately.</summary>
/// <summary>Gets the maximum PMC address number for a CNC series.</summary>
/// <param name="series">The CNC series.</param>
/// <returns>The maximum address number.</returns>
internal static int PmcMaxNumber(FocasCncSeries series) => series switch
{
FocasCncSeries.Sixteen_i => 999,
@@ -27,6 +27,9 @@ public enum FocasDataType
public static class FocasDataTypeExtensions
{
/// <summary>Converts a FOCAS data type to the corresponding driver data type.</summary>
/// <param name="t">The FOCAS data type to convert.</param>
/// <returns>The equivalent driver data type.</returns>
public static DriverDataType ToDriverDataType(this FocasDataType t) => t switch
{
FocasDataType.Bit => DriverDataType.Boolean,
@@ -38,10 +38,18 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// record is immutable so there is no torn-read risk on the object itself.
private DriverHealth _health = new(DriverState.Unknown, null, null);
/// <summary>Occurs when data changes on a subscribed tag.</summary>
public event EventHandler<DataChangeEventArgs>? OnDataChange;
/// <summary>Occurs when a device host connection status changes.</summary>
public event EventHandler<HostStatusChangedEventArgs>? OnHostStatusChanged;
/// <summary>Occurs when an alarm event is raised.</summary>
public event EventHandler<AlarmEventArgs>? OnAlarmEvent;
/// <summary>Initializes a new instance of the <see cref="FocasDriver"/> class with the provided options and dependencies.</summary>
/// <param name="options">The driver configuration options.</param>
/// <param name="driverInstanceId">The unique identifier for this driver instance.</param>
/// <param name="clientFactory">Optional factory for creating FOCAS client instances.</param>
/// <param name="logger">Optional logger instance.</param>
public FocasDriver(FocasDriverOptions options, string driverInstanceId,
IFocasClientFactory? clientFactory = null,
ILogger<FocasDriver>? logger = null)
@@ -57,9 +65,15 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot)));
}
/// <summary>Gets the driver instance identifier.</summary>
public string DriverInstanceId => _driverInstanceId;
/// <summary>Gets the driver type name.</summary>
public string DriverType => "FOCAS";
/// <summary>Initializes the driver with configuration and prepares device connections and polling.</summary>
/// <param name="driverConfigJson">JSON configuration string for the driver.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous initialization operation.</returns>
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
Volatile.Write(ref _health, new DriverHealth(DriverState.Initializing, null, null));
@@ -142,12 +156,19 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
return Task.CompletedTask;
}
/// <summary>Reinitializes the driver by shutting down and restarting with new configuration.</summary>
/// <param name="driverConfigJson">JSON configuration string for the driver.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous reinitialization operation.</returns>
public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
await ShutdownAsync(cancellationToken).ConfigureAwait(false);
await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false);
}
/// <summary>Shuts down the driver, cancelling all running operations and releasing resources.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous shutdown operation.</returns>
public async Task ShutdownAsync(CancellationToken cancellationToken)
{
await _poll.DisposeAsync().ConfigureAwait(false);
@@ -181,11 +202,20 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
Volatile.Write(ref _health, new DriverHealth(DriverState.Unknown, Volatile.Read(ref _health).LastSuccessfulRead, null));
}
/// <summary>Gets the current health status of the driver.</summary>
public DriverHealth GetHealth() => Volatile.Read(ref _health);
/// <summary>Gets the current memory footprint of the driver.</summary>
public long GetMemoryFootprint() => 0;
/// <summary>Flushes optional internal caches.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous cache flush operation.</returns>
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>Gets the number of configured devices.</summary>
internal int DeviceCount => _devices.Count;
/// <summary>Gets the state of a device by host address.</summary>
/// <param name="hostAddress">The host address of the device.</param>
/// <returns>The device state if found; otherwise null.</returns>
internal DeviceState? GetDeviceState(string hostAddress) =>
_devices.TryGetValue(hostAddress, out var s) ? s : null;
@@ -194,6 +224,10 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- IReadable ----
/// <summary>Reads values from one or more tags asynchronously.</summary>
/// <param name="fullReferences">A read-only list of tag references to read.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous read operation.</returns>
public async Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
{
@@ -260,6 +294,10 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- IWritable ----
/// <summary>Writes values to one or more tags asynchronously.</summary>
/// <param name="writes">A read-only list of write requests.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous write operation.</returns>
public async Task<IReadOnlyList<WriteResult>> WriteAsync(
IReadOnlyList<WriteRequest> writes, CancellationToken cancellationToken)
{
@@ -325,6 +363,10 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- ITagDiscovery ----
/// <summary>Discovers tags and builds the OPC UA address space asynchronously.</summary>
/// <param name="builder">The address space builder for constructing the OPC UA namespace.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous discovery operation.</returns>
public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(builder);
@@ -493,15 +535,27 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// the path inside the tree. Matches what poll-loop snapshots publish +
/// what <see cref="ReadAsync"/> looks up.
/// </summary>
/// <param name="deviceHost">The host address of the device.</param>
/// <param name="path">The path within the fixed tree.</param>
/// <returns>The canonical full reference for the node.</returns>
internal static string FixedTreeReference(string deviceHost, string path) =>
$"{deviceHost}/{path}";
// ---- ISubscribable (polling overlay via shared engine) ----
/// <summary>Subscribes to data changes on one or more tags.</summary>
/// <param name="fullReferences">A read-only list of tag references to subscribe to.</param>
/// <param name="publishingInterval">The interval at which to publish data changes.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous subscription operation.</returns>
public Task<ISubscriptionHandle> SubscribeAsync(
IReadOnlyList<string> fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) =>
Task.FromResult(_poll.Subscribe(fullReferences, publishingInterval));
/// <summary>Unsubscribes from a previous subscription.</summary>
/// <param name="handle">The subscription handle to unsubscribe from.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous unsubscription operation.</returns>
public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken)
{
_poll.Unsubscribe(handle);
@@ -510,6 +564,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- IHostConnectivityProbe ----
/// <summary>Gets the connectivity status of all configured devices.</summary>
public IReadOnlyList<HostConnectivityStatus> GetHostStatuses() =>
[.. _devices.Values.Select(s => new HostConnectivityStatus(s.Options.HostAddress, s.HostState, s.HostStateChangedUtc))];
@@ -915,6 +970,10 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- IAlarmSource ----
/// <summary>Subscribes to alarm events from the driver.</summary>
/// <param name="sourceNodeIds">A read-only list of source node IDs to subscribe to.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous subscription operation.</returns>
public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
{
@@ -924,13 +983,23 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
return _alarmProjection.SubscribeAsync(sourceNodeIds, cancellationToken);
}
/// <summary>Unsubscribes from a previous alarm subscription.</summary>
/// <param name="handle">The alarm subscription handle to unsubscribe from.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous unsubscription operation.</returns>
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) =>
_alarmProjection is { } p ? p.UnsubscribeAsync(handle, cancellationToken) : Task.CompletedTask;
/// <summary>Acknowledges one or more alarms.</summary>
/// <param name="acknowledgements">A read-only list of alarm acknowledgement requests.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous acknowledgement operation.</returns>
public Task AcknowledgeAsync(
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken) =>
_alarmProjection is { } p ? p.AcknowledgeAsync(acknowledgements, cancellationToken) : Task.CompletedTask;
/// <summary>Raises an alarm event with the provided arguments.</summary>
/// <param name="args">The alarm event arguments.</param>
internal void InvokeAlarmEvent(AlarmEventArgs args) => OnAlarmEvent?.Invoke(this, args);
/// <summary>
@@ -938,6 +1007,9 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// projection — kept <c>internal</c> rather than <c>public</c> because callers that
/// want alarm events should subscribe through <c>IAlarmSource</c> instead.
/// </summary>
/// <param name="deviceFilter">Optional set of device host addresses to filter results; null includes all devices.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A list of tuples containing host address and active alarms for each device.</returns>
internal async Task<IReadOnlyList<(string HostAddress, IReadOnlyList<FocasActiveAlarm> Alarms)>>
ReadActiveAlarmsAcrossDevicesAsync(HashSet<string>? deviceFilter, CancellationToken ct)
{
@@ -963,6 +1035,9 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- IPerCallHostResolver ----
/// <summary>Resolves the host address for a given tag reference.</summary>
/// <param name="fullReference">The full reference of the tag.</param>
/// <returns>The host address for the tag reference.</returns>
public string ResolveHost(string fullReference)
{
if (_tagsByName.TryGetValue(fullReference, out var def))
@@ -999,7 +1074,9 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
return device.Client;
}
/// <summary>Disposes the driver and releases all resources synchronously.</summary>
public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();
/// <summary>Disposes the driver and releases all resources asynchronously.</summary>
public async ValueTask DisposeAsync() => await ShutdownAsync(CancellationToken.None).ConfigureAwait(false);
/// <summary>
@@ -1031,25 +1108,41 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
internal sealed class DeviceState(FocasHostAddress parsedAddress, FocasDeviceOptions options)
{
/// <summary>Gets the parsed host address.</summary>
public FocasHostAddress ParsedAddress { get; } = parsedAddress;
/// <summary>Gets the device configuration options.</summary>
public FocasDeviceOptions Options { get; } = options;
/// <summary>Gets or sets the FOCAS client instance.</summary>
public IFocasClient? Client { get; set; }
/// <summary>Gets the lock object for probe synchronization.</summary>
public object ProbeLock { get; } = new();
/// <summary>Gets or sets the current host connectivity state.</summary>
public HostState HostState { get; set; } = HostState.Unknown;
/// <summary>Gets or sets the timestamp when host state last changed.</summary>
public DateTime HostStateChangedUtc { get; set; } = DateTime.UtcNow;
/// <summary>Gets or sets the cancellation token source for the probe loop.</summary>
public CancellationTokenSource? ProbeCts { get; set; }
/// <summary>Gets or sets the cancellation token source for the recycle loop.</summary>
public CancellationTokenSource? RecycleCts { get; set; }
/// <summary>Gets or sets the cancellation token source for the fixed-tree loop.</summary>
public CancellationTokenSource? FixedTreeCts { get; set; }
/// <summary>Gets or sets the fixed-tree cache for this device.</summary>
public FocasFixedTreeCache? FixedTreeCache { get; set; }
/// <summary>Gets the last fixed tree snapshots by field name.</summary>
public Dictionary<string, int> LastFixedSnapshots { get; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>Gets or sets the last program information snapshot.</summary>
public FocasProgramInfo? LastProgramInfo { get; set; }
/// <summary>Cached first-axis dynamic snapshot — feeds Program/Number, /MainNumber, /Sequence.</summary>
/// <summary>Gets or sets the cached first-axis dynamic snapshot — feeds Program/Number, /MainNumber, /Sequence.</summary>
public FocasDynamicSnapshot? LastProgramAxisRef { get; set; }
/// <summary>Gets the last timer values by timer kind.</summary>
public Dictionary<FocasTimerKind, FocasTimer> LastTimers { get; } = [];
/// <summary>Gets the last servo load percentages by servo name.</summary>
public Dictionary<string, double> LastServoLoads { get; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>Gets the last spindle load percentages by spindle index.</summary>
public Dictionary<int, int> LastSpindleLoads { get; } = [];
/// <summary>Disposes the FOCAS client instance.</summary>
public void DisposeClient()
{
Client?.Dispose();
@@ -32,12 +32,19 @@ public static class FocasDriverFactoryExtensions
/// Register the FOCAS driver factory in the supplied <see cref="DriverFactoryRegistry"/>.
/// 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)
{
ArgumentNullException.ThrowIfNull(registry);
registry.Register(DriverTypeName, CreateInstance);
}
/// <summary>
/// Creates a new <see cref="FocasDriver"/> instance from the supplied configuration ID and JSON.
/// </summary>
/// <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)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
@@ -88,6 +95,12 @@ public static class FocasDriverFactoryExtensions
return new FocasDriver(options, driverInstanceId, clientFactory);
}
/// <summary>
/// Builds the appropriate <see cref="IFocasClientFactory"/> based on the config DTO's backend selection.
/// </summary>
/// <param name="dto">The driver configuration DTO.</param>
/// <param name="driverInstanceId">The driver instance identifier for error reporting.</param>
/// <returns>A configured <see cref="IFocasClientFactory"/> instance.</returns>
internal static IFocasClientFactory BuildClientFactory(
FocasDriverConfigDto dto, string driverInstanceId)
{
@@ -183,38 +196,76 @@ public static class FocasDriverFactoryExtensions
internal sealed class FocasDriverConfigDto
{
/// <summary>Gets or sets the FOCAS client factory backend name (e.g. "wire" or "stub").</summary>
public string? Backend { get; init; }
/// <summary>Gets or sets the CNC series for this driver.</summary>
public string? Series { get; init; }
/// <summary>Gets or sets the operation timeout in milliseconds.</summary>
public int? TimeoutMs { get; init; }
/// <summary>Gets or sets the list of CNC devices to manage.</summary>
public List<FocasDeviceDto>? Devices { get; init; }
/// <summary>Gets or sets the list of FOCAS tags to poll.</summary>
public List<FocasTagDto>? Tags { get; init; }
/// <summary>Gets or sets the probe configuration options.</summary>
public FocasProbeDto? Probe { get; init; }
/// <summary>Gets or sets the fixed-tree configuration options.</summary>
public FocasFixedTreeDto? FixedTree { get; init; }
/// <summary>Gets or sets the alarm projection configuration options.</summary>
public FocasAlarmProjectionDto? AlarmProjection { get; init; }
/// <summary>Gets or sets the handle recycle configuration options.</summary>
public FocasHandleRecycleDto? HandleRecycle { get; init; }
}
internal sealed class FocasDeviceDto
{
/// <summary>Gets or sets the hostname or IP address of the CNC.</summary>
public string? HostAddress { get; init; }
/// <summary>Gets or sets the logical device name.</summary>
public string? DeviceName { get; init; }
/// <summary>Gets or sets the CNC series for this device (overrides top-level series if provided).</summary>
public string? Series { get; init; }
}
internal sealed class FocasTagDto
{
/// <summary>Gets or sets the tag name.</summary>
public string? Name { get; init; }
/// <summary>Gets or sets the hostname or IP address of the CNC device for this tag.</summary>
public string? DeviceHostAddress { get; init; }
/// <summary>Gets or sets the FOCAS address or path for this tag.</summary>
public string? Address { get; init; }
/// <summary>Gets or sets the data type for this tag.</summary>
public string? DataType { get; init; }
/// <summary>Gets or sets a value indicating whether this tag is writable.</summary>
public bool? Writable { get; init; }
/// <summary>Gets or sets a value indicating whether writes to this tag are idempotent.</summary>
public bool? WriteIdempotent { get; init; }
}
internal sealed class FocasProbeDto
{
/// <summary>Gets or sets a value indicating whether probing is enabled.</summary>
public bool? Enabled { get; init; }
/// <summary>Gets or sets the probe interval in milliseconds.</summary>
public int? IntervalMs { get; init; }
/// <summary>Gets or sets the probe timeout in milliseconds.</summary>
public int? TimeoutMs { get; init; }
}
@@ -225,23 +276,36 @@ public static class FocasDriverFactoryExtensions
/// </summary>
internal sealed class FocasFixedTreeDto
{
/// <summary>Gets or sets a value indicating whether fixed-tree discovery is enabled.</summary>
public bool? Enabled { get; init; }
/// <summary>Gets or sets the poll interval for discovering fixed-tree items.</summary>
public TimeSpan? PollInterval { get; init; }
/// <summary>Gets or sets the poll interval for discovering program-related items.</summary>
public TimeSpan? ProgramPollInterval { get; init; }
/// <summary>Gets or sets the poll interval for discovering timer-related items.</summary>
public TimeSpan? TimerPollInterval { get; init; }
}
/// <summary>Optional <c>AlarmProjection</c> config section.</summary>
internal sealed class FocasAlarmProjectionDto
{
/// <summary>Gets or sets a value indicating whether alarm projection is enabled.</summary>
public bool? Enabled { get; init; }
/// <summary>Gets or sets the alarm poll interval.</summary>
public TimeSpan? PollInterval { get; init; }
}
/// <summary>Optional <c>HandleRecycle</c> config section.</summary>
internal sealed class FocasHandleRecycleDto
{
/// <summary>Gets or sets a value indicating whether handle recycling is enabled.</summary>
public bool? Enabled { get; init; }
/// <summary>Gets or sets the handle recycle interval.</summary>
public TimeSpan? Interval { get; init; }
}
}
@@ -7,12 +7,19 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
/// </summary>
public sealed class FocasDriverOptions
{
/// <summary>Gets the list of configured CNC devices.</summary>
public IReadOnlyList<FocasDeviceOptions> Devices { get; init; } = [];
/// <summary>Gets the list of FOCAS tag definitions.</summary>
public IReadOnlyList<FocasTagDefinition> Tags { get; init; } = [];
/// <summary>Gets the probe options.</summary>
public FocasProbeOptions Probe { get; init; } = new();
/// <summary>Gets the timeout duration for operations.</summary>
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
/// <summary>Gets the alarm projection options.</summary>
public FocasAlarmProjectionOptions AlarmProjection { get; init; } = new();
/// <summary>Gets the handle recycle options.</summary>
public FocasHandleRecycleOptions HandleRecycle { get; init; } = new();
/// <summary>Gets the fixed tree options.</summary>
public FocasFixedTreeOptions FixedTree { get; init; } = new();
}
@@ -25,7 +32,7 @@ public sealed class FocasDriverOptions
/// </summary>
public sealed class FocasFixedTreeOptions
{
/// <summary>Enable the fixed-node tree for every configured device.</summary>
/// <summary>Gets or sets a value indicating whether the fixed-node tree is enabled for every configured device.</summary>
public bool Enabled { get; init; } = false;
/// <summary>
@@ -67,7 +74,9 @@ public sealed class FocasFixedTreeOptions
/// </remarks>
public sealed class FocasHandleRecycleOptions
{
/// <summary>Gets or sets a value indicating whether handle recycling is enabled.</summary>
public bool Enabled { get; init; } = false;
/// <summary>Gets or sets the interval for handle recycle operations.</summary>
public TimeSpan Interval { get; init; } = TimeSpan.FromHours(1);
}
@@ -78,6 +87,7 @@ public sealed class FocasHandleRecycleOptions
/// </summary>
public sealed class FocasAlarmProjectionOptions
{
/// <summary>Gets or sets a value indicating whether alarm projection is enabled.</summary>
public bool Enabled { get; init; } = false;
/// <summary>Poll cadence. One <c>cnc_rdalmmsg2</c> call per device per tick.</summary>
@@ -109,7 +119,10 @@ public sealed record FocasTagDefinition(
public sealed class FocasProbeOptions
{
/// <summary>Gets or sets a value indicating whether probing is enabled.</summary>
public bool Enabled { get; init; } = true;
/// <summary>Gets or sets the probe interval.</summary>
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
/// <summary>Gets or sets the probe timeout.</summary>
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
}
@@ -9,10 +9,14 @@ public sealed record FocasHostAddress(string Host, int Port)
/// <summary>Fanuc-reserved TCP port for FOCAS Ethernet.</summary>
public const int DefaultPort = 8193;
/// <inheritdoc />
public override string ToString() => Port == DefaultPort
? $"focas://{Host}"
: $"focas://{Host}:{Port}";
/// <summary>Attempts to parse a FOCAS address string.</summary>
/// <param name="value">The address string to parse.</param>
/// <returns>A parsed address, or null if the string is invalid.</returns>
public static FocasHostAddress? TryParse(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
@@ -25,6 +25,8 @@ public static class FocasStatusMapper
/// EW_DATA=8, EW_NOOPT=6, EW_PROT=5, EW_OVRFLOW=2, EW_PARITY=9, EW_PASSWD=11,
/// EW_BUSY=-1, EW_HANDLE=-8, EW_VERSION=-9, EW_UNEXP=-10, EW_SOCKET=-16).
/// </summary>
/// <param name="ret">The FWLIB return code.</param>
/// <returns>The corresponding OPC UA status code.</returns>
public static uint MapFocasReturn(int ret) => ret switch
{
0 => Good,
@@ -17,6 +17,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
public interface IFocasClient : IDisposable
{
/// <summary>Open the FWLIB handle + TCP session. Idempotent.</summary>
/// <param name="address">The CNC host address and port.</param>
/// <param name="timeout">The connection timeout duration.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken cancellationToken);
/// <summary>True when the FWLIB handle is valid + the socket is up.</summary>
@@ -27,6 +30,9 @@ public interface IFocasClient : IDisposable
/// <paramref name="type"/>. Returns a boxed .NET value + the OPC UA status mapped
/// through <see cref="FocasStatusMapper"/>.
/// </summary>
/// <param name="address">The CNC memory address to read from.</param>
/// <param name="type">The FOCAS data type to read.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task<(object? value, uint status)> ReadAsync(
FocasAddress address,
FocasDataType type,
@@ -36,6 +42,10 @@ public interface IFocasClient : IDisposable
/// Write <paramref name="value"/> to <paramref name="address"/>. Returns the mapped
/// OPC UA status (0 = Good).
/// </summary>
/// <param name="address">The CNC memory address to write to.</param>
/// <param name="type">The FOCAS data type to write.</param>
/// <param name="value">The value to write.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task<uint> WriteAsync(
FocasAddress address,
FocasDataType type,
@@ -46,6 +56,7 @@ public interface IFocasClient : IDisposable
/// Cheap health probe — e.g. <c>cnc_rdcncstat</c>. Returns <c>true</c> when the CNC
/// responds with any valid status.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
Task<bool> ProbeAsync(CancellationToken cancellationToken);
/// <summary>
@@ -54,6 +65,7 @@ public interface IFocasClient : IDisposable
/// active". IAlarmSource projection polls this at a configurable interval +
/// emits transitions (raise / clear) on the driver's <c>OnAlarmEvent</c>.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
Task<IReadOnlyList<FocasActiveAlarm>> ReadAlarmsAsync(CancellationToken cancellationToken);
// ---- Fixed-tree T1 (identity + axis discovery + fast-poll dynamic bundle) ----
@@ -63,6 +75,7 @@ public interface IFocasClient : IDisposable
/// subtree of the fixed-node surface. Callable once at session open; the
/// values don't change across the session.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
Task<FocasSysInfo> GetSysInfoAsync(CancellationToken cancellationToken);
/// <summary>
@@ -70,12 +83,14 @@ public interface IFocasClient : IDisposable
/// uses these to build the <c>Axes/{name}/</c> subtree and to index
/// <see cref="ReadDynamicAsync"/> calls.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
Task<IReadOnlyList<FocasAxisName>> GetAxisNamesAsync(CancellationToken cancellationToken);
/// <summary>
/// Read the CNC's configured spindle names via <c>cnc_rdspdlname</c>. Drives
/// the <c>Spindle/{name}/</c> subtree.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
Task<IReadOnlyList<FocasSpindleName>> GetSpindleNamesAsync(CancellationToken cancellationToken);
/// <summary>
@@ -84,6 +99,8 @@ public interface IFocasClient : IDisposable
/// distance-to-go) plus actual feed rate + actual spindle speed + alarm
/// flags + program / sequence numbers — one network round-trip per call.
/// </summary>
/// <param name="axisIndex">The axis index to read dynamics for.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task<FocasDynamicSnapshot> ReadDynamicAsync(int axisIndex, CancellationToken cancellationToken);
// ---- Fixed-tree T2 (program + operation mode) ----
@@ -95,6 +112,7 @@ public interface IFocasClient : IDisposable
/// <see cref="ReadDynamicAsync"/> since program / mode transitions happen
/// on human-operator timescales.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
Task<FocasProgramInfo> GetProgramInfoAsync(CancellationToken cancellationToken);
// ---- Fixed-tree T3 (timers) ----
@@ -104,6 +122,8 @@ public interface IFocasClient : IDisposable
/// Cycle. Values are seconds — the managed side already converted the native
/// minute+msec representation so downstream nodes display uniform units.
/// </summary>
/// <param name="kind">The timer kind to read.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task<FocasTimer> GetTimerAsync(FocasTimerKind kind, CancellationToken cancellationToken);
// ---- Fixed-tree T3.5 (servo meters) ----
@@ -113,6 +133,7 @@ public interface IFocasClient : IDisposable
/// Values are percentages (scaled by <c>10^Dec</c>). Empty list on a
/// disconnected session or unsupported CNC.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
Task<IReadOnlyList<FocasServoLoad>> GetServoLoadsAsync(CancellationToken cancellationToken);
// ---- Fixed-tree T3.6 (spindle meters) ----
@@ -123,12 +144,14 @@ public interface IFocasClient : IDisposable
/// disconnected session or when the CNC doesn't support the call (older
/// series like 16i may return EW_FUNC).
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
Task<IReadOnlyList<int>> GetSpindleLoadsAsync(CancellationToken cancellationToken);
/// <summary>
/// Read per-spindle maximum RPM values. Static configuration, fetched once at
/// bootstrap. Index alignment as per <see cref="GetSpindleLoadsAsync"/>.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
Task<IReadOnlyList<int>> GetSpindleMaxRpmsAsync(CancellationToken cancellationToken);
}
@@ -180,6 +203,7 @@ public sealed record FocasAxisName(string Name, string Suffix)
/// <summary>One configured spindle name (e.g. "S1").</summary>
public sealed record FocasSpindleName(string Name, string Suffix1, string Suffix2, string Suffix3)
{
/// <summary>Gets the display name — name + concatenated suffixes, trimmed.</summary>
public string Display
{
get
@@ -210,6 +234,9 @@ public sealed record FocasProgramInfo(
/// <summary>Human-readable text for the <see cref="FocasProgramInfo.Mode"/> integer.</summary>
public static class FocasOpMode
{
/// <summary>Converts a numeric operation mode to its text representation.</summary>
/// <param name="mode">The operation mode integer code.</param>
/// <returns>The text representation of the mode, or a fallback format if unknown.</returns>
public static string ToText(int mode) => mode switch
{
0 => "MDI",
@@ -252,6 +279,8 @@ public sealed record FocasActiveAlarm(
/// <summary>Factory for <see cref="IFocasClient"/>s. One client per configured device.</summary>
public interface IFocasClientFactory
{
/// <summary>Creates a new FOCAS client instance.</summary>
/// <returns>A new IFocasClient instance.</returns>
IFocasClient Create();
}
@@ -263,6 +292,9 @@ public interface IFocasClientFactory
/// </summary>
public sealed class UnimplementedFocasClientFactory : IFocasClientFactory
{
/// <summary>Creates a new client instance (always throws NotSupportedException).</summary>
/// <returns>Never returns; always throws NotSupportedException.</returns>
/// <exception cref="NotSupportedException">Always thrown to indicate backend is not yet provisioned.</exception>
public IFocasClient Create() => throw new NotSupportedException(
"FOCAS driver backend is 'unimplemented'. Switch to 'Backend: \"wire\"' in driver config " +
"once the CNC is provisioned — see docs/drivers/FOCAS.md.");
@@ -57,12 +57,14 @@ public enum FocasOperationMode : short
public static class FocasOperationModeExtensions
{
/// <summary>
/// Canonical operator-facing label for an operation mode (e.g. <c>"AUTO"</c>,
/// Gets the canonical operator-facing label for an operation mode (e.g. <c>"AUTO"</c>,
/// <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>
/// <param name="mode">The operation mode.</param>
/// <returns>The operator-facing label.</returns>
public static string ToText(this FocasOperationMode mode) =>
FocasOpMode.ToText((short)mode);
}
@@ -74,6 +76,9 @@ public static class FocasOperationModeExtensions
/// </summary>
internal static class FocasPmcAreaLookup
{
/// <summary>Looks up a PMC area code by letter.</summary>
/// <param name="letter">The PMC area letter (case-insensitive).</param>
/// <returns>The corresponding FocasPmcArea, or null if not recognized.</returns>
public static FocasPmcArea? FromLetter(string letter) => letter.ToUpperInvariant() switch
{
"G" => FocasPmcArea.G,
@@ -98,6 +103,9 @@ internal static class FocasPmcAreaLookup
/// </summary>
internal static class FocasPmcDataTypeLookup
{
/// <summary>Maps a FocasDataType to the corresponding PMC wire data type.</summary>
/// <param name="t">The FOCAS data type.</param>
/// <returns>The PMC data type for wire communication.</returns>
public static FocasPmcDataType FromFocasDataType(FocasDataType t) => t switch
{
FocasDataType.Bit or FocasDataType.Byte => FocasPmcDataType.Byte,
@@ -46,6 +46,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
/// Construct a disconnected client. Optional <paramref name="logger"/> receives
/// <c>Debug</c>-level entries per response block (command ID, RC, payload length).
/// </summary>
/// <param name="logger">Optional logger for debug-level wire protocol entries.</param>
public FocasWireClient(ILogger<FocasWireClient>? logger = null)
{
_logger = logger;
@@ -65,6 +66,10 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
/// call while already connected is a no-op. Sub-second timeouts require the
/// <see cref="ConnectAsync(string, int, TimeSpan, CancellationToken)"/> overload.
/// </summary>
/// <param name="host">The CNC hostname or IP address.</param>
/// <param name="port">The FOCAS/2 TCP port (typically 8193).</param>
/// <param name="timeoutSeconds">Connection timeout in seconds; zero or negative disables the timeout.</param>
/// <param name="cancellationToken">Cancellation token for the connect operation.</param>
public Task ConnectAsync(
string host,
int port,
@@ -81,6 +86,10 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
/// <see cref="TimeSpan.Zero"/> to disable the timeout entirely (rely on the caller's
/// <paramref name="cancellationToken"/> instead). Idempotent.
/// </summary>
/// <param name="host">The CNC hostname or IP address.</param>
/// <param name="port">The FOCAS/2 TCP port (typically 8193).</param>
/// <param name="timeout">Connection timeout duration; <see cref="TimeSpan.Zero"/> disables the timeout.</param>
/// <param name="cancellationToken">Cancellation token for the connect operation.</param>
public Task ConnectAsync(
string host,
int port,
@@ -206,6 +215,9 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
/// Read CNC identity via <c>cnc_sysinfo</c>. Cached from the connect-time exchange
/// unless a per-call <paramref name="pathId"/> override is supplied.
/// </summary>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<WireSysInfo>> ReadSysInfoAsync(
CancellationToken cancellationToken = default,
TimeSpan? timeout = null,
@@ -218,6 +230,9 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Read CNC status bits via <c>cnc_statinfo</c> (3 command blocks aggregated into one <see cref="WireStatus"/>).</summary>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<WireStatus>> ReadStatusAsync(
CancellationToken cancellationToken = default,
TimeSpan? timeout = null,
@@ -253,6 +268,10 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Read configured axis names via <c>cnc_rdaxisname</c> (command <c>0x0089</c>).</summary>
/// <param name="maxCount">Maximum number of axis records to return.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<IReadOnlyList<WireAxisRecord>>> ReadAxisNamesAsync(
short maxCount = 32,
CancellationToken cancellationToken = default,
@@ -265,6 +284,10 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Read configured spindle names via <c>cnc_rdspdlname</c> (command <c>0x008a</c>).</summary>
/// <param name="maxCount">Maximum number of spindle records to return.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<IReadOnlyList<WireSpindleRecord>>> ReadSpindleNamesAsync(
short maxCount = 8,
CancellationToken cancellationToken = default,
@@ -281,6 +304,10 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
/// one PDU and aggregates the replies — alarm flags, program/sequence numbers, feed
/// and spindle actuals, plus the four-slot position quadruple.
/// </summary>
/// <param name="axis">The axis number to read from.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<WireDynamic>> ReadDynamic2Async(
short axis = 1,
CancellationToken cancellationToken = default,
@@ -322,6 +349,10 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Read servo-meter load percentages via <c>cnc_rdsvmeter</c> (command <c>0x0056</c>).</summary>
/// <param name="maxCount">Maximum number of servo meter records to return.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<IReadOnlyList<WireServoMeter>>> ReadServoMeterAsync(
short maxCount = 32,
CancellationToken cancellationToken = default,
@@ -355,6 +386,10 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Read per-spindle load percentages via <c>cnc_rdspload</c> (command <c>0x0040</c> with arg1=0).</summary>
/// <param name="spindleSelector">Spindle selector; -1 selects all spindles.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public Task<FocasResult<IReadOnlyList<WireSpindleMetric>>> ReadSpindleLoadAsync(
short spindleSelector = -1,
CancellationToken cancellationToken = default,
@@ -363,6 +398,10 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
=> ReadSpindleMetricAsync(0, spindleSelector, cancellationToken, timeout, pathId);
/// <summary>Read per-spindle maximum RPMs via <c>cnc_rdspmaxrpm</c> (command <c>0x0040</c> with arg1=1).</summary>
/// <param name="spindleSelector">Spindle selector; -1 selects all spindles.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public Task<FocasResult<IReadOnlyList<WireSpindleMetric>>> ReadSpindleMaxRpmAsync(
short spindleSelector = -1,
CancellationToken cancellationToken = default,
@@ -375,6 +414,11 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
/// payload to the type declared in the per-series parameter catalog. <paramref name="axis"/>
/// selects an axis-scoped parameter; <c>0</c> means global.
/// </summary>
/// <param name="dataNumber">The parameter data number.</param>
/// <param name="axis">Axis selector; zero means a global parameter.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<byte[]>> ReadParameterBytesAsync(
short dataNumber,
short axis = 0,
@@ -389,6 +433,11 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Typed Int32 parameter read — convenience over <see cref="ReadParameterBytesAsync"/>.</summary>
/// <param name="dataNumber">The parameter data number.</param>
/// <param name="type">The FOCAS parameter type code.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<WireParameter>> ReadParameterAsync(
short dataNumber,
short type = 0,
@@ -404,6 +453,11 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Typed 8-bit parameter read.</summary>
/// <param name="dataNumber">The parameter data number.</param>
/// <param name="axis">Axis selector; zero means a global parameter.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<byte>> ReadParameterByteAsync(short dataNumber, short axis = 0, CancellationToken cancellationToken = default, TimeSpan? timeout = null, ushort? pathId = null)
{
var result = await ReadParameterBytesAsync(dataNumber, axis, cancellationToken, timeout, pathId).ConfigureAwait(false);
@@ -413,6 +467,11 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Typed 16-bit parameter read.</summary>
/// <param name="dataNumber">The parameter data number.</param>
/// <param name="axis">Axis selector; zero means a global parameter.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<short>> ReadParameterInt16Async(short dataNumber, short axis = 0, CancellationToken cancellationToken = default, TimeSpan? timeout = null, ushort? pathId = null)
{
var result = await ReadParameterBytesAsync(dataNumber, axis, cancellationToken, timeout, pathId).ConfigureAwait(false);
@@ -422,6 +481,11 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Typed 32-bit parameter read.</summary>
/// <param name="dataNumber">The parameter data number.</param>
/// <param name="axis">Axis selector; zero means a global parameter.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<int>> ReadParameterInt32Async(short dataNumber, short axis = 0, CancellationToken cancellationToken = default, TimeSpan? timeout = null, ushort? pathId = null)
{
var result = await ReadParameterBytesAsync(dataNumber, axis, cancellationToken, timeout, pathId).ConfigureAwait(false);
@@ -431,6 +495,11 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Typed IEEE-754 single-precision parameter read.</summary>
/// <param name="dataNumber">The parameter data number.</param>
/// <param name="axis">Axis selector; zero means a global parameter.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<float>> ReadParameterFloat32Async(short dataNumber, short axis = 0, CancellationToken cancellationToken = default, TimeSpan? timeout = null, ushort? pathId = null)
{
var result = await ReadParameterBytesAsync(dataNumber, axis, cancellationToken, timeout, pathId).ConfigureAwait(false);
@@ -440,6 +509,11 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Typed IEEE-754 double-precision parameter read.</summary>
/// <param name="dataNumber">The parameter data number.</param>
/// <param name="axis">Axis selector; zero means a global parameter.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<double>> ReadParameterFloat64Async(short dataNumber, short axis = 0, CancellationToken cancellationToken = default, TimeSpan? timeout = null, ushort? pathId = null)
{
var result = await ReadParameterBytesAsync(dataNumber, axis, cancellationToken, timeout, pathId).ConfigureAwait(false);
@@ -449,6 +523,10 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Read a single macro variable via <c>cnc_rdmacro</c> (command <c>0x0015</c>).</summary>
/// <param name="number">The macro variable number.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public Task<FocasResult<WireMacro>> ReadMacroAsync(
short number,
CancellationToken cancellationToken = default,
@@ -465,6 +543,13 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
/// is the width code (see <see cref="FocasPmcDataType"/>). Payload is decoded into
/// <see cref="WirePmcRange.Values"/> — one entry per slot of the requested width.
/// </summary>
/// <param name="area">The PMC address-letter code numeric value.</param>
/// <param name="dataType">The PMC data width code numeric value.</param>
/// <param name="start">The starting address.</param>
/// <param name="end">The ending address; must be greater than or equal to start.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<WirePmcRange>> ReadPmcRangeAsync(
short area,
short dataType,
@@ -510,6 +595,13 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Typed overload for <see cref="ReadPmcRangeAsync(short, short, ushort, ushort, CancellationToken, TimeSpan?, ushort?)"/>.</summary>
/// <param name="area">The PMC address-letter code.</param>
/// <param name="dataType">The PMC data width code.</param>
/// <param name="start">The starting address.</param>
/// <param name="end">The ending address; must be greater than or equal to start.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public Task<FocasResult<WirePmcRange>> ReadPmcRangeAsync(
FocasPmcArea area,
FocasPmcDataType dataType,
@@ -525,6 +617,11 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
/// the 76-byte vendor <c>ODBALMMSG2_data</c> layout and the 80-byte legacy wire
/// shape so the same managed surface works across firmware revisions.
/// </summary>
/// <param name="type">Alarm type filter; -1 reads all active alarms.</param>
/// <param name="count">Maximum number of alarms to return.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public async Task<FocasResult<IReadOnlyList<WireAlarm>>> ReadAlarmsAsync(
short type = -1,
short count = 32,
@@ -541,6 +638,9 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
}
/// <summary>Read operation mode via <c>cnc_rdopmode</c>, returned as the typed <see cref="FocasOperationMode"/>.</summary>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public Task<FocasResult<FocasOperationMode>> ReadOperationModeAsync(
CancellationToken cancellationToken = default,
TimeSpan? timeout = null,
@@ -555,6 +655,9 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
/// FOCAS <c>short</c> so callers storing the raw mode code (e.g. OtOpcUa's
/// <c>FocasProgramInfo.Mode</c> int field) don't have to cast the enum.
/// </summary>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public Task<FocasResult<short>> ReadOperationModeCodeAsync(
CancellationToken cancellationToken = default,
TimeSpan? timeout = null,
@@ -565,6 +668,9 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
cancellationToken, timeout, EffectivePathId(pathId));
/// <summary>Read the currently-executing program name + O-number via <c>cnc_exeprgname2</c> (command <c>0x00fc</c>).</summary>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public Task<FocasResult<WireProgramName>> ReadExecutingProgramNameAsync(
CancellationToken cancellationToken = default,
TimeSpan? timeout = null,
@@ -572,6 +678,9 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
=> ReadSingleWithTimeoutAsync(0x00fc, ParseProgramName, cancellationToken, timeout, EffectivePathId(pathId));
/// <summary>Read the executed block count via <c>cnc_rdblkcount</c>.</summary>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public Task<FocasResult<int>> ReadBlockCountAsync(
CancellationToken cancellationToken = default,
TimeSpan? timeout = null,
@@ -585,6 +694,10 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
/// Read one cumulative timer via <c>cnc_rdtimer</c>. <paramref name="type"/> selects
/// PowerOn / Operating / Cutting / Cycle per the FOCAS spec (0..3).
/// </summary>
/// <param name="type">Timer type selector (0=PowerOn, 1=Operating, 2=Cutting, 3=Cycle).</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
/// <param name="timeout">Optional per-call timeout override.</param>
/// <param name="pathId">Optional path ID override; defaults to <see cref="PathId"/>.</param>
public Task<FocasResult<WireTimer>> ReadTimerAsync(
short type,
CancellationToken cancellationToken = default,
@@ -26,11 +26,17 @@ public class FocasWireException : Exception
/// </summary>
public bool IsTransient { get; }
/// <summary>Initializes a new FOCAS wire exception with a message.</summary>
/// <param name="message">The exception message.</param>
public FocasWireException(string message)
: base(message)
{
}
/// <summary>Initializes a new FOCAS wire exception with message, return code, and transient flag.</summary>
/// <param name="message">The exception message.</param>
/// <param name="rc">The FOCAS return code, if available.</param>
/// <param name="isTransient">Whether the transport was closed.</param>
public FocasWireException(string message, short? rc, bool isTransient = false)
: base(message)
{
@@ -38,11 +44,18 @@ public class FocasWireException : Exception
IsTransient = isTransient;
}
/// <summary>Initializes a new FOCAS wire exception with a message and inner exception.</summary>
/// <param name="message">The exception message.</param>
/// <param name="innerException">The inner exception.</param>
public FocasWireException(string message, Exception innerException)
: base(message, innerException)
{
}
/// <summary>Initializes a new FOCAS wire exception with message, inner exception, and transient flag.</summary>
/// <param name="message">The exception message.</param>
/// <param name="innerException">The inner exception.</param>
/// <param name="isTransient">Whether the transport was closed.</param>
public FocasWireException(string message, Exception innerException, bool isTransient)
: base(message, innerException)
{
@@ -29,6 +29,10 @@ internal static class FocasWireProtocol
private static readonly byte[] Magic = [0xa0, 0xa0, 0xa0, 0xa0];
/// <summary>Assemble a full PDU (10-byte header + body) for transmission.</summary>
/// <param name="type">The PDU type byte.</param>
/// <param name="direction">The direction byte (request or response).</param>
/// <param name="body">The PDU body bytes.</param>
/// <returns>The complete PDU bytes including header and body.</returns>
public static byte[] BuildPdu(byte type, byte direction, ReadOnlySpan<byte> body)
{
if (body.Length > ushort.MaxValue)
@@ -49,6 +53,8 @@ internal static class FocasWireProtocol
/// opens two TCP sockets in sequence and each sends its own initiate PDU carrying its
/// index.
/// </summary>
/// <param name="socketIndex">The socket index (1 or 2).</param>
/// <returns>The initiate body bytes.</returns>
public static byte[] BuildInitiateBody(ushort socketIndex)
{
var body = new byte[2];
@@ -57,6 +63,8 @@ internal static class FocasWireProtocol
}
/// <summary>Assemble a type-<c>0x21</c> body carrying one or more request blocks.</summary>
/// <param name="blocks">The request blocks to assemble.</param>
/// <returns>The assembled body bytes.</returns>
public static byte[] BuildRequestBody(IReadOnlyList<RequestBlock> blocks)
{
if (blocks.Count > ushort.MaxValue)
@@ -79,6 +87,9 @@ internal static class FocasWireProtocol
}
/// <summary>Async read of one full PDU off a stream. Throws <see cref="FocasWireException"/> on invalid magic / version / truncation.</summary>
/// <param name="stream">The network stream to read from.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The read PDU.</returns>
public static async Task<Pdu> ReadPduAsync(NetworkStream stream, CancellationToken cancellationToken)
{
var header = new byte[10];
@@ -100,6 +111,8 @@ internal static class FocasWireProtocol
}
/// <summary>Synchronous counterpart to <see cref="ReadPduAsync"/> — used by <see cref="FocasWireClient"/>'s sync dispose.</summary>
/// <param name="stream">The network stream to read from.</param>
/// <returns>The read PDU.</returns>
public static Pdu ReadPdu(NetworkStream stream)
{
var header = new byte[10];
@@ -149,6 +162,8 @@ internal static class FocasWireProtocol
/// block carries the command ID, the FOCAS <c>EW_*</c> return code, and the payload
/// bytes.
/// </summary>
/// <param name="body">The response body bytes.</param>
/// <returns>The parsed response blocks.</returns>
public static IReadOnlyList<ResponseBlock> ParseResponseBlocks(ReadOnlySpan<byte> body)
{
if (body.Length < 2)
@@ -181,6 +196,8 @@ internal static class FocasWireProtocol
}
/// <summary>Read an ASCII string out of a payload span, stopping at the first NUL and trimming trailing spaces.</summary>
/// <param name="bytes">The bytes to decode.</param>
/// <returns>The decoded ASCII string.</returns>
public static string ReadAscii(ReadOnlySpan<byte> bytes)
{
var end = bytes.IndexOf((byte)0);
@@ -193,6 +210,8 @@ internal static class FocasWireProtocol
/// (spindle) slot. Trailing spaces and NULs are stripped so <c>"X "</c> becomes
/// <c>"X"</c>.
/// </summary>
/// <param name="bytes">The bytes to decode.</param>
/// <returns>The decoded name record.</returns>
public static string ReadNameRecord(ReadOnlySpan<byte> bytes)
{
if (bytes.Length < 2) return string.Empty;
@@ -30,13 +30,19 @@ public sealed class WireFocasClient : IFocasClient
/// <see cref="FocasWireClient"/> so the per-response Debug entries actually reach
/// the host's logging pipeline (Driver.FOCAS-007).
/// </summary>
/// <param name="logger">Optional logger for debug output from wire client responses.</param>
public WireFocasClient(ILogger<FocasWireClient>? logger)
{
_wire = new FocasWireClient(logger);
}
/// <summary>Gets a value indicating whether the wire client is connected to the FOCAS host.</summary>
public bool IsConnected => _wire.IsConnected;
/// <summary>Connects to a FOCAS host at the specified address.</summary>
/// <param name="address">The host address containing the machine name and port.</param>
/// <param name="timeout">The connection timeout; values less than or equal to zero are clamped to 1 second.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
public async Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken cancellationToken)
{
if (_wire.IsConnected) return;
@@ -48,6 +54,11 @@ public sealed class WireFocasClient : IFocasClient
await _wire.ConnectAsync(address.Host, address.Port, effective, cancellationToken).ConfigureAwait(false);
}
/// <summary>Reads a value from the specified FOCAS address.</summary>
/// <param name="address">The FOCAS address to read from.</param>
/// <param name="type">The FOCAS data type of the value.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A tuple containing the read value and FOCAS status code.</returns>
public async Task<(object? value, uint status)> ReadAsync(
FocasAddress address, FocasDataType type, CancellationToken cancellationToken)
{
@@ -63,10 +74,19 @@ public sealed class WireFocasClient : IFocasClient
};
}
/// <summary>Writes a value to a FOCAS address (always returns BadNotWritable as OtOpcUa is read-only).</summary>
/// <param name="address">The FOCAS address to write to.</param>
/// <param name="type">The FOCAS data type of the value.</param>
/// <param name="value">The value to write.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that returns the BadNotWritable status code.</returns>
public Task<uint> WriteAsync(
FocasAddress address, FocasDataType type, object? value, CancellationToken cancellationToken)
=> Task.FromResult(FocasStatusMapper.BadNotWritable);
/// <summary>Probes the FOCAS host to verify connectivity.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>True if the probe succeeds; otherwise false.</returns>
public async Task<bool> ProbeAsync(CancellationToken cancellationToken)
{
if (!_wire.IsConnected) return false;
@@ -81,6 +101,9 @@ public sealed class WireFocasClient : IFocasClient
}
}
/// <summary>Reads all active alarms from the FOCAS host.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A list of active alarms; empty if read fails or not connected.</returns>
public async Task<IReadOnlyList<FocasActiveAlarm>> ReadAlarmsAsync(CancellationToken cancellationToken)
{
if (!_wire.IsConnected) return [];
@@ -102,6 +125,9 @@ public sealed class WireFocasClient : IFocasClient
Message: a.Message ?? string.Empty);
}
/// <summary>Gets system information from the FOCAS host.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The FOCAS system information.</returns>
public async Task<FocasSysInfo> GetSysInfoAsync(CancellationToken cancellationToken)
{
RequireConnected();
@@ -121,6 +147,9 @@ public sealed class WireFocasClient : IFocasClient
AxesCount: axesCount);
}
/// <summary>Gets the names of all axes on the FOCAS host.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A list of axis names; empty if read fails or not connected.</returns>
public async Task<IReadOnlyList<FocasAxisName>> GetAxisNamesAsync(CancellationToken cancellationToken)
{
if (!_wire.IsConnected) return [];
@@ -140,6 +169,9 @@ public sealed class WireFocasClient : IFocasClient
}
}
/// <summary>Gets the names of all spindles on the FOCAS host.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A list of spindle names; empty if read fails or not connected.</returns>
public async Task<IReadOnlyList<FocasSpindleName>> GetSpindleNamesAsync(CancellationToken cancellationToken)
{
if (!_wire.IsConnected) return [];
@@ -158,6 +190,10 @@ public sealed class WireFocasClient : IFocasClient
}
}
/// <summary>Reads the dynamic state of a specified axis.</summary>
/// <param name="axisIndex">The index of the axis to read.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The dynamic snapshot of the axis.</returns>
public async Task<FocasDynamicSnapshot> ReadDynamicAsync(int axisIndex, CancellationToken cancellationToken)
{
RequireConnected();
@@ -179,6 +215,9 @@ public sealed class WireFocasClient : IFocasClient
DistanceToGo: pos.Distance);
}
/// <summary>Gets information about the currently executing program.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The current program information.</returns>
public async Task<FocasProgramInfo> GetProgramInfoAsync(CancellationToken cancellationToken)
{
RequireConnected();
@@ -206,6 +245,10 @@ public sealed class WireFocasClient : IFocasClient
Mode: modeResult.IsOk ? modeResult.Value : 0);
}
/// <summary>Gets a timer value from the FOCAS host.</summary>
/// <param name="kind">The kind of timer to read (run time, cutting time, etc.).</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The timer value.</returns>
public async Task<FocasTimer> GetTimerAsync(FocasTimerKind kind, CancellationToken cancellationToken)
{
RequireConnected();
@@ -215,6 +258,9 @@ public sealed class WireFocasClient : IFocasClient
return new FocasTimer(kind, t.Minutes, t.Milliseconds);
}
/// <summary>Gets servo load information for all axes.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A list of servo load values for each axis; empty if read fails or not connected.</returns>
public async Task<IReadOnlyList<FocasServoLoad>> GetServoLoadsAsync(CancellationToken cancellationToken)
{
if (!_wire.IsConnected) return [];
@@ -226,9 +272,15 @@ public sealed class WireFocasClient : IFocasClient
.ToList();
}
/// <summary>Gets spindle load information for all spindles.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A list of spindle load percentages; empty if read fails or not connected.</returns>
public Task<IReadOnlyList<int>> GetSpindleLoadsAsync(CancellationToken cancellationToken) =>
ReadSpindleMetricAsync((sel, ct) => _wire.ReadSpindleLoadAsync(sel, ct), cancellationToken);
/// <summary>Gets maximum RPM information for all spindles.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A list of maximum RPM values for each spindle; empty if read fails or not connected.</returns>
public Task<IReadOnlyList<int>> GetSpindleMaxRpmsAsync(CancellationToken cancellationToken) =>
ReadSpindleMetricAsync((sel, ct) => _wire.ReadSpindleMaxRpmAsync(sel, ct), cancellationToken);
@@ -249,6 +301,7 @@ public sealed class WireFocasClient : IFocasClient
return list;
}
/// <summary>Disposes the wire client and releases all resources.</summary>
public void Dispose() => _wire.Dispose();
// ---- PMC / Parameter / Macro read paths ------------------------------------------
@@ -359,6 +412,7 @@ public sealed class WireFocasClientFactory : IFocasClientFactory
{
private readonly ILogger<FocasWireClient>? _logger;
/// <summary>Initializes a new instance of the WireFocasClientFactory without a logger.</summary>
public WireFocasClientFactory() : this(logger: null) { }
/// <summary>
@@ -367,10 +421,13 @@ public sealed class WireFocasClientFactory : IFocasClientFactory
/// client already emits Debug entries per FOCAS response, but the previous no-arg
/// factory path discarded them.
/// </summary>
/// <param name="logger">Optional logger for debug output from wire client responses.</param>
public WireFocasClientFactory(ILogger<FocasWireClient>? logger)
{
_logger = logger;
}
/// <summary>Creates a new WireFocasClient instance.</summary>
/// <returns>A new IFocasClient implementation.</returns>
public IFocasClient Create() => new WireFocasClient(_logger);
}