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; an empty list means "the CNC answered, and no alarm is /// active". The alarm projection polls this per device and diffs it into Part 9 /// condition transitions on the driver's OnAlarmEvent. /// /// A failed read MUST throw, never answer empty (Gitea #647): the projection treats an /// empty answer as "every alarm cleared", so a failure reported as [] becomes a mass /// clear followed by an un-acknowledged re-raise on recovery. A throw keeps the last state. /// /// /// The cancellation token. /// The list of currently active alarms; empty when none are active. /// Any failure to obtain the list — the session is down, the CNC refused, the wire faulted. Task> ReadAlarmsAsync(CancellationToken cancellationToken); /// /// Read one PMC range (pmc_rdpmcrng) as raw slots: (end - start + 1) bytes split into /// slots of 's width, from . The primitive behind PMC /// range coalescing (Gitea #266) — the driver slices individual tag values out of the returned slots with /// the same decode a per-tag read uses. /// /// PMC area letter. /// Decides the slot width (Bit/Byte = 1, Int16 = 2, Int32/Float32 = 4, Float64 = 8). /// First byte address. /// Last byte address, inclusive; a whole number of slots from . /// The cancellation token. /// /// The raw slot values (null when not Good) and the mapped OPC UA status. A returned non-Good status is /// always a CNC return-code refusal; a transport failure or timeout throws instead. /// Task<(IReadOnlyList? slots, uint status)> ReadPmcSlotsAsync( string letter, FocasDataType slotType, int start, int end, 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); }