Kstars

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

KDE's Doxygen guidelines are available online.