using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; /// /// Driver-level publish of self-describing cnc_rddynamic2 positions. Each block's own /// base^exponent wins over the configured PositionDecimalPlaces (which is now only /// the fallback for a scale-less block), and a block describing no usable position Bads its own /// node without disturbing its siblings. Replaces the cnc_getfigure (0x00D3) auto-scale /// tests — that command is not a FOCAS wire command; the real 31i-B refuses it with rc=1. /// [Trait("Category", "Unit")] public sealed class FocasPositionBlockScaleTests { private const string Host = "focas://10.0.0.5:8193"; private static FocasDynamicSnapshot Snap(int axisIndex, FocasPosition abs, FocasPosition mach) => new(AxisIndex: axisIndex, AlarmFlags: 0, ProgramNumber: 0, MainProgramNumber: 0, SequenceNumber: 0, ActualFeedRate: 0, ActualSpindleSpeed: 0, AbsolutePosition: abs, MachinePosition: mach, RelativePosition: 0, DistanceToGo: 0); private static FocasDriver NewDriver(int positionDecimalPlaces, FakeFocasClient client) => new(new FocasDriverOptions { Devices = [new FocasDeviceOptions(Host, PositionDecimalPlaces: positionDecimalPlaces)], Probe = new FocasProbeOptions { Enabled = false }, FixedTree = new FocasFixedTreeOptions { Enabled = true, PollInterval = TimeSpan.FromMilliseconds(10) }, }, "drv-1", new FakeFocasClientFactory { Customise = () => client }); private static async Task ReadWhenPolledAsync(FocasDriver drv, string reference) { var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5); DataValueSnapshot last; do { // Until the first poll lands the reference resolves to nothing; once it has, the node // reads either Good or (for an undescribable block) BadNoData. last = (await drv.ReadAsync([reference], CancellationToken.None)).Single(); if (last.StatusCode is FocasStatusMapper.Good or FocasStatusMapper.BadNoData) return last; await Task.Delay(15); } while (DateTime.UtcNow < deadline); return last; } private static FakeFocasClient XAxisClient(FocasDynamicSnapshot snap) { var c = new FakeFocasClient(); c.AxisNames.Clear(); c.AxisNames.Add(new("X", "")); c.DynamicByAxis[1] = snap; return c; } [Fact] public async Task The_blocks_own_scale_wins_over_PositionDecimalPlaces() { // The live 31i-B shape: raw 2801574, base 10, exponent 4 → 280.1574 mm. var client = XAxisClient(Snap(1, new FocasPosition(2_801_574, 10, 4, HasScale: true), 0)); var drv = NewDriver(positionDecimalPlaces: 1, client); await drv.InitializeAsync("{}", CancellationToken.None); var abs = await ReadWhenPolledAsync(drv, $"{Host}/Axes/X/AbsolutePosition"); abs.StatusCode.ShouldBe(FocasStatusMapper.Good); ((double)abs.Value!).ShouldBe(280.1574, 1e-9); // NOT 280157.4 (the ÷10 fallback) await drv.ShutdownAsync(CancellationToken.None); } [Fact] public async Task A_scale_less_block_falls_back_to_PositionDecimalPlaces() { var client = XAxisClient(Snap(1, 12345, 0)); var drv = NewDriver(positionDecimalPlaces: 2, client); await drv.InitializeAsync("{}", CancellationToken.None); var abs = await ReadWhenPolledAsync(drv, $"{Host}/Axes/X/AbsolutePosition"); abs.StatusCode.ShouldBe(FocasStatusMapper.Good); abs.Value.ShouldBe(123.45); await drv.ShutdownAsync(CancellationToken.None); } [Fact] public async Task A_vacant_block_bads_its_own_node_and_not_its_sibling() { var client = XAxisClient(Snap( 1, abs: new FocasPosition(0, 10, 0xFFFF, HasScale: true), // vacant-slot marker mach: new FocasPosition(-22000, 10, 3, HasScale: true))); var drv = NewDriver(positionDecimalPlaces: 0, client); await drv.InitializeAsync("{}", CancellationToken.None); var mach = await ReadWhenPolledAsync(drv, $"{Host}/Axes/X/MachinePosition"); var abs = await ReadWhenPolledAsync(drv, $"{Host}/Axes/X/AbsolutePosition"); mach.StatusCode.ShouldBe(FocasStatusMapper.Good); mach.Value.ShouldBe(-22.0); abs.StatusCode.ShouldBe(FocasStatusMapper.BadNoData); abs.Value.ShouldBeNull("an undescribable block is not a Good 0.0 at the origin"); await drv.ShutdownAsync(CancellationToken.None); } }