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:
@@ -62,7 +62,7 @@ public sealed class WireFocasClient : IFocasClient
|
||||
{
|
||||
FocasAreaKind.Pmc => await ReadPmcAsync(address, type, cancellationToken).ConfigureAwait(false),
|
||||
FocasAreaKind.Parameter => await ReadParameterAsync(address, type, cancellationToken).ConfigureAwait(false),
|
||||
FocasAreaKind.Macro => await ReadMacroAsync(address, cancellationToken).ConfigureAwait(false),
|
||||
FocasAreaKind.Macro => await ReadMacroAsync(address, type, cancellationToken).ConfigureAwait(false),
|
||||
_ => (null, FocasStatusMapper.BadNotSupported),
|
||||
};
|
||||
}
|
||||
@@ -368,17 +368,14 @@ public sealed class WireFocasClient : IFocasClient
|
||||
}
|
||||
|
||||
private async Task<(object? value, uint status)> ReadMacroAsync(
|
||||
FocasAddress address, CancellationToken cancellationToken)
|
||||
FocasAddress address, FocasDataType type, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _wire.ReadMacroAsync((short)address.Number, cancellationToken).ConfigureAwait(false);
|
||||
if (!result.IsOk || result.Value is null)
|
||||
return (null, FocasStatusMapper.MapFocasReturn(result.Rc));
|
||||
var m = result.Value;
|
||||
// Macro value is scaled-decimal: the real value is Value / 10^Decimal.
|
||||
var scaled = m.Value / Math.Pow(10.0, m.Decimal);
|
||||
return ((object)scaled, FocasStatusMapper.Good);
|
||||
return MapMacro(result.Value, type, address.BitIndex);
|
||||
}
|
||||
catch (FocasWireException ex)
|
||||
{
|
||||
@@ -386,6 +383,62 @@ public sealed class WireFocasClient : IFocasClient
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turn a decoded macro into the value + quality its tag publishes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>A <b>vacant</b> variable has no value at all → <see cref="FocasStatusMapper.BadNoData"/>,
|
||||
/// never a Good <c>0</c> (its mantissa is 0, indistinguishable from a genuine zero).</para>
|
||||
/// <para>Float tags get <c>mantissa / 10^decimal</c>. <b>Integer</b> tags (Byte / Int16 /
|
||||
/// Int32 / Bit) get the exact whole value, tested with integer arithmetic
|
||||
/// (<c>mantissa % 10^decimal</c>) so no epsilon is involved; a fractional macro on an
|
||||
/// integer tag is <see cref="FocasStatusMapper.BadTypeMismatch"/> — truncating or rounding
|
||||
/// would publish a plausible wrong count at Good quality — and a whole value that does not
|
||||
/// fit the type is <see cref="FocasStatusMapper.BadOutOfRange"/>. Ported from the sister
|
||||
/// ignitionoee driver's <c>macroInteger</c>/<c>macroReal</c> (Focas-017/018).</para>
|
||||
/// </remarks>
|
||||
/// <param name="m">The decoded macro.</param>
|
||||
/// <param name="type">The tag's declared data type.</param>
|
||||
/// <param name="bitIndex">Optional bit index for a <see cref="FocasDataType.Bit"/> tag.</param>
|
||||
/// <returns>The boxed value (null when Bad) and its status.</returns>
|
||||
internal static (object? value, uint status) MapMacro(WireMacro m, FocasDataType type, int? bitIndex)
|
||||
{
|
||||
if (m.Scaled is not { } scaled) return (null, FocasStatusMapper.BadNoData);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case FocasDataType.Float64:
|
||||
return (scaled, FocasStatusMapper.Good);
|
||||
case FocasDataType.Float32:
|
||||
return ((float)scaled, FocasStatusMapper.Good);
|
||||
case FocasDataType.Byte or FocasDataType.Int16 or FocasDataType.Int32 or FocasDataType.Bit:
|
||||
if (!TryWholeMacroValue(m, out var whole)) return (null, FocasStatusMapper.BadTypeMismatch);
|
||||
return type switch
|
||||
{
|
||||
FocasDataType.Bit => (bitIndex is int bit ? ((whole >> bit) & 1L) != 0 : whole != 0, FocasStatusMapper.Good),
|
||||
FocasDataType.Byte when whole is >= sbyte.MinValue and <= sbyte.MaxValue => ((sbyte)whole, FocasStatusMapper.Good),
|
||||
FocasDataType.Int16 when whole is >= short.MinValue and <= short.MaxValue => ((short)whole, FocasStatusMapper.Good),
|
||||
FocasDataType.Int32 when whole is >= int.MinValue and <= int.MaxValue => ((int)whole, FocasStatusMapper.Good),
|
||||
_ => (null, FocasStatusMapper.BadOutOfRange),
|
||||
};
|
||||
default:
|
||||
return (scaled, FocasStatusMapper.Good);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Exact <c>mantissa / 10^decimal</c> when it is a whole number; integer arithmetic only.</summary>
|
||||
private static bool TryWholeMacroValue(WireMacro m, out long whole)
|
||||
{
|
||||
whole = 0;
|
||||
if (m.Value == 0) return true; // 0 is whole at any decimal
|
||||
if (m.Decimal > 18) return false; // an i32 mantissa over 10^19+ is always fractional
|
||||
var divisor = 1L;
|
||||
for (var i = 0; i < m.Decimal; i++) divisor *= 10L;
|
||||
if (m.Value % divisor != 0) return false;
|
||||
whole = m.Value / divisor;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RequireConnected()
|
||||
{
|
||||
if (!_wire.IsConnected)
|
||||
|
||||
Reference in New Issue
Block a user