using System.Buffers.Binary; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; /// /// cnc_rddynamic2 position blocks (0x0026) are self-describing: [0..3] i32 raw, /// [4..5] u16 base (2 or 10), [6..7] u16 exponent (0xFFFF = vacant slot); /// value = raw / base^exponent. Ported from the sister ignitionoee FOCAS driver v0.17.0 /// (which also deleted the fabricated cnc_getfigure 0x00D3 — the real 31i-B refuses it /// with rc=1) and Focas.Wire-014 (absurd exponents are refused, not divided by). /// [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); } }