FOCAS version-matrix stabilization (PR 1 of #220 split) — ship the cheap half of the hardware-free stability gap ahead of the Tier-C out-of-process split. Without any CNC or simulator on the bench, the highest-leverage move is to catch operator config errors at init time instead of at steady-state per-read. Adds FocasCncSeries enum (Unknown/16i/0i-D/0i-F family/30i family/PowerMotion-i) + FocasCapabilityMatrix static class that encodes the per-series documented ranges for macro variables (cnc_rdmacro/wrmacro), parameters (cnc_rdparam/wrparam), and PMC letters + byte ceilings (pmc_rdpmcrng/wrpmcrng) straight from the Fanuc FOCAS Developer Kit. FocasDeviceOptions gains a Series knob (defaults Unknown = permissive so pre-matrix configs don't break on upgrade). FocasDriver.InitializeAsync now calls FocasAddress.TryParse on every tag + runs FocasCapabilityMatrix.Validate against the owning device's declared series, throwing InvalidOperationException with a reason string that names both the series and the documented limit ("Parameter #30000 is outside the documented range [0, 29999] for Thirty_i") so an operator can tell whether the mismatch is in the config or in their declared CNC model. Unknown series skips validation entirely. Ships 46 new theory cases in FocasCapabilityMatrixTests.cs — covering every boundary in the matrix (widen 16i->0i-F: macro ceiling 999->9999, param 9999->14999; widen 0i-F->30i: PMC letters +K+T; PMC-number 16i=999/0i-D=1999/0i-F=9999/30i=59999), permissive Unknown-series behavior, rejection-message content, and case-insensitive PMC-letter matching. Widening a range without updating docs/v2/focas-version-matrix.md fails a test because every InlineData cites the row it reflects. Full FOCAS test suite stays at 165/165 passing (119 existing + 46 new). Also authors docs/v2/focas-version-matrix.md as the authoritative range reference with per-function citations, CNC-series era context, error-surface shape, and the link back to the matrix code; docs/v2/implementation/focas-isolation-plan.md as the multi-PR plan for #220 Tier-C isolation (Shared contracts -> Host skeleton -> move Fwlib32 calls -> Supervisor+respawn -> MMF+ops glue, 2200-3200 LOC across 5 PRs mirroring the Galaxy Tier-C topology); and promotes docs/drivers/FOCAS-Test-Fixture.md from "version-matrix coverage = no" to explicit coverage via the new test file + cross-links to the matrix and isolation-plan docs. Leaves task #220 open since isolation itself (the expensive half) is still ahead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
139
src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs
Normal file
139
src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasCapabilityMatrix.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
|
||||
/// <summary>
|
||||
/// Documented-API capability matrix — per CNC series, what ranges each
|
||||
/// <see cref="FocasAreaKind"/> supports. Authoritative source for the driver's
|
||||
/// pre-flight validation in <see cref="FocasDriver.InitializeAsync"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Ranges come from the Fanuc FOCAS Developer Kit documentation matrix
|
||||
/// (see <c>docs/v2/focas-version-matrix.md</c> 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
|
||||
/// <c>EW_NUMBER</c> or <c>EW_PARAM</c> 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.</para>
|
||||
/// <para><see cref="FocasCncSeries.Unknown"/> 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.</para>
|
||||
/// </remarks>
|
||||
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.
|
||||
/// </summary>
|
||||
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),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Macro variable number accepted by a CNC series. Cites
|
||||
/// <c>cnc_rdmacro</c>/<c>cnc_wrmacro</c> in the Developer Kit.</summary>
|
||||
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),
|
||||
};
|
||||
|
||||
/// <summary>Parameter number accepted; from <c>cnc_rdparam</c>/<c>cnc_wrparam</c>.
|
||||
/// Ranges reflect the highest-numbered parameter documented per series.</summary>
|
||||
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),
|
||||
};
|
||||
|
||||
/// <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
|
||||
{
|
||||
FocasCncSeries.Sixteen_i => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "R", "D" },
|
||||
FocasCncSeries.Zero_i_D => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "R", "D", "E", "A" },
|
||||
FocasCncSeries.Zero_i_F or
|
||||
FocasCncSeries.Zero_i_MF or
|
||||
FocasCncSeries.Zero_i_TF => new HashSet<string>(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<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "F", "G", "R", "D", "E", "A", "M", "C", "K", "T" },
|
||||
FocasCncSeries.PowerMotion_i => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "R", "D" },
|
||||
_ => 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>
|
||||
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,
|
||||
};
|
||||
|
||||
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? 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user