[education/kstars] /: Bugfix for meridian flips attempts that did not change the pier side

Wolfgang Reissenberger <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit fb4bee521a8504af7e46e0f141b30548620177d3 by Wolfgang Reissenberger.
Committed on 15/08/2026 at 07:32.
Pushed by wreissenberger into branch 'master'.

Bugfix for meridian flips attempts that did not change the pier side

In case a meridian flip does not change the pier side, a subsequent alignment could mess up the meridian flip state, which is fixed with these changes.

M  +4    -0    Tests/kstars_ui/test_ekos_meridianflip_base.cpp
M  +140  -0    Tests/kstars_ui/test_ekos_meridianflip_specials.cpp
M  +10   -0    Tests/kstars_ui/test_ekos_meridianflip_specials.h
M  +5    -1    kstars/ekos/manager/meridianflipstate.cpp

https://invent.kde.org/education/kstars/-/commit/fb4bee521a8504af7e46e0f141b30548620177d3

diff --git a/Tests/kstars_ui/test_ekos_meridianflip_base.cpp b/Tests/kstars_ui/test_ekos_meridianflip_base.cpp
index 14144a10f7..5ab7e9161b 100644
--- a/Tests/kstars_ui/test_ekos_meridianflip_base.cpp
+++ b/Tests/kstars_ui/test_ekos_meridianflip_base.cpp
@@ -48,6 +48,10 @@ bool TestEkosMeridianFlipBase::startEkosProfile()
 
 void TestEkosMeridianFlipBase::initTestCase()
 {
+    // determine once whether real astrometry solving can be tested (index files installed);
+    // used by test cases to QSKIP gracefully instead of failing when solving is unavailable
+    astrometry_available = m_CaptureHelper->isAstrometryAvailable();
+
     // ensure EKOS is running
     KVERIFY_EKOS_IS_HIDDEN();
     KTRY_OPEN_EKOS();
diff --git a/Tests/kstars_ui/test_ekos_meridianflip_specials.cpp b/Tests/kstars_ui/test_ekos_meridianflip_specials.cpp
index 81aaa9ce9f..737c720459 100644
--- a/Tests/kstars_ui/test_ekos_meridianflip_specials.cpp
+++ b/Tests/kstars_ui/test_ekos_meridianflip_specials.cpp
@@ -15,6 +15,8 @@
 #include "ekos/capture/capture.h"
 #include "ekos/focus/focusmodule.h"
 
+#include <QScopeGuard>
+
 TestEkosMeridianFlipSpecials::TestEkosMeridianFlipSpecials(QObject *parent) : TestEkosMeridianFlipBase(parent)
 {
 }
@@ -344,6 +346,139 @@ void TestEkosMeridianFlipSpecials::testSimpleRepeatedMF()
     indi_setprop->start(QString("indi_setprop"), {QString("-n"), QString("%1.FLIP_HA.FLIP_HA=%2").arg(m_CaptureHelper->m_MountDevice).arg(0)});
 }
 
