482bafdfac
An ALARM: address (*, <Type>, <Type>/<n>, <Type>/<lo>-<hi>, T<code>, OTHER) on a raw tag that
also carries the cross-driver alarm object is ONE condition with ConditionId == RawPath and a
Kind (Raise / Retrigger / Clear), so DriverHostActor.ForwardNativeAlarm routes it and
NativeAlarmProjector activates it. Fixes all seven triage defects:
1. SubscribeAlarmsAsync treated the host's RawPaths as device hosts -> nothing polled.
2. Decorated "{host}#{Type}:{Number}" ConditionId -> every event dropped.
3. No Kind -> projector kept prior state.
4. WireFocasClient.ReadAlarmsAsync answered [] on failure -> mass clear + un-acked re-raise.
It now throws; a failed device read keeps every condition's state.
5. Acks lost on reconnect -> told-record dedup (MTConnect #626 pattern) held for the driver's
lifetime; same-set re-subscribe / re-init re-raise nothing; changed set prunes leavers only.
6. Subscribe threw while AlarmProjection.Enabled=false -> the tag is the opt-in; flag is a no-op.
7. Alarm tags could not be authored -> ALARM: grammar (FocasAlarmSelector, Contracts) backs the
editor, the address picker, the deploy-time inspector and driver init.
Severity: authored alarm.severity -> exact OpcUaSeverity; else the 16i-era type bucket. The type
table stays the only one (moved to Contracts, documented 16i-era; 30i codes not invented; raw
T<code> selector + raw code in every message). Read-only: ack is server-side. Also clears the
value table on a re-initialise (#630 gap that let a removed/moved tag keep reading).
E2E: Runtime.Tests FocasNativeAlarmEndToEndTests drives the real driver through the real
DriverHostActor and asserts the condition goes Active then inactive.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
150 lines
7.3 KiB
C#
150 lines
7.3 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
|
|
|
/// <summary>
|
|
/// Documented-API capability matrix — per CNC series, what ranges each addressed
|
|
/// <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>
|
|
/// Validates whether an address is accepted by a CNC of the given series.
|
|
/// Returns null on pass, or a failure reason on reject.
|
|
/// </summary>
|
|
/// <param name="series">The CNC series to validate against.</param>
|
|
/// <param name="address">The address to validate.</param>
|
|
/// <returns>Null if valid, otherwise a failure reason string.</returns>
|
|
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),
|
|
// Fixed-tree paths are not area reads, so there is no per-series numeric range to check.
|
|
// What a given CNC actually exposes there is discovered at runtime and capability-gated
|
|
// per subtree; a path this machine does not have resolves to BadNodeIdUnknown at read.
|
|
FocasAreaKind.Fixed => null,
|
|
// An ALARM: selector reads the device's whole active-alarm list (cnc_rdalmmsg2), which every
|
|
// series exposes; the selector filters it driver-side, so there is no range to check (#647).
|
|
FocasAreaKind.Alarm => null,
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
/// <summary>Gets the macro variable number range accepted by a CNC series.</summary>
|
|
/// <param name="series">The CNC series.</param>
|
|
/// <returns>A tuple of (min, max) macro numbers.</returns>
|
|
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>Gets the parameter number range accepted by a CNC series.</summary>
|
|
/// <param name="series">The CNC series.</param>
|
|
/// <returns>A tuple of (min, max) parameter numbers.</returns>
|
|
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>Gets the PMC letters accepted by a CNC series.</summary>
|
|
/// <param name="series">The CNC series.</param>
|
|
/// <returns>A set of accepted PMC letter strings.</returns>
|
|
internal static IReadOnlySet<string> PmcLetters(FocasCncSeries series) => series switch
|
|
{
|
|
FocasCncSeries.Sixteen_i => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "F", "G", "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>Gets the maximum PMC address number for a CNC series.</summary>
|
|
/// <param name="series">The CNC series.</param>
|
|
/// <returns>The maximum address number.</returns>
|
|
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;
|
|
}
|
|
}
|