1 /*
2     SPDX-FileCopyrightText: 2016 Jasem Mutlaq <mutlaqja@ikarustech.com>.
3 
4     Based on lin_guider
5 
6     SPDX-License-Identifier: GPL-2.0-or-later
7 */
8 
9 #include "internalguider.h"
10 
11 #include "ekos_guide_debug.h"
12 #include "gmath.h"
13 #include "Options.h"
14 #include "auxiliary/kspaths.h"
15 #include "fitsviewer/fitsdata.h"
16 #include "fitsviewer/fitsview.h"
17 #include "guidealgorithms.h"
18 #include "ksnotification.h"
19 #include "ekos/auxiliary/stellarsolverprofileeditor.h"
20 
21 #include <KMessageBox>
22 
23 #include <random>
24 #include <chrono>
25 #include <QTimer>
26 
27 #define MAX_GUIDE_STARS           10
28 
29 namespace Ekos
30 {
InternalGuider()31 InternalGuider::InternalGuider()
32 {
33     // Create math object
34     pmath.reset(new cgmath());
35     connect(pmath.get(), &cgmath::newStarPosition, this, &InternalGuider::newStarPosition);
36     connect(pmath.get(), &cgmath::guideStats, this, &InternalGuider::guideStats);
37 
38     // Do this so that stored calibration will be visible on the
39     // guide options menu. Calibration will get restored again when needed.
40     pmath->getMutableCalibration()->restore(
41         pierSide, Options::reverseDecOnPierSideChange(), subBinX, subBinY, nullptr);
42 
43     state = GUIDE_IDLE;
44 }
45 
guide()46 bool InternalGuider::guide()
47 {
48     if (state >= GUIDE_GUIDING)
49     {
50         return processGuiding();
51     }
52 
53     if (state == GUIDE_SUSPENDED)
54     {
55         return true;
56     }
57     guideFrame->disconnect(this);
58 
59     pmath->start();
60 
61     m_starLostCounter = 0;
62     m_highRMSCounter = 0;
63 
64     m_isFirstFrame = true;
65 
66     if (state == GUIDE_IDLE)
67     {
68         if (Options::saveGuideLog())
69             guideLog.enable();
70         GuideLog::GuideInfo info;
71         fillGuideInfo(&info);
72         guideLog.startGuiding(info);
73     }
74 
75     state = GUIDE_GUIDING;
76     emit newStatus(state);
77 
78     emit frameCaptureRequested();
79 
80     return true;
81 }
82 
83 /**
84  * @brief InternalGuider::abort Abort all internal guider operations.
85  * This includes calibration, dithering, guiding, capturing, and reaquiring.
86  * The state is set to IDLE or ABORTED depending on the current state since
87  * ABORTED can lead to different behavior by external actors than IDLE
88  * @return True if abort succeeds, false otherwise.
89  */
abort()90 bool InternalGuider::abort()
91 {
92     // calibrationStage = CAL_IDLE; remove totally when understand trackingStarSelected
93 
94     logFile.close();
95     guideLog.endGuiding();
96 
97     if (state == GUIDE_CALIBRATING ||
98             state == GUIDE_GUIDING ||
99             state == GUIDE_DITHERING ||
100             state == GUIDE_MANUAL_DITHERING ||
101             state == GUIDE_REACQUIRE)
102     {
103         if (state == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING)
104             emit newStatus(GUIDE_DITHERING_ERROR);
105         emit newStatus(GUIDE_ABORTED);
106 
107         qCDebug(KSTARS_EKOS_GUIDE) << "Aborting" << getGuideStatusString(state);
108     }
109     else
110     {
111         emit newStatus(GUIDE_IDLE);
112         qCDebug(KSTARS_EKOS_GUIDE) << "Stopping internal guider.";
113     }
114 
115     pmath->abort();
116 
117     m_ProgressiveDither.clear();
118     m_starLostCounter = 0;
119     m_highRMSCounter = 0;
120     accumulator.first = accumulator.second = 0;
121 
122     pmath->suspend(false);
123     state = GUIDE_IDLE;
124 
125     return true;
126 }
127 
suspend()128 bool InternalGuider::suspend()
129 {
130     guideLog.pauseInfo();
131     state = GUIDE_SUSPENDED;
132     emit newStatus(state);
133 
134     pmath->suspend(true);
135 
136     return true;
137 }
138 
resume()139 bool InternalGuider::resume()
140 {
141     guideLog.resumeInfo();
142     state = GUIDE_GUIDING;
143     emit newStatus(state);
144 
145     pmath->suspend(false);
146 
147     emit frameCaptureRequested();
148 
149     return true;
150 }
151 
ditherXY(double x,double y)152 bool InternalGuider::ditherXY(double x, double y)
153 {
154     m_ProgressiveDither.clear();
155     m_DitherRetries = 0;
156     double cur_x, cur_y;
157     pmath->getTargetPosition(&cur_x, &cur_y);
158 
159     // Find out how many "jumps" we need to perform in order to get to target.
160     // The current limit is now 1/4 of the box size to make sure the star stays within detection
161     // threashold inside the window.
162     double oneJump = (guideBoxSize / 4.0);
163     double targetX = cur_x, targetY = cur_y;
164     int xSign = (x >= cur_x) ? 1 : -1;
165     int ySign = (y >= cur_y) ? 1 : -1;
166 
167     do
168     {
169         if (fabs(targetX - x) > oneJump)
170             targetX += oneJump * xSign;
171         else if (fabs(targetX - x) < oneJump)
172             targetX = x;
173 
174         if (fabs(targetY - y) > oneJump)
175             targetY += oneJump * ySign;
176         else if (fabs(targetY - y) < oneJump)
177             targetY = y;
178 
179         m_ProgressiveDither.enqueue(GuiderUtils::Vector(targetX, targetY, -1));
180 
181     }
182     while (targetX != x || targetY != y);
183 
184     m_DitherTargetPosition = m_ProgressiveDither.dequeue();
185     pmath->setTargetPosition(m_DitherTargetPosition.x, m_DitherTargetPosition.y);
186     guideLog.ditherInfo(x, y, m_DitherTargetPosition.x, m_DitherTargetPosition.y);
187 
188     state = GUIDE_MANUAL_DITHERING;
189     emit newStatus(state);
190 
191     processGuiding();
192 
193     return true;
194 }
195 
dither(double pixels)196 bool InternalGuider::dither(double pixels)
197 {
198     double ret_x, ret_y;
199     pmath->getTargetPosition(&ret_x, &ret_y);
200 
201     // Just calling getStarScreenPosition() will get the position at the last time the guide star
202     // was found, which is likely before the most recent guide pulse.
203     // Instead we call findLocalStarPosition() which does the analysis from the image.
204     // Unfortunately, processGuiding() will repeat that computation.
205     // We currently don't cache it.
206     GuiderUtils::Vector star_position = pmath->findLocalStarPosition(m_ImageData, guideFrame);
207     if (pmath->isStarLost() || (star_position.x == -1) || (star_position.y == -1))
208     {
209         // If the star position is lost, just lose this iteration.
210         // If it happens too many time, abort.
211         constexpr int abortStarLostThreshold = MAX_LOST_STAR_THRESHOLD * 3;
212         if (++m_starLostCounter > abortStarLostThreshold)
213         {
214             qCDebug(KSTARS_EKOS_GUIDE) << "Too many consecutive lost stars." << m_starLostCounter << "Aborting dither.";
215             return abortDither();
216         }
217         qCDebug(KSTARS_EKOS_GUIDE) << "Dither lost star. Trying again.";
218         emit frameCaptureRequested();
219         return true;
220     }
221     else
222         m_starLostCounter = 0;
223 
224     if (state != GUIDE_DITHERING)
225     {
226         m_DitherRetries = 0;
227 
228         auto seed = std::chrono::system_clock::now().time_since_epoch().count();
229         std::default_random_engine generator(seed);
230         std::uniform_real_distribution<double> angleMagnitude(0, 360);
231 
232         double angle  = angleMagnitude(generator) * dms::DegToRad;
233         double diff_x = pixels * cos(angle);
234         double diff_y = pixels * sin(angle);
235 
236         if (pmath->getCalibration().declinationSwapEnabled())
237             diff_y *= -1;
238 
239         if (fabs(diff_x + accumulator.first) > MAX_DITHER_TRAVEL)
240             diff_x *= -1.5;
241         accumulator.first += diff_x;
242         if (fabs(diff_y + accumulator.second) > MAX_DITHER_TRAVEL)
243             diff_y *= -1.5;
244         accumulator.second += diff_y;
245 
246         m_DitherTargetPosition = GuiderUtils::Vector(ret_x, ret_y, 0) + GuiderUtils::Vector(diff_x, diff_y, 0);
247 
248         qCDebug(KSTARS_EKOS_GUIDE) << "Dithering process started.. Reticle Target Pos X " << m_DitherTargetPosition.x << " Y " <<
249                                    m_DitherTargetPosition.y;
250         guideLog.ditherInfo(diff_x, diff_y, m_DitherTargetPosition.x, m_DitherTargetPosition.y);
251 
252         pmath->setTargetPosition(m_DitherTargetPosition.x, m_DitherTargetPosition.y);
253 
254         if (Options::gPGEnabled())
255             // This is the offset in image coordinates, but needs to be converted to RA.
256             pmath->getGPG().startDithering(diff_x, diff_y, pmath->getCalibration());
257 
258         state = GUIDE_DITHERING;
259         emit newStatus(state);
260 
261         processGuiding();
262 
263         return true;
264     }
265 
266     // These will be the RA & DEC drifts of the current star position from the reticle position in pixels.
267     double driftRA, driftDEC;
268     pmath->getCalibration().computeDrift(
269         star_position,
270         GuiderUtils::Vector(m_DitherTargetPosition.x, m_DitherTargetPosition.y, 0),
271         &driftRA, &driftDEC);
272 
273     qCDebug(KSTARS_EKOS_GUIDE) << "Dithering in progress. Current" << star_position.x << star_position.y << "Target" <<
274                                m_DitherTargetPosition.x <<
275                                m_DitherTargetPosition.y << "Diff star X:" << driftRA << "Y:" << driftDEC;
276 
277     if (fabs(driftRA) < 1 && fabs(driftDEC) < 1)
278     {
279         pmath->setTargetPosition(star_position.x, star_position.y);
280         qCDebug(KSTARS_EKOS_GUIDE) << "Dither complete.";
281 
282         if (Options::ditherSettle() > 0)
283         {
284             state = GUIDE_DITHERING_SETTLE;
285             guideLog.settleStartedInfo();
286             emit newStatus(state);
287         }
288 
289         if (Options::gPGEnabled())
290             pmath->getGPG().ditheringSettled(true);
291 
292         QTimer::singleShot(Options::ditherSettle() * 1000, this, SLOT(setDitherSettled()));
293     }
294     else
295     {
296         if (++m_DitherRetries > Options::ditherMaxIterations())
297             return abortDither();
298 
299         processGuiding();
300     }
301 
302     return true;
303 }
304 
abortDither()305 bool InternalGuider::abortDither()
306 {
307     if (Options::ditherFailAbortsAutoGuide())
308     {
309         emit newStatus(Ekos::GUIDE_DITHERING_ERROR);
310         abort();
311         return false;
312     }
313     else
314     {
315         emit newLog(i18n("Warning: Dithering failed. Autoguiding shall continue as set in the options in case "
316                          "of dither failure."));
317 
318         if (Options::ditherSettle() > 0)
319         {
320             state = GUIDE_DITHERING_SETTLE;
321             guideLog.settleStartedInfo();
322             emit newStatus(state);
323         }
324 
325         if (Options::gPGEnabled())
326             pmath->getGPG().ditheringSettled(false);
327 
328         QTimer::singleShot(Options::ditherSettle() * 1000, this, SLOT(setDitherSettled()));
329         return true;
330     }
331 }
332 
processManualDithering()333 bool InternalGuider::processManualDithering()
334 {
335     double cur_x, cur_y;
336     pmath->getTargetPosition(&cur_x, &cur_y);
337     pmath->getStarScreenPosition(&cur_x, &cur_y);
338 
339     // These will be the RA & DEC drifts of the current star position from the reticle position in pixels.
340     double driftRA, driftDEC;
341     pmath->getCalibration().computeDrift(
342         GuiderUtils::Vector(cur_x, cur_y, 0),
343         GuiderUtils::Vector(m_DitherTargetPosition.x, m_DitherTargetPosition.y, 0),
344         &driftRA, &driftDEC);
345 
346     qCDebug(KSTARS_EKOS_GUIDE) << "Manual Dithering in progress. Diff star X:" << driftRA << "Y:" << driftDEC;
347 
348     if (fabs(driftRA) < guideBoxSize / 5.0 && fabs(driftDEC) < guideBoxSize / 5.0)
349     {
350         if (m_ProgressiveDither.empty() == false)
351         {
352             m_DitherTargetPosition = m_ProgressiveDither.dequeue();
353             pmath->setTargetPosition(m_DitherTargetPosition.x, m_DitherTargetPosition.y);
354             qCDebug(KSTARS_EKOS_GUIDE) << "Next Dither Jump X:" << m_DitherTargetPosition.x << "Jump Y:" << m_DitherTargetPosition.y;
355             m_DitherRetries = 0;
356 
357             processGuiding();
358 
359             return true;
360         }
361 
362         if (fabs(driftRA) < 1 && fabs(driftDEC) < 1)
363         {
364             pmath->setTargetPosition(cur_x, cur_y);
365             qCDebug(KSTARS_EKOS_GUIDE) << "Manual Dither complete.";
366 
367             if (Options::ditherSettle() > 0)
368             {
369                 state = GUIDE_DITHERING_SETTLE;
370                 guideLog.settleStartedInfo();
371                 emit newStatus(state);
372             }
373 
374             QTimer::singleShot(Options::ditherSettle() * 1000, this, SLOT(setDitherSettled()));
375         }
376         else
377         {
378             processGuiding();
379         }
380     }
381     else
382     {
383         if (++m_DitherRetries > Options::ditherMaxIterations())
384         {
385             emit newLog(i18n("Warning: Manual Dithering failed."));
386 
387             if (Options::ditherSettle() > 0)
388             {
389                 state = GUIDE_DITHERING_SETTLE;
390                 guideLog.settleStartedInfo();
391                 emit newStatus(state);
392             }
393 
394             QTimer::singleShot(Options::ditherSettle() * 1000, this, SLOT(setDitherSettled()));
395             return true;
396         }
397 
398         processGuiding();
399     }
400 
401     return true;
402 }
403 
setDitherSettled()404 void InternalGuider::setDitherSettled()
405 {
406     guideLog.settleCompletedInfo();
407     emit newStatus(Ekos::GUIDE_DITHERING_SUCCESS);
408 
409     // Back to guiding
410     state = GUIDE_GUIDING;
411 }
412 
calibrate()413 bool InternalGuider::calibrate()
414 {
415     bool ccdInfo = true, scopeInfo = true;
416     QString errMsg;
417 
418     if (subW == 0 || subH == 0)
419     {
420         errMsg  = "CCD";
421         ccdInfo = false;
422     }
423 
424     if (mountAperture == 0.0 || mountFocalLength == 0.0)
425     {
426         scopeInfo = false;
427         if (ccdInfo == false)
428             errMsg += " & Telescope";
429         else
430             errMsg += "Telescope";
431     }
432 
433     if (ccdInfo == false || scopeInfo == false)
434     {
435         KSNotification::error(i18n("%1 info are missing. Please set the values in INDI Control Panel.", errMsg),
436                               i18n("Missing Information"));
437         return false;
438     }
439 
440     if (state != GUIDE_CALIBRATING)
441     {
442         pmath->getTargetPosition(&calibrationStartX, &calibrationStartY);
443         calibrationProcess.reset(
444             new CalibrationProcess(calibrationStartX, calibrationStartY,
445                                    !Options::twoAxisEnabled()));
446         state = GUIDE_CALIBRATING;
447         emit newStatus(GUIDE_CALIBRATING);
448     }
449 
450     if (calibrationProcess->inProgress())
451     {
452         iterateCalibration();
453         return true;
454     }
455 
456     if (restoreCalibration())
457     {
458         calibrationProcess.reset();
459         emit newStatus(Ekos::GUIDE_CALIBRATION_SUCESS);
460         KSNotification::event(QLatin1String("CalibrationRestored"),
461                               i18n("Guiding calibration restored"));
462         reset();
463         return true;
464     }
465 
466     // Initialize the calibration parameters.
467     // CCD pixel values comes in in microns and we want mm.
468     pmath->getMutableCalibration()->setParameters(
469         ccdPixelSizeX / 1000.0, ccdPixelSizeY / 1000.0, mountFocalLength,
470         subBinX, subBinY, pierSide, mountRA, mountDEC);
471 
472     calibrationProcess->useCalibration(pmath->getMutableCalibration());
473 
474     guideFrame->disconnect(this);
475 
476     // Must reset dec swap before we run any calibration procedure!
477     emit DESwapChanged(false);
478     pmath->setLostStar(false);
479 
480     if (Options::saveGuideLog())
481         guideLog.enable();
482     GuideLog::GuideInfo info;
483     fillGuideInfo(&info);
484     guideLog.startCalibration(info);
485 
486     calibrationProcess->startup();
487     calibrationProcess->setGuideLog(&guideLog);
488     iterateCalibration();
489 
490     return true;
491 }
492 
iterateCalibration()493 void InternalGuider::iterateCalibration()
494 {
495     if (calibrationProcess->inProgress())
496     {
497         pmath->performProcessing(GUIDE_CALIBRATING, m_ImageData, guideFrame);
498         if (pmath->isStarLost())
499         {
500             emit newLog(i18n("Lost track of the guide star. Try increasing the square size or reducing pulse duration."));
501             emit newStatus(Ekos::GUIDE_CALIBRATION_ERROR);
502             emit calibrationUpdate(GuideInterface::CALIBRATION_MESSAGE_ONLY,
503                                    i18n("Guide Star lost."));
504             reset();
505             return;
506         }
507     }
508     double starX, starY;
509     pmath->getStarScreenPosition(&starX, &starY);
510     calibrationProcess->iterate(starX, starY);
511 
512     auto status = calibrationProcess->getStatus();
513     if (status != GUIDE_CALIBRATING)
514         emit newStatus(status);
515 
516     QString logStatus = calibrationProcess->getLogStatus();
517     if (logStatus.length())
518         emit newLog(logStatus);
519 
520     QString updateMessage;
521     double x, y;
522     GuideInterface::CalibrationUpdateType type;
523     calibrationProcess->getCalibrationUpdate(&type, &updateMessage, &x, &y);
524     if (updateMessage.length())
525         emit calibrationUpdate(type, updateMessage, x, y);
526 
527     GuideDirection pulseDirection;
528     int pulseMsecs;
529     calibrationProcess->getPulse(&pulseDirection, &pulseMsecs);
530     if (pulseDirection != NO_DIR)
531         emit newPulse(pulseDirection, pulseMsecs);
532 
533     if (status == GUIDE_CALIBRATION_ERROR)
534     {
535         KSNotification::event(QLatin1String("CalibrationFailed"), i18n("Guiding calibration failed"),
536                               KSNotification::EVENT_ALERT);
537         reset();
538     }
539     else if (status == GUIDE_CALIBRATION_SUCESS)
540     {
541         KSNotification::event(QLatin1String("CalibrationSuccessful"),
542                               i18n("Guiding calibration completed successfully"));
543         emit DESwapChanged(pmath->getCalibration().declinationSwapEnabled());
544         pmath->setTargetPosition(calibrationStartX, calibrationStartY);
545         reset();
546     }
547 }
548 
setGuideView(GuideView * guideView)549 void InternalGuider::setGuideView(GuideView *guideView)
550 {
551     guideFrame = guideView;
552 }
553 
setImageData(const QSharedPointer<FITSData> & data)554 void InternalGuider::setImageData(const QSharedPointer<FITSData> &data)
555 {
556     m_ImageData = data;
557 }
558 
reset()559 void InternalGuider::reset()
560 {
561     state = GUIDE_IDLE;
562 
563     connect(guideFrame, SIGNAL(trackingStarSelected(int, int)), this, SLOT(trackingStarSelected(int, int)),
564             Qt::UniqueConnection);
565     calibrationProcess.reset();
566 }
567 
clearCalibration()568 bool InternalGuider::clearCalibration()
569 {
570     Options::setSerializedCalibration("");
571     pmath->getMutableCalibration()->reset();
572     return true;
573 }
574 
restoreCalibration()575 bool InternalGuider::restoreCalibration()
576 {
577     bool success = Options::reuseGuideCalibration() &&
578                    pmath->getMutableCalibration()->restore(
579                        pierSide, Options::reverseDecOnPierSideChange(),
580                        subBinX, subBinY, &mountDEC);
581     if (success)
582         emit DESwapChanged(pmath->getCalibration().declinationSwapEnabled());
583     return success;
584 }
585 
setStarPosition(QVector3D & starCenter)586 void InternalGuider::setStarPosition(QVector3D &starCenter)
587 {
588     pmath->setTargetPosition(starCenter.x(), starCenter.y());
589 }
590 
trackingStarSelected(int x,int y)591 void InternalGuider::trackingStarSelected(int x, int y)
592 {
593     /*
594 
595       Not sure what's going on here--manual star selection for calibration?
596       Don't really see how the logic works.
597 
598     if (calibrationStage == CAL_IDLE)
599         return;
600 
601     pmath->setTargetPosition(x, y);
602 
603     calibrationStage = CAL_START;
604     */
605 }
606 
setDECSwap(bool enable)607 void InternalGuider::setDECSwap(bool enable)
608 {
609     pmath->getMutableCalibration()->setDeclinationSwapEnabled(enable);
610 }
611 
setSquareAlgorithm(int index)612 void InternalGuider::setSquareAlgorithm(int index)
613 {
614     if (index == SEP_MULTISTAR && !pmath->usingSEPMultiStar())
615         m_isFirstFrame = true;
616     pmath->setAlgorithmIndex(index);
617 }
618 
setReticleParameters(double x,double y)619 void InternalGuider::setReticleParameters(double x, double y)
620 {
621     pmath->setTargetPosition(x, y);
622 }
623 
getReticleParameters(double * x,double * y)624 bool InternalGuider::getReticleParameters(double *x, double *y)
625 {
626     return pmath->getTargetPosition(x, y);
627 }
628 
setGuiderParams(double ccdPixelSizeX,double ccdPixelSizeY,double mountAperture,double mountFocalLength)629 bool InternalGuider::setGuiderParams(double ccdPixelSizeX, double ccdPixelSizeY, double mountAperture,
630                                      double mountFocalLength)
631 {
632     this->ccdPixelSizeX    = ccdPixelSizeX;
633     this->ccdPixelSizeY    = ccdPixelSizeY;
634     this->mountAperture    = mountAperture;
635     this->mountFocalLength = mountFocalLength;
636     return pmath->setGuiderParameters(ccdPixelSizeX, ccdPixelSizeY, mountAperture, mountFocalLength);
637 }
638 
setFrameParams(uint16_t x,uint16_t y,uint16_t w,uint16_t h,uint16_t binX,uint16_t binY)639 bool InternalGuider::setFrameParams(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t binX, uint16_t binY)
640 {
641     if (w <= 0 || h <= 0)
642         return false;
643 
644     subX = x;
645     subY = y;
646     subW = w;
647     subH = h;
648 
649     subBinX = binX;
650     subBinY = binY;
651 
652     pmath->setVideoParameters(w, h, subBinX, subBinY);
653 
654     return true;
655 }
656 
processGuiding()657 bool InternalGuider::processGuiding()
658 {
659     const cproc_out_params *out;
660 
661     // On first frame, center the box (reticle) around the star so we do not start with an offset the results in
662     // unnecessary guiding pulses.
663     if (m_isFirstFrame)
664     {
665         if (state == GUIDE_GUIDING)
666         {
667             GuiderUtils::Vector star_pos = pmath->findLocalStarPosition(m_ImageData, guideFrame);
668             pmath->setTargetPosition(star_pos.x, star_pos.y);
669         }
670         m_isFirstFrame = false;
671     }
672     // calc math. it tracks square
673     pmath->performProcessing(state, m_ImageData, guideFrame, &guideLog);
674 
675     if (state == GUIDE_SUSPENDED)
676     {
677         if (Options::gPGEnabled())
678             emit frameCaptureRequested();
679         return true;
680     }
681 
682     if (pmath->isStarLost())
683         m_starLostCounter++;
684     else
685         m_starLostCounter = 0;
686 
687     // do pulse
688     out = pmath->getOutputParameters();
689 
690     bool sendPulses = true;
691 
692     double delta_rms = std::hypot(out->delta[GUIDE_RA], out->delta[GUIDE_DEC]);
693     if (delta_rms > Options::guideMaxDeltaRMS())
694         m_highRMSCounter++;
695     else
696         m_highRMSCounter = 0;
697 
698     uint8_t abortStarLostThreshold = (state == GUIDE_DITHERING
699                                       || state == GUIDE_MANUAL_DITHERING) ? MAX_LOST_STAR_THRESHOLD * 3 : MAX_LOST_STAR_THRESHOLD;
700     uint8_t abortRMSThreshold = (state == GUIDE_DITHERING
701                                  || state == GUIDE_MANUAL_DITHERING) ? MAX_RMS_THRESHOLD * 3 : MAX_RMS_THRESHOLD;
702     if (m_starLostCounter > abortStarLostThreshold || m_highRMSCounter > abortRMSThreshold)
703     {
704         qCDebug(KSTARS_EKOS_GUIDE) << "m_starLostCounter" << m_starLostCounter
705                                    << "m_highRMSCounter" << m_highRMSCounter
706                                    << "delta_rms" << delta_rms;
707 
708         if (m_starLostCounter > abortStarLostThreshold)
709             emit newLog(i18n("Lost track of the guide star. Searching for guide stars..."));
710         else
711             emit newLog(i18n("Delta RMS threshold value exceeded. Searching for guide stars..."));
712 
713         reacquireTimer.start();
714         rememberState = state;
715         state = GUIDE_REACQUIRE;
716         emit newStatus(state);
717         return true;
718     }
719 
720     if (sendPulses)
721     {
722         emit newPulse(out->pulse_dir[GUIDE_RA], out->pulse_length[GUIDE_RA],
723                       out->pulse_dir[GUIDE_DEC], out->pulse_length[GUIDE_DEC]);
724 
725         // Wait until pulse is over before capturing an image
726         const int waitMS = qMax(out->pulse_length[GUIDE_RA], out->pulse_length[GUIDE_DEC]);
727         // If less than MAX_IMMEDIATE_CAPTURE ms, then capture immediately
728         if (waitMS > MAX_IMMEDIATE_CAPTURE)
729             // Issue frame requests MAX_IMMEDIATE_CAPTURE ms before timeout to account for
730             // propagation delays
731             QTimer::singleShot(waitMS - PROPAGATION_DELAY, [&]()
732         {
733             emit frameCaptureRequested();
734         });
735         else
736             emit frameCaptureRequested();
737     }
738     else
739         emit frameCaptureRequested();
740 
741     if (state == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING)
742         return true;
743 
744     // Hy 9/13/21: Check above just looks for GUIDE_DITHERING or GUIDE_MANUAL_DITHERING
745     // but not the other dithering possibilities (error, success, settle).
746     // Not sure if they should be included above, so conservatively not changing the
747     // code, but don't think they should broadcast the newAxisDelta which might
748     // interrup a capture.
749     if (state < GUIDE_DITHERING)
750         emit newAxisDelta(out->delta[GUIDE_RA], out->delta[GUIDE_DEC]);
751 
752     double raPulse = out->pulse_length[GUIDE_RA];
753     double dePulse = out->pulse_length[GUIDE_DEC];
754 
755     //If the pulse was not sent to the mount, it should have 0 value
756     if(out->pulse_dir[GUIDE_RA] == NO_DIR)
757         raPulse = 0;
758     //If the pulse was not sent to the mount, it should have 0 value
759     if(out->pulse_dir[GUIDE_DEC] == NO_DIR)
760         dePulse = 0;
761     //If the pulse was in the Negative direction, it should have a negative sign.
762     if(out->pulse_dir[GUIDE_RA] == RA_INC_DIR)
763         raPulse = -raPulse;
764     //If the pulse was in the Negative direction, it should have a negative sign.
765     if(out->pulse_dir[GUIDE_DEC] == DEC_INC_DIR)
766         dePulse = -dePulse;
767 
768     emit newAxisPulse(raPulse, dePulse);
769 
770     emit newAxisSigma(out->sigma[GUIDE_RA], out->sigma[GUIDE_DEC]);
771     if (SEPMultiStarEnabled())
772         emit newSNR(pmath->getGuideStarSNR());
773 
774     return true;
775 }
776 
selectAutoStarSEPMultistar()777 bool InternalGuider::selectAutoStarSEPMultistar()
778 {
779     guideFrame->updateFrame();
780     QVector3D newStarCenter = pmath->selectGuideStar(m_ImageData);
781     if (newStarCenter.x() >= 0)
782     {
783         emit newStarPosition(newStarCenter, true);
784         return true;
785     }
786     return false;
787 }
788 
SEPMultiStarEnabled()789 bool InternalGuider::SEPMultiStarEnabled()
790 {
791     return Options::guideAlgorithm() == SEP_MULTISTAR;
792 }
793 
selectAutoStar()794 bool InternalGuider::selectAutoStar()
795 {
796     if (Options::guideAlgorithm() == SEP_MULTISTAR)
797         return selectAutoStarSEPMultistar();
798 
799     bool useNativeDetection = false;
800 
801     QList<Edge *> starCenters;
802 
803     if (Options::guideAlgorithm() != SEP_THRESHOLD)
804         starCenters = GuideAlgorithms::detectStars(m_ImageData, guideFrame->getTrackingBox());
805 
806     if (starCenters.empty())
807     {
808         QVariantMap settings;
809         settings["maxStarsCount"] = 50;
810         settings["optionsProfileIndex"] = Options::guideOptionsProfile();
811         settings["optionsProfileGroup"] = static_cast<int>(Ekos::GuideProfiles);
812         m_ImageData->setSourceExtractorSettings(settings);
813 
814         if (Options::guideAlgorithm() == SEP_THRESHOLD)
815             m_ImageData->findStars(ALGORITHM_SEP).waitForFinished();
816         else
817             m_ImageData->findStars().waitForFinished();
818 
819         starCenters = m_ImageData->getStarCenters();
820         if (starCenters.empty())
821             return false;
822 
823         useNativeDetection = true;
824         // For SEP, prefer flux total
825         if (Options::guideAlgorithm() == SEP_THRESHOLD)
826             std::sort(starCenters.begin(), starCenters.end(), [](const Edge * a, const Edge * b)
827         {
828             return a->val > b->val;
829         });
830         else
831             std::sort(starCenters.begin(), starCenters.end(), [](const Edge * a, const Edge * b)
832         {
833             return a->width > b->width;
834         });
835 
836         guideFrame->setStarsEnabled(true);
837         guideFrame->updateFrame();
838     }
839 
840     int maxX = m_ImageData->width();
841     int maxY = m_ImageData->height();
842 
843     int scores[MAX_GUIDE_STARS];
844 
845     int maxIndex = MAX_GUIDE_STARS < starCenters.count() ? MAX_GUIDE_STARS : starCenters.count();
846 
847     for (int i = 0; i < maxIndex; i++)
848     {
849         int score = 100;
850 
851         Edge *center = starCenters.at(i);
852 
853         if (useNativeDetection)
854         {
855             // Severely reject stars close to edges
856             if (center->x < (center->width * 5) || center->y < (center->width * 5) ||
857                     center->x > (maxX - center->width * 5) || center->y > (maxY - center->width * 5))
858                 score -= 1000;
859 
860             // Reject stars bigger than square
861             if (center->width > float(guideBoxSize) / subBinX)
862                 score -= 1000;
863             else
864             {
865                 if (Options::guideAlgorithm() == SEP_THRESHOLD)
866                     score += sqrt(center->val);
867                 else
868                     // Moderately favor brighter stars
869                     score += center->width * center->width;
870             }
871 
872             // Moderately reject stars close to other stars
873             foreach (Edge *edge, starCenters)
874             {
875                 if (edge == center)
876                     continue;
877 
878                 if (fabs(center->x - edge->x) < center->width * 2 && fabs(center->y - edge->y) < center->width * 2)
879                 {
880                     score -= 15;
881                     break;
882                 }
883             }
884         }
885         else
886         {
887             score = center->val;
888         }
889 
890         scores[i] = score;
891     }
892 
893     int maxScore      = -1;
894     int maxScoreIndex = -1;
895     for (int i = 0; i < maxIndex; i++)
896     {
897         if (scores[i] > maxScore)
898         {
899             maxScore      = scores[i];
900             maxScoreIndex = i;
901         }
902     }
903 
904     if (maxScoreIndex < 0)
905     {
906         qCDebug(KSTARS_EKOS_GUIDE) << "No suitable star detected.";
907         return false;
908     }
909 
910     QVector3D newStarCenter(starCenters[maxScoreIndex]->x, starCenters[maxScoreIndex]->y, 0);
911 
912     if (useNativeDetection == false)
913         qDeleteAll(starCenters);
914 
915     emit newStarPosition(newStarCenter, true);
916 
917     return true;
918 }
919 
reacquire()920 bool InternalGuider::reacquire()
921 {
922     bool rc = selectAutoStar();
923     if (rc)
924     {
925         m_highRMSCounter = m_starLostCounter = 0;
926         m_isFirstFrame = true;
927         pmath->reset();
928         // If we were in the process of dithering, wait until settle and resume
929         if (rememberState == GUIDE_DITHERING || state == GUIDE_MANUAL_DITHERING)
930         {
931             if (Options::ditherSettle() > 0)
932             {
933                 state = GUIDE_DITHERING_SETTLE;
934                 guideLog.settleStartedInfo();
935                 emit newStatus(state);
936             }
937 
938             QTimer::singleShot(Options::ditherSettle() * 1000, this, SLOT(setDitherSettled()));
939         }
940         else
941         {
942             state = GUIDE_GUIDING;
943             emit newStatus(state);
944         }
945 
946     }
947     else if (reacquireTimer.elapsed() > static_cast<int>(Options::guideLostStarTimeout() * 1000))
948     {
949         emit newLog(i18n("Failed to find any suitable guide stars. Aborting..."));
950         abort();
951         return false;
952     }
953 
954     emit frameCaptureRequested();
955     return rc;
956 }
957 
fillGuideInfo(GuideLog::GuideInfo * info)958 void InternalGuider::fillGuideInfo(GuideLog::GuideInfo *info)
959 {
960     // NOTE: just using the X values, phd2logview assumes x & y the same.
961     // pixel scale in arc-sec / pixel. The 2nd and 3rd values seem redundent, but are
962     // in the phd2 logs.
963     info->pixelScale = (206.26481 * this->ccdPixelSizeX * this->subBinX) / this->mountFocalLength;
964     info->binning = this->subBinX;
965     info->focalLength = this->mountFocalLength;
966     info->ra = this->mountRA.Degrees();
967     info->dec = this->mountDEC.Degrees();
968     info->azimuth = this->mountAzimuth.Degrees();
969     info->altitude = this->mountAltitude.Degrees();
970     info->pierSide = this->pierSide;
971     info->xangle = pmath->getCalibration().getRAAngle();
972     info->yangle = pmath->getCalibration().getDECAngle();
973     // Calibration values in ms/pixel, xrate is in pixels/second.
974     info->xrate = 1000.0 / pmath->getCalibration().raPulseMillisecondsPerPixel();
975     info->yrate = 1000.0 / pmath->getCalibration().decPulseMillisecondsPerPixel();
976 }
977 
updateGPGParameters()978 void InternalGuider::updateGPGParameters()
979 {
980     pmath->getGPG().updateParameters();
981 }
982 
resetGPG()983 void InternalGuider::resetGPG()
984 {
985     pmath->getGPG().reset();
986 }
987 
getCalibration() const988 const Calibration &InternalGuider::getCalibration() const
989 {
990     return pmath->getCalibration();
991 }
992 }
993