[education/kstars] kstars/ekos/guide: Guide: Always calibrate single-frame; stream only while guiding
Jasem Mutlaq <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit f23d23a8141a0bf48e097dd54fa9880b0d746605 by Jasem Mutlaq, on behalf of Andreas R..
Committed on 06/08/2026 at 12:24.
Pushed by mutlaqja into branch 'master'.
Guide: Always calibrate single-frame; stream only while guiding
# Guide: Always calibrate single-frame; stream only while guiding
## Summary
In streaming guide mode, guide **calibration** produces erratic results: scattered
calibration plots, mis-estimated pulse rates, and "Lost track of the guide star"
aborts. This is most visible on cameras run at short exposures (e.g. PlayerOne
Xena 585M at 2×2, 0.7 s) where the default 1000 ms calibration pulse spans more
than one frame. This MR makes calibration always run in single-frame mode and
starts the video stream only when active guiding begins.
## Root cause
Guide calibration is **frame-driven**: each processed frame runs one
`CalibrationProcess::iterate()` that measures the star's drift from the start
point and issues exactly one pulse. The algorithm assumes each frame reflects a
**completed, settled** pulse — its escalation branches double the pulse
(`Options::calibrationPulseDuration() * 2`) when the star moved < 0.5 px between
consecutive frames.
Streaming delivers frames continuously with no synchronization between a pulse
and the frame that measures its effect. With `CalibrationPulseDuration` (default
1000 ms) longer than the exposure:
1. a fresh pulse is issued on nearly every frame, so pulses overlap and the mount
never settles;
2. the exposure integrates star motion *during* the pulse, smearing the star and
producing a noisy centroid — the scattered points seen on the plot;
3. erratic frame-to-frame deltas mis-trip the "star barely moved → 2× pulse"
branch, overshooting and flinging the star out of the tracking box.
Single-frame calibration does not have this problem: its cycle is strictly serial
(expose → read → detect → pulse → settle → delay → expose), i.e. exactly one
settled observation per pulse. The extra readout latency is a *time* cost only;
calibration is an open-loop measurement, so a slower, settled cadence is
harmless (indeed preferable). Latency only hurts *active guiding*, which is a
closed loop — and that is exactly where streaming helps.
## Fix
Streaming provides no benefit during calibration and full benefit during guiding,
so simply don't stream while calibrating:
- `Guide::calibrate()` stops any active stream at the top, **before**
`buildOperationStack()`, so the whole calibration (including the initial
capture / star-select and dark/subframe handling) runs on the single-frame
path:
```cpp
if (guiderType == GUIDE_INTERNAL && m_StreamingGuide)
stopGuideStreaming();
```
- The `GUIDE_CALIBRATING` state handler no longer starts the stream; it stops it
defensively as a safeguard.
- The `GUIDE_GUIDING` handler is unchanged — it starts the stream on fresh
guiding entry when the user enabled it.
Because `startGuideStreaming()` is only reached from `Guide::setStatus()`
(`GUIDE_GUIDING`) and `Guide::loop()`, and every guide initiator (manual button,
scheduler D-Bus `Guide::guide()`, EkosLive) funnels through
`guide()` → `calibrate()` → the guider FSM → the single `newStatus` →
`Guide::setStatus()` connection, the result is guaranteed for all of them:
**calibration is always single-frame; guiding streams iff the checkbox is set.**
This obsoletes the calibration carve-out added in !1778 (excluding
`GUIDE_CALIBRATING` from the streaming pulse-guard): calibration no longer
streams at all, so that exclusion is dead and the pulse-guard reverts to its
original form (gating only closed-loop correction pulses).
## Testing
- Builds clean (`KStarsLib` / `guide.cpp`); astyle-clean.
- **On-sky confirmed** (PlayerOne Xena 585M, 2×2, 0.7 s exposure): calibration
now runs at a steady single-frame cadence with a clean plot, then the stream
starts automatically for guiding — verified both manually and via a
scheduler-driven job with the streaming checkbox enabled. Log tell: "Guide
streaming started" appears only after "Calibration completed", never at
"Calibration started".
## Files
- `kstars/ekos/guide/guide.cpp`
M +23 -18 kstars/ekos/guide/guide.cpp
https://invent.kde.org/education/kstars/-/commit/f23d23a8141a0bf48e097dd54fa9880b0d746605
diff --git a/kstars/ekos/guide/guide.cpp b/kstars/ekos/guide/guide.cpp
index f019c03e98..8dae2d0d64 100644
--- a/kstars/ekos/guide/guide.cpp
+++ b/kstars/ekos/guide/guide.cpp
@@ -1543,7 +1543,7 @@ bool Guide::sendMultiPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection d
m_PulseTimer.start(delay);
}
- else if (m_StreamingGuide && m_State != GUIDE_CALIBRATING)
+ else if (m_StreamingGuide)
{
// In streaming mode frames keep arriving continuously, so we must gate them
// while the mount is still responding to this pulse. Without this guard every
@@ -1551,12 +1551,8 @@ bool Guide::sendMultiPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection d
// rapid-fire overlapping pulses that produce oscillations.
// The gate duration is the longer of the two pulse lengths plus a small
// propagation margin, floored by the user's guide delay setting.
- //
- // Calibration is deliberately excluded: it is frame-driven and must observe the
- // star move through every pulse to measure the mount response. Discarding frames
- // during calibration loses star tracking and makes the drift look stalled, which
- // pushes calibration into its "double the pulse" branch and flings the star out
- // of the box ("Lost track of the guide star").
+ // (Calibration never streams — see Guide::calibrate() — so this only ever gates
+ // closed-loop correction pulses.)
auto ms = std::max(ra_msecs, dec_msecs) + 100;
auto delay = std::max(static_cast<int>(guideDelay->value() * 1000), ms);
qCDebug(KSTARS_EKOS_GUIDE) << "Streaming pulse guard started for" << delay << "ms";
@@ -1610,11 +1606,11 @@ bool Guide::sendSinglePulse(GuideDirection dir, int msecs, CaptureAfterPulses fo
m_PulseTimer.start(delay);
}
- else if (m_StreamingGuide && followWithCapture == DontCaptureAfterPulses && m_State != GUIDE_CALIBRATING)
+ else if (m_StreamingGuide && followWithCapture == DontCaptureAfterPulses)
{
// Same pulse-in-flight gate as sendMultiPulse() above — correction pulses only.
- // Calibration is excluded on purpose (see sendMultiPulse): its per-axis pulses need
- // continuous frames so the star stays tracked while it drifts across the field.
+ // (Calibration never streams — see Guide::calibrate() — so this only gates the
+ // closed-loop guiding correction pulses.)
auto ms = msecs + 100;
auto delay = std::max(static_cast<int>(guideDelay->value() * 1000), ms);
qCDebug(KSTARS_EKOS_GUIDE) << "Streaming pulse guard started for" << delay << "ms (single pulse)";
@@ -1665,6 +1661,17 @@ bool Guide::calibrate()
}
}
+ // Calibration must always run in single-frame mode. It is frame-driven and issues one pulse
+ // per frame, expecting each measurement to reflect a completed, settled pulse. Streaming
+ // delivers frames continuously with no pulse-to-frame synchronization, so the star is still
+ // moving (and smearing) when the next frame is measured, corrupting the per-step drift and
+ // tripping the "double the pulse" escalation. Stop any active stream here, before the
+ // operation stack is built, so the single-frame capture path (and the correct dark/subframe
+ // handling in buildOperationStack) is used throughout. Streaming is (re)started on entry to
+ // GUIDE_GUIDING if the user enabled it.
+ if (guiderType == GUIDE_INTERNAL && m_StreamingGuide)
+ stopGuideStreaming();
+
buildOperationStack(GUIDE_CALIBRATING);
executeOperationStack();
@@ -2056,14 +2063,12 @@ void Guide::setStatus(Ekos::GuideState newState)
appendLogText(i18n("Calibration started."));
setBusy(true);
manualPulseB->setEnabled(false);
- // Start streaming for calibration if enabled and camera supports it.
- // Guard against double-starting in case we are already streaming.
- if (guiderType == GUIDE_INTERNAL && m_Camera && !m_StreamingGuide)
- {
- auto streamingCheckbox = findChild<QCheckBox *>("guideStreamingEnabled");
- if (streamingCheckbox && streamingCheckbox->isChecked() && m_Camera->hasVideoStream())
- startGuideStreaming();
- }
+ // Calibration always runs single-frame (see Guide::calibrate()). Do NOT start
+ // streaming here regardless of the streaming checkbox — streaming has no benefit
+ // during calibration and desynchronizes pulses from frames. As a safeguard against
+ // any path that reaches this state with a stream still active, stop it now.
+ if (guiderType == GUIDE_INTERNAL && m_StreamingGuide)
+ stopGuideStreaming();
break;
case GUIDE_GUIDING: