136 lines
5.8 KiB
C#
136 lines
5.8 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
|
|
|
/// <summary>
|
|
/// Phase 4 data-type tier — axis-position scaling. <c>cnc_rddynamic2</c> returns
|
|
/// positions as scaled integers; the driver applies a <c>10^PositionDecimalPlaces</c>
|
|
/// divide at the <see cref="FocasDriver"/> publish seam so positions surface in
|
|
/// engineering units on the Float64 axis nodes. The managed wire backend fetches per-axis
|
|
/// figures live via <c>cnc_getfigure</c>; the config knob is the per-axis fallback used
|
|
/// when the CNC reports no figure for an axis. These tests cover the config-supplied path.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class FocasPositionScalingTests
|
|
{
|
|
private const string Host = "focas://10.0.0.5:8193";
|
|
|
|
// A single-axis CNC (X only) so the snapshot we seed is unambiguous.
|
|
private static FakeFocasClient SingleAxisClient(FocasDynamicSnapshot snap)
|
|
{
|
|
var c = new FakeFocasClient();
|
|
c.AxisNames.Clear();
|
|
c.AxisNames.Add(new("X", ""));
|
|
c.DynamicByAxis[1] = snap; // FOCAS axis indexing is 1-based
|
|
return c;
|
|
}
|
|
|
|
private static FocasDriver NewDriver(int positionDecimalPlaces, FakeFocasClientFactory factory)
|
|
{
|
|
return new FocasDriver(new FocasDriverOptions
|
|
{
|
|
Devices = [new FocasDeviceOptions(Host, PositionDecimalPlaces: positionDecimalPlaces)],
|
|
Probe = new FocasProbeOptions { Enabled = false },
|
|
FixedTree = new FocasFixedTreeOptions
|
|
{
|
|
Enabled = true,
|
|
PollInterval = TimeSpan.FromMilliseconds(10),
|
|
},
|
|
}, "drv-1", factory);
|
|
}
|
|
|
|
// Drive the fixed-tree poll loop and wait (bounded) for the cached value to appear.
|
|
private static async Task<object?> ReadWhenPublishedAsync(FocasDriver drv, string reference)
|
|
{
|
|
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
|
|
while (DateTime.UtcNow < deadline)
|
|
{
|
|
var snaps = await drv.ReadAsync([reference], CancellationToken.None);
|
|
var snap = snaps.Single();
|
|
if (snap.StatusCode == FocasStatusMapper.Good && snap.Value is not null)
|
|
return snap.Value;
|
|
await Task.Delay(15);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// With <c>PositionDecimalPlaces: 3</c> a raw AbsolutePosition of 12345 scales to
|
|
/// 12.345 on the Float64 node (engineering units).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task PositionDecimalPlaces_3_scales_AbsolutePosition_by_thousand()
|
|
{
|
|
var snap = new FocasDynamicSnapshot(
|
|
AxisIndex: 1, AlarmFlags: 0, ProgramNumber: 0, MainProgramNumber: 0,
|
|
SequenceNumber: 0, ActualFeedRate: 250, ActualSpindleSpeed: 0,
|
|
AbsolutePosition: 12345, MachinePosition: 67890,
|
|
RelativePosition: 11111, DistanceToGo: 500);
|
|
var factory = new FakeFocasClientFactory { Customise = () => SingleAxisClient(snap) };
|
|
var drv = NewDriver(positionDecimalPlaces: 3, factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var abs = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/AbsolutePosition");
|
|
var mach = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/MachinePosition");
|
|
var rel = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/RelativePosition");
|
|
var dtg = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/DistanceToGo");
|
|
|
|
abs.ShouldBe(12.345);
|
|
mach.ShouldBe(67.890);
|
|
rel.ShouldBe(11.111);
|
|
dtg.ShouldBe(0.500);
|
|
|
|
await drv.ShutdownAsync(CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <c>PositionDecimalPlaces: 0</c> (the default) must be byte-identical to today's
|
|
/// behaviour: the integer is published unchanged, widened to double on the Float64
|
|
/// node (12345 → 12345.0).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task PositionDecimalPlaces_0_leaves_position_unscaled()
|
|
{
|
|
var snap = new FocasDynamicSnapshot(
|
|
AxisIndex: 1, AlarmFlags: 0, ProgramNumber: 0, MainProgramNumber: 0,
|
|
SequenceNumber: 0, ActualFeedRate: 250, ActualSpindleSpeed: 0,
|
|
AbsolutePosition: 12345, MachinePosition: 0,
|
|
RelativePosition: 0, DistanceToGo: 0);
|
|
var factory = new FakeFocasClientFactory { Customise = () => SingleAxisClient(snap) };
|
|
var drv = NewDriver(positionDecimalPlaces: 0, factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var abs = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/AbsolutePosition");
|
|
abs.ShouldBe(12345.0);
|
|
|
|
await drv.ShutdownAsync(CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// FeedRate (and other non-position fields) is NOT position-scaled — it surfaces
|
|
/// unchanged even when PositionDecimalPlaces is set.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task FeedRate_is_not_position_scaled()
|
|
{
|
|
var snap = new FocasDynamicSnapshot(
|
|
AxisIndex: 1, AlarmFlags: 0, ProgramNumber: 0, MainProgramNumber: 0,
|
|
SequenceNumber: 0, ActualFeedRate: 250, ActualSpindleSpeed: 0,
|
|
AbsolutePosition: 12345, MachinePosition: 0,
|
|
RelativePosition: 0, DistanceToGo: 0);
|
|
var factory = new FakeFocasClientFactory { Customise = () => SingleAxisClient(snap) };
|
|
var drv = NewDriver(positionDecimalPlaces: 3, factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
// Sanity: the position IS scaled on this same device.
|
|
(await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/AbsolutePosition")).ShouldBe(12.345);
|
|
// FeedRate must remain the raw 250, not 0.250.
|
|
var feed = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/FeedRate/Actual");
|
|
feed.ShouldBe(250.0);
|
|
|
|
await drv.ShutdownAsync(CancellationToken.None);
|
|
}
|
|
}
|