1 /*
2     SPDX-FileCopyrightText: 2016 Jasem Mutlaq <mutlaqja@ikarustech.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "phd2.h"
8 
9 #include "Options.h"
10 #include "kspaths.h"
11 #include "kstars.h"
12 
13 #include "ekos/manager.h"
14 #include "fitsviewer/fitsdata.h"
15 
16 #include <cassert>
17 #include <fitsio.h>
18 #include <KMessageBox>
19 #include <QImage>
20 
21 #include <QJsonDocument>
22 #include <QNetworkReply>
23 
24 #include <ekos_guide_debug.h>
25 
26 #define MAX_SET_CONNECTED_RETRIES   3
27 
28 namespace Ekos
29 {
PHD2()30 PHD2::PHD2()
31 {
32     tcpSocket = new QTcpSocket(this);
33 
34     //This list of available PHD Events is on https://github.com/OpenPHDGuiding/phd2/wiki/EventMonitoring
35 
36     events["Version"]                 = Version;
37     events["LockPositionSet"]         = LockPositionSet;
38     events["Calibrating"]             = Calibrating;
39     events["CalibrationComplete"]     = CalibrationComplete;
40     events["StarSelected"]            = StarSelected;
41     events["StartGuiding"]            = StartGuiding;
42     events["Paused"]                  = Paused;
43     events["StartCalibration"]        = StartCalibration;
44     events["AppState"]                = AppState;
45     events["CalibrationFailed"]       = CalibrationFailed;
46     events["CalibrationDataFlipped"]  = CalibrationDataFlipped;
47     events["LoopingExposures"]        = LoopingExposures;
48     events["LoopingExposuresStopped"] = LoopingExposuresStopped;
49     events["SettleBegin"]             = SettleBegin;
50     events["Settling"]                = Settling;
51     events["SettleDone"]              = SettleDone;
52     events["StarLost"]                = StarLost;
53     events["GuidingStopped"]          = GuidingStopped;
54     events["Resumed"]                 = Resumed;
55     events["GuideStep"]               = GuideStep;
56     events["GuidingDithered"]         = GuidingDithered;
57     events["LockPositionLost"]        = LockPositionLost;
58     events["Alert"]                   = Alert;
59     events["GuideParamChange"]        = GuideParamChange;
60     events["ConfigurationChange"]     = ConfigurationChange;
61 
62     //This list of available PHD Methods is on https://github.com/OpenPHDGuiding/phd2/wiki/EventMonitoring
63     //Only some of the methods are implemented.  The ones that say COMMAND_RECEIVED simply return a 0 saying the command was received.
64     methodResults["capture_single_frame"]   = CAPTURE_SINGLE_FRAME;
65     methodResults["clear_calibration"]      = CLEAR_CALIBRATION_COMMAND_RECEIVED;
66     methodResults["dither"]                 = DITHER_COMMAND_RECEIVED;
67     //find_star
68     //flip_calibration
69     //get_algo_param_names
70     //get_algo_param
71     methodResults["get_app_state"]          = APP_STATE_RECEIVED;
72     //get_calibrated
73     //get_calibration_data
74     methodResults["get_connected"]          = IS_EQUIPMENT_CONNECTED;
75     //get_cooler_status
76     methodResults["get_current_equipment"]  = GET_CURRENT_EQUIPMENT;
77     methodResults["get_dec_guide_mode"]     = DEC_GUIDE_MODE;
78     methodResults["get_exposure"]           = EXPOSURE_TIME;
79     methodResults["get_exposure_durations"] = EXPOSURE_DURATIONS;
80     methodResults["get_lock_position"]      = LOCK_POSITION;
81     //get_lock_shift_enabled
82     //get_lock_shift_params
83     //get_paused
84     methodResults["get_pixel_scale"]        = PIXEL_SCALE;
85     //get_profile
86     //get_profiles
87     //get_search_region
88     //get_sensor_temperature
89     methodResults["get_star_image"]         = STAR_IMAGE;
90     //get_use_subframes
91     methodResults["guide"]                  = GUIDE_COMMAND_RECEIVED;
92     //guide_pulse
93     methodResults["loop"]                   = LOOP;
94     //save_image
95     //set_algo_param
96     methodResults["set_connected"]          = CONNECTION_RESULT;
97     methodResults["set_dec_guide_mode"]     = SET_DEC_GUIDE_MODE_COMMAND_RECEIVED;
98     methodResults["set_exposure"]           = SET_EXPOSURE_COMMAND_RECEIVED;
99     methodResults["set_lock_position"]      = SET_LOCK_POSITION;
100     //set_lock_shift_enabled
101     //set_lock_shift_params
102     methodResults["set_paused"]             = SET_PAUSED_COMMAND_RECEIVED;
103     //set_profile
104     //shutdown
105     methodResults["stop_capture"]           = STOP_CAPTURE_COMMAND_RECEIVED;
106 
107     abortTimer = new QTimer(this);
108     connect(abortTimer, &QTimer::timeout, this, [ = ]
109     {
110         if (state == CALIBRATING)
111             qCDebug(KSTARS_EKOS_GUIDE) << "Abort timeout expired while calibrating, retrying to guide.";
112         else if (state == LOSTLOCK)
113             qCDebug(KSTARS_EKOS_GUIDE) << "Abort timeout expired while reacquiring star, retrying to guide.";
114         else
115             qCDebug(KSTARS_EKOS_GUIDE) << "Abort timeout expired, stopping.";
116         abort();
117     });
118 
119     ditherTimer = new QTimer(this);
120     connect(ditherTimer, &QTimer::timeout, this, [ = ]
121     {
122         qCDebug(KSTARS_EKOS_GUIDE) << "ditherTimer expired, state" << state << "dithering" << isDitherActive << "settling" << isSettling;
123         ditherTimer->stop();
124         isDitherActive = false;
125         isSettling = false;
126         if (Options::ditherFailAbortsAutoGuide())
127         {
128             abort();
129             emit newStatus(GUIDE_DITHERING_ERROR);
130         }
131         else
132         {
133             emit newLog(i18n("PHD2: There was no dithering response from PHD2, but continue guiding."));
134             emit newStatus(Ekos::GUIDE_DITHERING_SUCCESS);
135         }
136     });
137 
138     stateTimer = new QTimer(this);
139     connect(stateTimer, &QTimer::timeout, this, [ = ]
140     {
141         if (tcpSocket->state() == QTcpSocket::UnconnectedState)
142         {
143             m_PHD2ReconnectCounter++;
144             if (m_PHD2ReconnectCounter > PHD2_RECONNECT_THRESHOLD)
145                 stateTimer->stop();
146             else
147             {
148                 emit newLog(i18n("Reconnecting to PHD2 Host: %1, on port %2. . .", Options::pHD2Host(), Options::pHD2Port()));
149 
150                 connect(tcpSocket, &QTcpSocket::readyRead, this, &PHD2::readPHD2, Qt::UniqueConnection);
151 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
152                 connect(tcpSocket, &QTcpSocket::errorOccurred, this, &PHD2::displayError, Qt::UniqueConnection);
153 #else
154                 connect(tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this,
155                         SLOT(displayError(QAbstractSocket::SocketError)));
156 #endif
157                 tcpSocket->connectToHost(Options::pHD2Host(), Options::pHD2Port());
158             }
159         }
160         else if (tcpSocket->state() == QTcpSocket::ConnectedState)
161         {
162             m_PHD2ReconnectCounter = 0;
163             checkIfEquipmentConnected();
164             requestAppState();
165         }
166     });
167 }
168 
~PHD2()169 PHD2::~PHD2()
170 {
171     delete abortTimer;
172     delete ditherTimer;
173 }
174 
Connect()175 bool PHD2::Connect()
176 {
177     switch (connection)
178     {
179         case DISCONNECTED:
180             // Not yet connected, let's connect server
181             connection = CONNECTING;
182             emit newLog(i18n("Connecting to PHD2 Host: %1, on port %2. . .", Options::pHD2Host(), Options::pHD2Port()));
183 
184             connect(tcpSocket, &QTcpSocket::readyRead, this, &PHD2::readPHD2, Qt::UniqueConnection);
185 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
186             connect(tcpSocket, &QTcpSocket::errorOccurred, this, &PHD2::displayError, Qt::UniqueConnection);
187 #else
188             connect(tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this,
189                     SLOT(displayError(QAbstractSocket::SocketError)));
190 #endif
191 
192             tcpSocket->connectToHost(Options::pHD2Host(), Options::pHD2Port());
193 
194             m_PHD2ReconnectCounter = 0;
195             stateTimer->start(PHD2_RECONNECT_TIMEOUT);
196             return true;
197 
198         case EQUIPMENT_DISCONNECTED:
199             // Equipment disconnected from PHD2, request reconnection
200             connectEquipment(true);
201             return true;
202 
203         case DISCONNECTING:
204             // Not supposed to interrupt a running disconnection
205             return false;
206 
207         default:
208             return false;
209     }
210 }
211 
ResetConnectionState()212 void PHD2::ResetConnectionState()
213 {
214     connection = DISCONNECTED;
215 
216     // clear the outstanding and queued RPC requests
217     pendingRpcResultType = NO_RESULT;
218     rpcRequestQueue.clear();
219 
220     starImageRequested = false;
221     isSettling = false;
222     isDitherActive = false;
223 
224     ditherTimer->stop();
225     abortTimer->stop();
226 
227     tcpSocket->disconnect(this);
228 
229     emit newStatus(GUIDE_DISCONNECTED);
230 }
231 
Disconnect()232 bool PHD2::Disconnect()
233 {
234     switch (connection)
235     {
236         case EQUIPMENT_CONNECTED:
237             emit newLog(i18n("Aborting any capture before disconnecting equipment..."));
238             abort();
239             connection = DISCONNECTING;
240             break;
241 
242         case CONNECTED:
243         case CONNECTING:
244         case EQUIPMENT_DISCONNECTED:
245             stateTimer->stop();
246             tcpSocket->disconnectFromHost();
247             ResetConnectionState();
248             if (tcpSocket->state() != QAbstractSocket::UnconnectedState)
249                 tcpSocket->waitForDisconnected(5000);
250             emit newLog(i18n("Disconnected from PHD2 Host: %1, on port %2.", Options::pHD2Host(), Options::pHD2Port()));
251             break;
252 
253         case DISCONNECTING:
254         case DISCONNECTED:
255             break;
256     }
257 
258     return true;
259 }
260 
displayError(QAbstractSocket::SocketError socketError)261 void PHD2::displayError(QAbstractSocket::SocketError socketError)
262 {
263     switch (socketError)
264     {
265         case QAbstractSocket::RemoteHostClosedError:
266             emit newLog(i18n("The host disconnected."));
267             break;
268         case QAbstractSocket::HostNotFoundError:
269             emit newLog(i18n("The host was not found. Please check the host name and port settings in Guide options."));
270             break;
271         case QAbstractSocket::ConnectionRefusedError:
272             emit newLog(i18n("The connection was refused by the peer. Make sure the PHD2 is running, and check that "
273                              "the host name and port settings are correct."));
274             break;
275         default:
276             emit newLog(i18n("The following error occurred: %1.", tcpSocket->errorString()));
277     }
278 
279     ResetConnectionState();
280 
281     emit newStatus(GUIDE_DISCONNECTED);
282 }
283 
readPHD2()284 void PHD2::readPHD2()
285 {
286     while (!tcpSocket->atEnd() && tcpSocket->canReadLine())
287     {
288         QByteArray line = tcpSocket->readLine();
289         if (line.isEmpty())
290             continue;
291 
292         QJsonParseError qjsonError;
293 
294         QJsonDocument jdoc = QJsonDocument::fromJson(line, &qjsonError);
295 
296         if (qjsonError.error != QJsonParseError::NoError)
297         {
298             emit newLog(i18n("PHD2: invalid response received: %1", QString(line)));
299             emit newLog(i18n("PHD2: JSON error: %1", qjsonError.errorString()));
300             continue;
301         }
302 
303         QJsonObject jsonObj = jdoc.object();
304 
305         if (jsonObj.contains("Event"))
306             processPHD2Event(jsonObj, line);
307         else if (jsonObj.contains("error"))
308             processPHD2Error(jsonObj, line);
309         else if (jsonObj.contains("result"))
310             processPHD2Result(jsonObj, line);
311     }
312 }
313 
processPHD2Event(const QJsonObject & jsonEvent,const QByteArray & line)314 void PHD2::processPHD2Event(const QJsonObject &jsonEvent, const QByteArray &line)
315 {
316     if (Options::verboseLogging())
317         qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: event:" << line;
318 
319     QString eventName = jsonEvent["Event"].toString();
320 
321     if (!events.contains(eventName))
322     {
323         emit newLog(i18n("Unknown PHD2 event: %1", eventName));
324         return;
325     }
326 
327     event = events.value(eventName);
328 
329     switch (event)
330     {
331         case Version:
332             emit newLog(i18n("PHD2: Version %1", jsonEvent["PHDVersion"].toString()));
333             break;
334 
335         case CalibrationComplete:
336             emit newLog(i18n("PHD2: Calibration Complete."));
337             emit newStatus(Ekos::GUIDE_CALIBRATION_SUCESS);
338             break;
339 
340         case StartGuiding:
341             updateGuideParameters();
342             requestCurrentEquipmentUpdate();
343             // Do not report guiding as started because it will start scheduled capture before guiding is settled
344             // just print the log message and GUIDE_STARTED status will be set in SettleDone
345             // phd2 will always send SettleDone event
346             emit newLog(i18n("PHD2: Waiting for guiding to settle."));
347             break;
348 
349         case Paused:
350             handlePHD2AppState(PAUSED);
351             break;
352 
353         case StartCalibration:
354             handlePHD2AppState(CALIBRATING);
355             break;
356 
357         case AppState:
358             // AppState is the last of the initial messages received when we first connect to PHD2
359             processPHD2State(jsonEvent["State"].toString());
360             // if the equipment is not already connected, then try to connect it.
361             if (connection == CONNECTING)
362             {
363                 emit newLog("PHD2: Connecting equipment and external guider...");
364                 connectEquipment(true);
365             }
366             break;
367 
368         case CalibrationFailed:
369             emit newLog(i18n("PHD2: Calibration Failed (%1).", jsonEvent["Reason"].toString()));
370             handlePHD2AppState(STOPPED);
371             break;
372 
373         case CalibrationDataFlipped:
374             emit newLog(i18n("Calibration Data Flipped."));
375             break;
376 
377         case LoopingExposures:
378             handlePHD2AppState(LOOPING);
379             break;
380 
381         case LoopingExposuresStopped:
382             handlePHD2AppState(STOPPED);
383             break;
384 
385         case Calibrating:
386         case Settling:
387         case SettleBegin:
388             //This can happen for guiding or for dithering.  A Settle done event will arrive when it finishes.
389             break;
390 
391         case SettleDone:
392         {
393             bool error = false;
394 
395             if (jsonEvent["Status"].toInt() != 0)
396             {
397                 error = true;
398                 emit newLog(i18n("PHD2: Settling failed (%1).", jsonEvent["Error"].toString()));
399             }
400 
401             bool wasDithering = isDitherActive;
402 
403             isDitherActive = false;
404             isSettling = false;
405 
406             if (wasDithering)
407             {
408                 ditherTimer->stop();
409                 if (error && Options::ditherFailAbortsAutoGuide())
410                 {
411                     abort();
412                     emit newStatus(GUIDE_DITHERING_ERROR);
413                 }
414                 else
415                 {
416                     if (error)
417                         emit newLog(i18n("PHD2: There was a dithering error, but continue guiding."));
418 
419                     emit newStatus(Ekos::GUIDE_DITHERING_SUCCESS);
420                 }
421             }
422             else
423             {
424                 if (error)
425                 {
426                     emit newLog(i18n("PHD2: Settling failed, aborted."));
427                     emit newStatus(GUIDE_ABORTED);
428                 }
429                 else
430                 {
431                     // settle completed after "guide" command
432                     emit newLog(i18n("PHD2: Settling complete, Guiding Started."));
433                     emit newStatus(GUIDE_GUIDING);
434                 }
435             }
436         }
437         break;
438 
439         case StarSelected:
440             handlePHD2AppState(SELECTED);
441             break;
442 
443         case StarLost:
444             // If we lost the guide star, let the state and abort timers update our state
445             handlePHD2AppState(LOSTLOCK);
446             break;
447 
448         case GuidingStopped:
449             handlePHD2AppState(STOPPED);
450             break;
451 
452         case Resumed:
453             handlePHD2AppState(GUIDING);
454             break;
455 
456         case GuideStep:
457         {
458             // If we lost the guide star, let the state timer update our state
459             // Sometimes PHD2 is actually not guiding at that time, so we'll either resume or abort
460             if (state == LOSTLOCK)
461                 emit newLog(i18n("PHD2: Star found, guiding is resuming..."));
462 
463             if (isDitherActive)
464                 return;
465 
466             double diff_ra_pixels, diff_de_pixels, diff_ra_arcsecs, diff_de_arcsecs, pulse_ra, pulse_dec, snr;
467             QString RADirection, DECDirection;
468             diff_ra_pixels = jsonEvent["RADistanceRaw"].toDouble();
469             diff_de_pixels = jsonEvent["DECDistanceRaw"].toDouble();
470             pulse_ra = jsonEvent["RADuration"].toDouble();
471             pulse_dec = jsonEvent["DECDuration"].toDouble();
472             RADirection = jsonEvent["RADirection"].toString();
473             DECDirection = jsonEvent["DECDirection"].toString();
474             snr = jsonEvent["SNR"].toDouble();
475 
476             if (RADirection == "East")
477                 pulse_ra = -pulse_ra;  //West Direction is Positive, East is Negative
478             if (DECDirection == "South")
479                 pulse_dec = -pulse_dec; //South Direction is Negative, North is Positive
480 
481             //If the pixelScale is properly set from PHD2, the second block of code is not needed, but if not, we will attempt to calculate the ra and dec error without it.
482             if (pixelScale != 0)
483             {
484                 diff_ra_arcsecs = diff_ra_pixels * pixelScale;
485                 diff_de_arcsecs = diff_de_pixels * pixelScale;
486             }
487             else
488             {
489                 diff_ra_arcsecs = 206.26480624709 * diff_ra_pixels * ccdPixelSizeX / mountFocalLength;
490                 diff_de_arcsecs = 206.26480624709 * diff_de_pixels * ccdPixelSizeY / mountFocalLength;
491             }
492 
493             if (std::isfinite(snr))
494                 emit newSNR(snr);
495 
496             if (std::isfinite(diff_ra_arcsecs) && std::isfinite(diff_de_arcsecs))
497             {
498                 errorLog.append(QPointF(diff_ra_arcsecs, diff_de_arcsecs));
499                 if(errorLog.size() > 50)
500                     errorLog.remove(0);
501 
502                 emit newAxisDelta(diff_ra_arcsecs, diff_de_arcsecs);
503                 emit newAxisPulse(pulse_ra, pulse_dec);
504 
505                 // Does PHD2 real a sky background or num-stars measure?
506                 emit guideStats(diff_ra_arcsecs, diff_de_arcsecs, pulse_ra, pulse_dec,
507                                 std::isfinite(snr) ? snr : 0, 0, 0);
508 
509                 double total_sqr_RA_error = 0.0;
510                 double total_sqr_DE_error = 0.0;
511 
512                 for (auto &point : errorLog)
513                 {
514                     total_sqr_RA_error += point.x() * point.x();
515                     total_sqr_DE_error += point.y() * point.y();
516                 }
517 
518                 emit newAxisSigma(sqrt(total_sqr_RA_error / errorLog.size()), sqrt(total_sqr_DE_error / errorLog.size()));
519 
520             }
521             //Note that if it is receiving full size remote images, it should not get the guide star image.
522             //But if it is not getting the full size images, or if the current camera is not in Ekos, it should get the guide star image
523             //If we are getting the full size image, we will want to know the lock position for the image that loads in the viewer.
524             if ( Options::guideSubframeEnabled() || currentCameraIsNotInEkos )
525                 requestStarImage(32); //This requests a star image for the guide view.  32 x 32 pixels
526             else
527                 requestLockPosition();
528         }
529         break;
530 
531         case GuidingDithered:
532             break;
533 
534         case LockPositionSet:
535             handlePHD2AppState(SELECTED);
536             break;
537 
538         case LockPositionLost:
539             handlePHD2AppState(LOSTLOCK);
540             break;
541 
542         case Alert:
543             emit newLog(i18n("PHD2 %1: %2", jsonEvent["Type"].toString(), jsonEvent["Msg"].toString()));
544             break;
545 
546         case GuideParamChange:
547         case ConfigurationChange:
548             //Don't do anything for now, might change this later.
549             //Some Possible Parameter Names:
550             //Backlash comp enabled, Backlash comp amount,
551             //For Each Axis: MinMove, Max Duration,
552             //PPEC aggressiveness, PPEC prediction weight,
553             //Resist switch minimum motion, Resist switch aggression,
554             //Low-pass minimum move, Low-pass slope weight,
555             //Low-pass2 minimum move, Low-pass2 aggressiveness,
556             //Hysteresis hysteresis, Hysteresis aggression
557             break;
558 
559     }
560 }
561 
processPHD2State(const QString & phd2State)562 void PHD2::processPHD2State(const QString &phd2State)
563 {
564     if (phd2State == "Stopped")
565         handlePHD2AppState(STOPPED);
566     else if (phd2State == "Selected")
567         handlePHD2AppState(SELECTED);
568     else if (phd2State == "Calibrating")
569         handlePHD2AppState(CALIBRATING);
570     else if (phd2State == "Guiding")
571         handlePHD2AppState(GUIDING);
572     else if (phd2State == "LostLock")
573         handlePHD2AppState(LOSTLOCK);
574     else if (phd2State == "Paused")
575         handlePHD2AppState(PAUSED);
576     else if (phd2State == "Looping")
577         handlePHD2AppState(LOOPING);
578     else emit newLog(QString("PHD2: Unsupported app state ") + phd2State + ".");
579 }
580 
handlePHD2AppState(PHD2State newstate)581 void PHD2::handlePHD2AppState(PHD2State newstate)
582 {
583     // do not handle the same state twice
584     if (state == newstate)
585         return;
586 
587     switch (newstate)
588     {
589         case STOPPED:
590             switch (state)
591             {
592                 case CALIBRATING:
593                     //emit newLog(i18n("PHD2: Calibration Failed (%1).", jsonEvent["Reason"].toString()));
594                     emit newStatus(Ekos::GUIDE_CALIBRATION_ERROR);
595                     break;
596                 case LOOPING:
597                     emit newLog(i18n("PHD2: Looping Exposures Stopped."));
598                     emit newStatus(Ekos::GUIDE_IDLE);
599                     break;
600                 case GUIDING:
601                 case LOSTLOCK:
602                     emit newLog(i18n("PHD2: Guiding Stopped."));
603                     emit newStatus(Ekos::GUIDE_ABORTED);
604                     break;
605                 default:
606                     if (connection == DISCONNECTING)
607                     {
608                         emit newLog("PHD2: Disconnecting equipment and external guider...");
609                         connectEquipment(false);
610                     }
611                     break;
612             }
613             break;
614 
615         case SELECTED:
616             switch (state)
617             {
618                 case STOPPED:
619                 case CALIBRATING:
620                 case GUIDING:
621                     emit newLog(i18n("PHD2: Lock Position Set."));
622                     if (isSettling)
623                     {
624                         newstate = CALIBRATING;
625                         emit newStatus(Ekos::GUIDE_CALIBRATING);
626                     }
627                     break;
628                 default:
629                     emit newLog(i18n("PHD2: Star Selected."));
630                     emit newStatus(GUIDE_STAR_SELECT);
631             }
632             break;
633 
634         case GUIDING:
635             switch (state)
636             {
637                 case PAUSED:
638                     emit newLog(i18n("PHD2: Guiding resumed."));
639                     abortTimer->stop();
640                     emit newStatus(Ekos::GUIDE_GUIDING);
641                     break;
642                 default:
643                     emit newLog(i18n("PHD2: Guiding started."));
644                     abortTimer->stop();
645                     emit newStatus(Ekos::GUIDE_GUIDING);
646                     break;
647             }
648             break;
649 
650         case LOSTLOCK:
651             switch (state)
652             {
653                 case CALIBRATING:
654                     emit newLog(i18n("PHD2: Lock Position Lost, continuing calibration."));
655                     // Don't be paranoid, accept star-lost events during calibration and trust PHD2 to complete
656                     //newstate = STOPPED;
657                     //emit newStatus(Ekos::GUIDE_CALIBRATION_ERROR);
658                     break;
659                 case GUIDING:
660                     emit newLog(i18n("PHD2: Star Lost. Trying to reacquire for %1s.", Options::guideLostStarTimeout()));
661                     abortTimer->start(static_cast<int>(Options::guideLostStarTimeout()) * 1000);
662                     qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: Lost star timeout started (" << Options::guideLostStarTimeout() << " sec)";
663                     emit newStatus(Ekos::GUIDE_REACQUIRE);
664                     break;
665                 default:
666                     emit newLog(i18n("PHD2: Lock Position Lost."));
667                     break;
668             }
669             break;
670 
671         case PAUSED:
672             emit newLog(i18n("PHD2: Guiding paused."));
673             emit newStatus(GUIDE_SUSPENDED);
674             break;
675 
676         case CALIBRATING:
677             emit newLog(i18n("PHD2: Calibrating, timing out in %1s.", Options::guideCalibrationTimeout()));
678             abortTimer->start(static_cast<int>(Options::guideCalibrationTimeout()) * 1000);
679             emit newStatus(GUIDE_CALIBRATING);
680             break;
681 
682         case LOOPING:
683             switch (state)
684             {
685                 case CALIBRATING:
686                     emit newLog(i18n("PHD2: Calibration turned to looping, failed."));
687                     emit newStatus(GUIDE_CALIBRATION_ERROR);
688                     break;
689                 default:
690                     emit newLog(i18n("PHD2: Looping Exposures."));
691                     emit newStatus(GUIDE_LOOPING);
692                     break;
693             }
694             break;
695     }
696 
697     state = newstate;
698 }
699 
processPHD2Result(const QJsonObject & jsonObj,const QByteArray & line)700 void PHD2::processPHD2Result(const QJsonObject &jsonObj, const QByteArray &line)
701 {
702     PHD2ResultType resultType = takeRequestFromList(jsonObj);
703 
704     if (resultType == STAR_IMAGE)
705         qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: received star image response, id" <<
706                                    jsonObj["id"].toInt();   // don't spam the log with image data
707     else
708         qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: response:" << line;
709 
710     switch (resultType)
711     {
712         case NO_RESULT:
713             //Ekos didn't ask for this result?
714             break;
715 
716         case CAPTURE_SINGLE_FRAME:                  //capture_single_frame
717             break;
718 
719         case CLEAR_CALIBRATION_COMMAND_RECEIVED:    //clear_calibration
720             emit newLog(i18n("PHD2: Calibration is cleared"));
721             break;
722 
723         case DITHER_COMMAND_RECEIVED:               //dither
724             emit newStatus(Ekos::GUIDE_DITHERING);
725             break;
726 
727         //find_star
728         //flip_calibration
729         //get_algo_param_names
730         //get_algo_param
731 
732         case APP_STATE_RECEIVED:                    //get_app_state
733         {
734             QString state = jsonObj["State"].toString();
735             if (state.isEmpty())
736                 state = jsonObj["result"].toString();
737             if (state.isEmpty())
738                 qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: received unsupported app state";
739             else
740                 processPHD2State(state);
741         }
742         break;
743 
744         //get_calibrated
745         //get_calibration_data
746 
747         case IS_EQUIPMENT_CONNECTED:                //get_connected
748         {
749             bool isConnected = jsonObj["result"].toBool();
750             switch (connection)
751             {
752                 case CONNECTING:
753                     // We just plugged in server, request equipment connection if needed
754                     if (isConnected)
755                     {
756                         connection = CONNECTED;
757                         setEquipmentConnected();
758                     }
759                     else connectEquipment(true);
760                     break;
761 
762                 case CONNECTED:
763                     // We were waiting for equipment to be connected after plugging in server
764                     if (isConnected)
765                         setEquipmentConnected();
766                     break;
767 
768                 case DISCONNECTING:
769                     // We were waiting for equipment to be disconnected before unplugging from server
770                     if (!isConnected)
771                     {
772                         connection = EQUIPMENT_DISCONNECTED;
773                         Disconnect();
774                     }
775                     else connectEquipment(false);
776                     break;
777 
778                 case EQUIPMENT_CONNECTED:
779                     // Equipment was disconnected from PHD2 side, so notify clients and wait.
780                     if (!isConnected)
781                     {
782                         // TODO: setEquipmentDisconnected()
783                         connection = EQUIPMENT_DISCONNECTED;
784                         emit newStatus(Ekos::GUIDE_DISCONNECTED);
785                     }
786                     break;
787 
788                 case DISCONNECTED:
789                 case EQUIPMENT_DISCONNECTED:
790                     // Equipment was connected from PHD2 side, so notify clients and wait.
791                     if (isConnected)
792                         setEquipmentConnected();
793                     break;
794             }
795         }
796         break;
797 
798         //get_cooler_status
799         case GET_CURRENT_EQUIPMENT:                 //get_current_equipment
800         {
801             QJsonObject equipObject = jsonObj["result"].toObject();
802             currentCamera = equipObject["camera"].toObject()["name"].toString();
803             currentMount = equipObject["mount"].toObject()["name"].toString();
804             currentAuxMount = equipObject["aux_mount"].toObject()["name"].toString();
805 
806             emit guideEquipmentUpdated();
807 
808             break;
809         }
810 
811 
812         case DEC_GUIDE_MODE:                        //get_dec_guide_mode
813         {
814             QString mode = jsonObj["result"].toString();
815             Ekos::Manager::Instance()->guideModule()->updateDirectionsFromPHD2(mode);
816             emit newLog(i18n("PHD2: DEC Guide Mode is Set to: %1", mode));
817         }
818         break;
819 
820 
821         case EXPOSURE_TIME:                         //get_exposure
822         {
823             int exposurems = jsonObj["result"].toInt();
824             double exposureTime = exposurems / 1000.0;
825             Ekos::Manager::Instance()->guideModule()->setExposure(exposureTime);
826             emit newLog(i18n("PHD2: Exposure Time set to: ") + QString::number(exposureTime, 'f', 2));
827             break;
828         }
829 
830 
831         case EXPOSURE_DURATIONS:                    //get_exposure_durations
832         {
833             QVariantList exposureListArray = jsonObj["result"].toArray().toVariantList();
834             logValidExposureTimes = i18n("PHD2: Valid Exposure Times: Auto, ");
835             QList<double> values;
836             for(int i = 1; i < exposureListArray.size();
837                     i ++) //For some reason PHD2 has a negative exposure time of 1 at the start of the array?
838                 values << exposureListArray.at(i).toDouble() / 1000.0; //PHD2 reports in ms.
839             logValidExposureTimes += Ekos::Manager::Instance()->guideModule()->setRecommendedExposureValues(values);
840             emit newLog(logValidExposureTimes);
841             break;
842         }
843         case LOCK_POSITION:                         //get_lock_position
844         {
845             if(jsonObj["result"].toArray().count() == 2)
846             {
847                 double x  = jsonObj["result"].toArray().at(0).toDouble();
848                 double y  = jsonObj["result"].toArray().at(1).toDouble();
849                 QVector3D newStarCenter(x, y, 0);
850                 emit newStarPosition(newStarCenter, true);
851 
852                 //This is needed so that PHD2 sends the new star pixmap when
853                 //remote images are enabled.
854                 emit newStarPixmap(guideFrame->getTrackingBoxPixmap());
855             }
856             break;
857         }
858         //get_lock_shift_enabled
859         //get_lock_shift_params
860         //get_paused
861 
862         case PIXEL_SCALE:                           //get_pixel_scale
863             pixelScale = jsonObj["result"].toDouble();
864             if (pixelScale == 0)
865                 emit newLog(i18n("PHD2: Please set CCD and telescope parameters in PHD2, Pixel Scale is invalid."));
866             else
867                 emit newLog(i18n("PHD2: Pixel Scale is %1 arcsec per pixel", QString::number(pixelScale, 'f', 2)));
868             break;
869 
870         //get_profile
871         //get_profiles
872         //get_search_region
873         //get_sensor_temperature
874 
875         case STAR_IMAGE:                            //get_star_image
876         {
877             starImageRequested = false;
878             QJsonObject jsonResult = jsonObj["result"].toObject();
879             processStarImage(jsonResult);
880             break;
881         }
882 
883         //get_use_subframes
884 
885         case GUIDE_COMMAND_RECEIVED:                //guide
886             if (0 != jsonObj["result"].toInt(0))
887             {
888                 emit newLog("PHD2: Guide command was rejected.");
889                 handlePHD2AppState(STOPPED);
890             }
891             break;
892 
893         //guide_pulse
894 
895         case LOOP:                                  //loop
896             handlePHD2AppState(jsonObj["result"].toBool() ? LOOPING : STOPPED);
897             break;
898 
899         //save_image
900         //set_algo_param
901 
902         case CONNECTION_RESULT:                     //set_connected
903             checkIfEquipmentConnected();
904             break;
905 
906         case SET_DEC_GUIDE_MODE_COMMAND_RECEIVED:   //set_dec_guide_mode
907             checkDEGuideMode();
908             break;
909 
910         case SET_EXPOSURE_COMMAND_RECEIVED:         //set_exposure
911             requestExposureTime(); //This will check what it was set to and print a message as to what it is.
912             break;
913 
914         case SET_LOCK_POSITION:                     //set_lock_position
915             handlePHD2AppState(SELECTED);
916             break;
917 
918         //set_lock_shift_enabled
919         //set_lock_shift_params
920 
921         case SET_PAUSED_COMMAND_RECEIVED:           //set_paused
922             handlePHD2AppState(PAUSED);
923             break;
924         //set_profile
925         //shutdown
926 
927         case STOP_CAPTURE_COMMAND_RECEIVED:         //stop_capture
928             handlePHD2AppState(STOPPED);
929             //emit newStatus(GUIDE_ABORTED);
930             break;
931     }
932 
933     // send the next pending call
934     sendNextRpcCall();
935 }
936 
processPHD2Error(const QJsonObject & jsonError,const QByteArray & line)937 void PHD2::processPHD2Error(const QJsonObject &jsonError, const QByteArray &line)
938 {
939     qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: error:" << line;
940 
941     QJsonObject jsonErrorObject = jsonError["error"].toObject();
942 
943     PHD2ResultType resultType = takeRequestFromList(jsonError);
944 
945     // This means the user mistakenly entered an invalid exposure time.
946     switch (resultType)
947     {
948         case SET_EXPOSURE_COMMAND_RECEIVED:
949             emit newLog(logValidExposureTimes);  //This will let the user know the valid exposure durations
950             QTimer::singleShot(300, [ = ] {requestExposureTime();}); //This will reset the Exposure time in Ekos to PHD2's current exposure time after a third of a second.
951             break;
952 
953         case CONNECTION_RESULT:
954             connection = EQUIPMENT_DISCONNECTED;
955             emit newStatus(Ekos::GUIDE_DISCONNECTED);
956             break;
957 
958         case DITHER_COMMAND_RECEIVED:
959             ditherTimer->stop();
960             isSettling = false;
961             isDitherActive = false;
962             emit newStatus(GUIDE_DITHERING_ERROR);
963 
964             if (Options::ditherFailAbortsAutoGuide())
965             {
966                 abort();
967                 emit newLog("PHD2: failing after dithering aborts.");
968                 emit newStatus(GUIDE_ABORTED);
969             }
970             else
971             {
972                 // !FIXME-ag why is this trying to resume (un-pause)?
973                 resume();
974             }
975             break;
976 
977         case GUIDE_COMMAND_RECEIVED:
978             isSettling = false;
979             break;
980 
981         default:
982             emit newLog(i18n("PHD2 Error: unhandled '%1'", jsonErrorObject["message"].toString()));
983             break;
984     }
985 
986     // send the next pending call
987     sendNextRpcCall();
988 }
989 
990 //These methods process the Star Images the PHD2 provides
991 
setGuideView(FITSView * guideView)992 void PHD2::setGuideView(FITSView *guideView)
993 {
994     guideFrame = guideView;
995 }
996 
processStarImage(const QJsonObject & jsonStarFrame)997 void PHD2::processStarImage(const QJsonObject &jsonStarFrame)
998 {
999     //The width and height of the received PHD2 Star Image
1000     int width =  jsonStarFrame["width"].toInt();
1001     int height = jsonStarFrame["height"].toInt();
1002 
1003     //This section sets up the FITS File
1004     fitsfile *fptr = nullptr;
1005     int status = 0;
1006     long fpixel = 1, naxis = 2, nelements, exposure;
1007     long naxes[2] = { width, height };
1008     char error_status[512] = {0};
1009 
1010     void* fits_buffer = nullptr;
1011     size_t fits_buffer_size = 0;
1012     if (fits_create_memfile(&fptr, &fits_buffer, &fits_buffer_size, 4096, realloc, &status))
1013     {
1014         qCWarning(KSTARS_EKOS_GUIDE) << "fits_create_file failed:" << error_status;
1015         return;
1016     }
1017 
1018     if (fits_create_img(fptr, USHORT_IMG, naxis, naxes, &status))
1019     {
1020         qCWarning(KSTARS_EKOS_GUIDE) << "fits_create_img failed:" << error_status;
1021         status = 0;
1022         fits_close_file(fptr, &status);
1023         free(fits_buffer);
1024         return;
1025     }
1026 
1027     //Note, this is made up.  If you want the actual exposure time, you have to request it from PHD2
1028     exposure = 1;
1029     fits_update_key(fptr, TLONG, "EXPOSURE", &exposure, "Total Exposure Time", &status);
1030 
1031     //This section takes the Pixels from the JSON Document
1032     //Then it converts from base64 to a QByteArray
1033     //Then it creates a datastream from the QByteArray to the pixel array for the FITS File
1034     QByteArray converted = QByteArray::fromBase64(jsonStarFrame["pixels"].toString().toLocal8Bit());
1035 
1036     //This finishes up and closes the FITS file
1037     nelements = naxes[0] * naxes[1];
1038     if (fits_write_img(fptr, TUSHORT, fpixel, nelements, converted.data(), &status))
1039     {
1040         fits_get_errstatus(status, error_status);
1041         qCWarning(KSTARS_EKOS_GUIDE) << "fits_write_img failed:" << error_status;
1042         status = 0;
1043         fits_close_file(fptr, &status);
1044         free(fits_buffer);
1045         return;
1046     }
1047 
1048     if (fits_flush_file(fptr, &status))
1049     {
1050         fits_get_errstatus(status, error_status);
1051         qCWarning(KSTARS_EKOS_GUIDE) << "fits_flush_file failed:" << error_status;
1052         status = 0;
1053         fits_close_file(fptr, &status);
1054         free(fits_buffer);
1055         return;
1056     }
1057 
1058     if (fits_close_file(fptr, &status))
1059     {
1060         fits_get_errstatus(status, error_status);
1061         qCWarning(KSTARS_EKOS_GUIDE) << "fits_close_file failed:" << error_status;
1062         free(fits_buffer);
1063         return;
1064     }
1065 
1066     //This loads the FITS file in the Guide FITSView
1067     //Then it updates the Summary Screen
1068     QSharedPointer<FITSData> fdata;
1069     QByteArray buffer = QByteArray::fromRawData(reinterpret_cast<char *>(fits_buffer), fits_buffer_size);
1070     fdata.reset(new FITSData(), &QObject::deleteLater);
1071     fdata->loadFromBuffer(buffer, "fits");
1072     free(fits_buffer);
1073     guideFrame->loadData(fdata);
1074 
1075     guideFrame->updateFrame();
1076     guideFrame->setTrackingBox(QRect(0, 0, width, height));
1077     emit newStarPixmap(guideFrame->getTrackingBoxPixmap());
1078 }
1079 
setEquipmentConnected()1080 void PHD2::setEquipmentConnected()
1081 {
1082     if (connection != EQUIPMENT_CONNECTED)
1083     {
1084         setConnectedRetries = 0;
1085         connection = EQUIPMENT_CONNECTED;
1086         emit newStatus(Ekos::GUIDE_CONNECTED);
1087         updateGuideParameters();
1088         requestExposureDurations();
1089         requestCurrentEquipmentUpdate();
1090     }
1091 }
1092 
updateGuideParameters()1093 void PHD2::updateGuideParameters()
1094 {
1095     if (pixelScale == 0)
1096         requestPixelScale();
1097     requestExposureTime();
1098     checkDEGuideMode();
1099 }
1100 
1101 //This section handles the methods/requests sent to PHD2, some are not implemented.
1102 
1103 //capture_single_frame
captureSingleFrame()1104 void PHD2::captureSingleFrame()
1105 {
1106     sendPHD2Request("capture_single_frame");
1107 }
1108 
1109 //clear_calibration
clearCalibration()1110 bool PHD2::clearCalibration()
1111 {
1112     if (connection != EQUIPMENT_CONNECTED)
1113     {
1114         emit newLog(i18n("PHD2 Error: Equipment not connected."));
1115         emit newStatus(Ekos::GUIDE_ABORTED);
1116         return false;
1117     }
1118 
1119     QJsonArray args;
1120     //This instructs PHD2 which calibration to clear.
1121     args << "mount";
1122     sendPHD2Request("clear_calibration", args);
1123 
1124     return true;
1125 }
1126 
1127 //dither
dither(double pixels)1128 bool PHD2::dither(double pixels)
1129 {
1130     if (connection != EQUIPMENT_CONNECTED)
1131     {
1132         emit newLog(i18n("PHD2 Error: Equipment not connected."));
1133         emit newStatus(Ekos::GUIDE_ABORTED);
1134         return false;
1135     }
1136 
1137     if (isSettling)
1138     {
1139         qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: ignoring dither requested while already settling";
1140 
1141         if (!isDitherActive)
1142         {
1143             // act like we just dithered so we get the appropriate
1144             // effects after the settling completes
1145             emit newStatus(Ekos::GUIDE_DITHERING);
1146             isDitherActive = true;
1147         }
1148         return true;
1149     }
1150 
1151     QJsonArray args;
1152     QJsonObject settle;
1153 
1154     int ditherTimeout = static_cast<int>(Options::ditherTimeout());
1155 
1156     settle.insert("pixels", static_cast<double>(Options::ditherThreshold()));
1157     settle.insert("time", static_cast<int>(Options::ditherSettle()));
1158     settle.insert("timeout", ditherTimeout);
1159 
1160     // Pixels
1161     args << pixels;
1162     // RA Only?
1163     args << false;
1164     // Settle
1165     args << settle;
1166 
1167     isSettling = true;
1168     isDitherActive = true;
1169 
1170     // PHD2 will send a SettleDone event shortly after the settling
1171     // timeout in PHD2. We don't really need a timer here, but we'll
1172     // set one anyway (belt and suspenders). Make sure to give an
1173     // extra time allowance since PHD2 won't report its timeout until
1174     // the completion of the next guide exposure after the timeout
1175     // period expires.
1176     enum { TIMEOUT_EXTRA_SECONDS = 60 };  // at least as long as any reasonable guide exposure
1177     int millis = (ditherTimeout + TIMEOUT_EXTRA_SECONDS) * 1000;
1178     ditherTimer->start(millis);
1179 
1180     sendPHD2Request("dither", args);
1181 
1182     emit newStatus(Ekos::GUIDE_DITHERING);
1183 
1184     return true;
1185 }
1186 
1187 //find_star
1188 //flip_calibration
1189 //get_algo_param_names
1190 //get_algo_param
1191 
1192 //get_app_state
requestAppState()1193 void PHD2::requestAppState()
1194 {
1195     sendPHD2Request("get_app_state");
1196 }
1197 
1198 //get_calibrated
1199 //get_calibration_data
1200 
1201 //get_connected
checkIfEquipmentConnected()1202 void PHD2::checkIfEquipmentConnected()
1203 {
1204     sendPHD2Request("get_connected");
1205 }
1206 
1207 //get_cooler_status
1208 //get_current_equipment
requestCurrentEquipmentUpdate()1209 void PHD2::requestCurrentEquipmentUpdate()
1210 {
1211     sendPHD2Request("get_current_equipment");
1212 }
1213 
1214 //get_dec_guide_mode
checkDEGuideMode()1215 void PHD2::checkDEGuideMode()
1216 {
1217     sendPHD2Request("get_dec_guide_mode");
1218 }
1219 
1220 //get_exposure
requestExposureTime()1221 void PHD2::requestExposureTime()
1222 {
1223     sendPHD2Request("get_exposure");
1224 }
1225 
1226 //get_exposure_durations
requestExposureDurations()1227 void PHD2::requestExposureDurations()
1228 {
1229     sendPHD2Request("get_exposure_durations");
1230 }
1231 
1232 //get_lock_position
requestLockPosition()1233 void PHD2::requestLockPosition()
1234 {
1235     sendPHD2Request("get_lock_position");
1236 }
1237 //get_lock_shift_enabled
1238 //get_lock_shift_params
1239 //get_paused
1240 
1241 //get_pixel_scale
requestPixelScale()1242 void PHD2::requestPixelScale()
1243 {
1244     sendPHD2Request("get_pixel_scale");
1245 }
1246 
1247 //get_profile
1248 //get_profiles
1249 //get_search_region
1250 //get_sensor_temperature
1251 
1252 //get_star_image
requestStarImage(int size)1253 void PHD2::requestStarImage(int size)
1254 {
1255     if (starImageRequested)
1256     {
1257         if (Options::verboseLogging())
1258             qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: skip extra star image request";
1259         return;
1260     }
1261 
1262     QJsonArray args2;
1263     args2 << size; // This is both the width and height.
1264     sendPHD2Request("get_star_image", args2);
1265 
1266     starImageRequested = true;
1267 }
1268 
1269 //get_use_subframes
1270 
1271 //guide
guide()1272 bool PHD2::guide()
1273 {
1274     if (state == GUIDING)
1275     {
1276         emit newLog(i18n("PHD2: Guiding is already running."));
1277         emit newStatus(Ekos::GUIDE_GUIDING);
1278         return true;
1279     }
1280 
1281     if (connection != EQUIPMENT_CONNECTED)
1282     {
1283         emit newLog(i18n("PHD2 Error: Equipment not connected."));
1284         emit newStatus(Ekos::GUIDE_ABORTED);
1285         return false;
1286     }
1287 
1288     QJsonArray args;
1289     QJsonObject settle;
1290 
1291     settle.insert("pixels", static_cast<double>(Options::ditherThreshold()));
1292     settle.insert("time", static_cast<int>(Options::ditherSettle()));
1293     settle.insert("timeout", static_cast<int>(Options::ditherTimeout()));
1294 
1295     // Settle param
1296     args << settle;
1297     // Recalibrate param
1298     args << false;
1299 
1300     errorLog.clear();
1301 
1302     isSettling = true;
1303     emit newStatus(GUIDE_CALIBRATING);
1304 
1305     sendPHD2Request("guide", args);
1306 
1307     return true;
1308 }
1309 
1310 //guide_pulse
1311 //loop
loop()1312 void PHD2::loop()
1313 {
1314     sendPHD2Request("loop");
1315 }
1316 //save_image
1317 //set_algo_param
1318 
1319 //set_connected
connectEquipment(bool enable)1320 void PHD2::connectEquipment(bool enable)
1321 {
1322     if (connection == EQUIPMENT_CONNECTED && enable == true)
1323         return;
1324 
1325     if (connection == EQUIPMENT_DISCONNECTED && enable == false)
1326         return;
1327 
1328     if (setConnectedRetries++ > MAX_SET_CONNECTED_RETRIES)
1329     {
1330         setConnectedRetries = 0;
1331         connection = EQUIPMENT_DISCONNECTED;
1332         emit newStatus(Ekos::GUIDE_DISCONNECTED);
1333         return;
1334     }
1335 
1336     pixelScale = 0 ;
1337 
1338     QJsonArray args;
1339 
1340     // connected = enable
1341     args << enable;
1342 
1343     if (enable)
1344         emit newLog(i18n("PHD2: Connecting Equipment. . ."));
1345     else
1346         emit newLog(i18n("PHD2: Disconnecting Equipment. . ."));
1347 
1348     sendPHD2Request("set_connected", args);
1349 }
1350 
1351 //set_dec_guide_mode
requestSetDEGuideMode(bool deEnabled,bool nEnabled,bool sEnabled)1352 void PHD2::requestSetDEGuideMode(bool deEnabled, bool nEnabled,
1353                                  bool sEnabled) //Possible Settings Off, Auto, North, and South
1354 {
1355     QJsonArray args;
1356 
1357     if(deEnabled)
1358     {
1359         if(nEnabled && sEnabled)
1360             args << "Auto";
1361         else if(nEnabled)
1362             args << "North";
1363         else if(sEnabled)
1364             args << "South";
1365         else
1366             args << "Off";
1367     }
1368     else
1369     {
1370         args << "Off";
1371     }
1372 
1373     sendPHD2Request("set_dec_guide_mode", args);
1374 }
1375 
1376 //set_exposure
requestSetExposureTime(int time)1377 void PHD2::requestSetExposureTime(int time) //Note: time is in milliseconds
1378 {
1379     QJsonArray args;
1380     args << time;
1381     sendPHD2Request("set_exposure", args);
1382 }
1383 
1384 //set_lock_position
setLockPosition(double x,double y)1385 void PHD2::setLockPosition(double x, double y)
1386 {
1387     // Note: false will mean if a guide star is near the coordinates, it will use that.
1388     QJsonArray args;
1389     args << x << y << false;
1390     sendPHD2Request("set_lock_position", args);
1391 }
1392 //set_lock_shift_enabled
1393 //set_lock_shift_params
1394 
1395 //set_paused
suspend()1396 bool PHD2::suspend()
1397 {
1398     if (connection != EQUIPMENT_CONNECTED)
1399     {
1400         emit newLog(i18n("PHD2 Error: Equipment not connected."));
1401         emit newStatus(Ekos::GUIDE_ABORTED);
1402         return false;
1403     }
1404 
1405     QJsonArray args;
1406 
1407     // Paused param
1408     args << true;
1409     // FULL param
1410     args << "full";
1411 
1412     sendPHD2Request("set_paused", args);
1413 
1414     if (abortTimer->isActive())
1415     {
1416         // Avoid that the a preceding lost star event leads to an abort while guiding is suspended.
1417         qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: Lost star timeout cancelled.";
1418         abortTimer->stop();
1419     }
1420 
1421     return true;
1422 }
1423 
1424 //set_paused (also)
resume()1425 bool PHD2::resume()
1426 {
1427     if (connection != EQUIPMENT_CONNECTED)
1428     {
1429         emit newLog(i18n("PHD2 Error: Equipment not connected."));
1430         emit newStatus(Ekos::GUIDE_ABORTED);
1431         return false;
1432     }
1433 
1434     QJsonArray args;
1435 
1436     // Paused param
1437     args << false;
1438 
1439     sendPHD2Request("set_paused", args);
1440 
1441     if (state == LOSTLOCK)
1442     {
1443         qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: Lost star timeout restarted.";
1444         abortTimer->start(static_cast<int>(Options::guideLostStarTimeout()) * 1000);
1445     }
1446 
1447     return true;
1448 }
1449 
1450 //set_profile
1451 //shutdown
1452 
1453 //stop_capture
abort()1454 bool PHD2::abort()
1455 {
1456     if (connection != EQUIPMENT_CONNECTED)
1457     {
1458         emit newLog(i18n("PHD2 Error: Equipment not connected."));
1459         emit newStatus(Ekos::GUIDE_ABORTED);
1460         return false;
1461     }
1462 
1463     abortTimer->stop();
1464 
1465     sendPHD2Request("stop_capture");
1466     return true;
1467 }
1468 
1469 //This method is not handled by PHD2
calibrate()1470 bool PHD2::calibrate()
1471 {
1472     // We don't explicitly do calibration since it is done in the guide step by PHD2 anyway
1473     //emit newStatus(Ekos::GUIDE_CALIBRATION_SUCESS);
1474     return true;
1475 }
1476 
1477 //This is how information requests and commands for PHD2 are handled
1478 
sendRpcCall(QJsonObject & call,PHD2ResultType resultType)1479 void PHD2::sendRpcCall(QJsonObject &call, PHD2ResultType resultType)
1480 {
1481     assert(resultType != NO_RESULT); // should be a real request
1482     assert(pendingRpcResultType == NO_RESULT);  // only one pending RPC call at a time
1483 
1484     int rpcId = nextRpcId++;
1485     call.insert("id", rpcId);
1486 
1487     pendingRpcId = rpcId;
1488     pendingRpcResultType = resultType;
1489 
1490     QByteArray request = QJsonDocument(call).toJson(QJsonDocument::Compact);
1491 
1492     qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: request:" << request;
1493 
1494     request.append("\r\n");
1495 
1496     if (tcpSocket->state() == QTcpSocket::ConnectedState)
1497     {
1498         qint64 const n = tcpSocket->write(request);
1499 
1500         if ((int) n != request.size())
1501         {
1502             qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: unexpected short write:" << n << "bytes of" << request.size();
1503         }
1504     }
1505 }
1506 
sendNextRpcCall()1507 void PHD2::sendNextRpcCall()
1508 {
1509     if (pendingRpcResultType != NO_RESULT)
1510         return; // a request is currently outstanding
1511 
1512     if (rpcRequestQueue.empty())
1513         return; // no queued requests
1514 
1515     RpcCall &call = rpcRequestQueue.front();
1516     sendRpcCall(call.call, call.resultType);
1517     rpcRequestQueue.pop_front();
1518 }
1519 
sendPHD2Request(const QString & method,const QJsonArray & args)1520 void PHD2::sendPHD2Request(const QString &method, const QJsonArray &args)
1521 {
1522     assert(methodResults.contains(method));
1523 
1524     PHD2ResultType resultType = methodResults[method];
1525 
1526     QJsonObject jsonRPC;
1527 
1528     jsonRPC.insert("jsonrpc", "2.0");
1529     jsonRPC.insert("method", method);
1530 
1531     if (!args.empty())
1532         jsonRPC.insert("params", args);
1533 
1534     if (pendingRpcResultType == NO_RESULT)
1535     {
1536         // no outstanding rpc call, send it right off
1537         sendRpcCall(jsonRPC, resultType);
1538     }
1539     else
1540     {
1541         // there is already an outstanding call, enqueue this call
1542         // until the prior call completes
1543 
1544         if (Options::verboseLogging())
1545             qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: defer call" << method;
1546 
1547         rpcRequestQueue.push_back(RpcCall(jsonRPC, resultType));
1548     }
1549 }
1550 
takeRequestFromList(const QJsonObject & response)1551 PHD2::PHD2ResultType PHD2::takeRequestFromList(const QJsonObject &response)
1552 {
1553     if (Q_UNLIKELY(!response.contains("id")))
1554     {
1555         qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: ignoring unexpected response with no id";
1556         return NO_RESULT;
1557     }
1558 
1559     int id = response["id"].toInt();
1560 
1561     if (Q_UNLIKELY(id != pendingRpcId))
1562     {
1563         // RPC id mismatch -- this should never happen, something is
1564         // seriously wrong
1565         qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: ignoring unexpected response with id" << id;
1566         return NO_RESULT;
1567     }
1568 
1569     PHD2ResultType val = pendingRpcResultType;
1570     pendingRpcResultType = NO_RESULT;
1571     return val;
1572 }
1573 
1574 }
1575