fix(focas): decode cnc_rdmacro dec_val at offset 6; vacant/fractional macros go Bad

The ODBM wire payload is [i32 mantissa][i16 length, a constant 10][i16 dec_val].
ReadMacroAsync read the decimal at offset 4, so every macro decoded without
failing but wrongly: the captured #500 = 1.0 (05 f5 e1 00 00 0a 00 08) published
0.01. FANUC normalises macros to a 9-significant-digit mantissa, so the error
was never small.

- dec_val is read at [6..7]; a payload < 8 bytes is refused, not guessed at.
- A vacant variable (dec_val 0xFFFF) publishes BadNoData, never a Good 0.
- Integer-typed tags get the exact whole value (integer arithmetic); a
  fractional macro on an integer tag is BadTypeMismatch, a whole value that
  does not fit the type BadOutOfRange. Float tags get mantissa / 10^dec.

Ported from the sister ignitionoee FOCAS driver v0.15.0 (Focas.Wire-013,
Focas-017/018); live vectors from Makino Z-34184 (31i-B, G431).
Adds a WireFixtures helper that loads the raw 31i-B captures in Fixtures/v3.
This commit is contained in:
Joseph Doherty
2026-10-07 13:57:11 -04:00
parent e3d7a8f071
commit 665ecd55c2
7 changed files with 292 additions and 10 deletions
@@ -86,13 +86,31 @@ public sealed record WireParameter(
int Value);
/// <summary>
/// Macro variable from <c>cnc_rdmacro</c>. Scaled decimal: the callable value is
/// <c>Value / 10^Decimal</c>.
/// 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);
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(