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