69e0a622f3
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.
108 lines
4.5 KiB
C#
108 lines
4.5 KiB
C#
using System.Buffers.Binary;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
|
|
|
/// <summary>
|
|
/// <c>cnc_rddynamic2</c> position blocks (0x0026) are 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> = vacant slot);
|
|
/// <c>value = raw / base^exponent</c>. Ported from the sister ignitionoee FOCAS driver v0.17.0
|
|
/// (which also deleted the fabricated <c>cnc_getfigure</c> 0x00D3 — the real 31i-B refuses it
|
|
/// with <c>rc=1</c>) and Focas.Wire-014 (absurd exponents are refused, not divided by).
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class FocasPositionDecodeTests
|
|
{
|
|
private static byte[] Block(int raw, int @base, int exponent)
|
|
{
|
|
var b = new byte[8];
|
|
BinaryPrimitives.WriteInt32BigEndian(b.AsSpan(0, 4), raw);
|
|
BinaryPrimitives.WriteUInt16BigEndian(b.AsSpan(4, 2), (ushort)@base);
|
|
BinaryPrimitives.WriteUInt16BigEndian(b.AsSpan(6, 2), (ushort)exponent);
|
|
return b;
|
|
}
|
|
|
|
[Fact]
|
|
public void The_live_dynamic2_capture_scales_every_position_from_its_own_block()
|
|
{
|
|
// ref-dynamic2-axis1.bin: four 0x0026 blocks (absolute / machine / relative / distance of
|
|
// axis 1), every one base 10, exponent 4. Undecoded they read 2801574 / 2801200 / 32 / 0.
|
|
var dynamic = FocasWireClient.ParseDynamic2(WireFixtures.Blocks("ref-dynamic2-axis1.bin"));
|
|
|
|
dynamic.Axis.Absolute.ToEngineering(0)!.Value.ShouldBe(280.1574, 1e-9);
|
|
dynamic.Axis.Machine.ToEngineering(0)!.Value.ShouldBe(280.1200, 1e-9);
|
|
dynamic.Axis.Relative.ToEngineering(0)!.Value.ShouldBe(0.0032, 1e-9);
|
|
dynamic.Axis.Distance.ToEngineering(0)!.Value.ShouldBe(0.0, 1e-9);
|
|
dynamic.FeedRate.ShouldBe(0);
|
|
dynamic.SpindleSpeed.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void The_block_scale_wins_over_the_configured_decimal_places()
|
|
{
|
|
var p = FocasWireClient.ParsePositionBlock(Block(123456, 10, 3));
|
|
p.ToEngineering(fallbackDecimalPlaces: 1)!.Value.ShouldBe(123.456, 1e-9);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("live-dynamic2-axis1-0x0026.bin", 0.0374)] // Z-34184 X
|
|
[InlineData("live-dynamic2-axis2-0x0026.bin", 370.177)] // Z-34184 Y
|
|
public void Axis_scoped_live_requests_return_that_axis_scaled(string fixture, double expected)
|
|
{
|
|
var blocks = WireFixtures.Blocks(fixture).Where(b => b.Command == 0x0026).ToList();
|
|
blocks.Count.ShouldBe(1, "an axis-scoped request returns exactly one 0x0026 block");
|
|
|
|
FocasWireClient.ParsePositionBlock(blocks[0].Payload).ToEngineering(0)!.Value.ShouldBe(expected, 1e-9);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(40, 2, 3, 5.0)] // inch-mode binary base
|
|
[InlineData(-22000, 10, 3, -22.0)]
|
|
[InlineData(42, 10, 0, 42.0)]
|
|
public void Positions_scale_by_base_to_the_exponent(int raw, int @base, int exponent, double expected)
|
|
{
|
|
FocasWireClient.ParsePositionBlock(Block(raw, @base, exponent)).ToEngineering(0)!.Value.ShouldBe(expected, 1e-9);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1, 10, 0xFFFF)] // vacant-slot marker
|
|
[InlineData(1234, 7, 2)] // base neither 2 nor 10
|
|
[InlineData(1234, 0, 2)]
|
|
[InlineData(123456, 10, 400)] // would divide by Infinity → a Good 0.0 at the origin
|
|
[InlineData(123456, 10, 40)]
|
|
public void An_undescribable_block_has_no_value(int raw, int @base, int exponent)
|
|
{
|
|
var p = FocasWireClient.ParsePositionBlock(Block(raw, @base, exponent));
|
|
p.IsValid.ShouldBeFalse();
|
|
p.ToEngineering(3).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Every_plausible_exponent_still_decodes()
|
|
{
|
|
for (var exp = 0; exp <= 18; exp++)
|
|
FocasWireClient.ParsePositionBlock(Block(1, 10, exp)).IsValid.ShouldBeTrue($"exponent {exp}");
|
|
}
|
|
|
|
[Fact]
|
|
public void A_block_without_a_scale_falls_back_to_the_configured_decimal_places()
|
|
{
|
|
// A 4-byte block carries no envelope (older simulators); only then does
|
|
// PositionDecimalPlaces apply.
|
|
var p = FocasWireClient.ParsePositionBlock([0x00, 0x00, 0x30, 0x39]); // 12345
|
|
p.HasScale.ShouldBeFalse();
|
|
p.ToEngineering(2)!.Value.ShouldBe(123.45, 1e-9);
|
|
p.ToEngineering(0)!.Value.ShouldBe(12345.0);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_raw_integer_position_converts_implicitly_as_a_scale_less_value()
|
|
{
|
|
FocasPosition p = 12345;
|
|
p.HasScale.ShouldBeFalse();
|
|
p.Raw.ShouldBe(12345);
|
|
}
|
|
}
|