- Driver.FOCAS-007: optional ILogger<FocasDriver> + alarm-projection logger; log Debug around every formerly-empty catch (probe / shutdown / fixed-tree / recycle / alarms-read / projection). - Driver.FOCAS-008: cache the parsed FocasAddress per tag at InitializeAsync; Read/WriteAsync look it up instead of re-parsing on every call. - Driver.FOCAS-009: ProbeLoopAsync now wraps client.ProbeAsync in a linked CTS honouring Probe.Timeout so a hung CNC socket can't block past the configured limit. - Driver.FOCAS-010: FocasOperationModeExtensions.ToText delegates to FocasOpMode.ToText — single canonical op-mode label surface. - Driver.FOCAS-011: FocasAlarmType constants are typed short to match the cnc_rdalmmsg2 wire field and the projection switch arms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
294 lines
12 KiB
C#
294 lines
12 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
|
|
|
/// <summary>
|
|
/// Wire-layer abstraction over one FOCAS session to a CNC. The driver holds one per
|
|
/// configured device; lifetime matches the device.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>The default implementation is <see cref="Wire.WireFocasClient"/> — a pure-managed
|
|
/// FOCAS/2 Ethernet client that speaks the wire protocol directly on TCP:8193. No
|
|
/// P/Invoke, no native DLLs, no out-of-process isolation.</para>
|
|
///
|
|
/// <para><see cref="UnimplementedFocasClientFactory"/> is a scaffolding backend that
|
|
/// throws on <see cref="IFocasClientFactory.Create"/> — selected by
|
|
/// <c>"Backend": "unimplemented"</c> so a DriverInstance row can be seeded before the CNC
|
|
/// endpoint is reachable without silently reading stale data.</para>
|
|
/// </remarks>
|
|
public interface IFocasClient : IDisposable
|
|
{
|
|
/// <summary>Open the FWLIB handle + TCP session. Idempotent.</summary>
|
|
Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken cancellationToken);
|
|
|
|
/// <summary>True when the FWLIB handle is valid + the socket is up.</summary>
|
|
bool IsConnected { get; }
|
|
|
|
/// <summary>
|
|
/// Read the value at <paramref name="address"/> in the requested
|
|
/// <paramref name="type"/>. Returns a boxed .NET value + the OPC UA status mapped
|
|
/// through <see cref="FocasStatusMapper"/>.
|
|
/// </summary>
|
|
Task<(object? value, uint status)> ReadAsync(
|
|
FocasAddress address,
|
|
FocasDataType type,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Write <paramref name="value"/> to <paramref name="address"/>. Returns the mapped
|
|
/// OPC UA status (0 = Good).
|
|
/// </summary>
|
|
Task<uint> WriteAsync(
|
|
FocasAddress address,
|
|
FocasDataType type,
|
|
object? value,
|
|
CancellationToken cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Cheap health probe — e.g. <c>cnc_rdcncstat</c>. Returns <c>true</c> when the CNC
|
|
/// responds with any valid status.
|
|
/// </summary>
|
|
Task<bool> ProbeAsync(CancellationToken cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Read active alarm messages from the CNC via <c>cnc_rdalmmsg2</c>. Returns
|
|
/// zero-or-more active alarms. Null / empty list means "no alarms currently
|
|
/// active". IAlarmSource projection polls this at a configurable interval +
|
|
/// emits transitions (raise / clear) on the driver's <c>OnAlarmEvent</c>.
|
|
/// </summary>
|
|
Task<IReadOnlyList<FocasActiveAlarm>> ReadAlarmsAsync(CancellationToken cancellationToken);
|
|
|
|
// ---- Fixed-tree T1 (identity + axis discovery + fast-poll dynamic bundle) ----
|
|
|
|
/// <summary>
|
|
/// Read CNC identity via <c>cnc_sysinfo</c>. Populates the <c>Identity/*</c>
|
|
/// subtree of the fixed-node surface. Callable once at session open; the
|
|
/// values don't change across the session.
|
|
/// </summary>
|
|
Task<FocasSysInfo> GetSysInfoAsync(CancellationToken cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Read the CNC's configured axis names via <c>cnc_rdaxisname</c>. The driver
|
|
/// uses these to build the <c>Axes/{name}/</c> subtree and to index
|
|
/// <see cref="ReadDynamicAsync"/> calls.
|
|
/// </summary>
|
|
Task<IReadOnlyList<FocasAxisName>> GetAxisNamesAsync(CancellationToken cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Read the CNC's configured spindle names via <c>cnc_rdspdlname</c>. Drives
|
|
/// the <c>Spindle/{name}/</c> subtree.
|
|
/// </summary>
|
|
Task<IReadOnlyList<FocasSpindleName>> GetSpindleNamesAsync(CancellationToken cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Read the fast-poll dynamic bundle for one axis via <c>cnc_rddynamic2</c>.
|
|
/// Returns the current position quadruple (absolute / machine / relative /
|
|
/// distance-to-go) plus actual feed rate + actual spindle speed + alarm
|
|
/// flags + program / sequence numbers — one network round-trip per call.
|
|
/// </summary>
|
|
Task<FocasDynamicSnapshot> ReadDynamicAsync(int axisIndex, CancellationToken cancellationToken);
|
|
|
|
// ---- Fixed-tree T2 (program + operation mode) ----
|
|
|
|
/// <summary>
|
|
/// Aggregate program + operation-mode snapshot. One wire round-trip per
|
|
/// underlying FWLIB call — <c>cnc_rdblkcount</c>, <c>cnc_exeprgname2</c>,
|
|
/// <c>cnc_rdopmode</c>. The driver polls this on a slower cadence than
|
|
/// <see cref="ReadDynamicAsync"/> since program / mode transitions happen
|
|
/// on human-operator timescales.
|
|
/// </summary>
|
|
Task<FocasProgramInfo> GetProgramInfoAsync(CancellationToken cancellationToken);
|
|
|
|
// ---- Fixed-tree T3 (timers) ----
|
|
|
|
/// <summary>
|
|
/// Read one CNC cumulative timer. Kind selects PowerOn / Operating / Cutting /
|
|
/// Cycle. Values are seconds — the managed side already converted the native
|
|
/// minute+msec representation so downstream nodes display uniform units.
|
|
/// </summary>
|
|
Task<FocasTimer> GetTimerAsync(FocasTimerKind kind, CancellationToken cancellationToken);
|
|
|
|
// ---- Fixed-tree T3.5 (servo meters) ----
|
|
|
|
/// <summary>
|
|
/// Read the servo-load meter percentages across all configured axes.
|
|
/// Values are percentages (scaled by <c>10^Dec</c>). Empty list on a
|
|
/// disconnected session or unsupported CNC.
|
|
/// </summary>
|
|
Task<IReadOnlyList<FocasServoLoad>> GetServoLoadsAsync(CancellationToken cancellationToken);
|
|
|
|
// ---- Fixed-tree T3.6 (spindle meters) ----
|
|
|
|
/// <summary>
|
|
/// Read per-spindle load percentages. Result list index corresponds to
|
|
/// spindle index from <see cref="GetSpindleNamesAsync"/>. Empty list on a
|
|
/// disconnected session or when the CNC doesn't support the call (older
|
|
/// series like 16i may return EW_FUNC).
|
|
/// </summary>
|
|
Task<IReadOnlyList<int>> GetSpindleLoadsAsync(CancellationToken cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Read per-spindle maximum RPM values. Static configuration, fetched once at
|
|
/// bootstrap. Index alignment as per <see cref="GetSpindleLoadsAsync"/>.
|
|
/// </summary>
|
|
Task<IReadOnlyList<int>> GetSpindleMaxRpmsAsync(CancellationToken cancellationToken);
|
|
}
|
|
|
|
/// <summary>One servo-meter entry — one axis's current load percentage.</summary>
|
|
public sealed record FocasServoLoad(string AxisName, double LoadPercent);
|
|
|
|
/// <summary>Which cumulative counter <see cref="IFocasClient.GetTimerAsync"/> reads.</summary>
|
|
public enum FocasTimerKind
|
|
{
|
|
/// <summary>Machine power-on hours — resets never.</summary>
|
|
PowerOn = 0,
|
|
/// <summary>Cycle operating time — resets when the operator clears the counter.</summary>
|
|
Operating = 1,
|
|
/// <summary>Cutting time — only counts while in cutting feed.</summary>
|
|
Cutting = 2,
|
|
/// <summary>Cycle time since the last program start.</summary>
|
|
Cycle = 3,
|
|
}
|
|
|
|
/// <summary>One cumulative timer reading. <see cref="TotalSeconds"/> is the canonical unit.</summary>
|
|
public sealed record FocasTimer(FocasTimerKind Kind, int Minutes, int Milliseconds)
|
|
{
|
|
/// <summary>Cumulative time in seconds — <c>Minutes * 60 + Milliseconds / 1000</c>.</summary>
|
|
public double TotalSeconds => Minutes * 60.0 + Milliseconds / 1000.0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// CNC identity snapshot from <c>cnc_sysinfo</c>. Strings are trimmed ASCII.
|
|
/// </summary>
|
|
public sealed record FocasSysInfo(
|
|
int AddInfo,
|
|
int MaxAxis,
|
|
string CncType, // "M" (mill) / "T" (lathe)
|
|
string MtType,
|
|
string Series, // e.g. "30i"
|
|
string Version, // e.g. "A1.0"
|
|
int AxesCount);
|
|
|
|
/// <summary>One configured axis name (e.g. "X", "X1").</summary>
|
|
public sealed record FocasAxisName(string Name, string Suffix)
|
|
{
|
|
/// <summary>
|
|
/// Display name — name + suffix concatenated, trimmed. Empty suffix yields
|
|
/// just the name (the common case on single-channel CNCs).
|
|
/// </summary>
|
|
public string Display => string.IsNullOrEmpty(Suffix) ? Name : $"{Name}{Suffix}";
|
|
}
|
|
|
|
/// <summary>One configured spindle name (e.g. "S1").</summary>
|
|
public sealed record FocasSpindleName(string Name, string Suffix1, string Suffix2, string Suffix3)
|
|
{
|
|
public string Display
|
|
{
|
|
get
|
|
{
|
|
var s = Name + Suffix1 + Suffix2 + Suffix3;
|
|
return s.TrimEnd('\0', ' ');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fast-poll bundle for one axis. Position values are scaled integers; the caller
|
|
/// divides by <c>10^DecimalPlaces</c> to get the decimal value. DecimalPlaces is
|
|
/// currently left to the caller to supply (via device config or a future
|
|
/// <c>cnc_getfigure</c> path once that export lands).
|
|
/// </summary>
|
|
/// <summary>
|
|
/// Program + operation-mode snapshot. Name is the currently-executing
|
|
/// program filename (e.g. "O0001.NC"); ONumber is its Fanuc O-number (1-9999).
|
|
/// Mode is the numeric code from <c>cnc_rdopmode</c> — see <see cref="FocasOpMode"/>.
|
|
/// </summary>
|
|
public sealed record FocasProgramInfo(
|
|
string Name,
|
|
int ONumber,
|
|
int BlockCount,
|
|
int Mode);
|
|
|
|
/// <summary>Human-readable text for the <see cref="FocasProgramInfo.Mode"/> integer.</summary>
|
|
public static class FocasOpMode
|
|
{
|
|
public static string ToText(int mode) => mode switch
|
|
{
|
|
0 => "MDI",
|
|
1 => "AUTO",
|
|
2 => "TJOG",
|
|
3 => "EDIT",
|
|
4 => "HANDLE",
|
|
5 => "JOG",
|
|
6 => "TEACH_IN_HANDLE",
|
|
7 => "REFERENCE",
|
|
8 => "REMOTE",
|
|
9 => "TEST",
|
|
_ => $"Mode{mode}",
|
|
};
|
|
}
|
|
|
|
public sealed record FocasDynamicSnapshot(
|
|
int AxisIndex,
|
|
int AlarmFlags,
|
|
int ProgramNumber,
|
|
int MainProgramNumber,
|
|
int SequenceNumber,
|
|
int ActualFeedRate,
|
|
int ActualSpindleSpeed,
|
|
int AbsolutePosition,
|
|
int MachinePosition,
|
|
int RelativePosition,
|
|
int DistanceToGo);
|
|
|
|
/// <summary>
|
|
/// One active alarm surfaced by <see cref="IFocasClient.ReadAlarmsAsync"/>. Shape
|
|
/// mirrors <c>ODBALMMSG2</c> but normalises the message bytes to a .NET string.
|
|
/// </summary>
|
|
public sealed record FocasActiveAlarm(
|
|
int AlarmNumber,
|
|
short Type,
|
|
short Axis,
|
|
string Message);
|
|
|
|
/// <summary>Factory for <see cref="IFocasClient"/>s. One client per configured device.</summary>
|
|
public interface IFocasClientFactory
|
|
{
|
|
IFocasClient Create();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scaffolding factory — throws on <see cref="Create"/> so a DriverInstance row can be
|
|
/// seeded ahead of the CNC endpoint being reachable without silently reading stale data.
|
|
/// Select via <c>"Backend": "unimplemented"</c> in driver config. Flip to
|
|
/// <c>"Backend": "wire"</c> once the CNC is provisioned.
|
|
/// </summary>
|
|
public sealed class UnimplementedFocasClientFactory : IFocasClientFactory
|
|
{
|
|
public IFocasClient Create() => throw new NotSupportedException(
|
|
"FOCAS driver backend is 'unimplemented'. Switch to 'Backend: \"wire\"' in driver config " +
|
|
"once the CNC is provisioned — see docs/drivers/FOCAS.md.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Well-known FOCAS alarm types from <c>fwlib32.h</c> <c>ALM_TYPE_*</c>. Narrow subset —
|
|
/// the full list is ~15 types per model; these cover the universally-present categories.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Constants are typed <see cref="short"/> so they match the wire field width on
|
|
/// <c>cnc_rdalmmsg2</c> (and so <see cref="FocasAlarmProjection"/>'s <c>switch (short)</c>
|
|
/// statements compile against a matching type rather than relying on implicit int→short
|
|
/// narrowing on the constants).
|
|
/// </remarks>
|
|
public static class FocasAlarmType
|
|
{
|
|
/// <summary>Pass to <see cref="IFocasClient.ReadAlarmsAsync"/>-equivalent to mean "any type".</summary>
|
|
public const short All = -1;
|
|
public const short Parameter = 0; // ALM_P
|
|
public const short PulseCode = 1; // ALM_Y (servo)
|
|
public const short Overtravel = 2; // ALM_O
|
|
public const short Overheat = 3; // ALM_H
|
|
public const short Servo = 4; // ALM_S
|
|
public const short DataIo = 5; // ALM_T
|
|
public const short MemoryCheck = 6; // ALM_M
|
|
public const short MacroAlarm = 13; // ALM_MC — used by #3006 etc.
|
|
}
|