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
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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user