Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/ModelingEvolution.GenericAxis.Plugin.Tests/DeclarationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public void The_Axis_Table_Declares_The_Designed_Suffixes_Types_And_Defaults()
("DefaultSpeedPercent", "double", "100"),
("Acceleration", "double", null),
("Tolerance", "double", "0.001"),
("RestTolerance", "double", "0.1"),
("HomingTimeoutSeconds", "int", "120"));
}

Expand All @@ -113,4 +114,19 @@ public void Labels_Carry_The_Units_Of_The_Axis_Kind_And_Headings_Carry_The_Axis_
.Should().Be("Status block base (input registers)", "ADR-36: the status block is input registers");
turntable.Group.Should().Be("Turntable — Machine limits");
}

[Fact]
public void The_Rest_Tolerance_Row_Is_Labelled_In_The_Axis_Unit_Under_Motion()
{
var carriage = PluginHarness.Track.Axes.Single().PropertySchemas
.Single(s => s.Name == PluginHarness.CarriageKey("RestTolerance"));
var turntable = PluginHarness.Positioner.Axes.Single().PropertySchemas
.Single(s => s.Name == PluginHarness.TurntableKey("RestTolerance"));

carriage.DisplayName.Should().Be("Rest tolerance (mm) — allowed change between two capture readings");
turntable.DisplayName.Should().Be("Rest tolerance (°) — allowed change between two capture readings");
carriage.Group.Should().Be("Carriage — Motion");
carriage.Required.Should().BeFalse();
turntable.Default.Should().Be("0.1");
}
}
27 changes: 27 additions & 0 deletions src/ModelingEvolution.GenericAxis.Plugin.Tests/FactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public void GA_U_44_Invalid_Device_Values_Are_Refused_Naming_The_Key(string key,
[InlineData("DefaultSpeedPercent", "0")]
[InlineData("DefaultSpeedPercent", "150")]
[InlineData("Tolerance", "0")]
[InlineData("RestTolerance", "-0.1")]
[InlineData("HomingTimeoutSeconds", "99999999999")]
[InlineData("StatusBase", "65530")] // S+14 past input register 65535
[InlineData("CommandBase", "65530")] // C+11 past holding register 65535
Expand Down Expand Up @@ -128,6 +129,32 @@ public void The_Positioner_Factory_Builds_An_IPositioner_With_The_Turntable_Axis
device.OwnerId.Should().Be((ushort)GenericAxisOwnerIdProperty.Default);
}

[Fact]
public void The_Carriage_Reports_Its_Rest_Tolerance_Through_The_Sdk_Leaf()
{
var stored = PluginHarness.Build(PluginHarness.Track, PluginHarness.ConfigFor(PluginHarness.Track,
(PluginHarness.CarriageKey("Host"), UnreachableHost),
(PluginHarness.CarriageKey("RestTolerance"), "0.25")));
var absent = PluginHarness.Build(PluginHarness.Track, PluginHarness.ConfigFor(PluginHarness.Track,
(PluginHarness.CarriageKey("Host"), UnreachableHost)));

((ILinearAxis)stored["carriage"]).RestTolerance.Value.Should().Be(0.25);
((ILinearAxis)absent["carriage"]).RestTolerance.Value.Should().Be(0.1);
}

[Fact]
public void The_Turntable_Reports_Its_Rest_Tolerance_Through_The_Sdk_Leaf()
{
var stored = PluginHarness.Build(PluginHarness.Positioner, PluginHarness.ConfigFor(PluginHarness.Positioner,
(PluginHarness.TurntableKey("Host"), UnreachableHost),
(PluginHarness.TurntableKey("RestTolerance"), "0")));
var absent = PluginHarness.Build(PluginHarness.Positioner, PluginHarness.ConfigFor(PluginHarness.Positioner,
(PluginHarness.TurntableKey("Host"), UnreachableHost)));

((double)((IRotaryAxis)stored["turntable"]).RestTolerance).Should().Be(0);
((double)((IRotaryAxis)absent["turntable"]).RestTolerance).Should().Be(0.1);
}

[Fact]
public void A_Rebuild_Replaces_The_Tracked_Instance()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ private static readonly (string Suffix, string Raw, Func<GenericAxisOptions, obj
("DefaultSpeedPercent", "20", o => o.DefaultSpeed, new Percentage(20)),
("Acceleration", "1500", o => o.Acceleration, 1500.0),
("Tolerance", "0.05", o => o.Tolerance, 0.05),
("RestTolerance", "0.25", o => o.RestTolerance, 0.25),
("HomingTimeoutSeconds", "90", o => o.HomingTimeout, TimeSpan.FromSeconds(90)),
];

Expand Down Expand Up @@ -74,6 +75,27 @@ public void An_Absent_Value_Keeps_The_Driver_Default()
options.Should().Be(GenericAxisPlugin.CarriageTemplate with { Host = "10.0.0.5" });
}

