feat(focas): add cnc_getfigure wire command + focas-mock handler

This commit is contained in:
Joseph Doherty
2026-06-18 12:23:14 -04:00
parent e5b1a5574a
commit f320f323ae
4 changed files with 68 additions and 7 deletions
@@ -385,6 +385,41 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable
return new FocasResult<IReadOnlyList<WireServoMeter>>(rc, result);
}
/// <summary>
/// Read the per-axis position decimal-place figures via <c>cnc_getfigure</c>
/// (command <c>0x00d3</c>). The response payload is a self-delimiting sequence of
/// big-endian <c>short</c> decimal-place counts — one per configured axis — which the
/// driver applies as a <c>10^figure</c> scale on raw position reads.
/// </summary>
/// <remarks>
/// The <c>0x00d3</c> command id is co-designed with the in-tree <c>focas_mock</c> sim and
/// validated against it; like the rest of this managed wire backend the binary shape is
/// sim-consistent and has not been validated against a real Fanuc CNC (bench-CNC-gated).
/// </remarks>
/// <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<int>>> ReadPositionFiguresAsync(
CancellationToken cancellationToken = default,
TimeSpan? timeout = null,
ushort? pathId = null)
{
using var callTimeout = CreateCallTimeout(cancellationToken, timeout);
var requestPathId = EffectivePathId(pathId);
var block = await SendSingleRequestAsync(
callTimeout.Token,
new RequestBlock(0x00d3, PathId: requestPathId)).ConfigureAwait(false);
if (block.Rc != 0) return new FocasResult<IReadOnlyList<int>>(block.Rc, null);
var payload = block.Payload;
var figures = new List<int>(payload.Length / 2);
for (var offset = 0; offset + 2 <= payload.Length; offset += 2)
figures.Add(ReadInt16(payload, offset));
return new FocasResult<IReadOnlyList<int>>(block.Rc, figures);
}
/// <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>
@@ -287,14 +287,26 @@ public sealed class WireFocasClient : IFocasClient
/// <summary>Gets the per-axis position decimal-place figures via <c>cnc_getfigure</c>.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>
/// An empty list — the managed FOCAS/2 Ethernet wire client (<see cref="FocasWireClient"/>)
/// does not currently expose the <c>cnc_getfigure</c> command, so this backend reports no
/// per-axis figures. Per the <see cref="IFocasClient.GetPositionFiguresAsync"/> contract an
/// empty list signals the driver to fall back to the configured <c>PositionDecimalPlaces</c>.
/// Never throws — figure reads degrade to the config knob rather than faulting.
/// The per-axis decimal-place figures read from the CNC via
/// <see cref="FocasWireClient.ReadPositionFiguresAsync"/>, or an empty list when the read
/// fails (non-zero RC — e.g. the series lacks <c>cnc_getfigure</c>) or is not connected.
/// Per the <see cref="IFocasClient.GetPositionFiguresAsync"/> contract an empty list signals
/// the driver to fall back to the configured <c>PositionDecimalPlaces</c>. Never throws —
/// figure reads degrade to the config knob rather than faulting.
/// </returns>
public Task<IReadOnlyList<int>> GetPositionFiguresAsync(CancellationToken cancellationToken) =>
Task.FromResult<IReadOnlyList<int>>(Array.Empty<int>());
public async Task<IReadOnlyList<int>> GetPositionFiguresAsync(CancellationToken cancellationToken)
{
if (!_wire.IsConnected) return [];
try
{
var result = await _wire.ReadPositionFiguresAsync(cancellationToken).ConfigureAwait(false);
return result.IsOk && result.Value is { } figures ? figures : [];
}
catch (FocasWireException)
{
return [];
}
}
private static async Task<IReadOnlyList<int>> ReadSpindleMetricAsync(
Func<short, CancellationToken, Task<FocasResult<IReadOnlyList<WireSpindleMetric>>>> call,