fix(focas): scale positions from each cnc_rddynamic2 block; delete the fabricated cnc_getfigure
Every 0x0026 position block is self-describing: [i32 raw][u16 base][u16 exponent],
value = raw / base^exponent. The live 31i-B capture (ref-dynamic2-axis1.bin)
reports base 10 / exponent 4 on every block, so axis 1 is 280.1574 mm, not
2801574. The driver ignored the envelope and instead sent 'cnc_getfigure'
(0x00D3) -- a command that does not exist in the FOCAS wire protocol, served only
by the in-tree mock; the real control refuses it with rc=1, so on hardware every
position fell back to PositionDecimalPlaces (default 0) and published raw counts.
- ParsePositionBlock decodes the envelope into FocasPosition; the driver applies
raw / base^exponent at publish. PositionDecimalPlaces is now only the fallback
for a scale-less (4-byte) block.
- A block describing no position (0xFFFF vacant marker, base not 2/10, exponent
> 18 -- raw / 10^400 would read as a Good 0.0) publishes BadNoData on that node
alone; siblings are untouched.
- ReadPositionFiguresAsync / IFocasClient.GetPositionFiguresAsync and the
per-axis figure cache are deleted.
Ported from the sister ignitionoee FOCAS driver v0.17.0 (+ Focas.Wire-014);
live axis-scoped captures live-dynamic2-axis{1,2}-0x0026.bin (Z-34184 X 0.0374,
Y 370.177) added as fixtures.
This commit is contained in:
@@ -167,16 +167,6 @@ public interface IFocasClient : IDisposable
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Per-spindle maximum RPM values, indexed as per <see cref="GetSpindleLoadsAsync"/>.</returns>
|
||||
Task<IReadOnlyList<int>> GetSpindleMaxRpmsAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Read the per-axis position decimal-place figure via <c>cnc_getfigure</c>. The returned
|
||||
/// list is parallel to <see cref="GetAxisNamesAsync"/> (index = axis). An EMPTY list means the
|
||||
/// CNC/backend does not report figures — the driver then falls back to the configured
|
||||
/// <c>PositionDecimalPlaces</c>. Values are clamped non-negative.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Per-axis position decimal-place figures; empty when the CNC/backend doesn't report them.</returns>
|
||||
Task<IReadOnlyList<int>> GetPositionFiguresAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>One servo-meter entry — one axis's current load percentage.</summary>
|
||||
@@ -238,12 +228,6 @@ public sealed record FocasSpindleName(string Name, string Suffix1, string Suffix
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fast-poll bundle for one axis. Position values are scaled integers; the caller
|
||||
/// divides by <c>10^DecimalPlaces</c> to get the decimal value. DecimalPlaces is
|
||||
/// supplied by the caller — either via device config (<c>PositionDecimalPlaces</c>) or
|
||||
/// the per-axis <see cref="IFocasClient.GetPositionFiguresAsync"/> (<c>cnc_getfigure</c>) path.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Program + operation-mode snapshot. Name is the currently-executing
|
||||
/// program filename (e.g. "O0001.NC"); ONumber is its Fanuc O-number (1-9999).
|
||||
@@ -277,6 +261,10 @@ public static class FocasOpMode
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fast-poll bundle for one axis (<c>cnc_rddynamic2</c>). Each position carries its own scale
|
||||
/// (see <see cref="FocasPosition"/>); the driver converts it to engineering units at publish.
|
||||
/// </summary>
|
||||
public sealed record FocasDynamicSnapshot(
|
||||
int AxisIndex,
|
||||
int AlarmFlags,
|
||||
@@ -285,10 +273,63 @@ public sealed record FocasDynamicSnapshot(
|
||||
int SequenceNumber,
|
||||
int ActualFeedRate,
|
||||
int ActualSpindleSpeed,
|
||||
int AbsolutePosition,
|
||||
int MachinePosition,
|
||||
int RelativePosition,
|
||||
int DistanceToGo);
|
||||
FocasPosition AbsolutePosition,
|
||||
FocasPosition MachinePosition,
|
||||
FocasPosition RelativePosition,
|
||||
FocasPosition DistanceToGo);
|
||||
|
||||
/// <summary>
|
||||
/// One <c>cnc_rddynamic2</c> position (command <c>0x0026</c>) as the CNC reported it. The 8-byte
|
||||
/// wire block is self-describing — <c>[0..3] i32 raw</c>, <c>[4..5] u16 base</c> (2 or 10),
|
||||
/// <c>[6..7] u16 exponent</c> (<c>0xFFFF</c> = the slot holds no valid position) — and the real
|
||||
/// coordinate is <c>raw / base^exponent</c>. Confirmed on a real FANUC 31i-B: every block of the
|
||||
/// <c>ref-dynamic2-axis1.bin</c> capture reports base 10 / exponent 4 (280.1574 mm, not
|
||||
/// 2801574). That envelope is why no separate figure read exists: the <c>cnc_getfigure</c>
|
||||
/// (0x00D3) this driver used to send is not a FOCAS wire command, and the 31i-B refuses it.
|
||||
/// </summary>
|
||||
/// <param name="Raw">The raw i32 position value.</param>
|
||||
/// <param name="Base">The block's scale base (only 2 and 10 are meaningful).</param>
|
||||
/// <param name="Exponent">The block's scale exponent.</param>
|
||||
/// <param name="HasScale">
|
||||
/// 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 <c>PositionDecimalPlaces</c>.
|
||||
/// </param>
|
||||
public readonly record struct FocasPosition(int Raw, int Base = 0, int Exponent = 0, bool HasScale = false)
|
||||
{
|
||||
/// <summary>
|
||||
/// Largest exponent that can describe a real coordinate. An i32 carries at most ten digits,
|
||||
/// so anything beyond is a misread block — and left unbounded, <c>raw / 10^400</c> is
|
||||
/// <c>raw / Infinity = 0.0</c>, a Good reading of a machine parked at the origin.
|
||||
/// </summary>
|
||||
public const int MaxExponent = 18;
|
||||
|
||||
/// <summary>
|
||||
/// False when the block says it holds no usable position: the <c>0xFFFF</c> 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.
|
||||
/// </summary>
|
||||
public bool IsValid => !HasScale || (Exponent <= MaxExponent && Base is 2 or 10);
|
||||
|
||||
/// <summary>
|
||||
/// The position in engineering units: <c>Raw / Base^Exponent</c> when the block carried its
|
||||
/// own scale (the configured fallback is then ignored), else <c>Raw / 10^fallbackDecimalPlaces</c>.
|
||||
/// Null when <see cref="IsValid"/> is false.
|
||||
/// </summary>
|
||||
/// <param name="fallbackDecimalPlaces">The device's configured <c>PositionDecimalPlaces</c>.</param>
|
||||
/// <returns>The scaled position, or null when the block describes none.</returns>
|
||||
public double? ToEngineering(int fallbackDecimalPlaces)
|
||||
{
|
||||
if (!IsValid) return null;
|
||||
if (HasScale) return Exponent == 0 ? Raw : Raw / Math.Pow(Base, Exponent);
|
||||
var dp = Math.Max(0, fallbackDecimalPlaces);
|
||||
return dp > 0 ? Raw / Math.Pow(10, dp) : Raw;
|
||||
}
|
||||
|
||||
/// <summary>A bare integer is a scale-less position (legacy shape; tests and fakes).</summary>
|
||||
/// <param name="raw">The raw position value.</param>
|
||||
public static implicit operator FocasPosition(int raw) => new(raw);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One active alarm surfaced by <see cref="IFocasClient.ReadAlarmsAsync"/>. Shape
|
||||
|
||||
Reference in New Issue
Block a user