Auto: focas-f2a — DIAG: address scheme

New FocasAreaKind.Diagnostic parsed from DIAG:nnn (whole-CNC) and
DIAG:nnn/axis (per-axis), validated against a per-series
FocasCapabilityMatrix.DiagnosticRange table (16i: 0-499; 0i-F family:
0-999; 30i/31i/32i: 0-1023; Power Motion i: 0-255; Unknown: permissive
per existing matrix convention).

IFocasClient gains ReadDiagnosticAsync(diagNumber, axisOrZero, type,
ct) with a default returning BadNotSupported so older transport
variants degrade gracefully. FwlibFocasClient implements it via a new
cnc_rddiag P/Invoke that reuses the IODBPSD struct (same shape as
cnc_rdparam). FocasDriver.ReadAsync dispatches Diagnostic addresses
through the new path; non-Diagnostic kinds keep the existing
ReadAsync route unchanged.

Tests: parser positives (DIAG:1031, DIAG:280/2, case-insensitive,
zero, axis-8) + negatives (malformed, axis>31), capability matrix
boundaries per series, driver-level dispatch verifying axis index
threads through, init-time rejection on out-of-range, and
BadNotSupported fallback when the wire client doesn't override the
default. 266/266 pass in Driver.FOCAS.Tests.

Closes #263

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-25 19:31:49 -04:00
parent 6743d51db8
commit 451b37a632
9 changed files with 364 additions and 9 deletions

View File

@@ -59,10 +59,20 @@ internal sealed class FwlibFocasClient : IFocasClient
FocasAreaKind.Pmc => Task.FromResult(ReadPmc(address, type)),
FocasAreaKind.Parameter => Task.FromResult(ReadParameter(address, type)),
FocasAreaKind.Macro => Task.FromResult(ReadMacro(address)),
FocasAreaKind.Diagnostic => Task.FromResult(
ReadDiagnostic(address.Number, address.BitIndex ?? 0, type)),
_ => Task.FromResult<(object?, uint)>((null, FocasStatusMapper.BadNotSupported)),
};
}
public Task<(object? value, uint status)> ReadDiagnosticAsync(
int diagNumber, int axisOrZero, FocasDataType type, CancellationToken cancellationToken)
{
if (!_connected) return Task.FromResult<(object?, uint)>((null, FocasStatusMapper.BadCommunicationError));
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(ReadDiagnostic(diagNumber, axisOrZero, type));
}
public async Task<uint> WriteAsync(
FocasAddress address, FocasDataType type, object? value, CancellationToken cancellationToken)
{
@@ -467,6 +477,36 @@ internal sealed class FwlibFocasClient : IFocasClient
return ret == 0 ? FocasStatusMapper.Good : FocasStatusMapper.MapFocasReturn(ret);
}
private (object? value, uint status) ReadDiagnostic(int diagNumber, int axisOrZero, FocasDataType type)
{
var buf = new FwlibNative.IODBPSD { Data = new byte[32] };
var length = DiagnosticReadLength(type);
var ret = FwlibNative.RdDiag(_handle, (ushort)diagNumber, (short)axisOrZero, (short)length, ref buf);
if (ret != 0) return (null, FocasStatusMapper.MapFocasReturn(ret));
var value = type switch
{
FocasDataType.Bit => (object)ExtractBit(buf.Data[0], 0),
FocasDataType.Byte => (object)(sbyte)buf.Data[0],
FocasDataType.Int16 => (object)BinaryPrimitives.ReadInt16LittleEndian(buf.Data),
FocasDataType.Int32 => (object)BinaryPrimitives.ReadInt32LittleEndian(buf.Data),
FocasDataType.Float32 => (object)BinaryPrimitives.ReadSingleLittleEndian(buf.Data),
FocasDataType.Float64 => (object)BinaryPrimitives.ReadDoubleLittleEndian(buf.Data),
_ => (object)BinaryPrimitives.ReadInt32LittleEndian(buf.Data),
};
return (value, FocasStatusMapper.Good);
}
private static int DiagnosticReadLength(FocasDataType type) => type switch
{
FocasDataType.Bit or FocasDataType.Byte => 4 + 1,
FocasDataType.Int16 => 4 + 2,
FocasDataType.Int32 => 4 + 4,
FocasDataType.Float32 => 4 + 4,
FocasDataType.Float64 => 4 + 8,
_ => 4 + 4,
};
private (object? value, uint status) ReadMacro(FocasAddress address)
{
var buf = new FwlibNative.ODBM();