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."); }