+void TestEkosMeridianFlipSpecials::testCaptureAlignFailedMFRetry()
+{
+    if (!astrometry_available)
+        QSKIP("No astrometry files available to run test");
+
+    // set up the capture sequence - leave enough lead time for the initial alignment
+    // cycle below to complete before the mount reaches the meridian
+    QVERIFY(prepareCaptureTestcase(40, false, false));
+
+    // run one alignment cycle so that post-flip re-alignment gets triggered automatically
+    // (CameraState::setAlignState() sets resumeAlignmentAfterFlip to true on every align
+    // state change, so a single completed alignment before the flip is enough)
+    QVERIFY(executeAlignment(5.0));
+
+    // widen align's post-slew settle window so it doesn't race ahead to a second solve
+    // before the HA reset injected below has had a chance to take effect. Restored via
+    // scope guard so it resets even if a QVERIFY below fails and returns early.
+    KTRY_GADGET(Ekos::Manager::Instance()->alignModule(), QSpinBox, alignSettlingTime);
+    const int originalSettlingTime = alignSettlingTime->value();
+    alignSettlingTime->setValue(5000);
+    const auto settlingTimeGuard = qScopeGuard([alignSettlingTime, originalSettlingTime]
+    {
+        alignSettlingTime->setValue(originalSettlingTime);
+    });
+
+    // force the simulated mount to not actually change pier side on the upcoming flip slew,
+    // so that the flip "fails" and the 4-minute retry logic in MeridianFlipState kicks in.
+    // Reset via scope guard for the same reason as above.
+    const QString mountDevice = m_CaptureHelper->m_MountDevice;
+    QProcess *indi_setprop = new QProcess(this);
+    indi_setprop->start(QString("indi_setprop"), {QString("-n"), QString("%1.FLIP_HA.FLIP_HA=%2").arg(mountDevice).arg(0.5)});
+    const auto flipHaGuard = qScopeGuard([this, mountDevice]
+    {
+        QProcess *reset = new QProcess(this);
+        reset->start(QString("indi_setprop"), {QString("-n"), QString("%1.FLIP_HA.FLIP_HA=%2").arg(mountDevice).arg(0)});
+        reset->waitForFinished(5000);
+    });
+
+    // start capturing
+    QVERIFY(startCapturing());
+
+    // check that the (failed) meridian flip runs and "completes" (pier side unchanged)
+    // (39s: matches the 40s lead time set in prepareCaptureTestcase() above, minus the
+    // time already spent on the initial alignment cycle)
+    QVERIFY(checkMFExecuted(39));
+    KTRY_GADGET(Ekos::Manager::Instance()->mountModule(), QLabel, pierSideLabel);
+    QTRY_VERIFY(pierSideLabel->text() == "Pier Side: West (pointing East)");
+    qCInfo(KSTARS_EKOS_TEST()) << "First (failed) meridian flip completed, pier side unchanged as expected.";
+
+    // post-flip re-alignment should start right away despite the failed flip
+    QTRY_VERIFY_WITH_TIMEOUT(m_CaptureHelper->getCaptureStatus() == Ekos::CAPTURE_ALIGNING, 15000);
+    qCInfo(KSTARS_EKOS_TEST()) << "Post-flip re-alignment started after the failed flip.";
+
+    // Between reaching Tracking (above, from the failed flip) and align's own correction
+    // slew, re-sync the simulated mount just PAST the (freshly calculated) meridian again
+    // (negative secsToMF -> target.ra = LST - |delta| -> ha = +|delta|). On real mounts the
+    // hour angle keeps advancing naturally while align captures and solves; the telescope
+    // simulator does not reproduce that drift on its own, so it has to be emulated
+    // explicitly here to recreate the same situation as on real hardware.
+    // Note: fast=false on purpose - the fast=true path adds a fixed +0.002h (~7.2s) pre-
+    // meridian pad meant for initial rough positioning, which would swamp the small delta
+    // used here and push ha negative again.
+    findMFTestTarget(-5, false);
+    Ekos::Manager::Instance()->mountModule()->sync(target->ra().Hours(), target->dec().Degrees());
+    qCInfo(KSTARS_EKOS_TEST()) << "Re-synced just past the meridian before align's correction slew.";
+
+    // Continuously watch for the actual bug condition from here on: MeridianFlipState
+    // re-arming a flip (leaving MOUNT_FLIP_NONE) while Capture is still mid post-flip
+    // re-alignment (CAPTURE_ALIGNING). A single point-in-time check after the fact is
+    // unreliable - the erroneous re-arm and its consequent (fast, since flipDelayHrs was
+    // wiped to 0) flip execution can both finish before any single QVERIFY below gets a
+    // chance to observe the intermediate state - so this is recorded live via a direct
+    // signal connection instead of polled after the fact.
+    bool prematureFlipDetected = false;
+    const auto mfState = Ekos::Manager::Instance()->mountModule()->getMeridianFlipState();
+    const auto prematureFlipConnection = connect(mfState.get(), &Ekos::MeridianFlipState::newMountMFStatus, this,
+                                         [&](Ekos::MeridianFlipState::MeridianFlipMountState status)
+    {
+        if (m_CaptureHelper->getCaptureStatus() == Ekos::CAPTURE_ALIGNING &&
+                status != Ekos::MeridianFlipState::MOUNT_FLIP_NONE)
+            prematureFlipDetected = true;
+    });
+    const auto prematureFlipGuard = qScopeGuard([prematureFlipConnection]
+    {
+        QObject::disconnect(prematureFlipConnection);
+    });
+
+    // Force a real, sizeable correction slew: nudge the mount off the synced position via a
+    // manual motion command (TELESCOPE_MOTION_NS), so align's next solve finds a real position
+    // error and issues its own genuine coordinate slew to correct it - as opposed to relying
+    // on align's solve noise alone, which may leave too small an offset for a reliably
+    // observable slew (see the commented-out attempt in startScheduler() above: "slewing
+    // detection unsure since the position is close to the target").
+    m_CaptureHelper->expectedMountStates.append(ISD::Mount::MOUNT_SLEWING);
+    m_CaptureHelper->expectedMountStates.append(ISD::Mount::MOUNT_TRACKING);
+    Ekos::Manager::Instance()->mountModule()->motionCommand(ISD::Mount::MOTION_START, ISD::Mount::MOTION_NORTH, -1);
+    QTest::qWait(2000);
+    Ekos::Manager::Instance()->mountModule()->motionCommand(ISD::Mount::MOTION_STOP, ISD::Mount::MOTION_NORTH, -1);
+    qCInfo(KSTARS_EKOS_TEST()) << "Nudged the mount off target to force a real correction slew.";
+
+    // Verify align's correction slew actually passed through MOUNT_SLEWING and MOUNT_TRACKING
+    // in that order - otherwise a PASS below would be meaningless (it could just mean the
+    // trigger never occurred, not that the bug is fixed).
+    KVERIFY_EMPTY_QUEUE_WITH_TIMEOUT(m_CaptureHelper->expectedMountStates, 60000);
+    qCInfo(KSTARS_EKOS_TEST()) << "Confirmed mount passed through MOUNT_SLEWING and MOUNT_TRACKING.";
+
+    // Let alignment run through its solve-and-correct cycle(s), triggered by the nudge above,
+    // until post-flip re-alignment is done (capture leaves CAPTURE_ALIGNING). This covers the
+    // window in which MeridianFlipState::updateTelescopeCoord() may incorrectly reset the
+    // pending 4-minute retry delay (flipDelayHrs) as a side effect of align's own correction
+    // slew finishing, because it only checks meridianFlipMountState (which already dropped
+    // back to MOUNT_FLIP_NONE) instead of meridianFlipStage / checkMeridianFlipActive()
+    // (which is still MF_ALIGNING throughout this window).
+    QTRY_VERIFY_WITH_TIMEOUT(m_CaptureHelper->getCaptureStatus() != Ekos::CAPTURE_ALIGNING, 60000);
+    qCInfo(KSTARS_EKOS_TEST()) << "Post-flip re-alignment (including align's correction slew) completed.";
+
+    // Stop watching now that re-alignment is done - the retry delay must not have been wiped
+    // out while it was still running: a meridian flip must not have been (re-)armed this
+    // early, it is only due after ~4 minutes.
+    QObject::disconnect(prematureFlipConnection);
+    QVERIFY2(!prematureFlipDetected,
+             "Meridian flip was (re-)armed while post-flip alignment was still running - "
+             "the pending retry delay was reset too early.");
+
+    // it should still complete correctly after the full ~4 minute delay
+    m_CaptureHelper->expectedMeridianFlipStates.enqueue(Ekos::MeridianFlipState::MOUNT_FLIP_PLANNED);
+    m_CaptureHelper->expectedMeridianFlipStates.enqueue(Ekos::MeridianFlipState::MOUNT_FLIP_RUNNING);
+    QVERIFY(checkMFExecuted(4 * 60 + 30));
+
+    // FLIP_HA and alignSettlingTime are restored automatically by the scope guards above,
+    // regardless of whether this point is reached normally or a QVERIFY above failed.
+}
+
 void TestEkosMeridianFlipSpecials::testCaptureRealignMF()
 {
     if (!astrometry_available)
@@ -461,6 +596,11 @@ void TestEkosMeridianFlipSpecials::testSimpleRepeatedMF_data()
     prepareTestData(18.0, {"Greenwich"}, {true}, {{"Luminance", 6}}, {0}, {false}, {false});
 }
 
