165 lines
6.6 KiB
C#
165 lines
6.6 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
|
|
|
/// <summary>
|
|
/// Phase 4b — per-axis auto-scale from <c>cnc_getfigure</c> figures. The driver fetches
|
|
/// per-axis position decimal-place figures at init and applies them at the
|
|
/// <see cref="FocasDriver"/> publish seam. Precedence: an auto figure for an axis WINS;
|
|
/// the configured <c>PositionDecimalPlaces</c> is the per-axis fallback when the CNC did
|
|
/// not report a figure for that axis. (The managed wire backend returns no figures today,
|
|
/// so the real backend always uses the manual fallback — these tests drive the Fake.)
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class FocasPositionAutoScaleTests
|
|
{
|
|
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, IReadOnlyList<int> figures)
|
|
{
|
|
var c = new FakeFocasClient();
|
|
c.AxisNames.Clear();
|
|
c.AxisNames.Add(new("X", ""));
|
|
c.DynamicByAxis[1] = snap; // FOCAS axis indexing is 1-based
|
|
c.PositionFigures = figures;
|
|
return c;
|
|
}
|
|
|
|
// A two-axis CNC (X, Y) — each axis gets its own seeded snapshot + figure.
|
|
private static FakeFocasClient TwoAxisClient(
|
|
FocasDynamicSnapshot xSnap, FocasDynamicSnapshot ySnap, IReadOnlyList<int> figures)
|
|
{
|
|
var c = new FakeFocasClient();
|
|
c.AxisNames.Clear();
|
|
c.AxisNames.Add(new("X", ""));
|
|
c.AxisNames.Add(new("Y", ""));
|
|
c.DynamicByAxis[1] = xSnap; // axis 0 → 1-based index 1
|
|
c.DynamicByAxis[2] = ySnap; // axis 1 → 1-based index 2
|
|
c.PositionFigures = figures;
|
|
return c;
|
|
}
|
|
|
|
private static FocasDynamicSnapshot AbsSnap(int axisIndex, int absolutePosition) =>
|
|
new(AxisIndex: axisIndex, AlarmFlags: 0, ProgramNumber: 0, MainProgramNumber: 0,
|
|
SequenceNumber: 0, ActualFeedRate: 0, ActualSpindleSpeed: 0,
|
|
AbsolutePosition: absolutePosition, MachinePosition: 0,
|
|
RelativePosition: 0, DistanceToGo: 0);
|
|
|
|
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>
|
|
/// An auto figure for an axis WINS over the configured manual value: figure 3 ⇒ ÷10^3
|
|
/// (12345 → 12.345), even though <c>PositionDecimalPlaces: 1</c> would have given ÷10.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Auto_figure_wins_over_manual_config()
|
|
{
|
|
var factory = new FakeFocasClientFactory
|
|
{
|
|
Customise = () => SingleAxisClient(AbsSnap(1, 12345), figures: [3]),
|
|
};
|
|
var drv = NewDriver(positionDecimalPlaces: 1, factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var abs = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/AbsolutePosition");
|
|
abs.ShouldBe(12.345); // ÷10^3 (auto), NOT 1234.5 (manual ÷10)
|
|
|
|
await drv.ShutdownAsync(CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// When <c>cnc_getfigure</c> reports nothing (empty list — the real wire backend's
|
|
/// behaviour), the driver falls back to the configured <c>PositionDecimalPlaces</c>:
|
|
/// manual 2 ⇒ ÷10^2 (12345 → 123.45).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Falls_back_to_manual_when_getfigure_empty()
|
|
{
|
|
var factory = new FakeFocasClientFactory
|
|
{
|
|
Customise = () => SingleAxisClient(AbsSnap(1, 12345), figures: []),
|
|
};
|
|
var drv = NewDriver(positionDecimalPlaces: 2, factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var abs = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/AbsolutePosition");
|
|
abs.ShouldBe(123.45); // ÷10^2 (manual fallback)
|
|
|
|
await drv.ShutdownAsync(CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Figures are applied per-axis independently: figures [3, 1] ⇒ axis 0 ÷10^3 and
|
|
/// axis 1 ÷10^1.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Per_axis_figures_scale_independently()
|
|
{
|
|
var factory = new FakeFocasClientFactory
|
|
{
|
|
Customise = () => TwoAxisClient(
|
|
AbsSnap(1, 12345), AbsSnap(2, 67890), figures: [3, 1]),
|
|
};
|
|
var drv = NewDriver(positionDecimalPlaces: 0, factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var x = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/AbsolutePosition");
|
|
var y = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/Y/AbsolutePosition");
|
|
x.ShouldBe(12.345); // ÷10^3
|
|
y.ShouldBe(6789.0); // ÷10^1
|
|
|
|
await drv.ShutdownAsync(CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The legacy manual-only path is unchanged: no auto figures + <c>PositionDecimalPlaces: 0</c>
|
|
/// ⇒ factor 1.0, so the raw integer is published widened to double (12345 → 12345.0).
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Manual_only_legacy_path_unchanged()
|
|
{
|
|
var factory = new FakeFocasClientFactory
|
|
{
|
|
Customise = () => SingleAxisClient(AbsSnap(1, 12345), figures: []),
|
|
};
|
|
var drv = NewDriver(positionDecimalPlaces: 0, factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var abs = await ReadWhenPublishedAsync(drv, $"{Host}/Axes/X/AbsolutePosition");
|
|
abs.ShouldBe(12345.0); // factor 1.0 — raw widened to double
|
|
|
|
await drv.ShutdownAsync(CancellationToken.None);
|
|
}
|
|
}
|