[Theory]
[InlineData("0", 0.0)]
[InlineData("0.25", 0.25)]
public void RestTolerance_Stored_LandsInTheOptions(string raw, double expected)
{
var options = GenericAxisProperties.Bind(GenericAxisPlugin.CarriageTemplate,
Stored([("Host", "h"), ("RestTolerance", raw)]));

options.RestTolerance.Should().Be(expected);
}

[Fact]
public void RestTolerance_Negative_IsRefusedByValidate_NamingTheHubKey()
{
var bind = () => GenericAxisProperties.Bind(GenericAxisPlugin.CarriageTemplate,
Stored([("Host", "h"), ("RestTolerance", "-0.1")]));

bind.Should().Throw<ArgumentException>()
.WithMessage($"'{PluginHarness.CarriageKey("RestTolerance")}' refused by GenericAxisOptions.Validate:*");
}

[Fact]
public void The_Frozen_Identity_Is_Not_Bindable()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ internal sealed record AxisProperty(
o => Num(o.Tolerance),
(o, v) => o with { Tolerance = ParseDouble(v) }),

new("RestTolerance", nameof(GenericAxisOptions.RestTolerance),
k => $"Rest tolerance ({Unit(k)}) — allowed change between two capture readings", "double", GroupMotion,
Required: false,
o => Num(o.RestTolerance),
(o, v) => o with { RestTolerance = ParseDouble(v) }),

