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);
}
}