Skip to content
Open
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
86 changes: 83 additions & 3 deletions PolarAlignment/AutomatedAdjustmentController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,23 @@ internal sealed class AutomatedAdjustmentController {
/// </summary>
private const double MinimumMoveMagnitude = 0.05;
/// <summary>
/// Maximum correction magnitude issued in a single solve or move cycle.
/// Large residuals are intentionally corrected over multiple iterations.
/// Absolute emergency ceiling on any single move, regardless of what the learned
/// response model predicts. This exists purely as a backstop against a badly wrong or
/// still-unconverged model (e.g. a near-singular fit early on) proposing an absurd
/// command — it is intentionally far above any move that should occur in normal
/// operation.
///
/// The REAL, proportional limiting factor is the 0.5/0.25/0.125 damping already applied
/// to the raw least-squares solution in <see cref="CreateCorrectivePlan"/>: since that
/// raw solution already predicts (given an accurate model) a command that fully zeroes
/// the current residual, a candidate at 50% of it is inherently proportional to the
/// current error — large errors produce large (but damped) corrections, small errors
/// produce small ones. A low fixed ceiling here (previously 5.0, ≈5 arcmin at gear ratio
/// 1) defeated that proportionality and forced many more solve/move cycles than
/// necessary for large initial residuals, even though hardware like OAT can safely move
/// several degrees in one command.
/// </summary>
private const double MaximumMoveMagnitude = 5.0;
private const double MaximumMoveMagnitude = 240.0; // ≈4° at gear ratio 1.0 — emergency backstop only
/// <summary>
/// Small damping term used as a numerical floor and as regularization when
/// inverting the local response model.
Expand All @@ -47,13 +60,42 @@ internal sealed class AutomatedAdjustmentController {
/// </summary>
private const int MaxSamples = 12;

/// <summary>
/// Below this measured total-error change (in degrees), a commanded move that was
/// reported as electrically/protocol-successful is treated as having produced no
/// detectable physical motion. This is deliberately small — well under any sane
/// single-axis move at any reasonable gear ratio — so it only fires on genuine
/// non-movement, not on a merely small correction.
/// </summary>
private const double StallMovementThresholdDegrees = 0.0015; // ≈0.1 arcmin

/// <summary>
/// Number of consecutive "commanded a real move but measured essentially zero change"
/// events required before declaring a stall. Requiring several in a row avoids false
/// positives from a single noisy/failed plate solve.
/// </summary>
private const int MaxConsecutiveStalls = 3;

private readonly Queue<ResponseSample> samples = new Queue<ResponseSample>();
private AutomatedAdjustmentObservation currentObservation;
private PendingPlan pendingPlan;
private bool hasObservation;
private int consecutiveStalls;

public int SampleCount => samples.Count;

/// <summary>
/// True once several consecutive commanded moves have produced no measurable change in
/// the observed polar error — the physical signature of a motor stuck at an end-stop (or
/// otherwise mechanically unable to move), even though the driver/INDI layer reported the
/// command as having completed successfully. Callers should stop issuing further moves
/// when this is set.
/// </summary>
public bool StallDetected { get; private set; }

/// <summary>Human-readable explanation of the last detected stall, for logging/notification.</summary>
public string StallReason { get; private set; }

/// <summary>
/// Gets whether the current sample set is rich enough and well-conditioned enough
/// to estimate a two-axis local response model.
Expand All @@ -70,6 +112,9 @@ public void Reset() {
currentObservation = null;
pendingPlan = null;
hasObservation = false;
consecutiveStalls = 0;
StallDetected = false;
StallReason = null;
}

/// <summary>
Expand All @@ -89,6 +134,8 @@ public void UpdateObservation(double azimuthErrorDegrees, double altitudeErrorDe
deltaAzimuth,
deltaAltitude));

CheckForStall(pendingPlan.Plan, Math.Sqrt(deltaAzimuth * deltaAzimuth + deltaAltitude * deltaAltitude));

if (!pendingPlan.Plan.IsProbe && latestObservation.TotalErrorDegrees > pendingPlan.BeforeMoveObservation.TotalErrorDegrees * ModelResetWorseningFactor) {
samples.Clear();
}
Expand All @@ -108,6 +155,10 @@ public void UpdateObservation(double azimuthErrorDegrees, double altitudeErrorDe
/// move that is predicted to reduce the residual error norm.
/// </summary>
public AutomatedAdjustmentPlan CreatePlan() {
if (StallDetected) {
return AutomatedAdjustmentPlan.Skip(StallReason ?? "Mount stall detected — automated adjustments paused.");
}

if (!hasObservation) {
return AutomatedAdjustmentPlan.Skip("No continuous error measurement is available yet.");
}
Expand Down Expand Up @@ -142,6 +193,35 @@ public void NoteFailedExecution() {
pendingPlan = null;
}

/// <summary>
/// Compares a commanded move against the measured error change it produced. A real move
/// (above the deadband) that yields essentially zero measured change, repeated several
/// times in a row, is the observable signature of a motor that physically cannot move
/// (end-stop reached, mechanical jam, disconnected coupler, etc.) — the driver can report
/// the command as "done" without the mount having actually moved.
/// </summary>
private void CheckForStall(AutomatedAdjustmentPlan commandedPlan, double measuredChangeDegrees) {
var commandedMagnitude = Math.Sqrt(commandedPlan.XMagnitude * commandedPlan.XMagnitude
+ commandedPlan.YMagnitude * commandedPlan.YMagnitude);

if (commandedMagnitude < MinimumMoveMagnitude) {
// Command was itself negligible — not a meaningful test of whether the mount moves.
return;
}

if (measuredChangeDegrees < StallMovementThresholdDegrees) {
consecutiveStalls++;
if (consecutiveStalls >= MaxConsecutiveStalls) {
StallDetected = true;
StallReason = $"Commanded {commandedMagnitude:F2} units but measured error changed by only " +
$"{measuredChangeDegrees * 60.0:F3}' over {consecutiveStalls} consecutive moves — " +
"the mount does not appear to be responding (possible end-stop or mechanical stall).";
}
} else {
consecutiveStalls = 0;
}
}

private void AddSample(ResponseSample sample) {
samples.Enqueue(sample);
while (samples.Count > MaxSamples) {
Expand Down
3 changes: 2 additions & 1 deletion PolarAlignment/IPolarAlignmentSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ namespace NINA.Plugins.PolarAlignment {
public enum PolarAlignmentSystemType {
None,
UPAS,
OAPA
OAPA,
OAT // OpenAstroExplorer via INDI LX200 OpenAstroTech driver
}

public enum Axis {
Expand Down
8 changes: 8 additions & 0 deletions PolarAlignment/Instructions/PolarAlignment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,14 @@ await messageBroker.Publish(
}
localCTS.Token.ThrowIfCancellationRequested();
await TPAPAVM.MoveCloser(progress, localCTS.Token);

if (TPAPAVM.AutomatedAdjustmentStalled) {
Logger.Error($"Automated polar alignment adjustment aborted: {TPAPAVM.AutomatedAdjustmentStallReason}");
Notification.ShowError(
$"Polar alignment automated adjustments stopped: the mount does not appear to be responding to move commands.{Environment.NewLine}" +
"Check for a mechanical end-stop, disconnected motor, or driver fault, then restart the process.");
localCTS.Cancel();
}
} else {
Logger.Warning("Skipping error publication and automated correction because the continuous estimate was unstable.");
}
Expand Down
17 changes: 9 additions & 8 deletions PolarAlignment/NINA.Plugins.PolarAlignment.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<TargetFrameworks>net10.0</TargetFrameworks>
<OutputType>Library</OutputType>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<UseWPF Condition="!$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">true</UseWPF>
<ImportWindowsDesktopTargets Condition="!$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">true</ImportWindowsDesktopTargets>
<EnableWindowsTargeting Condition="!$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">true</EnableWindowsTargeting>
<UseWPF Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">true</UseWPF>
<ImportWindowsDesktopTargets Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">true</ImportWindowsDesktopTargets>
<EnableWindowsTargeting Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">true</EnableWindowsTargeting>
<AssemblyTitle>Three Point Polar Alignment</AssemblyTitle>
<Title>Three Point Polar Alignment</Title>
<Authors>Stefan Berg @isbeorn</Authors>
Expand All @@ -15,16 +15,17 @@
<DefaultItemExcludes Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">$(DefaultItemExcludes);**/*.xaml.cs;**/*.xaml</DefaultItemExcludes>
<Version>2.2.6.3</Version>
</PropertyGroup>
<ItemGroup Condition="!$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">
<PackageReference Include="NINA.Plugin" Version="3.1.2.9001" />
<ItemGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">
<PackageReference Include="NINA.Plugin" Version="3.1.2.9001" />
</ItemGroup>
<ItemGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">
<ItemGroup Condition="!$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">
<ProjectReference Include="..\..\..\NINA.Astrometry\NINA.Astrometry.csproj" />
<ProjectReference Include="..\..\..\NINA.Plugin\NINA.Plugin.csproj" />
<ProjectReference Include="..\..\..\NINA.Core\NINA.Core.csproj" />
<ProjectReference Include="..\..\..\NINA.Equipment\NINA.Equipment.csproj" />
<ProjectReference Include="..\..\..\NINA.Image\NINA.Image.csproj" />
<ProjectReference Include="..\..\..\NINA.Profile\NINA.Profile.csproj" />
<ProjectReference Include="..\..\..\NINA.INDI\NINA.INDI.csproj" />
<ProjectReference Include="..\..\..\System.Windows.Compat\System.Windows.Compat.csproj" />
<PackageReference Include="System.Drawing.Common" Version="*" ExcludeAssets="all" />
</ItemGroup>
Expand All @@ -41,10 +42,10 @@
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="!$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">
<Exec IgnoreExitCode="true" Command="if $(ConfigurationName) == Debug (&#xD;&#xA; &#xD;&#xA;if not exist &quot;%25localappdata%25\NINA\Plugins&quot; (&#xD;&#xA; echo &quot;Creating Plugins %25localappdata%25\NINA\Plugins folder&quot;&#xD;&#xA; mkdir &quot;%25localappdata%25\NINA\Plugins&quot;&#xD;&#xA;)&#xD;&#xA;&#xD;&#xA;if exist &quot;%25localappdata%25\NINA\Plugins\3.0.0\Three Point Polar Alignment&quot; (&#xD;&#xA; echo &quot; folder cleanup&quot;&#xD;&#xA; rmdir &quot;%25localappdata%25\NINA\Plugins\3.0.0\Three Point Polar Alignment&quot; /S /Q&#xD;&#xA;)&#xD;&#xA;&#xD;&#xA;if not exist &quot;%25localappdata%25\NINA\Plugins\3.0.0\Three Point Polar Alignment&quot; (&#xD;&#xA; echo &quot;Creating %25localappdata%25\NINA\Plugin\Three Point Polar Alignment folder&quot;&#xD;&#xA; mkdir &quot;%25localappdata%25\NINA\Plugins\3.0.0\Three Point Polar Alignment&quot;&#xD;&#xA;)&#xD;&#xA;echo &quot;Copying $(PlatformName) $(TargetFileName)&quot;&#xD;&#xA;copy &quot;$(TargetDir)$(TargetFileName)&quot; &quot;%25localappdata%25\NINA\Plugins\3.0.0\Three Point Polar Alignment\$(TargetName)$(TargetExt)&quot; /Y&#xD;&#xA;&#xD;&#xA;&#xD;&#xA;)" />
</Target>
<Target Name="PostBuildLinux" AfterTargets="PostBuildEvent" Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">
<Target Name="PostBuildLinux" AfterTargets="PostBuildEvent" Condition="!$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">
<Exec Command="mkdir -p $HOME/.local/share/NINA/Plugins/3.0.0/Three\ Point\ Polar\ Alignment" />
<Exec Command="cp $(TargetPath) $HOME/.local/share/NINA/Plugins/3.0.0/Three\ Point\ Polar\ Alignment/$(TargetName)$(TargetExt)" />
</Target>
Expand Down
Loading