namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
///
/// Documented-API capability matrix — per CNC series, what ranges each
/// supports. Authoritative source for the driver's
/// pre-flight validation in .
///
///
/// Ranges come from the Fanuc FOCAS Developer Kit documentation matrix
/// (see docs/v2/focas-version-matrix.md for the authoritative copy with
/// per-function citations). Numbers chosen to match what the FOCAS library
/// accepts — a read against an address outside the documented range returns
/// EW_NUMBER or EW_PARAM at the wire, which this driver maps to
/// BadOutOfRange. Catching at init time surfaces the mismatch as a config
/// error before any session is opened.
/// is treated permissively: every
/// address passes validation. Pre-matrix configs don't break on upgrade; new
/// deployments are encouraged to declare a series in the device options.
///
public static class FocasCapabilityMatrix
{
///
/// Check whether is accepted by a CNC of
/// . Returns null on pass + a failure reason
/// on reject — the driver surfaces the reason string verbatim when failing
/// InitializeAsync so operators see the specific out-of-range without
/// guessing.
///
public static string? Validate(FocasCncSeries series, FocasAddress address)
{
if (series == FocasCncSeries.Unknown) return null;
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.Diagnostic => ValidateDiagnostic(series, address.Number),
_ => null,
};
}
/// Macro variable number accepted by a CNC series. Cites
/// cnc_rdmacro/cnc_wrmacro in the Developer Kit.
internal static (int min, int max) MacroRange(FocasCncSeries series) => series switch
{
// Common macros 1-33 + 100-199 + 500-999 universally; extended 10000+ only on
// higher-end series. Using the extended ceiling per series per DevKit notes.
FocasCncSeries.Sixteen_i => (0, 999),
FocasCncSeries.Zero_i_D => (0, 999),
FocasCncSeries.Zero_i_F or
FocasCncSeries.Zero_i_MF or
FocasCncSeries.Zero_i_TF => (0, 9999),
FocasCncSeries.Thirty_i or
FocasCncSeries.ThirtyOne_i or
FocasCncSeries.ThirtyTwo_i => (0, 99999),
FocasCncSeries.PowerMotion_i => (0, 999),
_ => (0, int.MaxValue),
};
/// Parameter number accepted; from cnc_rdparam/cnc_wrparam.
/// Ranges reflect the highest-numbered parameter documented per series.
internal static (int min, int max) ParameterRange(FocasCncSeries series) => series switch
{
FocasCncSeries.Sixteen_i => (0, 9999),
FocasCncSeries.Zero_i_D or
FocasCncSeries.Zero_i_F or
FocasCncSeries.Zero_i_MF or
FocasCncSeries.Zero_i_TF => (0, 14999),
FocasCncSeries.Thirty_i or
FocasCncSeries.ThirtyOne_i or
FocasCncSeries.ThirtyTwo_i => (0, 29999),
FocasCncSeries.PowerMotion_i => (0, 29999),
_ => (0, int.MaxValue),
};
///
/// CNC diagnostic number range accepted by a series; from cnc_rddiag
/// (and cnc_rddiagdgn for axis-scoped reads). Returning null
/// means the series doesn't support cnc_rddiag at all — the driver
/// rejects every DIAG: 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).
///
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),
};
/// PMC letters accepted per series. Legacy 16i ladders use X/Y/F/G
/// for handshakes plus R/D for retained/data; M/C/E/A/K/T are the 0i-F /
/// 30i-family extensions.
internal static IReadOnlySet PmcLetters(FocasCncSeries series) => series switch
{
FocasCncSeries.Sixteen_i => new HashSet(StringComparer.OrdinalIgnoreCase) { "X", "Y", "F", "G", "R", "D" },
FocasCncSeries.Zero_i_D => new HashSet(StringComparer.OrdinalIgnoreCase) { "X", "Y", "R", "D", "E", "A" },
FocasCncSeries.Zero_i_F or
FocasCncSeries.Zero_i_MF or
FocasCncSeries.Zero_i_TF => new HashSet(StringComparer.OrdinalIgnoreCase) { "X", "Y", "F", "G", "R", "D", "E", "A", "M", "C" },
FocasCncSeries.Thirty_i or
FocasCncSeries.ThirtyOne_i or
FocasCncSeries.ThirtyTwo_i => new HashSet(StringComparer.OrdinalIgnoreCase) { "X", "Y", "F", "G", "R", "D", "E", "A", "M", "C", "K", "T" },
FocasCncSeries.PowerMotion_i => new HashSet(StringComparer.OrdinalIgnoreCase) { "X", "Y", "R", "D" },
_ => new HashSet(StringComparer.OrdinalIgnoreCase),
};
/// 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.
internal static int PmcMaxNumber(FocasCncSeries series) => series switch
{
FocasCncSeries.Sixteen_i => 999,
FocasCncSeries.Zero_i_D => 1999,
FocasCncSeries.Zero_i_F or
FocasCncSeries.Zero_i_MF or
FocasCncSeries.Zero_i_TF => 9999,
FocasCncSeries.Thirty_i or
FocasCncSeries.ThirtyOne_i or
FocasCncSeries.ThirtyTwo_i => 59999,
FocasCncSeries.PowerMotion_i => 1999,
_ => int.MaxValue,
};
///
/// Whether the FOCAS driver should expose the per-device Tooling/
/// fixed-tree subfolder for a given . Backed by
/// cnc_rdtnum, which is documented for every modern Fanuc series
/// (0i / 16i / 30i families) — defaulting to true. The capability
/// hook exists so a future controller without cnc_rdtnum can opt
/// out without touching the driver.
/// stays permissive (matches the modal / override fixed-tree precedent in
/// issue #259). Issue #260.
///
public static bool SupportsTooling(FocasCncSeries series) => true;
///
/// Whether the FOCAS driver should expose the per-device Offsets/
/// fixed-tree subfolder for a given . Backed by
/// cnc_rdzofs(n=1..6) for the standard G54..G59 surfaces; extended
/// G54.1 P1..P48 surfaces are deferred to a follow-up. Same permissive
/// policy as . Issue #260.
///
public static bool SupportsWorkOffsets(FocasCncSeries series) => true;
private static string? ValidateMacro(FocasCncSeries series, int number)
{
var (min, max) = MacroRange(series);
return (number < min || number > max)
? $"Macro variable #{number} is outside the documented range [{min}, {max}] for {series}."
: null;
}
private static string? ValidateParameter(FocasCncSeries series, int number)
{
var (min, max) = ParameterRange(series);
return (number < min || number > max)
? $"Parameter #{number} is outside the documented range [{min}, {max}] for {series}."
: 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.";
var letters = PmcLetters(series);
if (!letters.Contains(letter))
{
var letterList = string.Join(", ", letters);
return $"PMC letter '{letter}' is not supported on {series}. Accepted: {{{letterList}}}.";
}
var max = PmcMaxNumber(series);
return number > max
? $"PMC address {letter}{number} is outside the documented range [0, {max}] for {series}."
: null;
}
}