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

@@ -32,9 +32,10 @@ public static class FocasCapabilityMatrix
return address.Kind switch
{
FocasAreaKind.Macro => ValidateMacro(series, address.Number),
FocasAreaKind.Parameter => ValidateParameter(series, address.Number),
FocasAreaKind.Pmc => ValidatePmc(series, address.PmcLetter, address.Number),
FocasAreaKind.Macro => ValidateMacro(series, address.Number),
FocasAreaKind.Parameter => ValidateParameter(series, address.Number),
FocasAreaKind.Pmc => ValidatePmc(series, address.PmcLetter, address.Number),
FocasAreaKind.Diagnostic => ValidateDiagnostic(series, address.Number),
_ => null,
};
}
@@ -73,6 +74,29 @@ public static class FocasCapabilityMatrix
_ => (0, int.MaxValue),
};
/// <summary>
/// CNC diagnostic number range accepted by a series; from <c>cnc_rddiag</c>
/// (and <c>cnc_rddiagdgn</c> for axis-scoped reads). Returning <c>null</c>
/// means the series doesn't support <c>cnc_rddiag</c> at all — the driver
/// rejects every <c>DIAG:</c> address on that series. Conservative ceilings
/// per the FOCAS Developer Kit: legacy 16i-family caps at 499; modern 0i-F
/// family at 999; 30i / 31i / 32i extend to 1023. Power Motion i has a
/// narrow diagnostic surface (0..255).
/// </summary>
internal static (int min, int max)? DiagnosticRange(FocasCncSeries series) => series switch
{
FocasCncSeries.Sixteen_i => (0, 499),
FocasCncSeries.Zero_i_D => (0, 499),
FocasCncSeries.Zero_i_F or
FocasCncSeries.Zero_i_MF or
FocasCncSeries.Zero_i_TF => (0, 999),
FocasCncSeries.Thirty_i or
FocasCncSeries.ThirtyOne_i or
FocasCncSeries.ThirtyTwo_i => (0, 1023),
FocasCncSeries.PowerMotion_i => (0, 255),
_ => (0, int.MaxValue),
};
/// <summary>PMC letters accepted per series. Legacy controllers omit F/M/C
/// signal groups that 30i-family ladder programs use.</summary>
internal static IReadOnlySet<string> PmcLetters(FocasCncSeries series) => series switch
@@ -143,6 +167,16 @@ public static class FocasCapabilityMatrix
: null;
}
private static string? ValidateDiagnostic(FocasCncSeries series, int number)
{
if (DiagnosticRange(series) is not { } range)
return $"Diagnostic addresses are not supported on {series} (no documented cnc_rddiag range).";
var (min, max) = range;
return (number < min || number > max)
? $"Diagnostic #{number} is outside the documented range [{min}, {max}] for {series}."
: null;
}
private static string? ValidatePmc(FocasCncSeries series, string? letter, int number)
{
if (string.IsNullOrEmpty(letter)) return "PMC address is missing its letter prefix.";