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:
Joseph Doherty
2026-10-07 14:03:08 -04:00
parent 9460da513c
commit 69e0a622f3
16 changed files with 380 additions and 356 deletions
@@ -106,10 +106,6 @@ public sealed class SynchronizedFocasClient : IFocasClient
public Task<IReadOnlyList<int>> GetSpindleMaxRpmsAsync(CancellationToken cancellationToken) =>
RunBoundedAsync(ct => _inner.GetSpindleMaxRpmsAsync(ct), cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<int>> GetPositionFiguresAsync(CancellationToken cancellationToken) =>
RunBoundedAsync(ct => _inner.GetPositionFiguresAsync(ct), cancellationToken);
/// <summary>Disposes the inner client and the call gate.</summary>
public void Dispose()
{