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.
///
///
/// No default wire implementation ships with this assembly. FWLIB
/// (Fwlib32.dll) is Fanuc-proprietary and requires a valid customer license — it
/// cannot legally be redistributed. The deployment team supplies an
/// that wraps the licensed Fwlib32.dll via
/// P/Invoke and registers it at server startup.
///
/// The default throws with a pointer at
/// the deployment docs so misconfigured servers fail fast with a clear error rather than
/// mysteriously hanging.
///
public interface IFocasClient : IDisposable
{
/// Open the FWLIB handle + TCP session. Idempotent.
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 .
///
Task<(object? value, uint status)> ReadAsync(
FocasAddress address,
FocasDataType type,
CancellationToken cancellationToken);
///
/// Write to . Returns the mapped
/// OPC UA status (0 = Good).
///
Task WriteAsync(
FocasAddress address,
FocasDataType type,
object? value,
CancellationToken cancellationToken);
///
/// Write a CNC parameter value via cnc_wrparam (FWLIB IODBPSD packet —
/// byte layout symmetric with the cnc_rdparam read side). Plan PR F4-b
/// (issue #269). The is parsed from a PARAM:N
/// tag string; drives the payload width (Byte / Int16 /
/// Int32). Default impl returns
/// so transports that haven't yet routed the write keep compiling.
/// EW_PASSWD from the CNC (parameter-write switch off / unlock required)
/// surfaces as ; F4-d will
/// wire the unlock workflow on top.
///
Task WriteParameterAsync(
FocasAddress address,
FocasDataType type,
object? value,
CancellationToken cancellationToken)
=> Task.FromResult(FocasStatusMapper.BadNotSupported);
///
/// Write a CNC macro variable value via cnc_wrmacro (FWLIB ODBM packet
/// symmetric with the cnc_rdmacro read side). Plan PR F4-b (issue #269).
/// The implementation encodes as (intValue,
/// decimalPointCount); today we ship integer-only (decimalPointCount = 0)
/// to match the most common HMI pattern, and a future WriteMacroScaled
/// overload can land if the field calls for fractional macro setpoints.
/// Default impl returns .
///
Task WriteMacroAsync(
FocasAddress address,
object? value,
CancellationToken cancellationToken)
=> Task.FromResult(FocasStatusMapper.BadNotSupported);
///
/// Cheap health probe — e.g. cnc_rdcncstat. Returns true when the CNC
/// responds with any valid status.
///
Task ProbeAsync(CancellationToken cancellationToken);
///
/// Read the full cnc_rdcncstat ODBST struct (9 small-int status flags). The
/// boolean is preserved for cheap reachability checks; this
/// method exposes the per-field detail used by the FOCAS driver's Status/
/// fixed-tree nodes (see issue #257). Returns null if the wire client cannot
/// supply the struct (e.g. transport/IPC variant where the contract has not been
/// extended yet) — callers fall back to surfacing Bad on the per-field nodes.
///
Task GetStatusAsync(CancellationToken cancellationToken)
=> Task.FromResult(null);
///
/// Read the per-CNC production counters (parts produced / required / total via
/// cnc_rdparam(6711/6712/6713)) plus the current cycle-time seconds counter
/// (cnc_rdtimer(2)). Surfaced on the FOCAS driver's Production/
/// fixed-tree per device (issue #258). Returns null when the wire client
/// cannot supply the snapshot (e.g. older transport variant) — the driver leaves
/// the cache untouched and the per-field nodes report Bad until the first refresh.
///
Task GetProductionAsync(CancellationToken cancellationToken)
=> Task.FromResult(null);
///
/// Read the active modal M/S/T/B codes via cnc_modal. G-group decoding is
/// deferred — the FWLIB ODBMDL union differs per series + group and the
/// issue body permits surfacing only the universally-present M/S/T/B fields in
/// the first cut (issue #259). Returns null when the wire client cannot
/// supply the snapshot.
///
Task GetModalAsync(CancellationToken cancellationToken)
=> Task.FromResult(null);
///
/// Read the four operator override values (feed / rapid / spindle / jog) via
/// cnc_rdparam. The parameter numbers are MTB-specific so the caller passes
/// them in via ; a null entry suppresses that
/// field's read (the corresponding node is also omitted from the address space).
/// Returns null when the wire client cannot supply the snapshot (issue #259).
///
Task GetOverrideAsync(
FocasOverrideParameters parameters, CancellationToken cancellationToken)
=> Task.FromResult(null);
///
/// Read the current tool number via cnc_rdtnum. Surfaced on the FOCAS driver's
/// Tooling/ fixed-tree per device (issue #260). Tool life + current offset
/// index are deferred — cnc_rdtlinfo/cnc_rdtlsts vary heavily across
/// CNC series + the FWLIB ODBTLIFE* unions need per-series shape handling
/// that exceeds the L-sized scope of this PR. Returns null when the wire
/// client cannot supply the snapshot (e.g. older transport variant).
///
Task GetToolingAsync(CancellationToken cancellationToken)
=> Task.FromResult(null);
///
/// Read the standard G54..G59 work-coordinate offsets via
/// cnc_rdzofs(handle, n=1..6). Returns one
/// per slot (issue #260). Extended G54.1 P1..P48 offsets are deferred — they use
/// a different FOCAS call (cnc_rdzofsr) + different range handling. Each
/// offset surfaces a fixed X/Y/Z view; lathes/mills with extra rotational axes
/// have those columns reported as 0.0. Returns null when the wire client
/// cannot supply the snapshot.
///
Task GetWorkOffsetsAsync(CancellationToken cancellationToken)
=> Task.FromResult(null);
///
/// Read the four FANUC operator-message classes via cnc_rdopmsg3 (issue #261).
/// The call returns up to 4 active messages per class; the driver collapses the
/// latest non-empty message per class onto the Messages/External/Latest
/// fixed-tree node — the issue body permits this minimal surface in the first cut.
/// Trailing nulls / spaces are trimmed before publishing so the same message
/// round-trips with stable text. Returns null when the wire client cannot
/// supply the snapshot (older transport variant).
///
Task GetOperatorMessagesAsync(CancellationToken cancellationToken)
=> Task.FromResult(null);
///
/// Read the currently-executing block text via cnc_rdactpt (issue #261).
/// The call returns the active block of the running program; surfaced as
/// Program/CurrentBlock Float-trimmed string. Returns null when the
/// wire client cannot supply the snapshot.
///
Task GetCurrentBlockAsync(CancellationToken cancellationToken)
=> Task.FromResult(null);
///
/// Read the per-axis decimal-place counts via cnc_getfigure (issue #262).
/// Returned dictionary maps axis name (or fallback "axis{n}" when
/// cnc_rdaxisname isn't available) to the decimal-place count the CNC
/// reports for that axis's increment system. Cached at bootstrap by the driver +
/// applied to position values before publishing — raw integer / 10^decimalPlaces.
/// Returns null when the wire client cannot supply the snapshot (older
/// transport variant) — the driver leaves the cache untouched and falls back to
/// publishing raw values.
///
Task?> GetFigureScalingAsync(CancellationToken cancellationToken)
=> Task.FromResult?>(null);
///
/// Read a CNC diagnostic value via cnc_rddiag. is
/// the diagnostic number (validated against
/// by ).
/// is 0 for whole-CNC diagnostics or the 1-based axis index for per-axis diagnostics.
/// The shape of the returned value depends on the diagnostic — Int / Float / Bit are
/// all possible. Returns null on default (transport variants that haven't yet
/// implemented diagnostics) so the driver falls back to BadNotSupported on those nodes
/// until the wire client is extended (issue #263).
///
Task<(object? value, uint status)> ReadDiagnosticAsync(
int diagNumber, int axisOrZero, FocasDataType type, CancellationToken cancellationToken)
=> Task.FromResult<(object?, uint)>((null, FocasStatusMapper.BadNotSupported));
///
/// Discover the number of CNC paths (channels) the controller exposes via
/// cnc_rdpathnum. Multi-path CNCs (lathe + sub-spindle, dual-turret,
/// etc.) report 2..N; single-path CNCs return 1. The driver caches the result
/// once per device after connect + uses it to validate per-tag PathId
/// values (issue #264). Default returns 1 so transports that haven't extended
/// their wire surface keep behaving as single-path.
///
Task GetPathCountAsync(CancellationToken cancellationToken)
=> Task.FromResult(1);
///
/// Switch the active CNC path (channel) for subsequent reads via
/// cnc_setpath. Called by the driver before every read whose
/// FocasAddress.PathId differs from the path most recently set on the
/// session — single-path devices (PathId=1 only) skip the wire call entirely.
/// Default is a no-op so transports that haven't extended their wire surface
/// simply read whatever path the CNC has selected (issue #264).
///
Task SetPathAsync(int pathId, CancellationToken cancellationToken)
=> Task.CompletedTask;
///
/// Read up to most-recent entries from the CNC's alarm-history
/// ring buffer via cnc_rdalmhistry. Used by
/// when is
/// (issue #267, plan PR F3-a).
/// Default returns an empty list so transport variants that have not yet implemented
/// the call keep working — the projection's history poll becomes a no-op rather than
/// faulting. Wire decode of the FWLIB ODBALMHIS struct lives in
/// .
///
Task> ReadAlarmHistoryAsync(
int depth, CancellationToken cancellationToken)
=> Task.FromResult>(Array.Empty());
///
/// Read a contiguous range of PMC bytes in a single wire call (FOCAS
/// pmc_rdpmcrng with byte data type) for the given
/// (R, D, X, etc.) starting at and
/// spanning bytes. Returned tuple has the byte buffer
/// (length on success) + the OPC UA status mapped through
/// . Used by to coalesce
/// same-letter/same-path PMC reads in a batch into one round trip per range
/// (issue #266 — see ).
///
/// Default falls back to per-byte
/// calls so transport variants that haven't extended their wire surface still work
/// correctly — they just won't see the round-trip reduction. The fallback short-circuits
/// on the first non-Good status so a partial buffer isn't returned with a Good code.
///
///
async Task<(byte[]? buffer, uint status)> ReadPmcRangeAsync(
string letter, int pathId, int startByte, int byteCount, CancellationToken cancellationToken)
{
if (byteCount <= 0) return (Array.Empty(), FocasStatusMapper.Good);
var buf = new byte[byteCount];
for (var i = 0; i < byteCount; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var addr = new FocasAddress(FocasAreaKind.Pmc, letter, startByte + i, BitIndex: null, PathId: pathId);
var (value, status) = await ReadAsync(addr, FocasDataType.Byte, cancellationToken).ConfigureAwait(false);
if (status != FocasStatusMapper.Good) return (null, status);
buf[i] = value switch
{
sbyte s => unchecked((byte)s),
byte b => b,
int n => unchecked((byte)n),
short s => unchecked((byte)s),
_ => 0,
};
}
return (buf, FocasStatusMapper.Good);
}
}
///
/// Snapshot of the 9 fields returned by Fanuc's cnc_rdcncstat (ODBST). All fields
/// are short per the FWLIB header — small enums whose meaning is documented in the
/// Fanuc FOCAS reference (e.g. emergency: 0=released, 1=stop, 2=reset). Surfaced as
/// Int16 in the OPC UA address space rather than mapped enums so operators see
/// exactly what the CNC reported.
///
public sealed record FocasStatusInfo(
short Dummy,
short Tmmode,
short Aut,
short Run,
short Motion,
short Mstb,
short EmergencyStop,
short Alarm,
short Edit);
///
/// Snapshot of per-CNC production counters refreshed on the probe tick (issue #258).
/// Sourced from cnc_rdparam(6711/6712/6713) for the parts counts + the cycle-time
/// timer counter (FWLIB cnc_rdtimer when available). All values surfaced as
/// Int32 in the OPC UA address space.
///
public sealed record FocasProductionInfo(
int PartsProduced,
int PartsRequired,
int PartsTotal,
int CycleTimeSeconds);
///
/// Snapshot of the active modal M/S/T/B codes (issue #259). G-group decoding is a
/// deferred follow-up — the FWLIB ODBMDL union differs per series + group, and
/// the issue body permits the first cut to surface only the universally-present
/// M/S/T/B fields. short matches the FWLIB aux_data width.
///
public sealed record FocasModalInfo(
short MCode,
short SCode,
short TCode,
short BCode);
///
/// MTB-specific FOCAS parameter numbers for the four operator overrides (issue #259).
/// Defaults match Fanuc 30i — Feed=6010, Rapid=6011, Spindle=6014, Jog=6015. A
/// null entry suppresses that field's read on the wire and removes the matching
/// node from the address space; this lets a deployment hide overrides their MTB doesn't
/// wire up rather than always serving Bad.
///
public sealed record FocasOverrideParameters(
ushort? FeedParam,
ushort? RapidParam,
ushort? SpindleParam,
ushort? JogParam)
{
/// Stock 30i defaults — Feed=6010, Rapid=6011, Spindle=6014, Jog=6015.
public static FocasOverrideParameters Default { get; } = new(6010, 6011, 6014, 6015);
}
///
/// Snapshot of the four operator overrides (issue #259). Each value is a percentage
/// surfaced as Int16; a value of null means the corresponding parameter
/// was not configured (suppressed at ). All four
/// fields nullable so the driver can omit nodes whose MTB parameter is unset.
///
public sealed record FocasOverrideInfo(
short? Feed,
short? Rapid,
short? Spindle,
short? Jog);
///
/// Snapshot of the currently selected tool number (issue #260). Sourced from
/// cnc_rdtnum. The active offset index is deferred — most modern CNCs
/// interleave tool number and offset H/D codes through different FOCAS calls
/// (cnc_rdtofs against a specific slot) and the issue body permits
/// surfacing tool number alone in the first cut. Surfaced as Int16 in
/// the OPC UA address space.
///
public sealed record FocasToolingInfo(short CurrentTool);
///
/// One work-coordinate offset slot (G54..G59). Three axis columns are surfaced
/// (X / Y / Z) — the issue body permits a fixed 3-axis view because lathes and
/// mills typically don't expose extended rotational offsets via the standard
/// cnc_rdzofs call. Extended G54.1 Pn offsets via cnc_rdzofsr
/// are deferred to a follow-up PR. Values surfaced as Float64 in microns
/// converted to user units (the FWLIB data field is an integer + decimal-
/// point count, decoded the same way cnc_rdmacro values are).
///
public sealed record FocasWorkOffset(string Name, double X, double Y, double Z);
///
/// Snapshot of the six standard work-coordinate offsets (G54..G59). Refreshed on
/// the probe tick + served from the per-device cache by reads of the
/// Offsets/{name}/{X|Y|Z} fixed-tree nodes (issue #260).
///
public sealed record FocasWorkOffsetsInfo(IReadOnlyList Offsets);
///
/// One FANUC operator message — the +
/// + tuple returned by cnc_rdopmsg3 for a single
/// active message slot. is one of "OPMSG" /
/// "MACRO" / "EXTERN" / "REJ-EXT" per the FOCAS reference
/// for the four message types. is trimmed of trailing
/// nulls + spaces so round-trips through the OPC UA address space stay stable
/// (issue #261).
///
public sealed record FocasOperatorMessage(short Number, string Class, string Text);
///
/// Snapshot of all active FANUC operator messages across the four message
/// classes (issue #261). Surfaced under the FOCAS driver's
/// Messages/External/Latest fixed-tree node — the latest non-empty
/// message in the list is what gets published. Empty list means the CNC
/// reported no active messages; the node publishes an empty string in that
/// case.
///
public sealed record FocasOperatorMessagesInfo(IReadOnlyList Messages);
///
/// Snapshot of the currently-executing program block text via
/// cnc_rdactpt (issue #261). is trimmed of trailing
/// nulls + spaces so the same block round-trips with stable text. Surfaced
/// as a String node at Program/CurrentBlock.
///
public sealed record FocasCurrentBlockInfo(string Text);
///
/// One entry returned by cnc_rdalmhistry — a single historical alarm
/// occurrence the CNC retained in its ring buffer (issue #267, plan PR F3-a).
/// The projection emits these as historic
/// with SourceTimestampUtc set from so OPC UA clients
/// see the real CNC timestamp rather than the moment the projection polled.
///
///
/// The dedup key for the projection is
/// (, , ).
/// Same triple across two polls only emits once — see
/// .
///
/// FANUC ring buffers are typically capped at ~100 entries; the host parameter that
/// governs the cap varies by series + MTB so the driver clamps user-requested depth to a
/// conservative 250 ceiling (see ).
///
public sealed record FocasAlarmHistoryEntry(
DateTimeOffset OccurrenceTime,
int AxisNo,
int AlarmType,
int AlarmNumber,
string Message);
/// Factory for s. One client per configured device.
public interface IFocasClientFactory
{
IFocasClient Create();
}
///
/// Default factory that throws at construction time — the deployment must register a real
/// factory. Keeps the driver assembly licence-clean while still allowing the skeleton to
/// compile + the abstraction tests to run.
///
public sealed class UnimplementedFocasClientFactory : IFocasClientFactory
{
public IFocasClient Create() => throw new NotSupportedException(
"FOCAS driver has no wire client configured. Register a real IFocasClientFactory at " +
"server startup wrapping the licensed Fwlib32.dll — see docs/v2/focas-deployment.md. " +
"Fanuc licensing forbids shipping Fwlib32.dll in the OtOpcUa package.");
}