namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire;
///
/// Return envelope over a parsed wire response. carries the FOCAS
/// EW_* code from the response block — 0 / means the
/// call succeeded and is populated; non-zero means the CNC rejected
/// the call and is default. Callers use the RC to distinguish
/// "feature missing on this series" (EW_FUNC / EW_NOOPT) from genuine
/// failures.
///
public readonly record struct FocasResult(short Rc, T? Value)
{
/// True when is zero (EW_OK).
public bool IsOk => Rc == 0;
}
/// CNC identity payload returned by cnc_sysinfo.
public sealed record WireSysInfo(
short AddInfo,
short MaxAxis,
string CncType,
string MachineType,
string Series,
string Version,
string Axes);
/// Coarse CNC state bits returned by cnc_statinfo — the seven-word status block plus TM mode.
public sealed record WireStatus(
short Auto,
short Run,
short Motion,
short Mstb,
short Emergency,
short Alarm,
short Edit,
short TmMode);
///
/// Four-slot position quadruple for one axis: absolute / machine / relative / distance-to-go,
/// each carrying the scale its own 0x0026 block reported.
///
public sealed record WireAxisPosition(
FocasPosition Absolute,
FocasPosition Machine,
FocasPosition Relative,
FocasPosition Distance);
///
/// Fast-poll bundle for one axis from cnc_rddynamic2 — alarm flags, active program
/// numbers, sequence number, actual feed rate, actual spindle speed, and the position
/// quadruple.
///
public sealed record WireDynamic(
int Alarm,
int ProgramNumber,
int MainProgramNumber,
int SequenceNumber,
int FeedRate,
int SpindleSpeed,
WireAxisPosition Axis);
///
/// The self-describing (value, base, exponent) envelope shared by every value record on
/// the FOCAS wire — cnc_rddynamic2 positions, servo meters, spindle meters:
/// scaled = value / base^exponent, where only base 2 and base 10 are meaningful.
///
internal static class FocasValueEnvelope
{
///
/// Largest exponent that can describe a real value. An i32 carries at most ten digits, so
/// anything beyond is a misread record — and value / 10^400 would be
/// value / Infinity = 0.0, a plausible Good zero.
///
public const int MaxExponent = 18;
///
/// True when the record describes a usable value. A fixed multi-slot buffer's slots past the
/// configured axis/spindle count hold unrelated memory, which shows up exactly here: a base
/// that is neither 2 nor 10, or an absurd exponent (incl. the 0xFFFF vacant marker).
///
/// The record's base field.
/// The record's exponent field.
/// Whether the record may be published.
public static bool IsValid(int @base, int exponent) => exponent <= MaxExponent && @base is 2 or 10;
/// value / base^exponent, or null when is false.
/// The raw value.
/// The record's base field.
/// The record's exponent field.
/// The scaled value, or null.
public static double? Scale(int value, int @base, int exponent) =>
!IsValid(@base, exponent) ? null : exponent == 0 ? value : value / Math.Pow(@base, exponent);
}
///
/// One servo-meter entry from cnc_rdsvmeter (0x0056): one axis's load. The 8-byte record
/// is [+0..3] i32 value, [+4..5] u16 base, [+6..7] u16 exponent and the load
/// is a direct percentage, = value / base^exponent.
///
///
/// Offset 4 used to be read as a decimal count ("data / 10^dec"). It is the base — a
/// steady 10 — so every load was divided by 10^10 and published ~0 (a real Makino cutting at
/// 20,000 rpm read servo 0% on every axis). Found by the sister ignitionoee driver,
/// Focas.Wire-019 (v0.17.2).
///
/// 1-based axis slot.
/// Axis name from the companion 0x0089 block (empty when none).
/// Raw load value.
/// Scale base (2 or 10 when meaningful).
/// Scale exponent.
public sealed record WireServoMeter(
short Index,
string Name,
int Value,
int Base,
int Exponent)
{
/// False for a record whose envelope describes no usable value (buffer residue).
public bool IsValid => FocasValueEnvelope.IsValid(Base, Exponent);
/// The load percentage, Value / Base^Exponent; null when is false.
public double? Scaled => FocasValueEnvelope.Scale(Value, Base, Exponent);
}
///
/// One spindle-meter slot from cnc_rdspmeter (0x0040; type 0 = load, type 1 = motor
/// speed). Same 8-byte envelope as , but the scaled value is a
/// fraction of full scale in 1/16384 units — see .
///
/// 1-based spindle slot.
/// Raw meter value, in 1/16384-of-full-scale units.
/// Scale base.
/// Scale exponent.
public sealed record WireSpindleMetric(
short Index,
int Value,
int Base,
int Exponent)
{
///
/// The meter's full-scale count: 16384 = 2^14 = 100% of rated load (type 0) or of
/// maximum spindle speed (type 1). Settled on the real Z-34184 by regressing the type-1
/// meter against cnc_rddynamic2 spindle speed over 596–20,000 rpm:
/// rpm = 1.220749 × meter, 16384 × 1.220749 = 20000.75 (Focas.Wire-019).
///
public const int FullScale = 1 << 14;
/// False for a record whose envelope describes no usable value (buffer residue).
public bool IsValid => FocasValueEnvelope.IsValid(Base, Exponent);
///
/// The meter as a percentage — (Value / Base^Exponent) × 100 / 16384 — or null when
/// is false. Values above 100 are real (short-term overload; the i16
/// record clamps one count short of 200%).
///
public double? Percent => FocasValueEnvelope.Scale(Value, Base, Exponent) is { } scaled
? scaled * 100.0 / FullScale
: null;
}
///
/// One axis-name slot from cnc_rdaxisname. is the 1-based
/// axis index (preserved even when the name is empty so callers can pass it to
/// cnc_rddynamic2).
///
public readonly record struct WireAxisRecord(short Index, string Name);
/// One spindle-name slot from cnc_rdspdlname.
public readonly record struct WireSpindleRecord(short Index, string Name);
/// Parameter value returned by cnc_rdparam, interpreted as a scalar Int32.
public sealed record WireParameter(
short DataNumber,
short Type,
int Value);
///
/// Macro variable from cnc_rdmacro. Scaled decimal: the real value is
/// Value / 10^Decimal (). FANUC normalises every macro to a
/// 9-significant-digit mantissa, so alone is never a value to publish
/// (live: #502 = 270 arrives as 270000000 / 10^6).
///
/// Macro variable number (the request argument; not carried in the payload).
/// The 9-significant-digit mantissa.
/// Implied decimal places; negative (0xFFFF on the wire) = vacant.
public sealed record WireMacro(
short Number,
int Value,
short Decimal)
{
///
/// True when the CNC reported the variable as vacant (undefined / never set):
/// dec_val == 0xFFFF. A vacant macro's mantissa is 0, indistinguishable from a
/// genuine 0, so this must be tested before means anything. Any
/// negative decimal is treated as vacant — no other negative has ever been observed and
/// failing closed is the safe reading.
///
public bool IsVacant => Decimal < 0;
/// The real value, Value / 10^Decimal; null when .
public double? Scaled => IsVacant ? null : Decimal == 0 ? Value : Value / Math.Pow(10.0, Decimal);
}
/// PMC range read-back from pmc_rdpmcrng: one or more values of the requested width.
public sealed record WirePmcRange(
short Area,
short DataType,
ushort Start,
ushort End,
IReadOnlyList Values);
///
/// One active alarm from cnc_rdalmmsg2. Mirrors the vendor ODBALMMSG2
/// layout; is populated when the wire responder carries it
/// (currently null for both the 76-byte vendor shape and the 80-byte legacy
/// shape).
///
public sealed record WireAlarm(
int AlarmNumber,
short Type,
short Axis,
short MessageLength,
string Message,
int? AlarmGroup = null);
///
/// Executing-program identity from cnc_exeprgname2: the NUL-terminated name and
/// the trailing 32-bit O-number (null when the wire responder omits the trailing int).
///
public sealed record WireProgramName(
string Name,
int? ONumber);
/// One cumulative timer reading from cnc_rdtimer (minutes + fractional milliseconds).
public sealed record WireTimer(
short Type,
int Minutes,
int Milliseconds);