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