new("HomingTimeoutSeconds", nameof(GenericAxisOptions.HomingTimeout),
_ => "Homing budget (s)", "int", GroupSupervision, Required: false,
o => Int((int)o.HomingTimeout.TotalSeconds),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
<!-- The host does NOT reference this project: rw2's PluginLoader discovers it from the staged
plugins/<id>/<version>/ folder. Every host-facing reference is therefore a PackageReference. -->
<ItemGroup>
<PackageReference Include="RocketWelder.SDK.Abstractions" Version="2.30.0" />
<PackageReference Include="RocketWelder.SDK.Automation.Abstractions" Version="2.30.0" />
<PackageReference Include="RocketWelder.SDK.Devices.Motion" Version="2.30.0" />
<PackageReference Include="RocketWelder.SDK.Abstractions" Version="3.1.0" />
<PackageReference Include="RocketWelder.SDK.Automation.Abstractions" Version="3.1.0" />
<PackageReference Include="RocketWelder.SDK.Devices.Motion" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.8" />
</ItemGroup>
Expand Down
13 changes: 13 additions & 0 deletions src/ModelingEvolution.GenericAxis.Tests/OptionsAndHygieneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ public class OptionsAndHygieneTests
{ nameof(GenericAxisOptions.DefaultSpeed), o => o with { DefaultSpeed = new Percentage(0f) } },
{ nameof(GenericAxisOptions.Acceleration), o => o with { Acceleration = 0 } },
{ nameof(GenericAxisOptions.Tolerance), o => o with { Tolerance = 0 } },
{ nameof(GenericAxisOptions.Tolerance), o => o with { Tolerance = double.PositiveInfinity } },
{ nameof(GenericAxisOptions.RestTolerance), o => o with { RestTolerance = -0.1 } },
{ nameof(GenericAxisOptions.RestTolerance), o => o with { RestTolerance = double.NaN } },
{ nameof(GenericAxisOptions.RestTolerance), o => o with { RestTolerance = double.PositiveInfinity } },
{ nameof(GenericAxisOptions.EnableTimeout), o => o with { EnableTimeout = TimeSpan.Zero } },
{ nameof(GenericAxisOptions.StopTimeout), o => o with { StopTimeout = TimeSpan.Zero } },
{ nameof(GenericAxisOptions.HomingTimeout), o => o with { HomingTimeout = TimeSpan.Zero } },
Expand All @@ -42,6 +46,14 @@ public void Validate_InvalidValue_ArgumentExceptionNamesField(string field, Func
spoil(Valid).Invoking(o => o.Validate()).Should().Throw<ArgumentException>().Which.ParamName.Should().Be(field);
}

[Theory(DisplayName = "GA-U-41 a rest tolerance of 0 (exact equality) or above is accepted (bug-034)")]
[InlineData(0.0)]
[InlineData(0.25)]
public void Validate_RestToleranceZeroOrPositive_IsAccepted(double restTolerance)
{
(Valid with { RestTolerance = restTolerance }).Invoking(o => o.Validate()).Should().NotThrow();
}

[Fact(DisplayName = "GA-U-41 the defaults are the protocol's and the design's")]
public void Defaults_AreNamedSources()
{
Expand All @@ -56,6 +68,7 @@ public void Defaults_AreNamedSources()
Valid.DefaultSpeed.Value.Should().Be(100f);
Valid.Acceleration.Should().BeNull();
Valid.Tolerance.Should().Be(0.001);
Valid.RestTolerance.Should().Be(0.1, "bug-034 owner ruling 2026-10-19");
Valid.EnableTimeout.Should().Be(TimeSpan.FromSeconds(5));
Valid.StopTimeout.Should().Be(TimeSpan.FromSeconds(5));
Valid.HomingTimeout.Should().Be(TimeSpan.FromSeconds(120));
Expand Down
23 changes: 21 additions & 2 deletions src/ModelingEvolution.GenericAxis/GenericAxisOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ public sealed record GenericAxisOptions
/// <summary>ADR-19: one register quantum.</summary>
public const double DefaultTolerance = Words.Quantum;

/// <summary>
/// bug-034 owner ruling 2026-10-19: a capture may see the reported position change by up to 0.1 (mm or °)
/// between its two readings at standstill — a servo in position hold moves in its last digits. Matches the
/// SDK's <c>ILinearAxis</c>/<c>IRotaryAxis.RestTolerance</c> default.
/// </summary>
public const double DefaultRestTolerance = 0.1;

/// <summary>ADR-20: enable/disable and reset confirmation budget.</summary>
public static readonly TimeSpan DefaultEnableTimeout = TimeSpan.FromSeconds(5);

Expand Down Expand Up @@ -93,6 +100,14 @@ public sealed record GenericAxisOptions
/// <summary>Reported in-position tolerance. The PLC's <c>InPosition</c> decides arrival.</summary>
public double Tolerance { get; init; } = DefaultTolerance;

/// <summary>
/// How much the reported position may change between two readings taken at standstill before the host
/// refuses a capture, in the axis's unit; reported as the SDK's <c>RestTolerance</c>. A host-side reading
/// rule, not a register. Finite and ≥ 0; 0 = exact equality (a drive that holds a quantised register).
/// Default <see cref="DefaultRestTolerance"/> (bug-034 owner ruling 2026-10-19).
/// </summary>
public double RestTolerance { get; init; } = DefaultRestTolerance;

/// <summary>Budget for Enable/Disable and Reset to show their state.</summary>
public TimeSpan EnableTimeout { get; init; } = DefaultEnableTimeout;

Expand Down Expand Up @@ -160,8 +175,12 @@ public void Validate()
throw new ArgumentException($"{Name}: DefaultSpeed must be above 0 %", nameof(DefaultSpeed));
if (Acceleration is { } a && a <= 0)
throw new ArgumentException($"{Name}: Acceleration {a} must be > 0 when set", nameof(Acceleration));
if (Tolerance <= 0)
throw new ArgumentException($"{Name}: Tolerance {Tolerance} must be > 0", nameof(Tolerance));
if (!double.IsFinite(Tolerance) || Tolerance <= 0)
throw new ArgumentException($"{Name}: Tolerance {Tolerance} must be a finite number > 0", nameof(Tolerance));
if (!double.IsFinite(RestTolerance) || RestTolerance < 0)
throw new ArgumentException(
$"{Name}: RestTolerance {RestTolerance} must be a finite number ≥ 0 (0 = exact equality)",
nameof(RestTolerance));
if (EnableTimeout <= TimeSpan.Zero)
throw new ArgumentException($"{Name}: EnableTimeout must be > 0", nameof(EnableTimeout));
if (StopTimeout <= TimeSpan.Zero)
Expand Down
3 changes: 3 additions & 0 deletions src/ModelingEvolution.GenericAxis/ModbusLinearAxis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ internal ModbusLinearAxis(AxisEngine engine)
/// <inheritdoc/>
public Length<double, Millimetre<double>> Tolerance => new(_engine.Options.Tolerance);

/// <inheritdoc/>
public Length<double, Millimetre<double>> RestTolerance => new(_engine.Options.RestTolerance);

/// <inheritdoc/>
public Speed<double, MillimetrePerSecond<double>> MinSpeed => new(Words.Quantum);

Expand Down
3 changes: 3 additions & 0 deletions src/ModelingEvolution.GenericAxis/ModbusRotaryAxis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ internal ModbusRotaryAxis(AxisEngine engine)
/// <inheritdoc/>
public Degree<double> Tolerance => Degree<double>.Create(_engine.Options.Tolerance);

/// <inheritdoc/>
public Degree<double> RestTolerance => Degree<double>.Create(_engine.Options.RestTolerance);

/// <inheritdoc/>
public AngularSpeed<double, DegreePerSecond<double>> MinSpeed => new(Words.Quantum);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="RocketWelder.SDK.Devices.Motion" Version="2.30.0" />
<PackageReference Include="RocketWelder.SDK.Devices.Motion" Version="3.1.0" />
<PackageReference Include="FluentModbus" Version="5.3.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
</ItemGroup>
Expand Down
Loading