File indexing completed on 2024-05-12 15:23:36

0001 /*
0002     SPDX-FileCopyrightText: 2016 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "phd2.h"
0008 
0009 #include "Options.h"
0010 #include "kspaths.h"
0011 #include "kstars.h"
0012 
0013 #include "ekos/manager.h"
0014 #include "fitsviewer/fitsdata.h"
0015 #include "ekos/guide/guide.h"
0016 #include "fitsviewer/fitsview.h"
0017 
0018 #include <cassert>
0019 #include <fitsio.h>
0020 #include <KMessageBox>
0021 #include <QImage>
0022 
0023 #include <QJsonDocument>
0024 #include <QNetworkReply>
0025 
0026 #include <ekos_guide_debug.h>
0027 
0028 #define MAX_SET_CONNECTED_RETRIES   3
0029 
0030 namespace Ekos
0031 {
0032 PHD2::PHD2()
0033 {
0034     tcpSocket = new QTcpSocket(this);
0035 
0036     //This list of available PHD Events is on https://github.com/OpenPHDGuiding/phd2/wiki/EventMonitoring
0037 
0038     events["Version"]                 = Version;
0039     events["LockPositionSet"]         = LockPositionSet;
0040     events["Calibrating"]             = Calibrating;
0041     events["CalibrationComplete"]     = CalibrationComplete;
0042     events["StarSelected"]            = StarSelected;
0043     events["StartGuiding"]            = StartGuiding;
0044     events["Paused"]                  = Paused;
0045     events["StartCalibration"]        = StartCalibration;
0046     events["AppState"]                = AppState;
0047     events["CalibrationFailed"]       = CalibrationFailed;
0048     events["CalibrationDataFlipped"]  = CalibrationDataFlipped;
0049     events["LoopingExposures"]        = LoopingExposures;
0050     events["LoopingExposuresStopped"] = LoopingExposuresStopped;
0051     events["SettleBegin"]             = SettleBegin;
0052     events["Settling"]                = Settling;
0053     events["SettleDone"]              = SettleDone;
0054     events["StarLost"]                = StarLost;
0055     events["GuidingStopped"]          = GuidingStopped;
0056     events["Resumed"]                 = Resumed;
0057     events["GuideStep"]               = GuideStep;
0058     events["GuidingDithered"]         = GuidingDithered;
0059     events["LockPositionLost"]        = LockPositionLost;
0060     events["Alert"]                   = Alert;
0061     events["GuideParamChange"]        = GuideParamChange;
0062     events["ConfigurationChange"]     = ConfigurationChange;
0063 
0064     //This list of available PHD Methods is on https://github.com/OpenPHDGuiding/phd2/wiki/EventMonitoring
0065     //Only some of the methods are implemented.  The ones that say COMMAND_RECEIVED simply return a 0 saying the command was received.
0066     methodResults["capture_single_frame"]   = CAPTURE_SINGLE_FRAME;
0067     methodResults["clear_calibration"]      = CLEAR_CALIBRATION_COMMAND_RECEIVED;
0068     methodResults["dither"]                 = DITHER_COMMAND_RECEIVED;
0069     //find_star
0070     //flip_calibration
0071     //get_algo_param_names
0072     //get_algo_param
0073     methodResults["get_app_state"]          = APP_STATE_RECEIVED;
0074     //get_calibrated
0075     //get_calibration_data
0076     methodResults["get_connected"]          = IS_EQUIPMENT_CONNECTED;
0077     //get_cooler_status
0078     methodResults["get_current_equipment"]  = GET_CURRENT_EQUIPMENT;
0079     methodResults["get_dec_guide_mode"]     = DEC_GUIDE_MODE;
0080     methodResults["get_exposure"]           = EXPOSURE_TIME;
0081     methodResults["get_exposure_durations"] = EXPOSURE_DURATIONS;
0082     methodResults["get_lock_position"]      = LOCK_POSITION;
0083     //get_lock_shift_enabled
0084     //get_lock_shift_params
0085     //get_paused
0086     methodResults["get_pixel_scale"]        = PIXEL_SCALE;
0087     //get_profile
0088     //get_profiles
0089     //get_search_region
0090     //get_sensor_temperature
0091     methodResults["get_star_image"]         = STAR_IMAGE;
0092     //get_use_subframes
0093     methodResults["guide"]                  = GUIDE_COMMAND_RECEIVED;
0094     //guide_pulse
0095     methodResults["loop"]                   = LOOP;
0096     //save_image
0097     //set_algo_param
0098     methodResults["set_connected"]          = CONNECTION_RESULT;
0099     methodResults["set_dec_guide_mode"]     = SET_DEC_GUIDE_MODE_COMMAND_RECEIVED;
0100     methodResults["set_exposure"]           = SET_EXPOSURE_COMMAND_RECEIVED;
0101     methodResults["set_lock_position"]      = SET_LOCK_POSITION;
0102     //set_lock_shift_enabled
0103     //set_lock_shift_params
0104     methodResults["set_paused"]             = SET_PAUSED_COMMAND_RECEIVED;
0105     //set_profile
0106     //shutdown
0107     methodResults["stop_capture"]           = STOP_CAPTURE_COMMAND_RECEIVED;
0108 
0109     abortTimer = new QTimer(this);
0110     connect(abortTimer, &QTimer::timeout, this, [ = ]
0111     {
0112         if (state == CALIBRATING)
0113             qCDebug(KSTARS_EKOS_GUIDE) << "Abort timeout expired while calibrating, retrying to guide.";
0114         else if (state == LOSTLOCK)
0115             qCDebug(KSTARS_EKOS_GUIDE) << "Abort timeout expired while reacquiring star, retrying to guide.";
0116         else
0117             qCDebug(KSTARS_EKOS_GUIDE) << "Abort timeout expired, stopping.";
0118         abort();
0119     });
0120 
0121     ditherTimer = new QTimer(this);
0122     connect(ditherTimer, &QTimer::timeout, this, [ = ]
0123     {
0124         qCDebug(KSTARS_EKOS_GUIDE) << "ditherTimer expired, state" << state << "dithering" << isDitherActive << "settling" << isSettling;
0125         ditherTimer->stop();
0126         isDitherActive = false;
0127         isSettling = false;
0128         if (Options::ditherFailAbortsAutoGuide())
0129         {
0130             abort();
0131             emit newStatus(GUIDE_DITHERING_ERROR);
0132         }
0133         else
0134         {
0135             emit newLog(i18n("PHD2: There was no dithering response from PHD2, but continue guiding."));
0136             emit newStatus(Ekos::GUIDE_DITHERING_SUCCESS);
0137         }
0138     });
0139 
0140     stateTimer = new QTimer(this);
0141     connect(stateTimer, &QTimer::timeout, this, [ = ]
0142     {
0143         QTcpSocket::SocketState socketstate = tcpSocket->state();
0144         switch (socketstate)
0145         {
0146             case QTcpSocket::UnconnectedState:
0147                 m_PHD2ReconnectCounter++;
0148                 if (m_PHD2ReconnectCounter > PHD2_RECONNECT_THRESHOLD)
0149                 {
0150                     stateTimer->stop();
0151                     emit newLog(i18n("Giving up reconnecting."));
0152                 }
0153                 else
0154                 {
0155                     emit newLog(i18n("Reconnecting to PHD2 Host: %1, on port %2. . .", Options::pHD2Host(), Options::pHD2Port()));
0156 
0157                     connect(tcpSocket, &QTcpSocket::readyRead, this, &PHD2::readPHD2, Qt::UniqueConnection);
0158 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
0159                     connect(tcpSocket, &QTcpSocket::errorOccurred, this, &PHD2::displayError, Qt::UniqueConnection);
0160 #else
0161                     connect(tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this,
0162                             SLOT(displayError(QAbstractSocket::SocketError)));
0163 #endif
0164                     tcpSocket->connectToHost(Options::pHD2Host(), Options::pHD2Port());
0165                 }
0166                 break;
0167             case QTcpSocket::ConnectedState:
0168                 m_PHD2ReconnectCounter = 0;
0169                 checkIfEquipmentConnected();
0170                 requestAppState();
0171                 break;
0172             default:
0173                 qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: TCP connection state:" << socketstate;
0174                 break;
0175         }
0176     });
0177 }
0178 
0179 PHD2::~PHD2()
0180 {
0181     delete abortTimer;
0182     delete ditherTimer;
0183 }
0184 
0185 bool PHD2::Connect()
0186 {
0187     switch (connection)
0188     {
0189         case DISCONNECTED:
0190             // Not yet connected, let's connect server
0191             connection = CONNECTING;
0192             emit newLog(i18n("Connecting to PHD2 Host: %1, on port %2. . .", Options::pHD2Host(), Options::pHD2Port()));
0193 
0194             connect(tcpSocket, &QTcpSocket::readyRead, this, &PHD2::readPHD2, Qt::UniqueConnection);
0195 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
0196             connect(tcpSocket, &QTcpSocket::errorOccurred, this, &PHD2::displayError, Qt::UniqueConnection);
0197 #else
0198             connect(tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this,
0199                     SLOT(displayError(QAbstractSocket::SocketError)));
0200 #endif
0201 
0202             tcpSocket->connectToHost(Options::pHD2Host(), Options::pHD2Port());
0203 
0204             m_PHD2ReconnectCounter = 0;
0205             stateTimer->start(PHD2_RECONNECT_TIMEOUT);
0206             return true;
0207 
0208         case EQUIPMENT_DISCONNECTED:
0209             // Equipment disconnected from PHD2, request reconnection
0210             connectEquipment(true);
0211             return true;
0212 
0213         case DISCONNECTING:
0214             // Not supposed to interrupt a running disconnection
0215             return false;
0216 
0217         default:
0218             return false;
0219     }
0220 }
0221 
0222 void PHD2::ResetConnectionState()
0223 {
0224     connection = DISCONNECTED;
0225 
0226     // clear the outstanding and queued RPC requests
0227     pendingRpcResultType = NO_RESULT;
0228     rpcRequestQueue.clear();
0229 
0230     starImageRequested = false;
0231     isSettling = false;
0232     isDitherActive = false;
0233 
0234     ditherTimer->stop();
0235     abortTimer->stop();
0236 
0237     tcpSocket->disconnect(this);
0238 
0239     emit newStatus(GUIDE_DISCONNECTED);
0240 }
0241 
0242 bool PHD2::Disconnect()
0243 {
0244     switch (connection)
0245     {
0246         case EQUIPMENT_CONNECTED:
0247             emit newLog(i18n("Aborting any capture before disconnecting equipment..."));
0248             abort();
0249             connection = DISCONNECTING;
0250             break;
0251 
0252         case CONNECTED:
0253         case CONNECTING:
0254         case EQUIPMENT_DISCONNECTED:
0255             stateTimer->stop();
0256             tcpSocket->disconnectFromHost();
0257             ResetConnectionState();
0258             if (tcpSocket->state() != QAbstractSocket::UnconnectedState)
0259                 tcpSocket->waitForDisconnected(5000);
0260             emit newLog(i18n("Disconnected from PHD2 Host: %1, on port %2.", Options::pHD2Host(), Options::pHD2Port()));
0261             break;
0262 
0263         case DISCONNECTING:
0264         case DISCONNECTED:
0265             break;
0266     }
0267 
0268     return true;
0269 }
0270 
0271 void PHD2::displayError(QAbstractSocket::SocketError socketError)
0272 {
0273     switch (socketError)
0274     {
0275         case QAbstractSocket::RemoteHostClosedError:
0276             emit newLog(i18n("The host disconnected."));
0277             break;
0278         case QAbstractSocket::HostNotFoundError:
0279             emit newLog(i18n("The host was not found. Please check the host name and port settings in Guide options."));
0280             break;
0281         case QAbstractSocket::ConnectionRefusedError:
0282             emit newLog(i18n("The connection was refused by the peer. Make sure the PHD2 is running, and check that "
0283                              "the host name and port settings are correct."));
0284             break;
0285         default:
0286             emit newLog(i18n("The following error occurred: %1.", tcpSocket->errorString()));
0287     }
0288 
0289     ResetConnectionState();
0290 
0291     emit newStatus(GUIDE_DISCONNECTED);
0292 }
0293 
0294 void PHD2::readPHD2()
0295 {
0296     while (!tcpSocket->atEnd() && tcpSocket->canReadLine())
0297     {
0298         QByteArray line = tcpSocket->readLine();
0299         if (line.isEmpty())
0300             continue;
0301 
0302         QJsonParseError qjsonError;
0303 
0304         QJsonDocument jdoc = QJsonDocument::fromJson(line, &qjsonError);
0305 
0306         if (qjsonError.error != QJsonParseError::NoError)
0307         {
0308             emit newLog(i18n("PHD2: invalid response received: %1", QString(line)));
0309             emit newLog(i18n("PHD2: JSON error: %1", qjsonError.errorString()));
0310             continue;
0311         }
0312 
0313         QJsonObject jsonObj = jdoc.object();
0314 
0315         if (jsonObj.contains("Event"))
0316             processPHD2Event(jsonObj, line);
0317         else if (jsonObj.contains("error"))
0318             processPHD2Error(jsonObj, line);
0319         else if (jsonObj.contains("result"))
0320             processPHD2Result(jsonObj, line);
0321     }
0322 }
0323 
0324 void PHD2::processPHD2Event(const QJsonObject &jsonEvent, const QByteArray &line)
0325 {
0326     if (Options::verboseLogging())
0327         qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: event:" << line;
0328 
0329     QString eventName = jsonEvent["Event"].toString();
0330 
0331     if (!events.contains(eventName))
0332     {
0333         emit newLog(i18n("Unknown PHD2 event: %1", eventName));
0334         return;
0335     }
0336 
0337     event = events.value(eventName);
0338 
0339     switch (event)
0340     {
0341         case Version:
0342             emit newLog(i18n("PHD2: Version %1", jsonEvent["PHDVersion"].toString()));
0343             break;
0344 
0345         case CalibrationComplete:
0346             emit newLog(i18n("PHD2: Calibration Complete."));
0347             emit newStatus(Ekos::GUIDE_CALIBRATION_SUCCESS);
0348             break;
0349 
0350         case StartGuiding:
0351             updateGuideParameters();
0352             requestCurrentEquipmentUpdate();
0353             // Do not report guiding as started because it will start scheduled capture before guiding is settled
0354             // just print the log message and GUIDE_STARTED status will be set in SettleDone
0355             // phd2 will always send SettleDone event
0356             emit newLog(i18n("PHD2: Waiting for guiding to settle."));
0357             break;
0358 
0359         case Paused:
0360             handlePHD2AppState(PAUSED);
0361             break;
0362 
0363         case StartCalibration:
0364             handlePHD2AppState(CALIBRATING);
0365             break;
0366 
0367         case AppState:
0368             // AppState is the last of the initial messages received when we first connect to PHD2
0369             processPHD2State(jsonEvent["State"].toString());
0370             // if the equipment is not already connected, then try to connect it.
0371             if (connection == CONNECTING)
0372             {
0373                 emit newLog("PHD2: Connecting equipment and external guider...");
0374                 connectEquipment(true);
0375             }
0376             break;
0377 
0378         case CalibrationFailed:
0379             emit newLog(i18n("PHD2: Calibration Failed (%1).", jsonEvent["Reason"].toString()));
0380             handlePHD2AppState(STOPPED);
0381             break;
0382 
0383         case CalibrationDataFlipped:
0384             emit newLog(i18n("Calibration Data Flipped."));
0385             break;
0386 
0387         case LoopingExposures:
0388             handlePHD2AppState(LOOPING);
0389             break;
0390 
0391         case LoopingExposuresStopped:
0392             handlePHD2AppState(STOPPED);
0393             break;
0394 
0395         case Calibrating:
0396         case Settling:
0397         case SettleBegin:
0398             //This can happen for guiding or for dithering.  A Settle done event will arrive when it finishes.
0399             break;
0400 
0401         case SettleDone:
0402         {
0403             // guiding stopped during dithering
0404             if (state == PHD2::STOPPED)
0405                 return;
0406 
0407             bool error = false;
0408 
0409             if (jsonEvent["Status"].toInt() != 0)
0410             {
0411                 error = true;
0412                 emit newLog(i18n("PHD2: Settling failed (%1).", jsonEvent["Error"].toString()));
0413             }
0414 
0415             bool wasDithering = isDitherActive;
0416 
0417             isDitherActive = false;
0418             isSettling = false;
0419 
0420             if (wasDithering)
0421             {
0422                 ditherTimer->stop();
0423                 if (error && Options::ditherFailAbortsAutoGuide())
0424                 {
0425                     abort();
0426                     emit newStatus(GUIDE_DITHERING_ERROR);
0427                 }
0428                 else
0429                 {
0430                     if (error)
0431                         emit newLog(i18n("PHD2: There was a dithering error, but continue guiding."));
0432 
0433                     emit newStatus(Ekos::GUIDE_DITHERING_SUCCESS);
0434                 }
0435             }
0436             else
0437             {
0438                 if (error)
0439                 {
0440                     emit newLog(i18n("PHD2: Settling failed, aborted."));
0441                     emit newStatus(GUIDE_ABORTED);
0442                 }
0443                 else
0444                 {
0445                     // settle completed after "guide" command
0446                     emit newLog(i18n("PHD2: Settling complete, Guiding Started."));
0447                     emit newStatus(GUIDE_GUIDING);
0448                 }
0449             }
0450         }
0451         break;
0452 
0453         case StarSelected:
0454             handlePHD2AppState(SELECTED);
0455             break;
0456 
0457         case StarLost:
0458             // If we lost the guide star, let the state and abort timers update our state
0459             handlePHD2AppState(LOSTLOCK);
0460             break;
0461 
0462         case GuidingStopped:
0463             handlePHD2AppState(STOPPED);
0464             break;
0465 
0466         case Resumed:
0467             handlePHD2AppState(GUIDING);
0468             break;
0469 
0470         case GuideStep:
0471         {
0472             // If we lost the guide star, let the state timer update our state
0473             // Sometimes PHD2 is actually not guiding at that time, so we'll either resume or abort
0474             if (state == LOSTLOCK)
0475                 emit newLog(i18n("PHD2: Star found, guiding is resuming..."));
0476 
0477             if (isDitherActive)
0478                 return;
0479 
0480             double diff_ra_pixels, diff_de_pixels, diff_ra_arcsecs, diff_de_arcsecs, pulse_ra, pulse_dec, snr;
0481             QString RADirection, DECDirection;
0482             diff_ra_pixels = jsonEvent["RADistanceRaw"].toDouble();
0483             diff_de_pixels = jsonEvent["DECDistanceRaw"].toDouble();
0484             pulse_ra = jsonEvent["RADuration"].toDouble();
0485             pulse_dec = jsonEvent["DECDuration"].toDouble();
0486             RADirection = jsonEvent["RADirection"].toString();
0487             DECDirection = jsonEvent["DECDirection"].toString();
0488             snr = jsonEvent["SNR"].toDouble();
0489 
0490             if (RADirection == "East")
0491                 pulse_ra = -pulse_ra;  //West Direction is Positive, East is Negative
0492             if (DECDirection == "South")
0493                 pulse_dec = -pulse_dec; //South Direction is Negative, North is Positive
0494 
0495             //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.
0496             if (pixelScale != 0)
0497             {
0498                 diff_ra_arcsecs = diff_ra_pixels * pixelScale;
0499                 diff_de_arcsecs = diff_de_pixels * pixelScale;
0500             }
0501             else
0502             {
0503                 diff_ra_arcsecs = 206.26480624709 * diff_ra_pixels * ccdPixelSizeX / mountFocalLength;
0504                 diff_de_arcsecs = 206.26480624709 * diff_de_pixels * ccdPixelSizeY / mountFocalLength;
0505             }
0506 
0507             if (std::isfinite(snr))
0508                 emit newSNR(snr);
0509 
0510             if (std::isfinite(diff_ra_arcsecs) && std::isfinite(diff_de_arcsecs))
0511             {
0512                 errorLog.append(QPointF(diff_ra_arcsecs, diff_de_arcsecs));
0513                 if(errorLog.size() > 50)
0514                     errorLog.remove(0);
0515 
0516                 emit newAxisDelta(diff_ra_arcsecs, diff_de_arcsecs);
0517                 emit newAxisPulse(pulse_ra, pulse_dec);
0518 
0519                 // Does PHD2 real a sky background or num-stars measure?
0520                 emit guideStats(diff_ra_arcsecs, diff_de_arcsecs, pulse_ra, pulse_dec,
0521                                 std::isfinite(snr) ? snr : 0, 0, 0);
0522 
0523                 double total_sqr_RA_error = 0.0;
0524                 double total_sqr_DE_error = 0.0;
0525 
0526                 for (auto &point : errorLog)
0527                 {
0528                     total_sqr_RA_error += point.x() * point.x();
0529                     total_sqr_DE_error += point.y() * point.y();
0530                 }
0531 
0532                 emit newAxisSigma(sqrt(total_sqr_RA_error / errorLog.size()), sqrt(total_sqr_DE_error / errorLog.size()));
0533 
0534             }
0535             //Note that if it is receiving full size remote images, it should not get the guide star image.
0536             //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
0537             //If we are getting the full size image, we will want to know the lock position for the image that loads in the viewer.
0538             if ( Options::guideSubframe() || currentCameraIsNotInEkos )
0539                 requestStarImage(32); //This requests a star image for the guide view.  32 x 32 pixels
0540             else
0541                 requestLockPosition();
0542         }
0543         break;
0544 
0545         case GuidingDithered:
0546             break;
0547 
0548         case LockPositionSet:
0549             handlePHD2AppState(SELECTED);
0550             break;
0551 
0552         case LockPositionLost:
0553             handlePHD2AppState(LOSTLOCK);
0554             break;
0555 
0556         case Alert:
0557             emit newLog(i18n("PHD2 %1: %2", jsonEvent["Type"].toString(), jsonEvent["Msg"].toString()));
0558             break;
0559 
0560         case GuideParamChange:
0561         case ConfigurationChange:
0562             //Don't do anything for now, might change this later.
0563             //Some Possible Parameter Names:
0564             //Backlash comp enabled, Backlash comp amount,
0565             //For Each Axis: MinMove, Max Duration,
0566             //PPEC aggressiveness, PPEC prediction weight,
0567             //Resist switch minimum motion, Resist switch aggression,
0568             //Low-pass minimum move, Low-pass slope weight,
0569             //Low-pass2 minimum move, Low-pass2 aggressiveness,
0570             //Hysteresis hysteresis, Hysteresis aggression
0571             break;
0572 
0573     }
0574 }
0575 
0576 void PHD2::processPHD2State(const QString &phd2State)
0577 {
0578     if (phd2State == "Stopped")
0579         handlePHD2AppState(STOPPED);
0580     else if (phd2State == "Selected")
0581         handlePHD2AppState(SELECTED);
0582     else if (phd2State == "Calibrating")
0583         handlePHD2AppState(CALIBRATING);
0584     else if (phd2State == "Guiding")
0585         handlePHD2AppState(GUIDING);
0586     else if (phd2State == "LostLock")
0587         handlePHD2AppState(LOSTLOCK);
0588     else if (phd2State == "Paused")
0589         handlePHD2AppState(PAUSED);
0590     else if (phd2State == "Looping")
0591         handlePHD2AppState(LOOPING);
0592     else emit newLog(QString("PHD2: Unsupported app state ") + phd2State + ".");
0593 }
0594 
0595 void PHD2::handlePHD2AppState(PHD2State newstate)
0596 {
0597     // do not handle the same state twice
0598     if (state == newstate)
0599         return;
0600 
0601     switch (newstate)
0602     {
0603         case STOPPED:
0604             switch (state)
0605             {
0606                 case CALIBRATING:
0607                     //emit newLog(i18n("PHD2: Calibration Failed (%1).", jsonEvent["Reason"].toString()));
0608                     emit newStatus(Ekos::GUIDE_CALIBRATION_ERROR);
0609                     break;
0610                 case LOOPING:
0611                     emit newLog(i18n("PHD2: Looping Exposures Stopped."));
0612                     emit newStatus(Ekos::GUIDE_IDLE);
0613                     break;
0614                 case GUIDING:
0615                 case LOSTLOCK:
0616                     emit newLog(i18n("PHD2: Guiding Stopped."));
0617                     emit newStatus(Ekos::GUIDE_ABORTED);
0618                     break;
0619                 default:
0620                     if (connection == DISCONNECTING)
0621                     {
0622                         emit newLog("PHD2: Disconnecting equipment and external guider...");
0623                         connectEquipment(false);
0624                     }
0625                     break;
0626             }
0627             break;
0628 
0629         case SELECTED:
0630             switch (state)
0631             {
0632                 case STOPPED:
0633                 case CALIBRATING:
0634                 case GUIDING:
0635                     emit newLog(i18n("PHD2: Lock Position Set."));
0636                     if (isSettling)
0637                     {
0638                         newstate = CALIBRATING;
0639                         emit newStatus(Ekos::GUIDE_CALIBRATING);
0640                     }
0641                     break;
0642                 case DITHERING:
0643                     // do nothing, this is the initial step in PHD2 for dithering
0644                     break;
0645                 default:
0646                     emit newLog(i18n("PHD2: Star Selected."));
0647                     emit newStatus(GUIDE_STAR_SELECT);
0648             }
0649             break;
0650 
0651         case GUIDING:
0652             switch (state)
0653             {
0654                 case PAUSED:
0655                 case DITHERING:
0656                     emit newLog(i18n("PHD2: Dithering successful."));
0657                     abortTimer->stop();
0658                     emit newStatus(Ekos::GUIDE_DITHERING_SUCCESS);
0659                     break;
0660                 default:
0661                     emit newLog(i18n("PHD2: Guiding started."));
0662                     abortTimer->stop();
0663                     emit newStatus(Ekos::GUIDE_GUIDING);
0664                     break;
0665             }
0666             break;
0667 
0668         case LOSTLOCK:
0669             switch (state)
0670             {
0671                 case CALIBRATING:
0672                     emit newLog(i18n("PHD2: Lock Position Lost, continuing calibration."));
0673                     // Don't be paranoid, accept star-lost events during calibration and trust PHD2 to complete
0674                     //newstate = STOPPED;
0675                     //emit newStatus(Ekos::GUIDE_CALIBRATION_ERROR);
0676                     break;
0677                 case GUIDING:
0678                     emit newLog(i18n("PHD2: Star Lost. Trying to reacquire for %1s.", Options::guideLostStarTimeout()));
0679                     abortTimer->start(static_cast<int>(Options::guideLostStarTimeout()) * 1000);
0680                     qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: Lost star timeout started (" << Options::guideLostStarTimeout() << " sec)";
0681                     emit newStatus(Ekos::GUIDE_REACQUIRE);
0682                     break;
0683                 default:
0684                     emit newLog(i18n("PHD2: Lock Position Lost."));
0685                     break;
0686             }
0687             break;
0688 
0689         case PAUSED:
0690             emit newLog(i18n("PHD2: Guiding paused."));
0691             emit newStatus(GUIDE_SUSPENDED);
0692             break;
0693 
0694         case CALIBRATING:
0695             emit newLog(i18n("PHD2: Calibrating, timing out in %1s.", Options::guideCalibrationTimeout()));
0696             abortTimer->start(static_cast<int>(Options::guideCalibrationTimeout()) * 1000);
0697             emit newStatus(GUIDE_CALIBRATING);
0698             break;
0699 
0700         case LOOPING:
0701             switch (state)
0702             {
0703                 case CALIBRATING:
0704                     emit newLog(i18n("PHD2: Calibration turned to looping, failed."));
0705                     emit newStatus(GUIDE_CALIBRATION_ERROR);
0706                     break;
0707                 default:
0708                     emit newLog(i18n("PHD2: Looping Exposures."));
0709                     emit newStatus(GUIDE_LOOPING);
0710                     break;
0711             }
0712             break;
0713         case DITHERING:
0714             emit newLog(i18n("PHD2: Dithering started."));
0715             emit newStatus(GUIDE_DITHERING);
0716             break;
0717     }
0718 
0719     state = newstate;
0720 }
0721 
0722 void PHD2::processPHD2Result(const QJsonObject &jsonObj, const QByteArray &line)
0723 {
0724     PHD2ResultType resultType = takeRequestFromList(jsonObj);
0725 
0726     if (resultType == STAR_IMAGE)
0727         qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: received star image response, id" <<
0728                                    jsonObj["id"].toInt();   // don't spam the log with image data
0729     else
0730         qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: response:" << line;
0731 
0732     switch (resultType)
0733     {
0734         case NO_RESULT:
0735             //Ekos didn't ask for this result?
0736             break;
0737 
0738         case CAPTURE_SINGLE_FRAME:                  //capture_single_frame
0739             break;
0740 
0741         case CLEAR_CALIBRATION_COMMAND_RECEIVED:    //clear_calibration
0742             emit newLog(i18n("PHD2: Calibration is cleared"));
0743             break;
0744 
0745         case DITHER_COMMAND_RECEIVED:               //dither
0746             handlePHD2AppState(DITHERING);
0747             break;
0748 
0749         //find_star
0750         //flip_calibration
0751         //get_algo_param_names
0752         //get_algo_param
0753 
0754         case APP_STATE_RECEIVED:                    //get_app_state
0755         {
0756             QString state = jsonObj["State"].toString();
0757             if (state.isEmpty())
0758                 state = jsonObj["result"].toString();
0759             if (state.isEmpty())
0760                 qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: received unsupported app state";
0761             else
0762                 processPHD2State(state);
0763         }
0764         break;
0765 
0766         //get_calibrated
0767         //get_calibration_data
0768 
0769         case IS_EQUIPMENT_CONNECTED:                //get_connected
0770         {
0771             bool isConnected = jsonObj["result"].toBool();
0772             switch (connection)
0773             {
0774                 case CONNECTING:
0775                     // We just plugged in server, request equipment connection if needed
0776                     if (isConnected)
0777                     {
0778                         connection = CONNECTED;
0779                         setEquipmentConnected();
0780                     }
0781                     else connectEquipment(true);
0782                     break;
0783 
0784                 case CONNECTED:
0785                     // We were waiting for equipment to be connected after plugging in server
0786                     if (isConnected)
0787                         setEquipmentConnected();
0788                     break;
0789 
0790                 case DISCONNECTING:
0791                     // We were waiting for equipment to be disconnected before unplugging from server
0792                     if (!isConnected)
0793                     {
0794                         connection = EQUIPMENT_DISCONNECTED;
0795                         Disconnect();
0796                     }
0797                     else connectEquipment(false);
0798                     break;
0799 
0800                 case EQUIPMENT_CONNECTED:
0801                     // Equipment was disconnected from PHD2 side, so notify clients and wait.
0802                     if (!isConnected)
0803                     {
0804                         // TODO: setEquipmentDisconnected()
0805                         connection = EQUIPMENT_DISCONNECTED;
0806                         emit newStatus(Ekos::GUIDE_DISCONNECTED);
0807                     }
0808                     break;
0809 
0810                 case DISCONNECTED:
0811                 case EQUIPMENT_DISCONNECTED:
0812                     // Equipment was connected from PHD2 side, so notify clients and wait.
0813                     if (isConnected)
0814                         setEquipmentConnected();
0815                     break;
0816             }
0817         }
0818         break;
0819 
0820         //get_cooler_status
0821         case GET_CURRENT_EQUIPMENT:                 //get_current_equipment
0822         {
0823             QJsonObject equipObject = jsonObj["result"].toObject();
0824             currentCamera = equipObject["camera"].toObject()["name"].toString();
0825             currentMount = equipObject["mount"].toObject()["name"].toString();
0826             currentAuxMount = equipObject["aux_mount"].toObject()["name"].toString();
0827 
0828             emit guideEquipmentUpdated();
0829 
0830             break;
0831         }
0832 
0833 
0834         case DEC_GUIDE_MODE:                        //get_dec_guide_mode
0835         {
0836             QString mode = jsonObj["result"].toString();
0837             Ekos::Manager::Instance()->guideModule()->updateDirectionsFromPHD2(mode);
0838             emit newLog(i18n("PHD2: DEC Guide Mode is Set to: %1", mode));
0839         }
0840         break;
0841 
0842 
0843         case EXPOSURE_TIME:                         //get_exposure
0844         {
0845             int exposurems = jsonObj["result"].toInt();
0846             double exposureTime = exposurems / 1000.0;
0847             Ekos::Manager::Instance()->guideModule()->setExposure(exposureTime);
0848             emit newLog(i18n("PHD2: Exposure Time set to: ") + QString::number(exposureTime, 'f', 2));
0849             break;
0850         }
0851 
0852 
0853         case EXPOSURE_DURATIONS:                    //get_exposure_durations
0854         {
0855             QVariantList exposureListArray = jsonObj["result"].toArray().toVariantList();
0856             logValidExposureTimes = i18n("PHD2: Valid Exposure Times: Auto, ");
0857             QList<double> values;
0858             for(int i = 1; i < exposureListArray.size();
0859                     i ++) //For some reason PHD2 has a negative exposure time of 1 at the start of the array?
0860                 values << exposureListArray.at(i).toDouble() / 1000.0; //PHD2 reports in ms.
0861             logValidExposureTimes += Ekos::Manager::Instance()->guideModule()->setRecommendedExposureValues(values);
0862             emit newLog(logValidExposureTimes);
0863             break;
0864         }
0865         case LOCK_POSITION:                         //get_lock_position
0866         {
0867             if(jsonObj["result"].toArray().count() == 2)
0868             {
0869                 double x  = jsonObj["result"].toArray().at(0).toDouble();
0870                 double y  = jsonObj["result"].toArray().at(1).toDouble();
0871                 QVector3D newStarCenter(x, y, 0);
0872                 emit newStarPosition(newStarCenter, true);
0873 
0874                 //This is needed so that PHD2 sends the new star pixmap when
0875                 //remote images are enabled.
0876                 emit newStarPixmap(m_GuideFrame->getTrackingBoxPixmap());
0877             }
0878             break;
0879         }
0880         //get_lock_shift_enabled
0881         //get_lock_shift_params
0882         //get_paused
0883 
0884         case PIXEL_SCALE:                           //get_pixel_scale
0885             pixelScale = jsonObj["result"].toDouble();
0886             if (pixelScale == 0)
0887                 emit newLog(i18n("PHD2: Please set CCD and telescope parameters in PHD2, Pixel Scale is invalid."));
0888             else
0889                 emit newLog(i18n("PHD2: Pixel Scale is %1 arcsec per pixel", QString::number(pixelScale, 'f', 2)));
0890             break;
0891 
0892         //get_profile
0893         //get_profiles
0894         //get_search_region
0895         //get_sensor_temperature
0896 
0897         case STAR_IMAGE:                            //get_star_image
0898         {
0899             starImageRequested = false;
0900             QJsonObject jsonResult = jsonObj["result"].toObject();
0901             processStarImage(jsonResult);
0902             break;
0903         }
0904 
0905         //get_use_subframes
0906 
0907         case GUIDE_COMMAND_RECEIVED:                //guide
0908             if (0 != jsonObj["result"].toInt(0))
0909             {
0910                 emit newLog("PHD2: Guide command was rejected.");
0911                 handlePHD2AppState(STOPPED);
0912             }
0913             break;
0914 
0915         //guide_pulse
0916 
0917         case LOOP:                                  //loop
0918             handlePHD2AppState(jsonObj["result"].toBool() ? LOOPING : STOPPED);
0919             break;
0920 
0921         //save_image
0922         //set_algo_param
0923 
0924         case CONNECTION_RESULT:                     //set_connected
0925             checkIfEquipmentConnected();
0926             break;
0927 
0928         case SET_DEC_GUIDE_MODE_COMMAND_RECEIVED:   //set_dec_guide_mode
0929             checkDEGuideMode();
0930             break;
0931 
0932         case SET_EXPOSURE_COMMAND_RECEIVED:         //set_exposure
0933             requestExposureTime(); //This will check what it was set to and print a message as to what it is.
0934             break;
0935 
0936         case SET_LOCK_POSITION:                     //set_lock_position
0937             handlePHD2AppState(SELECTED);
0938             break;
0939 
0940         //set_lock_shift_enabled
0941         //set_lock_shift_params
0942 
0943         case SET_PAUSED_COMMAND_RECEIVED:           //set_paused
0944             handlePHD2AppState(PAUSED);
0945             break;
0946         //set_profile
0947         //shutdown
0948 
0949         case STOP_CAPTURE_COMMAND_RECEIVED:         //stop_capture
0950             handlePHD2AppState(STOPPED);
0951             //emit newStatus(GUIDE_ABORTED);
0952             break;
0953     }
0954 
0955     // send the next pending call
0956     sendNextRpcCall();
0957 }
0958 
0959 void PHD2::processPHD2Error(const QJsonObject &jsonError, const QByteArray &line)
0960 {
0961     qCDebug(KSTARS_EKOS_GUIDE) << "PHD2: error:" << line;
0962 
0963     QJsonObject jsonErrorObject = jsonError["error"].toObject();
0964 
0965     PHD2ResultType resultType = takeRequestFromList(jsonError);
0966 
0967     // This means the user mistakenly entered an invalid exposure time.
0968     switch (resultType)
0969     {
0970         case SET_EXPOSURE_COMMAND_RECEIVED:
0971             emit newLog(logValidExposureTimes);  //This will let the user know the valid exposure durations
0972             QTimer::singleShot(300, [ = ] {requestExposureTime();}); //This will reset the Exposure time in Ekos to PHD2's current exposure time after a third of a second.
0973             break;
0974 
0975         case CONNECTION_RESULT:
0976             connection = EQUIPMENT_DISCONNECTED;
0977             emit newStatus(Ekos::GUIDE_DISCONNECTED);
0978             break;
0979 
0980         case DITHER_COMMAND_RECEIVED:
0981             ditherTimer->stop();
0982             isSettling = false;
0983             isDitherActive = false;
0984             emit newStatus(GUIDE_DITHERING_ERROR);
0985 
0986             if (Options::ditherFailAbortsAutoGuide())
0987             {
0988                 abort();
0989                 emit newLog("PHD2: failing after dithering aborts.");
0990                 emit newStatus(GUIDE_ABORTED);
0991             }
0992             else
0993             {
0994                 // !FIXME-ag why is this trying to resume (un-pause)?
0995                 resume();
0996             }
0997             break;
0998 
0999         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
1090     QSharedPointer<FITSData> fdata;
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 
1509         QByteArray request = QJsonDocument(call).toJson(QJsonDocument::Compact);
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 }