using System.Buffers.Binary; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; /// /// Servo (cnc_rdsvmeter, 0x0056) and spindle (cnc_rdspmeter, 0x0040) meter decode. /// Both are arrays of 8-byte records carrying the same self-describing envelope as every other /// value block on this wire — [+0..3] i32 value, [+4..5] u16 base (2 or 10), /// [+6..7] u16 exponent — so scaled = value / base^exponent. /// Ported from the sister ignitionoee FOCAS driver v0.17.2 / v0.17.3 (Focas.Wire-019). /// /// /// Servo: offset 4 is the BASE, not a decimal count. It holds a steady 10, so the /// old value / 10^decimal divided every load by 10^10 and published ~0 — on a real /// Makino cutting at 20,000 rpm the panel read servo 0% on every axis. Servo meters are /// direct percentages (the live capture shows −57..227). /// Spindle: a fraction of full scale in 1/16384 units. Settled on the real Z-34184 /// by regressing the type-1 meter against the independently read cnc_rddynamic2 spindle /// speed over 596–20,000 rpm: rpm = 1.220749 × meter, and 16384 × 1.220749 = /// 20000.75 — full scale is the spindle's 20,000 rpm maximum. So the type-1 meter /// (cnc_rdspmaxrpm by name) is the motor speed as a % of maximum, not a max-rpm /// constant; the type-0 load meter shares the envelope and the 16384 = 100% scale (its /// acceleration peaks pin at 32767 — one count short of a 200% overload). /// [Trait("Category", "Unit")] public sealed class FocasMeterDecodeTests { private static byte[] Record(int value, int @base, int exponent) { var b = new byte[8]; BinaryPrimitives.WriteInt32BigEndian(b.AsSpan(0, 4), value); BinaryPrimitives.WriteUInt16BigEndian(b.AsSpan(4, 2), (ushort)@base); BinaryPrimitives.WriteUInt16BigEndian(b.AsSpan(6, 2), (ushort)exponent); return b; } // ---- servo ---- [Fact] public void The_live_servo_capture_reads_direct_percentages_not_value_over_ten_to_the_tenth() { var blocks = WireFixtures.Blocks("svmeter-pair-0x0056-0x0089.bin"); var meters = FocasWireClient.ParseServoMeters( blocks.First(b => b.Command == 0x0056).Payload, blocks.First(b => b.Command == 0x0089).Payload, maxCount: 32); meters[0].Base.ShouldBe(10); meters[0].Exponent.ShouldBe(0); meters.Take(3).Select(m => m.Scaled).ShouldBe([0.0, 50.0, -3.0]); } [Fact] public void A_servo_record_applies_its_own_exponent() { var m = FocasWireClient.ParseServoMeters(Record(1234, 10, 1), Array.Empty(), 32).Single(); m.Scaled!.Value.ShouldBe(123.4, 1e-9); } [Theory] [InlineData(5, 7, 0)] // base neither 2 nor 10 [InlineData(5, 10, 2560)] // residue past the controlled-axis count public void An_undescribable_servo_record_has_no_value_not_zero(int value, int @base, int exponent) { var m = FocasWireClient.ParseServoMeters(Record(value, @base, exponent), Array.Empty(), 32).Single(); m.IsValid.ShouldBeFalse(); m.Scaled.ShouldBeNull(); } // ---- spindle ---- [Fact] public void The_live_spindle_load_capture_is_a_percentage_of_16384() { var metrics = FocasWireClient.ParseSpindleMetrics( WireFixtures.Payload("live-spmeter-load-0x0040.bin", 0x0040)); // Slot 1: raw 0x23C3 = 9155, base 10, exponent 0 → 9155 × 100 / 16384 ≈ 55.88 %. metrics[0].Value.ShouldBe(9155); metrics[0].Percent!.Value.ShouldBe(9155 * 100.0 / 16384, 1e-9); // Slot 2 is buffer residue (exponent 0x0A00 = 2560): no value, not a number. metrics[1].IsValid.ShouldBeFalse(); metrics[1].Percent.ShouldBeNull(); } [Fact] public void The_live_motor_speed_capture_is_a_percentage_of_maximum_speed() { var metrics = FocasWireClient.ParseSpindleMetrics( WireFixtures.Payload("live-spmeter-maxrpm-0x0040.bin", 0x0040)); // 0x3FFD = 16381 ≈ 99.98 % — a spindle at (just under) its 20,000 rpm maximum. metrics[0].Value.ShouldBe(16381); metrics[0].Percent!.Value.ShouldBe(16381 * 100.0 / 16384, 1e-9); } [Theory] [InlineData(16382, 19998)] [InlineData(12286, 14998)] [InlineData(9830, 12000)] [InlineData(695, 850)] [InlineData(488, 595)] public void Percent_reproduces_the_live_motor_speed_regression(int meter, int rpm) { var m = FocasWireClient.ParseSpindleMetrics(Record(meter, 10, 0)).Single(); m.Percent!.Value.ShouldBe(rpm * 100.0 / 20000.0, 0.02); } [Fact] public void Full_scale_is_16384_and_overload_is_not_clamped() { WireSpindleMetric.FullScale.ShouldBe(16384); FocasWireClient.ParseSpindleMetrics(Record(16384, 10, 0)).Single().Percent.ShouldBe(100.0); var overload = FocasWireClient.ParseSpindleMetrics(Record(32767, 10, 0)).Single().Percent!.Value; overload.ShouldBeInRange(199.9, 200.0); } [Fact] public void The_spindle_envelope_exponent_applies_before_the_full_scale_division() { // 163840 / 10^1 = 16384 counts = 100 %. FocasWireClient.ParseSpindleMetrics(Record(163840, 10, 1)).Single().Percent!.Value.ShouldBe(100.0, 1e-9); } }