+void TestEkosMeridianFlipSpecials::testCaptureAlignFailedMFRetry_data()
+{
+    prepareTestData(18.0, {"Greenwich"}, {true}, {{"Luminance", 6}}, {0}, {false}, {false});
+}
+
 void TestEkosMeridianFlipSpecials::testCaptureRealignMF_data()
 {
     prepareTestData(18.0, {"Greenwich"}, {true}, {{"Luminance", 6}}, {0}, {false}, {false});
diff --git a/Tests/kstars_ui/test_ekos_meridianflip_specials.h b/Tests/kstars_ui/test_ekos_meridianflip_specials.h
index 74bf3b9b94..1b0a2317ab 100644
--- a/Tests/kstars_ui/test_ekos_meridianflip_specials.h
+++ b/Tests/kstars_ui/test_ekos_meridianflip_specials.h
@@ -89,6 +89,16 @@ class TestEkosMeridianFlipSpecials : public TestEkosMeridianFlipBase
         /** @brief Test data for @see testSimpleRepeatedMF() */
         void testSimpleRepeatedMF_data();
 
+        /** @brief Test the situation where the mount does not change its pier side (failed flip) while
+         * alignment is active. Post-flip re-alignment starts immediately despite the failed flip and its
+         * own correction slew must not prematurely reset the pending 4-minute retry delay for the next
+         * flip attempt (regression test).
+         * */
+        void testCaptureAlignFailedMFRetry();
+
+        /** @brief Test data for @see testCaptureAlignFailedMFRetry() */
+        void testCaptureAlignFailedMFRetry_data();
+
         /** @brief Test if a re-alignment is aborted during a meridian flip and post flip actions are executed.
          */
         void testCaptureRealignMF();
diff --git a/kstars/ekos/manager/meridianflipstate.cpp b/kstars/ekos/manager/meridianflipstate.cpp
index a884c1dba6..51df561a50 100644
--- a/kstars/ekos/manager/meridianflipstate.cpp
+++ b/kstars/ekos/manager/meridianflipstate.cpp
@@ -528,7 +528,11 @@ void MeridianFlipState::updateTelescopeCoord(const SkyPoint &position, ISD::Moun
     if (m_MountStatus == ISD::Mount::MOUNT_TRACKING && m_PrevMountStatus == ISD::Mount::MOUNT_SLEWING
             && isEnabled())
     {
-        if (meridianFlipMountState == MOUNT_FLIP_NONE)
+        // Only clear a pending retry delay once the whole meridian flip process (including any
+        // post-flip re-alignment / re-guiding) is done - otherwise a Slewing->Tracking transition
+        // caused by Align's own correction slew during MF_ALIGNING would wipe out the delay set
+        // for the next flip attempt after a failed (pier side unchanged) flip.
+        if (meridianFlipMountState == MOUNT_FLIP_NONE && !checkMeridianFlipActive())
         {
             setFlipDelayHrs(0);
         }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.