namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
///
/// Wire-layer abstraction over one FOCAS session to a CNC. The driver holds one per
/// configured device; lifetime matches the device.
///
///
/// The default implementation is — 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.
///
/// is a scaffolding backend that
/// throws on — selected by
/// "Backend": "unimplemented" so a DriverInstance row can be seeded before the CNC
/// endpoint is reachable without silently reading stale data.
///
public interface IFocasClient : IDisposable
{
/// Open the FWLIB handle + TCP session. Idempotent.
/// The CNC host address and port.
/// The connection timeout duration.
/// The cancellation token.
/// A task that represents the asynchronous operation.
Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken cancellationToken);
/// True when the FWLIB handle is valid + the socket is up.
bool IsConnected { get; }
///
/// Read the value at in the requested
/// . Returns a boxed .NET value + the OPC UA status mapped
/// through .
///
/// The CNC memory address to read from.
/// The FOCAS data type to read.
/// The cancellation token.
/// A tuple of the boxed value read and the mapped OPC UA status code.
Task<(object? value, uint status)> ReadAsync(
FocasAddress address,
FocasDataType type,
CancellationToken cancellationToken);
///
/// Write to . Returns the mapped
/// OPC UA status (0 = Good).
///
/// The CNC memory address to write to.
/// The FOCAS data type to write.
/// The value to write.
/// The cancellation token.
/// The mapped OPC UA status code (0 = Good).
Task WriteAsync(
FocasAddress address,
FocasDataType type,
object? value,
CancellationToken cancellationToken);
///
/// Cheap health probe — e.g. cnc_rdcncstat. Returns true when the CNC
/// responds with any valid status.
///
/// The cancellation token.
/// true when the CNC responds with a valid status; otherwise false.
Task ProbeAsync(CancellationToken cancellationToken);
///
/// Read active alarm messages from the CNC via cnc_rdalmmsg2. 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 OnAlarmEvent.
///
/// The cancellation token.
/// The list of currently active alarms; empty when none are active.
Task> ReadAlarmsAsync(CancellationToken cancellationToken);
// ---- Fixed-tree T1 (identity + axis discovery + fast-poll dynamic bundle) ----
///
/// Read CNC identity via cnc_sysinfo. Populates the Identity/*
/// subtree of the fixed-node surface. Callable once at session open; the
/// values don't change across the session.
///
/// The cancellation token.
/// The CNC identity snapshot.
Task GetSysInfoAsync(CancellationToken cancellationToken);
///
/// Read the CNC's configured axis names via cnc_rdaxisname. The driver
/// uses these to build the Axes/{name}/ subtree and to index
/// calls.
///
/// The cancellation token.
/// The CNC's configured axis names.
Task> GetAxisNamesAsync(CancellationToken cancellationToken);
///
/// Read the CNC's configured spindle names via cnc_rdspdlname. Drives
/// the Spindle/{name}/ subtree.
///
/// The cancellation token.
/// The CNC's configured spindle names.
Task> GetSpindleNamesAsync(CancellationToken cancellationToken);
///
/// Read the fast-poll dynamic bundle for one axis via cnc_rddynamic2.
/// 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.
///
/// The axis index to read dynamics for.
/// The cancellation token.
/// The fast-poll dynamic bundle for the requested axis.
Task ReadDynamicAsync(int axisIndex, CancellationToken cancellationToken);
// ---- Fixed-tree T2 (program + operation mode) ----
///
/// Aggregate program + operation-mode snapshot. One wire round-trip per
/// underlying FWLIB call — cnc_rdblkcount, cnc_exeprgname2,
/// cnc_rdopmode. The driver polls this on a slower cadence than
/// since program / mode transitions happen
/// on human-operator timescales.
///
/// The cancellation token.
/// The aggregate program + operation-mode snapshot.
Task GetProgramInfoAsync(CancellationToken cancellationToken);
// ---- Fixed-tree T2.5 (CNC status flags, Gitea #257) ----
///
/// Read the CNC status flags via cnc_statinfo (ODBST). Populates the
/// Status/* subtree. Throws when the CNC refuses the call, which the driver's
/// bootstrap probe reads as "this series has no status block" and suppresses the subtree.
///
/// The cancellation token.
/// The CNC status flag snapshot.
Task GetStatusAsync(CancellationToken cancellationToken);
// ---- Fixed-tree T3 (timers) ----
///
/// 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.
///
/// The timer kind to read.
/// The cancellation token.
/// The requested cumulative timer reading.
Task GetTimerAsync(FocasTimerKind kind, CancellationToken cancellationToken);
// ---- Fixed-tree T3.5 (servo meters) ----
///
/// Read the servo-load meter percentages across all configured axes. Each record's own
/// value / base^exponent envelope is applied (servo loads are direct percentages);
/// a record whose envelope describes no value carries a null .
/// Empty list on a disconnected session or unsupported CNC.
///
/// The cancellation token.
/// The servo-load meter percentages across all configured axes.
Task> GetServoLoadsAsync(CancellationToken cancellationToken);
// ---- Fixed-tree T3.6 (spindle meters) ----
///
/// Read per-spindle load as a percentage of rated load (cnc_rdspmeter type 0; the
/// raw meter is a 1/16384 fraction of full scale). Result list index corresponds to
/// spindle index from ; a slot whose record describes no
/// value is null. Empty list on a disconnected session or when the CNC doesn't support the
/// call (older series like 16i may return EW_FUNC).
///
/// The cancellation token.
/// Per-spindle load percentages, indexed as per .
Task> GetSpindleLoadsAsync(CancellationToken cancellationToken);
///
/// Read per-spindle motor speed as a percentage of maximum speed (cnc_rdspmeter
/// type 1, historically labelled cnc_rdspmaxrpm). A live value, not a max-RPM
/// constant. Index alignment and null semantics as per .
///
/// The cancellation token.
/// Per-spindle motor-speed percentages, indexed as per .
Task> GetSpindleMotorSpeedsAsync(CancellationToken cancellationToken);
}
///
/// CNC status flags from cnc_statinfo (ODBST) — raw FANUC codes, published verbatim as
/// Int32 so a client sees exactly what the control reported. additionally has a
/// text projection ().
///
/// Automatic-operation mode code.
/// Automatic-operation state (0 RESET, 1 STOP, 2 HOLD, 3 START, 4 MSTR).
/// Axis motion (0 none, 1 motion, 2 dwell).
/// M/S/T/B function state (0 none, 1 executing).
/// Emergency stop (0 not, 1 emergency, 2 reset).
/// Alarm state (0 none, 1 alarm, 2 battery-low, 3 fan).
/// Program-edit state.
/// T/M mode as reported by the control.
public sealed record FocasStatus(int Auto, int Run, int Motion, int Mstb, int Emergency, int Alarm, int Edit, int TmMode);
/// Human-readable text for .
public static class FocasRunState
{
/// Converts the ODBST.run code to its operator-facing label.
/// The run-state code.
/// The label, or Run{n} for a code outside the documented set.
public static string ToText(int run) => run switch
{
0 => "RESET",
1 => "STOP",
2 => "HOLD",
3 => "START",
4 => "MSTR",
_ => $"Run{run}",
};
}
///
/// One servo-meter entry — one axis's current load percentage; null when the CNC's record
/// described no usable value (never a fabricated 0).
///
public sealed record FocasServoLoad(string AxisName, double? LoadPercent);
/// Which cumulative counter reads.
public enum FocasTimerKind
{
/// Machine power-on hours — resets never.
PowerOn = 0,
/// Cycle operating time — resets when the operator clears the counter.
Operating = 1,
/// Cutting time — only counts while in cutting feed.
Cutting = 2,
/// Cycle time since the last program start.
Cycle = 3,
}
/// One cumulative timer reading. is the canonical unit.
public sealed record FocasTimer(FocasTimerKind Kind, int Minutes, int Milliseconds)
{
/// Cumulative time in seconds — Minutes * 60 + Milliseconds / 1000.
public double TotalSeconds => Minutes * 60.0 + Milliseconds / 1000.0;
}
///
/// CNC identity snapshot from cnc_sysinfo. Strings are trimmed ASCII.
///
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);
/// One configured axis name (e.g. "X", "X1").
public sealed record FocasAxisName(string Name, string Suffix)
{
///
/// Display name — name + suffix concatenated, trimmed. Empty suffix yields
/// just the name (the common case on single-channel CNCs).
///
public string Display => string.IsNullOrEmpty(Suffix) ? Name : $"{Name}{Suffix}";
}
/// One configured spindle name (e.g. "S1").
public sealed record FocasSpindleName(string Name, string Suffix1, string Suffix2, string Suffix3)
{
/// Gets the display name — name + concatenated suffixes, trimmed.
public string Display
{
get
{
var s = Name + Suffix1 + Suffix2 + Suffix3;
return s.TrimEnd('\0', ' ');
}
}
}
///
/// 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 cnc_rdopmode — see .
/// A null field means the CNC did not report it (its sub-read was refused, or the
/// payload was too short to carry it); the driver publishes that node Bad. It is never
/// defaulted to 0, which is a genuine O-number, block count, and mode (MDI).
///
public sealed record FocasProgramInfo(
string? Name,
int? ONumber,
int? BlockCount,
int? Mode);
/// Human-readable text for the integer.
public static class FocasOpMode
{
/// Converts a numeric operation mode to its text representation.
/// The operation mode integer code.
/// The text representation of the mode, or a fallback format if unknown.
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}",
};
}
///
/// Fast-poll bundle for one axis (cnc_rddynamic2). Each position carries its own scale
/// (see ); the driver converts it to engineering units at publish.
///
public sealed record FocasDynamicSnapshot(
int AxisIndex,
int AlarmFlags,
int ProgramNumber,
int MainProgramNumber,
int SequenceNumber,
int ActualFeedRate,
int ActualSpindleSpeed,
FocasPosition AbsolutePosition,
FocasPosition MachinePosition,
FocasPosition RelativePosition,
FocasPosition DistanceToGo);
///
/// One cnc_rddynamic2 position (command 0x0026) as the CNC reported it. The 8-byte
/// wire block is self-describing — [0..3] i32 raw, [4..5] u16 base (2 or 10),
/// [6..7] u16 exponent (0xFFFF = the slot holds no valid position) — and the real
/// coordinate is raw / base^exponent. Confirmed on a real FANUC 31i-B: every block of the
/// ref-dynamic2-axis1.bin capture reports base 10 / exponent 4 (280.1574 mm, not
/// 2801574). That envelope is why no separate figure read exists: the cnc_getfigure
/// (0x00D3) this driver used to send is not a FOCAS wire command, and the 31i-B refuses it.
///
/// The raw i32 position value.
/// The block's scale base (only 2 and 10 are meaningful).
/// The block's scale exponent.
///
/// True when the block carried the base/exponent envelope. A scale-less block (a 4-byte payload
/// from an older simulator, or a raw integer a test supplies) falls back to the device's
/// configured PositionDecimalPlaces.
///
public readonly record struct FocasPosition(int Raw, int Base = 0, int Exponent = 0, bool HasScale = false)
{
///
/// False when the block says it holds no usable position: the 0xFFFF vacant marker, a
/// base other than 2 or 10, or an absurd exponent. A per-node quality decision — that node
/// publishes Bad, its siblings are unaffected.
///
///
/// The exponent bound matters: left unbounded, raw / 10^400 is
/// raw / Infinity = 0.0 — a Good reading of a machine parked at the origin.
///
public bool IsValid => !HasScale || Wire.FocasValueEnvelope.IsValid(Base, Exponent);
///
/// The position in engineering units: Raw / Base^Exponent when the block carried its
/// own scale (the configured fallback is then ignored), else Raw / 10^fallbackDecimalPlaces.
/// Null when is false.
///
/// The device's configured PositionDecimalPlaces.
/// The scaled position, or null when the block describes none.
public double? ToEngineering(int fallbackDecimalPlaces)
{
if (!IsValid) return null;
if (HasScale) return Wire.FocasValueEnvelope.Scale(Raw, Base, Exponent);
var dp = Math.Max(0, fallbackDecimalPlaces);
return dp > 0 ? Raw / Math.Pow(10, dp) : Raw;
}
/// A bare integer is a scale-less position (legacy shape; tests and fakes).
/// The raw position value.
public static implicit operator FocasPosition(int raw) => new(raw);
}
///
/// One active alarm surfaced by . Shape
/// mirrors ODBALMMSG2 but normalises the message bytes to a .NET string.
///
public sealed record FocasActiveAlarm(
int AlarmNumber,
short Type,
short Axis,
string Message);
/// Factory for s. One client per configured device.
public interface IFocasClientFactory
{
/// Creates a new FOCAS client instance.
/// A new IFocasClient instance.
IFocasClient Create();
///
/// Config-time usability probe called by before
/// any background loops start. Must NOT create a live wire client. Implementations that
/// are permanently non-functional (e.g. )
/// throw here so the driver faults immediately at
/// init rather than lazily on the first read.
///
///
/// Thrown by stub/unimplemented backends to force a fail-fast init fault.
///
void EnsureUsable();
}
///
/// Scaffolding factory — throws on so a DriverInstance row can be
/// seeded ahead of the CNC endpoint being reachable without silently reading stale data.
/// Select via "Backend": "unimplemented" in driver config. Flip to
/// "Backend": "wire" once the CNC is provisioned.
///
public sealed class UnimplementedFocasClientFactory : IFocasClientFactory
{
private const string Message =
"FOCAS driver backend is 'unimplemented'. Switch to 'Backend: \"wire\"' in driver config " +
"once the CNC is provisioned — see docs/drivers/FOCAS.md.";
///
public void EnsureUsable() => throw new NotSupportedException(Message);
///
public IFocasClient Create() => throw new NotSupportedException(Message);
}
///
/// Well-known FOCAS alarm types from fwlib32.h ALM_TYPE_*. Narrow subset —
/// the full list is ~15 types per model; these cover the universally-present categories.
///
///
/// Constants are typed so they match the wire field width on
/// cnc_rdalmmsg2 (and so 's switch (short)
/// statements compile against a matching type rather than relying on implicit int→short
/// narrowing on the constants).
///
public static class FocasAlarmType
{
/// Pass to -equivalent to mean "any type".
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
}