diff --git a/kstars/ekos/align/align.h b/kstars/ekos/align/align.h index 636de481a..5d1a3f1c1 100644 --- a/kstars/ekos/align/align.h +++ b/kstars/ekos/align/align.h @@ -1,940 +1,940 @@ /* Ekos Polar Alignment Tool Copyright (C) 2013 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "ui_align.h" #include "ui_mountmodel.h" #include "ekos/ekos.h" #include "indi/indiccd.h" #include "indi/indistd.h" #include "indi/inditelescope.h" #include "indi/indidome.h" #include "ekos/auxiliary/filtermanager.h" #include #include #if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0) #include #else #include #endif #include class QProgressIndicator; class AlignView; class FOV; class StarObject; class ProfileInfo; namespace Ekos { class AstrometryParser; class OnlineAstrometryParser; class OfflineAstrometryParser; class RemoteAstrometryParser; class ASTAPAstrometryParser; class OpsAstrometry; class OpsAlign; class OpsASTAP; class OpsAstrometryCfg; class OpsAstrometryIndexFiles; /** *@class Align *@short Align class handles plate-solving and polar alignment measurement and correction using astrometry.net * The align class can capture images from the CCD and use either online or offline astrometry.net engine to solve the plate constants and find the center RA/DEC coordinates. The user selects the action * to perform when the solver completes successfully. * Align module provide Polar Align Helper tool which enables easy-to-follow polar alignment procedure given wide FOVs (> 1.5 degrees) * For small FOVs, the Legacy polar alignment measurement should be used. * LEGACY: Measurement of polar alignment errors is performed by capturing two images on selected points in the sky and measuring the declination drift to calculate * the error in the mount's azimuth and altitude displacement from optimal. Correction is carried by asking the user to re-center a star by adjusting the telescope's azimuth and/or altitude knobs. *@author Jasem Mutlaq *@version 1.4 */ class Align : public QWidget, public Ui::Align { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kstars.Ekos.Align") Q_PROPERTY(Ekos::AlignState status READ status NOTIFY newStatus) Q_PROPERTY(QStringList logText READ logText NOTIFY newLog) Q_PROPERTY(QString camera READ camera WRITE setCamera) Q_PROPERTY(QString filterWheel READ filterWheel WRITE setFilterWheel) Q_PROPERTY(QString filter READ filter WRITE setFilter) Q_PROPERTY(double exposure READ exposure WRITE setExposure) Q_PROPERTY(QList fov READ fov) Q_PROPERTY(QList cameraInfo READ cameraInfo) Q_PROPERTY(QList telescopeInfo READ telescopeInfo) Q_PROPERTY(QString solverArguments READ solverArguments WRITE setSolverArguments) public: explicit Align(ProfileInfo *activeProfile); virtual ~Align() override; typedef enum { AZ_INIT, AZ_FIRST_TARGET, AZ_SYNCING, AZ_SLEWING, AZ_SECOND_TARGET, AZ_CORRECTING, AZ_FINISHED } AZStage; typedef enum { ALT_INIT, ALT_FIRST_TARGET, ALT_SYNCING, ALT_SLEWING, ALT_SECOND_TARGET, ALT_CORRECTING, ALT_FINISHED } ALTStage; typedef enum { GOTO_SYNC, GOTO_SLEW, GOTO_NOTHING } GotoMode; typedef enum { SOLVER_ONLINE, SOLVER_OFFLINE, SOLVER_REMOTE } AstrometrySolverType; typedef enum { SOLVER_ASTAP, SOLVER_ASTROMETRYNET } SolverBackend; typedef enum { PAH_IDLE, PAH_FIRST_CAPTURE, PAH_FIND_CP, PAH_FIRST_ROTATE, PAH_SECOND_CAPTURE, PAH_SECOND_ROTATE, PAH_THIRD_CAPTURE, PAH_STAR_SELECT, PAH_PRE_REFRESH, PAH_REFRESH, PAH_ERROR } PAHStage; typedef enum { NORTH_HEMISPHERE, SOUTH_HEMISPHERE } HemisphereType; // Image Scales const QStringList ImageScales = { "dw", "aw", "app" }; enum CircleSolution { NO_CIRCLE_SOLUTION, ONE_CIRCLE_SOLUTION, TWO_CIRCLE_SOLUTION, INFINITE_CIRCLE_SOLUTION }; enum ModelObjectType { OBJECT_ANY_STAR, OBJECT_NAMED_STAR, OBJECT_ANY_OBJECT, OBJECT_FIXED_DEC, OBJECT_FIXED_GRID }; /** @defgroup AlignDBusInterface Ekos DBus Interface - Align Module * Ekos::Align interface provides advanced scripting capabilities to solve images using online or offline astrometry.net */ /*@{*/ /** DBUS interface function. * Select CCD * @param device CCD device name * @return Returns true if device if found and selected, false otherwise. */ Q_SCRIPTABLE bool setCamera(const QString &device); Q_SCRIPTABLE QString camera(); /** DBUS interface function. * select the filter device from the available filter drivers. The filter device can be the same as the CCD driver if the filter functionality was embedded within the driver. * @param device The filter device name * @return Returns true if filter device is found and set, false otherwise. */ Q_SCRIPTABLE bool setFilterWheel(const QString &device); Q_SCRIPTABLE QString filterWheel(); /** DBUS interface function. * select the filter from the available filters. * @param filter The filter name * @return Returns true if filter is found and set, false otherwise. */ Q_SCRIPTABLE bool setFilter(const QString &filter); Q_SCRIPTABLE QString filter(); /** DBUS interface function. * Start the plate-solving process given the passed image file. * @param filename Name of image file to solve. FITS and JPG/JPG/TIFF formats are accepted. * @param isGenerated Set to true if filename is generated from a CCD capture operation. If the file is loaded from any storage or network media, pass false. * @return Returns true if device if found and selected, false otherwise. */ Q_SCRIPTABLE Q_NOREPLY void startSolving(const QString &filename, bool isGenerated = true); /** DBUS interface function. * Select Solver Action after successfully solving an image. * @param mode 0 for Sync, 1 for Slew To Target, 2 for Nothing (just display solution results) */ Q_SCRIPTABLE Q_NOREPLY void setSolverAction(int mode); /** DBUS interface function. * Returns the solver's solution results * @return Returns array of doubles. First item is RA in degrees. Second item is DEC in degrees. */ Q_SCRIPTABLE QList getSolutionResult(); /** DBUS interface function. * Returns the solver's current status * @return Returns solver status (Ekos::AlignState) */ Q_SCRIPTABLE Ekos::AlignState status() { return state; } /** DBUS interface function. * @return Returns State of load slew procedure. Idle if not started. Busy if in progress. Ok if complete. Alert if procedure failed. */ Q_SCRIPTABLE int getLoadAndSlewStatus() { return loadSlewState; } /** DBUS interface function. * Sets the exposure of the selected CCD device. * @param value Exposure value in seconds */ Q_SCRIPTABLE Q_NOREPLY void setExposure(double value); Q_SCRIPTABLE double exposure() { return exposureIN->value(); } /** DBUS interface function. * Sets the arguments that gets passed to the astrometry.net offline solver. * @param value space-separated arguments. */ Q_SCRIPTABLE Q_NOREPLY void setSolverArguments(const QString &value); /** DBUS interface function. * Get existing solver options. * @return String containing all arguments. */ Q_SCRIPTABLE QString solverArguments(); /** DBUS interface function. * Sets the telescope type (PRIMARY or GUIDE) that should be used for FOV calculations. This value is loaded form driver settings by default. * @param index 0 for PRIMARY telescope, 1 for GUIDE telescope */ Q_SCRIPTABLE Q_NOREPLY void setFOVTelescopeType(int index); int FOVTelescopeType() { return FOVScopeCombo->currentIndex(); } /** DBUS interface function. * Get currently active camera info in this order: * width, height, pixel_size_x, pixel_size_y */ Q_SCRIPTABLE QList cameraInfo(); /** DBUS interface function. * Get current active telescope info in this order: * focal length, aperture */ Q_SCRIPTABLE QList telescopeInfo(); /** @}*/ /** * @brief Add CCD to the list of available CCD. * @param newCCD pointer to CCD device. */ void addCCD(ISD::GDInterface *newCCD); /** * @brief addFilter Add filter to the list of available filters. * @param newFilter pointer to filter device. */ void addFilter(ISD::GDInterface *newFilter); /** * @brief Set the current telescope * @param newTelescope pointer to telescope device. */ void setTelescope(ISD::GDInterface *newTelescope); /** * @brief Set the current dome * @param newDome pointer to telescope device. */ void setDome(ISD::GDInterface *newDome); void setRotator(ISD::GDInterface *newRotator); void removeDevice(ISD::GDInterface *device); /* @brief Set telescope and guide scope info. All measurements is in millimeters. * @param primaryFocalLength Primary Telescope Focal Length. Set to 0 to skip setting this value. * @param primaryAperture Primary Telescope Aperture. Set to 0 to skip setting this value. * @param guideFocalLength Guide Telescope Focal Length. Set to 0 to skip setting this value. * @param guideAperture Guide Telescope Aperture. Set to 0 to skip setting this value. */ void setTelescopeInfo(double primaryFocalLength, double primaryAperture, double guideFocalLength, double guideAperture); /** * @brief setAstrometryDevice * @param newAstrometry */ void setAstrometryDevice(ISD::GDInterface *newAstrometry); /** * @brief CCD information is updated, sync them. */ void syncCCDInfo(); /** * @brief Generate arguments we pass to the online and offline solvers. Keep user own arguments in place. */ void generateArgs(); /** * @brief Does our parser exist in the system? */ bool isParserOK(); // Log QStringList logText() { return m_LogText; } QString getLogText() { return m_LogText.join("\n"); } void clearLog(); /** * @brief getFOVScale Returns calculated FOV values * @param fov_w FOV width in arcmins * @param fov_h FOV height in arcmins * @param fov_scale FOV scale in arcsec per pixel */ void getFOVScale(double &fov_w, double &fov_h, double &fov_scale); QList fov(); /** * @brief getCalculatedFOVScale Get calculated FOV scales from the current CCD+Telescope combination. * @param fov_w return calculated fov width in arcminutes * @param fov_h return calculated fov height in arcminutes * @param fov_scale return calculated fov pixcale in arcsecs per pixel. * @note This is NOT the same as effective FOV which is the measured FOV from astrometry. It is the * theoretical FOV from calculated values. */ void getCalculatedFOVScale(double &fov_w, double &fov_h, double &fov_scale); void setFilterManager(const QSharedPointer &manager); // Ekos Live Client helper functions QStringList getActiveSolvers() const; int getActiveSolverIndex() const; void setCaptureSettings(const QJsonObject &settings); /** * @brief generateOptions Generate astrometry.net option given the supplied map * @param optionsMap List of key=value pairs for all astrometry.net options * @return String List of valid astrometry.net options */ static QStringList generateOptions(const QVariantMap &optionsMap, uint8_t solverType = SOLVER_ASTROMETRYNET); static void generateFOVBounds(double fov_h, QString &fov_low, QString &fov_high, double tolerance = 0.05); public slots: /** * @brief Process updated device properties * @param nvp pointer to updated property. */ void processNumber(INumberVectorProperty *nvp); /** * @brief Process updated device properties * @param svp pointer to updated property. */ void processSwitch(ISwitchVectorProperty *svp); /** * @brief Check CCD and make sure information is updated and FOV is re-calculated. * @param CCDNum By default, we check the already selected CCD in the dropdown menu. If CCDNum is specified, the check is made against this specific CCD in the dropdown menu. CCDNum is the index of the CCD in the dropdown menu. */ void checkCCD(int CCDNum = -1); /** * @brief Check Filter and make sure information is updated accordingly. * @param filterNum By default, we check the already selected filter in the dropdown menu. If filterNum is specified, the check is made against this specific filter in the dropdown menu. * filterNum is the index of the filter in the dropdown menu. */ void checkFilter(int filterNum = -1); /** * @brief checkCCDExposureProgress Track the progress of CCD exposure * @param targetChip Target chip under exposure * @param remaining how many seconds remaining * @param state status of exposure */ void checkCCDExposureProgress(ISD::CCDChip *targetChip, double remaining, IPState state); /** * @brief Process new FITS received from CCD. * @param bp pointer to blob property */ void newFITS(IBLOB *bp); /** \addtogroup AlignDBusInterface * @{ */ /** DBUS interface function. * Aborts the solving operation. */ Q_SCRIPTABLE Q_NOREPLY void abort(); /** DBUS interface function. * Select the solver type * @param type Set solver type. 0 ASTAP, 1 astrometry.net */ Q_SCRIPTABLE Q_NOREPLY void setSolverBackend(int type); /** DBUS interface function. * Select the astrometry solver type * @param type Set solver type. 0 online, 1 offline, 2 remote */ Q_SCRIPTABLE Q_NOREPLY void setAstrometrySolverType(int type); /** DBUS interface function. * Capture and solve an image using the astrometry.net engine * @return Returns true if the procedure started successful, false otherwise. */ Q_SCRIPTABLE bool captureAndSolve(); /** DBUS interface function. * Loads an image (FITS or JPG/TIFF) and solve its coordinates, then it slews to the solved coordinates and an image is captured and solved to ensure * the telescope is pointing to the same coordinates of the image. * @param fileURL URL to the image to solve */ Q_SCRIPTABLE bool loadAndSlew(QString fileURL = QString()); /** DBUS interface function. * Sets the binning of the selected CCD device. * @param binIndex Index of binning value. Default values range from 0 (binning 1x1) to 3 (binning 4x4) */ Q_SCRIPTABLE Q_NOREPLY void setBinningIndex(int binIndex); /** @}*/ /** * @brief Solver finished successfully, process the data and execute the required actions depending on the mode. * @param orientation Orientation of image in degrees (East of North) * @param ra Center RA in solved image, degrees. * @param dec Center DEC in solved image, degrees. * @param pixscale Image scale is arcsec/pixel */ void solverFinished(double orientation, double ra, double dec, double pixscale); /** * @brief Process solver failure. */ void solverFailed(); /** * @brief We received new telescope info, process them and update FOV. */ bool syncTelescopeInfo(); void setFocusStatus(Ekos::FocusState state); // Log void appendLogText(const QString &); // Capture void setCaptureComplete(); // Update Capture Module status void setCaptureStatus(Ekos::CaptureState newState); // Update Mount module status void setMountStatus(ISD::Telescope::Status newState); // PAH Ekos Live QString getPAHStage() const { return PAHStages[pahStage]; } bool isPAHEnabled() const { return isPAHReady; } QString getPAHMessage() const; void startPAHProcess(); void stopPAHProcess(); void setPAHCorrectionOffsetPercentage(double dx, double dy); void setPAHMountDirection(int index) { PAHDirectionCombo->setCurrentIndex(index); } void setPAHMountRotation(int value) { PAHRotationSpin->setValue(value); } void setPAHRefreshDuration(double value) { PAHExposure->setValue(value); } void startPAHRefreshProcess(); void setPAHRefreshComplete(); void setPAHSlewDone(); void setPAHCorrectionSelectionComplete(); void zoomAlignView(); // Align Settings QJsonObject getSettings() const; void setSettings(const QJsonObject &settings); // PAH Settings. PAH should be in separate class QJsonObject getPAHSettings() const; void setPAHSettings(const QJsonObject &settings); private slots: /* Polar Alignment */ void measureAltError(); void measureAzError(); void correctAzError(); void correctAltError(); void setDefaultCCD(QString ccd); void saveSettleTime(); // Solver timeout void checkAlignmentTimeout(); void updateTelescopeType(int index); // External View void showFITSViewer(); void toggleAlignWidgetFullScreen(); // Polar Alignment Helper slots void rotatePAH(); void setPAHCorrectionOffset(int x, int y); void setWCSToggled(bool result); //Solutions Display slots void buildTarget(); void handlePointTooltip(QMouseEvent *event); void handleVerticalPlotSizeChange(); void handleHorizontalPlotSizeChange(); void selectSolutionTableRow(int row, int column); void slotClearAllSolutionPoints(); void slotRemoveSolutionPoint(); void slotMountModel(); //Mount Model Slots void slotWizardAlignmentPoints(); void slotStarSelected(const QString selectedStar); void slotLoadAlignmentPoints(); void slotSaveAsAlignmentPoints(); void slotSaveAlignmentPoints(); void slotClearAllAlignPoints(); void slotRemoveAlignPoint(); void slotAddAlignPoint(); void slotFindAlignObject(); void resetAlignmentProcedure(); void startStopAlignmentProcedure(); void startAlignmentPoint(); void finishAlignmentPoint(bool solverSucceeded); void moveAlignPoint(int logicalIndex, int oldVisualIndex, int newVisualIndex); void exportSolutionPoints(); void alignTypeChanged(int alignType); void togglePreviewAlignPoints(); void slotSortAlignmentPoints(); void slotAutoScaleGraph(); // Settings void syncSettings(); protected slots: /** * @brief After a solver process is completed successfully, sync, slew to target, or do nothing as set by the user. */ void executeGOTO(); /** * @brief refreshAlignOptions is called when settings are updated in OpsAlign. */ void refreshAlignOptions(); signals: void newLog(const QString &text); void newStatus(Ekos::AlignState state); void newSolution(const QVariantMap &solution); // This is sent when we load an image in the view void newImage(FITSView *view); // This is sent when the pixmap is updated within the view void newFrame(FITSView *view); void polarResultUpdated(QLineF correctionVector, QString polarError); void newCorrectionVector(QLineF correctionVector); void newSolverResults(double orientation, double ra, double dec, double pixscale); // Polar Assistant Tool void newPAHStage(PAHStage stage); void newPAHMessage(const QString &message); void newFOVTelescopeType(int index); void PAHEnabled(bool); // Settings void settingsUpdated(const QJsonObject &settings); private: /** * @brief Calculate Field of View of CCD+Telescope combination that we need to pass to astrometry.net solver. */ void calculateFOV(); /** * @brief After a solver process is completed successfully, measure Azimuth or Altitude error as requested by the user. */ void executePolarAlign(); /** * @brief Sync the telescope to the solved alignment coordinate. */ void Sync(); /** * @brief Slew the telescope to the solved alignment coordinate. */ void Slew(); /** * @brief Sync the telescope to the solved alignment coordinate, and then slew to the target coordinate. */ void SlewToTarget(); /** * @brief Calculate polar alignment error magnitude and direction. * The calculation is performed by first capturing and solving a frame, then slewing 30 arcminutes and solving another frame to find the exact coordinates, then computing the error. * @param initRA RA of first frame. * @param initDEC DEC of first frame * @param finalRA RA of second frame * @param finalDEC DEC of second frame * @param initAz Azimuth of first frame */ void calculatePolarError(double initRA, double initDEC, double finalRA, double finalDEC, double initAz); /** * @brief Get formatted RA & DEC coordinates compatible with astrometry.net format. * @param ra Right ascension * @param dec Declination * @param ra_str will contain the formatted RA string * @param dec_str will contain the formatted DEC string */ void getFormattedCoords(double ra, double dec, QString &ra_str, QString &dec_str); /** * @brief getSolverOptionsFromFITS Generates a set of solver options given the supplied FITS image. The function reads FITS keyword headers and build the argument list accordingly. In case of a missing header keyword, it falls back to * the Alignment module existing values. * @param filename FITS path * @return List of Solver options */ QStringList getSolverOptionsFromFITS(const QString &filename); uint8_t getSolverDownsample(uint16_t binnedW); /** * @brief setWCSEnabled enables/disables World Coordinate System settings in the CCD driver. * @param enable true to enable WCS, false to disable. */ void setWCSEnabled(bool enable); /** * @brief calculatePAHError Calculate polar alignment error in the Polar Alignment Helper (PAH) method */ void calculatePAHError(); /** * @brief syncCorrectionVector Flip correction vector based on user settings. */ void syncCorrectionVector(); /** * @brief processPAHStage After solver is complete, handle PAH Stage processing */ void processPAHStage(double orientation, double ra, double dec, double pixscale); CircleSolution findCircleSolutions(const QPointF &p1, const QPointF p2, double angle, QPair &circleSolutions); double distance(const QPointF &p1, const QPointF &p2); bool findRACircle(QVector3D &RACircle); bool isPerpendicular(const QPointF &p1, const QPointF &p2, const QPointF &p3); bool calcCircle(const QPointF &p1, const QPointF &p2, const QPointF &p3, QVector3D &RACircle); void resizeEvent(QResizeEvent *event) override; bool alignmentPointsAreBad(); bool loadAlignmentPoints(const QString &fileURL); bool saveAlignmentPoints(const QString &path); void generateAlignStarList(); bool isVisible(const SkyObject *so); double getAltitude(const SkyObject *so); const SkyObject *getWizardAlignObject(double ra, double de); void calculateAngleForRALine(double &raIncrement, double &initRA, double initDEC, double lat, double raPoints, double minAlt); void calculateAZPointsForDEC(dms dec, dms alt, dms &AZEast, dms &AZWest); void updatePreviewAlignPoints(); int findNextAlignmentPointAfter(int currentSpot); int findClosestAlignmentPointToTelescope(); void swapAlignPoints(int firstPt, int secondPt); /** * @brief React when a mount motion has been detected */ void handleMountMotion(); /** * @brief Continue aligning according to the current mount status */ void handleMountStatus(); // Effective FOV /** * @brief getEffectiveFOV Search database for effective FOV that matches the current profile and settings * @return Variant Map containing effect FOV data or empty variant map if none found */ QVariantMap getEffectiveFOV(); void saveNewEffectiveFOV(double newFOVW, double newFOVH); QList effectiveFOVs; void syncFOV(); // We are using calculated FOV now until a more accurate effective FOV is found. bool m_EffectiveFOVPending { false }; /// Which chip should we invoke in the current CCD? bool useGuideHead { false }; /// Can the mount sync its coordinates to those set by Ekos? bool canSync { false }; // LoadSlew mode is when we load an image and solve it, no capture is done. //bool loadSlewMode; /// If load and slew is solved successfully, coordinates obtained, slewed to target, and then captured, solved, and re-slewed to target again. IPState loadSlewState { IPS_IDLE }; // Target Position Angle of solver Load&Slew image to be used for rotator if necessary double loadSlewTargetPA { std::numeric_limits::quiet_NaN() }; double currentRotatorPA { -1 }; /// Solver iterations count uint8_t solverIterations { 0 }; // FOV double ccd_hor_pixel { -1 }; double ccd_ver_pixel { -1 }; double focal_length { -1 }; double aperture { -1 }; double fov_x { 0 }; double fov_y { 0 }; double fov_pixscale { 0 }; int ccd_width { 0 }; int ccd_height { 0 }; // Keep track of solver results double sOrientation { INVALID_VALUE }; double sRA { INVALID_VALUE }; double sDEC { INVALID_VALUE }; /// Solver alignment coordinates SkyPoint alignCoord; /// Target coordinates we need to slew to SkyPoint targetCoord; /// Actual current telescope coordinates SkyPoint telescopeCoord; /// Coord from Load & Slew SkyPoint loadSlewCoord; /// Difference between solution and target coordinate double targetDiff { 1e6 }; /// Progress icon if the solver is running std::unique_ptr pi; /// Keep track of how long the solver is running - QTime solverTimer; + QElapsedTimer solverTimer; // Polar Alignment AZStage azStage; ALTStage altStage; double azDeviation { 0 }; double altDeviation { 0 }; double decDeviation { 0 }; static const double RAMotion; static const double SIDRATE; /// Have we slewed? bool m_wasSlewStarted { false }; // Online and Offline parsers AstrometryParser* parser { nullptr }; std::unique_ptr onlineParser; std::unique_ptr offlineParser; std::unique_ptr remoteParser; ISD::GDInterface *remoteParserDevice { nullptr }; std::unique_ptr astapParser; // Pointers to our devices ISD::Telescope *currentTelescope { nullptr }; ISD::Dome *currentDome { nullptr }; ISD::CCD *currentCCD { nullptr }; ISD::GDInterface *currentRotator { nullptr }; QList CCDs; /// Optional device filter ISD::GDInterface *currentFilter { nullptr }; /// They're generic GDInterface because they could be either ISD::CCD or ISD::Filter QList Filters; int currentFilterPosition { -1 }; /// True if we need to change filter position and wait for result before continuing capture bool filterPositionPending { false }; /// Keep track of solver FOV to be plotted in the skymap after each successful solve operation std::shared_ptr solverFOV; std::shared_ptr sensorFOV; /// WCS bool m_wcsSynced { false }; /// Log QStringList m_LogText; /// Issue counters uint8_t m_CaptureTimeoutCounter { 0 }; uint8_t m_CaptureErrorCounter { 0 }; uint8_t m_SlewErrorCounter { 0 }; QTimer m_CaptureTimer; // State AlignState state { ALIGN_IDLE }; FocusState focusState { FOCUS_IDLE }; // Track which upload mode the CCD is set to. If set to UPLOAD_LOCAL, then we need to switch it to UPLOAD_CLIENT in order to do focusing, and then switch it back to UPLOAD_LOCAL ISD::CCD::UploadMode rememberUploadMode { ISD::CCD::UPLOAD_CLIENT }; GotoMode currentGotoMode; QString dirPath; // Timer QTimer m_AlignTimer; // BLOB Type ISD::CCD::BlobType blobType; QString blobFileName; // Align Frame AlignView *alignView { nullptr }; // FITS Viewer in case user want to display in it instead of internal view QPointer fv; // Polar Alignment Helper PAHStage pahStage { PAH_IDLE }; SkyPoint targetPAH; bool isPAHReady { false }; // keep track of autoWSC bool rememberAutoWCS { false }; bool rememberSolverWCS { false }; bool rememberMeridianFlip { false }; // Sky centers typedef struct { SkyPoint skyCenter; QPointF celestialPole; QPointF pixelCenter; double pixelScale { 0 }; double orientation { 0 }; KStarsDateTime ts; } PAHImageInfo; QVector pahImageInfos; // User desired offset when selecting a bright star in the image QPointF celestialPolePoint, correctionOffset, RACenterPoint; // Correction vector line between mount RA Axis and celestial pole QLineF correctionVector; // CCDs using Guide Scope for parameters //QStringList guideScopeCCDs; // Which hemisphere are we located on? HemisphereType hemisphere; // Differential Slewing bool differentialSlewingActivated { false }; // Astrometry Options OpsAstrometry *opsAstrometry { nullptr }; OpsAlign *opsAlign { nullptr }; OpsAstrometryCfg *opsAstrometryCfg { nullptr }; OpsAstrometryIndexFiles *opsAstrometryIndexFiles { nullptr }; OpsASTAP *opsASTAP { nullptr }; QCPCurve *centralTarget { nullptr }; QCPCurve *yellowTarget { nullptr }; QCPCurve *redTarget { nullptr }; QCPCurve *concentricRings { nullptr }; QDialog mountModelDialog; Ui_mountModel mountModel; int currentAlignmentPoint { 0 }; bool mountModelRunning { false }; bool mountModelReset { false }; bool targetAccuracyNotMet { false }; bool previewShowing { false }; QUrl alignURL; QUrl alignURLPath; QVector alignStars; ISD::CCD::TelescopeType rememberTelescopeType = { ISD::CCD::TELESCOPE_UNKNOWN }; double primaryFL = -1, primaryAperture = -1, guideFL = -1, guideAperture = -1; bool m_isRateSynced = false; bool domeReady = true; // CCD Exposure Looping bool rememberCCDExposureLooping = { false }; // Filter Manager QSharedPointer filterManager; // Active Profile ProfileInfo *m_ActiveProfile { nullptr }; // PAH Stage Map static const QMap PAHStages; // Threshold to notify settle time is 3 seconds static constexpr uint16_t DELAY_THRESHOLD_NOTIFY { 3000 }; // Threshold to stop PAH rotation in degrees static constexpr uint8_t PAH_ROTATION_THRESHOLD { 5 }; }; } diff --git a/kstars/ekos/align/opsastrometryindexfiles.cpp b/kstars/ekos/align/opsastrometryindexfiles.cpp index d152cbcbe..632acb10f 100644 --- a/kstars/ekos/align/opsastrometryindexfiles.cpp +++ b/kstars/ekos/align/opsastrometryindexfiles.cpp @@ -1,553 +1,561 @@ #include "opsastrometryindexfiles.h" #include "align.h" #include "kstars.h" #include "ksutils.h" #include "Options.h" #include "kspaths.h" #include "ksnotification.h" #include #include namespace Ekos { OpsAstrometryIndexFiles::OpsAstrometryIndexFiles(Align *parent) : QDialog(KStars::Instance()) { setupUi(this); downloadSpeed = 100; actualdownloadSpeed = downloadSpeed; alignModule = parent; manager = new QNetworkAccessManager(); //Get a pointer to the KConfigDialog // m_ConfigDialog = KConfigDialog::exists( "alignsettings" ); connect(openIndexFileDirectory, SIGNAL(clicked()), this, SLOT(slotOpenIndexFileDirectory())); astrometryIndex[2.8] = "00"; astrometryIndex[4.0] = "01"; astrometryIndex[5.6] = "02"; astrometryIndex[8] = "03"; astrometryIndex[11] = "04"; astrometryIndex[16] = "05"; astrometryIndex[22] = "06"; astrometryIndex[30] = "07"; astrometryIndex[42] = "08"; astrometryIndex[60] = "09"; astrometryIndex[85] = "10"; astrometryIndex[120] = "11"; astrometryIndex[170] = "12"; astrometryIndex[240] = "13"; astrometryIndex[340] = "14"; astrometryIndex[480] = "15"; astrometryIndex[680] = "16"; astrometryIndex[1000] = "17"; astrometryIndex[1400] = "18"; astrometryIndex[2000] = "19"; QList checkboxes = findChildren(); - connect(indexLocations, static_cast(&QComboBox::currentIndexChanged), this, &OpsAstrometryIndexFiles::slotUpdate); + connect(indexLocations, static_cast(&QComboBox::currentIndexChanged), this, + &OpsAstrometryIndexFiles::slotUpdate); for (auto &checkBox : checkboxes) { connect(checkBox, &QCheckBox::clicked, this, &OpsAstrometryIndexFiles::downloadOrDeleteIndexFiles); } QList progressBars = findChildren(); QList qLabels = findChildren(); QList qButtons = findChildren(); for (auto &bar : progressBars) { if(bar->objectName().contains("progress")) { bar->setVisible(false); bar->setTextVisible(false); } } for (auto &button : qButtons) { if(button->objectName().contains("cancel")) { button->setVisible(false); } } for (QLabel * label : qLabels) { if(label->text().contains("info") || label->text().contains("perc")) { label->setVisible(false); } } } void OpsAstrometryIndexFiles::showEvent(QShowEvent *) { QStringList astrometryDataDirs = KSUtils::getAstrometryDataDirs(); if (astrometryDataDirs.count() == 0) return; indexLocations->clear(); if(astrometryDataDirs.count() > 1) indexLocations->addItem("All Sources"); indexLocations->addItems(astrometryDataDirs); slotUpdate(); } void OpsAstrometryIndexFiles::slotUpdate() { QList checkboxes = findChildren(); for (auto &checkBox : checkboxes) { checkBox->setChecked(false); } if(indexLocations->count() == 0) return; double fov_w, fov_h, fov_pixscale; // Values in arcmins. Scale in arcsec per pixel alignModule->getFOVScale(fov_w, fov_h, fov_pixscale); double fov_check = qMax(fov_w, fov_h); FOVOut->setText(QString("%1' x %2'").arg(QString::number(fov_w, 'f', 2), QString::number(fov_h, 'f', 2))); QStringList nameFilter("*.fits"); if (Options::astrometrySolverIsInternal()) KSUtils::configureLocalAstrometryConfIfNecessary(); QStringList astrometryDataDirs = KSUtils::getAstrometryDataDirs();; bool allDirsSelected = (indexLocations->currentIndex() == 0 && astrometryDataDirs.count() > 1); bool folderIsWriteable; QStringList astrometryDataDirsToIndex; if(allDirsSelected) { folderDetails->setText(i18n("Downloads Disabled, this is not a directory, it is a list of all index files.")); folderIsWriteable = false; astrometryDataDirsToIndex = astrometryDataDirs; openIndexFileDirectory->setEnabled(false); } else { QString folderPath = indexLocations->currentText(); folderIsWriteable = QFileInfo(folderPath).isWritable(); if(folderIsWriteable) folderDetails->setText(i18n("Downloads Enabled, the directory exists and is writeable.")); else folderDetails->setText(i18n("Downloads Disabled, directory permissions issue.")); if(!QFileInfo(folderPath).exists()) folderDetails->setText(i18n("Downloads Disabled, directory does not exist.")); astrometryDataDirsToIndex << folderPath; openIndexFileDirectory->setEnabled(true); } folderDetails->setCursorPosition(0); //This loop checks all the folders that are supposed to be checked for the files //It checks the box if it finds them for(QString astrometryDataDir : astrometryDataDirsToIndex) { QDir directory(astrometryDataDir); QStringList indexList = directory.entryList(nameFilter); for (auto &indexName : indexList) { if (fileCountMatches(directory, indexName)) { indexName = indexName.replace('-', '_').left(10); QCheckBox *indexCheckBox = findChild(indexName); if (indexCheckBox) indexCheckBox->setChecked(true); } } } for (auto &checkBox : checkboxes) { checkBox->setEnabled(folderIsWriteable); checkBox->setIcon(QIcon(":/icons/astrometry-optional.svg")); checkBox->setToolTip(i18n("Optional")); checkBox->setStyleSheet(""); } float last_skymarksize = 2; for (auto &skymarksize : astrometryIndex.keys()) { if ((skymarksize >= 0.40 * fov_check && skymarksize <= 0.9 * fov_check) || (fov_check > last_skymarksize && fov_check < skymarksize)) { QString indexName1 = "index_41" + astrometryIndex.value(skymarksize); QString indexName2 = "index_42" + astrometryIndex.value(skymarksize); QCheckBox *indexCheckBox1 = findChild(indexName1); QCheckBox *indexCheckBox2 = findChild(indexName2); if (indexCheckBox1) { indexCheckBox1->setIcon(QIcon(":/icons/astrometry-required.svg")); indexCheckBox1->setToolTip(i18n("Required")); } if (indexCheckBox2) { indexCheckBox2->setIcon(QIcon(":/icons/astrometry-required.svg")); indexCheckBox2->setToolTip(i18n("Required")); } } else if (skymarksize >= 0.10 * fov_check && skymarksize <= fov_check) { QString indexName1 = "index_41" + astrometryIndex.value(skymarksize); QString indexName2 = "index_42" + astrometryIndex.value(skymarksize); QCheckBox *indexCheckBox1 = findChild(indexName1); QCheckBox *indexCheckBox2 = findChild(indexName2); if (indexCheckBox1) { indexCheckBox1->setIcon(QIcon(":/icons/astrometry-recommended.svg")); indexCheckBox1->setToolTip(i18n("Recommended")); } if (indexCheckBox2) { indexCheckBox2->setIcon(QIcon(":/icons/astrometry-recommended.svg")); indexCheckBox2->setToolTip(i18n("Recommended")); } } last_skymarksize = skymarksize; } //This loop goes over all the directories and adds a stylesheet to change the look of the checkbox text //if the File is installed in any directory. Note that this indicator is then used below in the //Index File download function to check if they really want to do install a file that is installed. for(QString astrometryDataDir : astrometryDataDirs) { QDir directory(astrometryDataDir); QStringList indexList = directory.entryList(nameFilter); for (auto &indexName : indexList) { if (fileCountMatches(directory, indexName)) { indexName = indexName.replace('-', '_').left(10); QCheckBox *indexCheckBox = findChild(indexName); if (indexCheckBox) indexCheckBox->setStyleSheet("QCheckBox{font-weight: bold; color:green}"); } } } } bool OpsAstrometryIndexFiles::fileCountMatches(QDir directory, QString indexName) { QString indexNameMatch = indexName.left(10) + "*.fits"; QStringList list = directory.entryList(QStringList(indexNameMatch)); int count = 0; if(indexName.contains("4207") || indexName.contains("4206") || indexName.contains("4205")) count = 12; - else if(indexName.contains("4204") || indexName.contains("4203") || indexName.contains("4202") || indexName.contains("4201") || indexName.contains("4200")) + else if(indexName.contains("4204") || indexName.contains("4203") || indexName.contains("4202") + || indexName.contains("4201") || indexName.contains("4200")) count = 48; else count = 1; return list.count() == count; } void OpsAstrometryIndexFiles::slotOpenIndexFileDirectory() { if(indexLocations->count() == 0) return; QUrl path = QUrl::fromLocalFile(indexLocations->currentText()); QDesktopServices::openUrl(path); } bool OpsAstrometryIndexFiles::astrometryIndicesAreAvailable() { QNetworkReply *response = manager->get(QNetworkRequest(QUrl("http://broiler.astrometry.net"))); QTimer timeout(this); timeout.setInterval(5000); timeout.setSingleShot(true); timeout.start(); while (!response->isFinished()) { if (!timeout.isActive()) { response->deleteLater(); return false; } qApp->processEvents(); } timeout.stop(); bool wasSuccessful = (response->error() == QNetworkReply::NoError); response->deleteLater(); return wasSuccessful; } void OpsAstrometryIndexFiles::downloadIndexFile(const QString &URL, const QString &fileN, QCheckBox *checkBox, int currentIndex, int maxIndex, double fileSize) { - QTime downloadTime; + QElapsedTimer downloadTime; downloadTime.start(); QString indexString = QString::number(currentIndex); if (currentIndex < 10) indexString = '0' + indexString; QString indexSeriesName = checkBox->text().remove('&'); QProgressBar *indexDownloadProgress = findChild(indexSeriesName.replace('-', '_').left(10) + "_progress"); QLabel *indexDownloadInfo = findChild(indexSeriesName.replace('-', '_').left(10) + "_info"); QPushButton *indexDownloadCancel = findChild(indexSeriesName.replace('-', '_').left(10) + "_cancel"); QLabel *indexDownloadPerc = findChild(indexSeriesName.replace('-', '_').left(10) + "_perc"); setDownloadInfoVisible(indexSeriesName, checkBox, true); if(indexDownloadInfo) { if (indexDownloadProgress && maxIndex > 0) indexDownloadProgress->setValue(currentIndex * 100 / maxIndex); indexDownloadInfo->setText("(" + QString::number(currentIndex) + '/' + QString::number(maxIndex + 1) + ") "); } QString indexURL = URL; indexURL.replace('*', indexString); QNetworkReply *response = manager->get(QNetworkRequest(QUrl(indexURL))); //Shut it down after too much time elapses. //If the filesize is less than 4 MB, it sets the timeout for 1 minute or 60000 ms. //If it's larger, it assumes a bad download rate of 1 Mbps (100 bytes/ms) //and the calculation estimates the time in milliseconds it would take to download. int timeout = 60000; if(fileSize > 4000000) timeout = fileSize / downloadSpeed; //qDebug()<<"Filesize: "<< fileSize << ", timeout: " << timeout; QMetaObject::Connection *cancelConnection = new QMetaObject::Connection(); QMetaObject::Connection *replyConnection = new QMetaObject::Connection(); QMetaObject::Connection *percentConnection = new QMetaObject::Connection(); if(indexDownloadPerc) { *percentConnection = connect(response, &QNetworkReply::downloadProgress, [ = ](qint64 bytesReceived, qint64 bytesTotal) { if (indexDownloadProgress) { indexDownloadProgress->setValue(bytesReceived); indexDownloadProgress->setMaximum(bytesTotal); } indexDownloadPerc->setText(QString::number(bytesReceived * 100 / bytesTotal) + '%'); }); } timeoutTimer.disconnect(); connect(&timeoutTimer, &QTimer::timeout, [&]() { - KSNotification::error(i18n("Download Timed out. Either the network is not fast enough, the file is not accessible, or you are not connected.")); + KSNotification::error( + i18n("Download Timed out. Either the network is not fast enough, the file is not accessible, or you are not connected.")); disconnectDownload(cancelConnection, replyConnection, percentConnection); if(response) { response->abort(); response->deleteLater(); } setDownloadInfoVisible(indexSeriesName, checkBox, false); }); timeoutTimer.start(timeout); *cancelConnection = connect(indexDownloadCancel, &QPushButton::clicked, [ = ]() { qDebug() << "Download Cancelled."; timeoutTimer.stop(); disconnectDownload(cancelConnection, replyConnection, percentConnection); if(response) { response->abort(); response->deleteLater(); } setDownloadInfoVisible(indexSeriesName, checkBox, false); }); *replyConnection = connect(response, &QNetworkReply::finished, this, [ = ]() { timeoutTimer.stop(); if(response) { disconnectDownload(cancelConnection, replyConnection, percentConnection); setDownloadInfoVisible(indexSeriesName, checkBox, false); response->deleteLater(); if (response->error() != QNetworkReply::NoError) return; QByteArray responseData = response->readAll(); QString indexFileN = fileN; indexFileN.replace('*', indexString); QFile file(indexFileN); if (QFileInfo(QFileInfo(file).path()).isWritable()) { if (!file.open(QIODevice::WriteOnly)) { KSNotification::error(i18n("File Write Error")); slotUpdate(); return; } else { file.write(responseData.data(), responseData.size()); file.close(); int downloadedFileSize = QFileInfo(file).size(); int dtime = downloadTime.elapsed(); actualdownloadSpeed = (actualdownloadSpeed + (downloadedFileSize / dtime)) / 2; - qDebug() << "Filesize: " << downloadedFileSize << ", time: " << dtime << ", inst speed: " << downloadedFileSize / dtime << ", averaged speed: " << actualdownloadSpeed; + qDebug() << "Filesize: " << downloadedFileSize << ", time: " << dtime << ", inst speed: " << downloadedFileSize / dtime << + ", averaged speed: " << actualdownloadSpeed; } } else { KSNotification::error(i18n("Astrometry Folder Permissions Error")); } if (currentIndex == maxIndex) { slotUpdate(); } else downloadIndexFile(URL, fileN, checkBox, currentIndex + 1, maxIndex, fileSize); } }); } void OpsAstrometryIndexFiles::setDownloadInfoVisible(QString indexSeriesName, QCheckBox *checkBox, bool set) { Q_UNUSED(checkBox); QProgressBar *indexDownloadProgress = findChild(indexSeriesName.replace('-', '_').left(10) + "_progress"); QLabel *indexDownloadInfo = findChild(indexSeriesName.replace('-', '_').left(10) + "_info"); QPushButton *indexDownloadCancel = findChild(indexSeriesName.replace('-', '_').left(10) + "_cancel"); QLabel *indexDownloadPerc = findChild(indexSeriesName.replace('-', '_').left(10) + "_perc"); if (indexDownloadProgress) indexDownloadProgress->setVisible(set); if (indexDownloadInfo) indexDownloadInfo->setVisible(set); if (indexDownloadCancel) indexDownloadCancel->setVisible(set); if (indexDownloadPerc) indexDownloadPerc->setVisible(set); } -void OpsAstrometryIndexFiles::disconnectDownload(QMetaObject::Connection *cancelConnection, QMetaObject::Connection *replyConnection, QMetaObject::Connection *percentConnection) +void OpsAstrometryIndexFiles::disconnectDownload(QMetaObject::Connection *cancelConnection, + QMetaObject::Connection *replyConnection, QMetaObject::Connection *percentConnection) { if(cancelConnection) disconnect(*cancelConnection); if(replyConnection) disconnect(*replyConnection); if(percentConnection) disconnect(*percentConnection); } void OpsAstrometryIndexFiles::downloadOrDeleteIndexFiles(bool checked) { QCheckBox *checkBox = qobject_cast(QObject::sender()); if (indexLocations->count() == 0) return; QString astrometryDataDir = indexLocations->currentText(); if(!QFileInfo(astrometryDataDir).exists()) { - KSNotification::sorry(i18n("The selected Index File directory does not exist. Please either create it or choose another.")); + KSNotification::sorry( + i18n("The selected Index File directory does not exist. Please either create it or choose another.")); } if (checkBox) { QString indexSeriesName = checkBox->text().remove('&'); QString filePath = astrometryDataDir + '/' + indexSeriesName; QString fileNumString = indexSeriesName.mid(8, 2); int indexFileNum = fileNumString.toInt(); if (checked) { if(!checkBox->styleSheet().isEmpty()) //This means that the checkbox has a stylesheet so the index file was installed someplace. { if (KMessageBox::Cancel == KMessageBox::warningContinueCancel( - nullptr, i18n("The file %1 already exists in another directory. Are you sure you want to download it to this directory as well?", indexSeriesName), + nullptr, i18n("The file %1 already exists in another directory. Are you sure you want to download it to this directory as well?", + indexSeriesName), i18n("Install File(s)"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "install_index_files_warning")) { slotUpdate(); return; } } checkBox->setChecked(!checked); if (astrometryIndicesAreAvailable()) { QString URL; if (indexSeriesName.startsWith(QLatin1String("index-41"))) URL = "http://broiler.astrometry.net/~dstn/4100/" + indexSeriesName; else if (indexSeriesName.startsWith(QLatin1String("index-42"))) URL = "http://broiler.astrometry.net/~dstn/4200/" + indexSeriesName; int maxIndex = 0; if (indexFileNum < 8 && URL.contains("*")) { maxIndex = 11; if (indexFileNum < 5) maxIndex = 47; } - double fileSize = 1E11 * qPow(astrometryIndex.key(fileNumString), -1.909); //This estimates the file size based on skymark size obtained from the index number. + double fileSize = 1E11 * qPow(astrometryIndex.key(fileNumString), + -1.909); //This estimates the file size based on skymark size obtained from the index number. if(maxIndex != 0) fileSize /= maxIndex; //FileSize is divided between multiple files for some index series. downloadIndexFile(URL, filePath, checkBox, 0, maxIndex, fileSize); } else { KSNotification::sorry(i18n("Could not contact Astrometry Index Server: broiler.astrometry.net")); } } else { if (KMessageBox::Continue == KMessageBox::warningContinueCancel( nullptr, i18n("Are you sure you want to delete these index files? %1", indexSeriesName), i18n("Delete File(s)"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "delete_index_files_warning")) { if (QFileInfo(astrometryDataDir).isWritable()) { QStringList nameFilter("*.fits"); QDir directory(astrometryDataDir); QStringList indexList = directory.entryList(nameFilter); for (auto &fileName : indexList) { if (fileName.contains(indexSeriesName.left(10))) { if (!directory.remove(fileName)) { KSNotification::error(i18n("File Delete Error")); slotUpdate(); return; } slotUpdate(); } } } else { KSNotification::error(i18n("Astrometry Folder Permissions Error")); slotUpdate(); } } } } } } diff --git a/kstars/ekos/align/remoteastrometryparser.h b/kstars/ekos/align/remoteastrometryparser.h index 33f47dac6..ab50d5fc7 100644 --- a/kstars/ekos/align/remoteastrometryparser.h +++ b/kstars/ekos/align/remoteastrometryparser.h @@ -1,58 +1,61 @@ /* Astrometry.net Parser - Remote Copyright (C) 2016 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "astrometryparser.h" #include "indi/indiccd.h" namespace Ekos { class Align; /** * @class RemoteAstrometryParser * RemoteAstrometryParser invokes the remote astrometry.net solver in the remote CCD driver to solve the captured image. * The offline astrometry.net plus index files must be installed and configured on the remote host running the INDI CCD driver. * * @author Jasem Mutlaq */ class RemoteAstrometryParser : public AstrometryParser { - Q_OBJECT - - public: - RemoteAstrometryParser(); - virtual ~RemoteAstrometryParser() override = default; - - virtual void setAlign(Align *_align) override { align = _align; } - virtual bool init() override; - virtual void verifyIndexFiles(double fov_x, double fov_y) override; - virtual bool startSovler(const QString &filename, const QStringList &args, bool generated = true) override; - virtual bool stopSolver() override; - - void setAstrometryDevice(ISD::GDInterface *device); - void setEnabled(bool enable); - bool sendArgs(const QStringList &args); - bool setCCD(const QString& ccd); - - public slots: - void checkStatus(ISwitchVectorProperty *svp); - void checkResults(INumberVectorProperty *nvp); - - private: - ISD::GDInterface *remoteAstrometry { nullptr }; - bool solverRunning { false }; - bool captureRunning { false }; - Align *align { nullptr }; - QTime solverTimer; - QString parity; - QString targetCCD; + Q_OBJECT + + public: + RemoteAstrometryParser(); + virtual ~RemoteAstrometryParser() override = default; + + virtual void setAlign(Align *_align) override + { + align = _align; + } + virtual bool init() override; + virtual void verifyIndexFiles(double fov_x, double fov_y) override; + virtual bool startSovler(const QString &filename, const QStringList &args, bool generated = true) override; + virtual bool stopSolver() override; + + void setAstrometryDevice(ISD::GDInterface *device); + void setEnabled(bool enable); + bool sendArgs(const QStringList &args); + bool setCCD(const QString &ccd); + + public slots: + void checkStatus(ISwitchVectorProperty *svp); + void checkResults(INumberVectorProperty *nvp); + + private: + ISD::GDInterface *remoteAstrometry { nullptr }; + bool solverRunning { false }; + bool captureRunning { false }; + Align *align { nullptr }; + QElapsedTimer solverTimer; + QString parity; + QString targetCCD; }; } diff --git a/kstars/ekos/capture/capture.h b/kstars/ekos/capture/capture.h index 867d91083..d86a00e7a 100644 --- a/kstars/ekos/capture/capture.h +++ b/kstars/ekos/capture/capture.h @@ -1,975 +1,975 @@ /* Ekos Capture tool Copyright (C) 2012 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "ui_capture.h" #include "customproperties.h" #include "oal/filter.h" #include "ekos/ekos.h" #include "ekos/mount/mount.h" #include "indi/indiccd.h" #include "indi/indicap.h" #include "indi/indidome.h" #include "indi/indilightbox.h" #include "indi/inditelescope.h" #include "ekos/auxiliary/filtermanager.h" #include "ekos/scheduler/schedulerjob.h" #include "dslrinfodialog.h" #include #include #include #include class QProgressIndicator; class QTableWidgetItem; class KDirWatch; class RotatorSettings; /** * @namespace Ekos * @short Ekos is an advanced Astrophotography tool for Linux. * It is based on a modular extensible framework to perform common astrophotography tasks. This includes highly accurate GOTOs using astrometry solver, ability to measure and correct polar alignment errors , * auto-focus & auto-guide capabilities, and capture of single or stack of images with filter wheel support.\n * Features: * - Control your telescope, CCD (& DSLRs), filter wheel, focuser, guider, adaptive optics unit, and any INDI-compatible auxiliary device from Ekos. * - Extremely accurate GOTOs using astrometry.net solver (both Online and Offline solvers supported). * - Load & Slew: Load a FITS image, slew to solved coordinates, and center the mount on the exact image coordinates in order to get the same desired frame. * - Measure & Correct Polar Alignment errors using astrometry.net solver. * - Auto and manual focus modes using Half-Flux-Radius (HFR) method. * - Automated unattended meridian flip. Ekos performs post meridian flip alignment, calibration, and guiding to resume the capture session. * - Automatic focus between exposures when a user-configurable HFR limit is exceeded. * - Automatic focus between exposures when the temperature has changed a lot since last focus. * - Auto guiding with support for automatic dithering between exposures and support for Adaptive Optics devices in addition to traditional guiders. * - Powerful sequence queue for batch capture of images with optional prefixes, timestamps, filter wheel selection, and much more! * - Export and import sequence queue sets as Ekos Sequence Queue (.esq) files. * - Center the telescope anywhere in a captured FITS image or any FITS with World Coordinate System (WCS) header. * - Automatic flat field capture, just set the desired ADU and let Ekos does the rest! * - Automatic abort and resumption of exposure tasks if guiding errors exceed a user-configurable value. * - Support for dome slaving. * - Complete integration with KStars Observation Planner and SkyMap * - Integrate with all INDI native devices. * - Powerful scripting capabilities via \ref EkosDBusInterface "DBus." * * The primary class is Ekos::Manager. It handles startup and shutdown of local and remote INDI devices, manages and orchesterates the various Ekos modules, and provides advanced DBus * interface to enable unattended scripting. * * @author Jasem Mutlaq * @version 1.8 */ namespace Ekos { class SequenceJob; /** *@class Capture *@short Captures single or sequence of images from a CCD. * The capture class support capturing single or multiple images from a CCD, it provides a powerful sequence queue with filter wheel selection. Any sequence queue can be saved as Ekos Sequence Queue (.esq). * All image capture operations are saved as Sequence Jobs that encapsulate all the different options in a capture process. The user may select in sequence autofocusing by setting a maximum HFR limit. When the limit * is exceeded, it automatically trigger autofocus operation. The capture process can also be linked with guide module. If guiding deviations exceed a certain threshold, the capture operation aborts until * the guiding deviation resume to acceptable levels and the capture operation is resumed. *@author Jasem Mutlaq *@version 1.4 */ class Capture : public QWidget, public Ui::Capture { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kstars.Ekos.Capture") Q_PROPERTY(Ekos::CaptureState status READ status NOTIFY newStatus) Q_PROPERTY(QString targetName MEMBER m_TargetName) Q_PROPERTY(QString observerName MEMBER m_ObserverName) Q_PROPERTY(QString camera READ camera WRITE setCamera) Q_PROPERTY(QString filterWheel READ filterWheel WRITE setFilterWheel) Q_PROPERTY(QString filter READ filter WRITE setFilter) Q_PROPERTY(bool coolerControl READ hasCoolerControl WRITE setCoolerControl) Q_PROPERTY(QStringList logText READ logText NOTIFY newLog) public: typedef enum { MF_NONE, MF_REQUESTED, MF_READY, MF_INITIATED, MF_FLIPPING, MF_SLEWING, MF_COMPLETED, MF_ALIGNING, MF_GUIDING } MFStage; typedef enum { CAL_NONE, CAL_DUSTCAP_PARKING, CAL_DUSTCAP_PARKED, CAL_LIGHTBOX_ON, CAL_SLEWING, CAL_SLEWING_COMPLETE, CAL_MOUNT_PARKING, CAL_MOUNT_PARKED, CAL_DOME_PARKING, CAL_DOME_PARKED, CAL_PRECAPTURE_COMPLETE, CAL_CALIBRATION, CAL_CALIBRATION_COMPLETE, CAL_CAPTURING, CAL_DUSTCAP_UNPARKING, CAL_DUSTCAP_UNPARKED } CalibrationStage; typedef enum { CAL_CHECK_TASK, CAL_CHECK_CONFIRMATION, } CalibrationCheckType; typedef enum { ADU_LEAST_SQUARES, ADU_POLYNOMIAL } ADUAlgorithm; typedef bool (Capture::*PauseFunctionPointer)(); Capture(); ~Capture(); /** @defgroup CaptureDBusInterface Ekos DBus Interface - Capture Module * Ekos::Capture interface provides advanced scripting capabilities to capture image sequences. */ /*@{*/ /** DBUS interface function. * select the CCD device from the available CCD drivers. * @param device The CCD device name */ Q_SCRIPTABLE bool setCamera(const QString &device); Q_SCRIPTABLE QString camera(); /** DBUS interface function. * select the filter device from the available filter drivers. The filter device can be the same as the CCD driver if the filter functionality was embedded within the driver. * @param device The filter device name */ Q_SCRIPTABLE bool setFilterWheel(const QString &device); Q_SCRIPTABLE QString filterWheel(); /** DBUS interface function. * select the filter name from the available filters in case a filter device is active. * @param filter The filter name */ Q_SCRIPTABLE bool setFilter(const QString &filter); Q_SCRIPTABLE QString filter(); /** DBUS interface function. * Aborts any current jobs and remove all sequence queue jobs. */ Q_SCRIPTABLE Q_NOREPLY void clearSequenceQueue(); /** DBUS interface function. * Returns the overall sequence queue status. If there are no jobs pending, it returns "Invalid". If all jobs are idle, it returns "Idle". If all jobs are complete, it returns "Complete". If one or more jobs are aborted * it returns "Aborted" unless it was temporarily aborted due to guiding deviations, then it would return "Suspended". If one or more jobs have errors, it returns "Error". If any jobs is under progress, returns "Running". */ Q_SCRIPTABLE QString getSequenceQueueStatus(); /** DBUS interface function. * Loads the Ekos Sequence Queue file in the Sequence Queue. Jobs are appended to existing jobs. * @param fileURL full URL of the filename */ Q_SCRIPTABLE bool loadSequenceQueue(const QString &fileURL); /** DBUS interface function. * Enables or disables the maximum guiding deviation and sets its value. * @param enable If true, enable the guiding deviation check, otherwise, disable it. * @param value if enable is true, it sets the maximum guiding deviation in arcsecs. If the value is exceeded, the capture operation is aborted until the value falls below the value threshold. */ Q_SCRIPTABLE Q_NOREPLY void setMaximumGuidingDeviation(bool enable, double value); /** DBUS interface function. * Enables or disables the in sequence focus and sets Half-Flux-Radius (HFR) limit. * @param enable If true, enable the in sequence auto focus check, otherwise, disable it. * @param HFR if enable is true, it sets HFR in pixels. After each exposure, the HFR is re-measured and if it exceeds the specified value, an autofocus operation will be commanded. */ Q_SCRIPTABLE Q_NOREPLY void setInSequenceFocus(bool enable, double HFR); /** DBUS interface function. * Does the CCD has a cooler control (On/Off) ? */ Q_SCRIPTABLE bool hasCoolerControl(); /** DBUS interface function. * Set the CCD cooler ON/OFF * */ Q_SCRIPTABLE bool setCoolerControl(bool enable); /** DBUS interface function. * @return Returns the percentage of completed captures in all active jobs */ Q_SCRIPTABLE double getProgressPercentage(); /** DBUS interface function. * @return Returns the number of jobs in the sequence queue. */ Q_SCRIPTABLE int getJobCount() { return jobs.count(); } /** DBUS interface function. * @return Returns the number of pending uncompleted jobs in the sequence queue. */ Q_SCRIPTABLE int getPendingJobCount(); /** DBUS interface function. * @return Returns ID of current active job if any, or -1 if there are no active jobs. */ Q_SCRIPTABLE int getActiveJobID(); /** DBUS interface function. * @return Returns time left in seconds until active job is estimated to be complete. */ Q_SCRIPTABLE int getActiveJobRemainingTime(); /** DBUS interface function. * @return Returns overall time left in seconds until all jobs are estimated to be complete */ Q_SCRIPTABLE int getOverallRemainingTime(); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns the job state (Idle, In Progress, Error, Aborted, Complete) */ Q_SCRIPTABLE QString getJobState(int id); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns The number of images completed capture in the job. */ Q_SCRIPTABLE int getJobImageProgress(int id); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns the total number of images to capture in the job. */ Q_SCRIPTABLE int getJobImageCount(int id); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns the number of seconds left in an exposure operation. */ Q_SCRIPTABLE double getJobExposureProgress(int id); /** DBUS interface function. * @param id job number. Job IDs start from 0 to N-1. * @return Returns the total requested exposure duration in the job. */ Q_SCRIPTABLE double getJobExposureDuration(int id); /** DBUS interface function. * Clear in-sequence focus settings. It sets the autofocus HFR to zero so that next autofocus value is remembered for the in-sequence focusing. */ Q_SCRIPTABLE Q_NOREPLY void clearAutoFocusHFR(); /** DBUS interface function. * Jobs will NOT be checked for progress against the file system and will be always assumed as new jobs. */ Q_SCRIPTABLE Q_NOREPLY void ignoreSequenceHistory(); /** DBUS interface function. * Set count of already completed frames. This is required when we have identical external jobs * with identical paths, but we need to continue where we left off. For example, if we have 3 identical * jobs, each capturing 5 images. Let's suppose 9 images were captured before. If the count for this signature * is set to 1, then we continue to capture frame #2 even though the number of completed images is already * larger than required count (5). It is mostly used in conjunction with Ekos Scheduler. */ Q_SCRIPTABLE Q_NOREPLY void setCapturedFramesMap(const QString &signature, int count); Q_SCRIPTABLE QStringList logText() { return m_LogText; } Q_SCRIPTABLE Ekos::CaptureState status() { return m_State; } /** @}*/ void addCCD(ISD::GDInterface *newCCD); void addFilter(ISD::GDInterface *newFilter); void setDome(ISD::GDInterface *device) { currentDome = dynamic_cast(device); } void setDustCap(ISD::GDInterface *device) { currentDustCap = dynamic_cast(device); } void setLightBox(ISD::GDInterface *device) { currentLightBox = dynamic_cast(device); } void removeDevice(ISD::GDInterface *device); void addGuideHead(ISD::GDInterface *newCCD); void syncFrameType(ISD::GDInterface *ccd); void setTelescope(ISD::GDInterface *newTelescope); void setRotator(ISD::GDInterface *newRotator); void setFilterManager(const QSharedPointer &manager); void syncTelescopeInfo(); void syncFilterInfo(); // Restart driver void reconnectDriver(const QString &camera, const QString &filterWheel); void clearLog(); QString getLogText() { return m_LogText.join("\n"); } /* Capture */ void updateSequencePrefix(const QString &newPrefix, const QString &dir); /** * @brief getSequence Return the JSON representation of the current sequeue queue * @return Reference to JSON array containing sequence queue jobs. */ const QJsonArray &getSequence() const { return m_SequenceArray; } /** * @brief setSettings Set capture settings * @param settings list of settings */ void setSettings(const QJsonObject &settings); /** * @brief getSettings get current capture settings as a JSON Object * @return settings as JSON object */ QJsonObject getSettings(); /** * @brief addDSLRInfo Save DSLR Info the in the database. If the interactive dialog was open, close it. * @param model Camera name * @param maxW Maximum width in pixels * @param maxH Maximum height in pixels * @param pixelW Pixel horizontal size in microns * @param pixelH Pizel vertical size in microns */ void addDSLRInfo(const QString &model, uint32_t maxW, uint32_t maxH, double pixelW, double pixelH); /** * @brief syncDSLRToTargetChip Syncs INDI driver CCD_INFO property to the DSLR values. * This include Max width, height, and pixel sizes. * @param model Name of camera driver in the DSLR database. */ void syncDSLRToTargetChip(const QString &model); double getEstimatedDownloadTime(); public slots: /** \addtogroup CaptureDBusInterface * @{ */ /* Capture */ /** DBUS interface function. * Starts the sequence queue capture procedure sequentially by starting all jobs that are either Idle or Aborted in order. */ Q_SCRIPTABLE Q_NOREPLY void start(); /** DBUS interface function. * Stop all jobs and set current job status to aborted if abort is set to true, otherwise status is idle until * sequence is resumed or restarted. * @param targetState status of the job after abortion */ Q_SCRIPTABLE Q_NOREPLY void stop(CaptureState targetState = CAPTURE_IDLE); /** DBUS interface function. * Aborts all jobs and mark current state as ABORTED. It simply calls stop(CAPTURE_ABORTED) */ Q_SCRIPTABLE Q_NOREPLY void abort() { stop(CAPTURE_ABORTED); } /** DBUS interface function. * Aborts all jobs and mark current state as SUSPENDED. It simply calls stop(CAPTURE_SUSPENDED) * The only difference between SUSPENDED and ABORTED it that capture module can automatically resume a suspended * state on its own without external trigger once the right conditions are met. When whatever reason caused the module * to go into suspended state ceases to exist, the capture module automatically resumes. On the other hand, ABORTED state * must be started via an external programmatic or user trigger (e.g. click the start button again). */ Q_SCRIPTABLE Q_NOREPLY void suspend() { stop(CAPTURE_SUSPENDED); } /** DBUS interface function. * Toggle video streaming if supported by the device. * @param enabled Set to true to start video streaming, false to stop it if active. */ Q_SCRIPTABLE Q_NOREPLY void toggleVideo(bool enabled); /** DBUS interface function. * @brief pause Pauses the Sequence Queue progress AFTER the current capture is complete. */ Q_SCRIPTABLE Q_NOREPLY void pause(); /** DBUS interface function. * @brief toggleSequence Toggle sequence state depending on its current state. * 1. If paused, then resume sequence. * 2. If idle or completed, then start sequence. * 3. Otherwise, abort current sequence. */ Q_SCRIPTABLE Q_NOREPLY void toggleSequence(); /** @}*/ /** * @brief captureOne Capture one preview image */ void captureOne(); /** * @brief startFraming Like captureOne but repeating. */ void startFraming(); /** * @brief setExposure Set desired exposure value in seconds * @param value exposure values in seconds */ void setExposure(double value) { exposureIN->setValue(value); } /** * @brief seqCount Set required number of images to capture in one sequence job * @param count number of images to capture */ void setCount(uint16_t count) { countIN->setValue(count); } /** * @brief setDelay Set delay between capturing images within a sequence in seconds * @param delay numbers of seconds to wait before starting the next image. */ void setDelay(uint16_t delay) { delayIN->setValue(delay); } /** * @brief setPrefix Set target or prefix name used in constructing the generated file name * @param prefix Leading text of the generated image name. */ void setPrefix(const QString &prefix) { prefixIN->setText(prefix); } /** * @brief setBinning Set binning * @param horBin Horizontal binning * @param verBin Vertical binning */ void setBinning(int horBin, int verBin) { binXIN->setValue(horBin); binYIN->setValue(verBin); } /** * @brief setISO Set index of ISO list. * @param index index of ISO list. */ void setISO(int index) { ISOCombo->setCurrentIndex(index); } /** * @brief captureImage Initiates image capture in the active job. */ void captureImage(); /** * @brief newFITS process new FITS data received from camera. Update status of active job and overall sequence. * @param bp pointer to blob containing FITS data */ void newFITS(IBLOB *bp); /** * @brief checkCCD Refreshes the CCD information in the capture module. * @param CCDNum The CCD index in the CCD combo box to select as the active CCD. */ void checkCCD(int CCDNum = -1); /** * @brief checkFilter Refreshes the filter wheel information in the capture module. * @param filterNum The filter wheel index in the filter device combo box to set as the active filter. */ void checkFilter(int filterNum = -1); /** * @brief processCCDNumber Process number properties arriving from CCD. Currently, only CCD and Guider frames are processed. * @param nvp pointer to number property. */ void processCCDNumber(INumberVectorProperty *nvp); /** * @brief processTelescopeNumber Process number properties arriving from telescope for meridian flip purposes. * @param nvp pointer to number property. */ void processTelescopeNumber(INumberVectorProperty *nvp); /** * @brief addJob Add a new job to the sequence queue given the settings in the GUI. * @param preview True if the job is a preview job, otherwise, it is added as a batch job. * @return True if job is added successfully, false otherwise. */ bool addJob(bool preview = false); /** * @brief removeJob Remove a job sequence from the queue * @param index Row index for job to remove, if left as -1 (default), the currently selected row will be removed. * if no row is selected, the last job shall be removed. */ void removeJob(int index = -1); void removeJobFromQueue(); /** * @brief moveJobUp Move the job in the sequence queue one place up. */ void moveJobUp(); /** * @brief moveJobDown Move the job in the sequence queue one place down. */ void moveJobDown(); /** * @brief setGuideDeviation Set the guiding deviation as measured by the guiding module. Abort capture if deviation exceeds user value. Resume capture if capture was aborted and guiding deviations are below user value. * @param delta_ra Deviation in RA in arcsecs from the selected guide star. * @param delta_dec Deviation in DEC in arcsecs from the selected guide star. */ void setGuideDeviation(double delta_ra, double delta_dec); /** * @brief resumeCapture Resume capture after dither and/or focusing processes are complete. */ bool resumeCapture(); /** * @brief updateCCDTemperature Update CCD temperature in capture module. * @param value Temperature in celcius. */ void updateCCDTemperature(double value); /** * @brief setTemperature Set the target CCD temperature in the GUI settings. */ void setTargetTemperature(double temperature); void setForceTemperature(bool enabled) { temperatureCheck->setChecked(enabled); } /** * @brief prepareActiveJob Reset calibration state machine and prepare capture job actions. */ void prepareActiveJob(); /** * @brief preparePreCaptureActions Check if we need to update filter position or CCD temperature before starting capture process */ void preparePreCaptureActions(); void setFrameType(const QString &type) { frameTypeCombo->setCurrentText(type); } // Logs void appendLogText(const QString &); // Auto Focus void setFocusStatus(Ekos::FocusState state); void setHFR(double newHFR, int) { focusHFR = newHFR; } void setFocusTemperatureDelta(double focusTemperatureDelta); // Return TRUE if we need to run focus/autofocus. Otherwise false if not necessary bool startFocusIfRequired(); // Guide void setGuideStatus(Ekos::GuideState state); // short cut for all guiding states that indicate guiding is active bool isGuidingActive(); // Align void setAlignStatus(Ekos::AlignState state); void setAlignResults(double orientation, double ra, double de, double pixscale); // Update Mount module status void setMountStatus(ISD::Telescope::Status newState); void setGuideChip(ISD::CCDChip *chip); void setGeneratedPreviewFITS(const QString &previewFITS); // Clear Camera Configuration void clearCameraConfiguration(); // Meridian flip void meridianFlipStatusChanged(Mount::MeridianFlipStatus status); private slots: /** * @brief setDirty Set dirty bit to indicate sequence queue file was modified and needs saving. */ void setDirty(); void checkFrameType(int index); void resetFrame(); void setExposureProgress(ISD::CCDChip *tChip, double value, IPState state); void checkSeqBoundary(const QString &path); void saveFITSDirectory(); void setDefaultCCD(QString ccd); void setDefaultFilterWheel(QString filterWheel); void setNewRemoteFile(QString file); // Sequence Queue void loadSequenceQueue(); void saveSequenceQueue(); void saveSequenceQueueAs(); // Jobs void resetJobs(); void selectJob(QModelIndex i); void editJob(QModelIndex i); void resetJobEdit(); void executeJob(); // AutoGuide void checkGuideDeviationTimeout(); // Auto Focus // Timed refocus void startRefocusEveryNTimer() { startRefocusTimer(false); } void restartRefocusEveryNTimer() { startRefocusTimer(true); } int getRefocusEveryNTimerElapsedSec(); // Flat field void openCalibrationDialog(); IPState processPreCaptureCalibrationStage(); bool processPostCaptureCalibrationStage(); void updatePreCaptureCalibrationStatus(); // Frame Type calibration checks IPState checkLightFramePendingTasks(); IPState checkLightFrameAuxiliaryTasks(); IPState checkFlatFramePendingTasks(); IPState checkDarkFramePendingTasks(); // Send image info void sendNewImage(const QString &filename, ISD::CCDChip *myChip); // Capture bool setCaptureComplete(); // post capture script void postScriptFinished(int exitCode, QProcess::ExitStatus status); void setVideoStreamEnabled(bool enabled); // Observer void showObserverDialog(); // Active Job Prepare State void updatePrepareState(Ekos::CaptureState prepareState); // Rotator void updateRotatorNumber(INumberVectorProperty *nvp); // Cooler void setCoolerToggled(bool enabled); /** * @brief registerNewModule Register an Ekos module as it arrives via DBus * and create the appropriate DBus interface to communicate with it. * @param name of module */ void registerNewModule(const QString &name); void setDownloadProgress(); signals: Q_SCRIPTABLE void newLog(const QString &text); Q_SCRIPTABLE void meridianFlipStarted(); Q_SCRIPTABLE void meridianFlipCompleted(); Q_SCRIPTABLE void newStatus(Ekos::CaptureState status); Q_SCRIPTABLE void newSequenceImage(const QString &filename, const QString &previewFITS); void ready(); void checkFocus(double); void resetFocus(); void suspendGuiding(); void resumeGuiding(); void newImage(Ekos::SequenceJob *job); void newExposureProgress(Ekos::SequenceJob *job); void newDownloadProgress(double); void sequenceChanged(const QJsonArray &sequence); void settingsUpdated(const QJsonObject &settings); void newMeridianFlipStatus(Mount::MeridianFlipStatus status); void newMeridianFlipSetup(bool activate, double hours); void dslrInfoRequested(const QString &cameraName); void driverTimedout(const QString &deviceName); private: void setBusy(bool enable); bool resumeSequence(); bool startNextExposure(); // reset = 0 --> Do not reset // reset = 1 --> Full reset // reset = 2 --> Only update limits if needed void updateFrameProperties(int reset = 0); void prepareJob(SequenceJob *job); void syncGUIToJob(SequenceJob *job); bool processJobInfo(XMLEle *root); void processJobCompletion(); bool saveSequenceQueue(const QString &path); void constructPrefix(QString &imagePrefix); double setCurrentADU(double value); void llsq(QVector x, QVector y, double &a, double &b); // Gain // This sets and gets the custom properties target gain // it does not access the ccd gain property void setGain(double value); double getGain(); // DSLR Info void cullToDSLRLimits(); //void syncDriverToDSLRLimits(); bool isModelinDSLRInfo(const QString &model); /* Meridian Flip */ bool checkMeridianFlip(); void checkGuidingAfterFlip(); // check if a pause has been planned bool checkPausing(); // Remaining Time in seconds int getJobRemainingTime(SequenceJob *job); void resetFrameToZero(); /* Refocus */ void startRefocusTimer(bool forced = false); // If exposure timed out, let's handle it. void processCaptureTimeout(); // selection of a job void selectedJobChanged(QModelIndex current, QModelIndex previous); // Change filter name in INDI void editFilterName(); /* Capture */ /** * @brief Determine the overall number of target frames with the same signature. * Assume capturing RGBLRGB, where in each sequence job there is only one frame. * For "L" the result will be 1, for "R" it will be 2 etc. * @param frame signature (typically the filter name) * @return */ int getTotalFramesCount(QString signature); double seqExpose { 0 }; int seqTotalCount; int seqCurrentCount { 0 }; int seqDelay { 0 }; int retries { 0 }; QTimer *seqTimer { nullptr }; QString seqPrefix; int nextSequenceID { 0 }; int seqFileCount { 0 }; bool isBusy { false }; bool m_isLooping { false }; // Capture timeout timer QTimer captureTimeout; uint8_t m_CaptureTimeoutCounter { 0 }; uint8_t m_DeviceRestartCounter { 0 }; bool useGuideHead { false }; bool autoGuideReady { false}; QString m_TargetName; QString m_ObserverName; SequenceJob *activeJob { nullptr }; QList CCDs; ISD::CCDChip *targetChip { nullptr }; ISD::CCDChip *guideChip { nullptr }; ISD::CCDChip *blobChip { nullptr }; QString blobFilename; QString m_GeneratedPreviewFITS; // They're generic GDInterface because they could be either ISD::CCD or ISD::Filter QList Filters; QList jobs; ISD::Telescope *currentTelescope { nullptr }; ISD::CCD *currentCCD { nullptr }; ISD::GDInterface *currentFilter { nullptr }; ISD::GDInterface *currentRotator { nullptr }; ISD::DustCap *currentDustCap { nullptr }; ISD::LightBox *currentLightBox { nullptr }; ISD::Dome *currentDome { nullptr }; QPointer mountInterface { nullptr }; QStringList m_LogText; QUrl m_SequenceURL; bool m_Dirty { false }; bool m_JobUnderEdit { false }; int m_CurrentFilterPosition { -1 }; QProgressIndicator *pi { nullptr }; // Guide Deviation bool m_DeviationDetected { false }; bool m_SpikeDetected { false }; bool m_FilterOverride { false }; QTimer guideDeviationTimer; // Autofocus /** * @brief updateHFRThreshold calculate new HFR threshold based on median value for current selected filter */ void updateHFRThreshold(); bool isInSequenceFocus { false }; bool m_AutoFocusReady { false }; //bool requiredAutoFocusStarted { false }; //bool firstAutoFocus { true }; double focusHFR { 0 }; // HFR value as received from the Ekos focus module QMap> HFRMap; double fileHFR { 0 }; // HFR value as loaded from the sequence file // Refocus in progress because of time forced refocus or temperature change bool isRefocus { false }; // Focus on Temperature change bool isTemperatureDeltaCheckActive { false }; double focusTemperatureDelta { 0 }; // Temperature delta as received from the Ekos focus module // Refocus every N minutes int refocusEveryNMinutesValue { 0 }; // number of minutes between forced refocus QElapsedTimer refocusEveryNTimer; // used to determine when next force refocus should occur // Meridian flip SkyPoint initialMountCoords; bool resumeAlignmentAfterFlip { false }; bool resumeGuidingAfterFlip { false }; MFStage meridianFlipStage { MF_NONE }; QString MFStageString(MFStage stage); // Flat field automation QVector ExpRaw, ADURaw; double targetADU { 0 }; double targetADUTolerance { 1000 }; ADUAlgorithm targetADUAlgorithm { ADU_LEAST_SQUARES}; SkyPoint wallCoord; bool preMountPark { false }; bool preDomePark { false }; FlatFieldDuration flatFieldDuration { DURATION_MANUAL }; FlatFieldSource flatFieldSource { SOURCE_MANUAL }; CalibrationStage calibrationStage { CAL_NONE }; CalibrationCheckType calibrationCheckType { CAL_CHECK_TASK }; bool dustCapLightEnabled { false }; bool lightBoxLightEnabled { false }; bool m_TelescopeCoveredDarkExposure { false }; bool m_TelescopeCoveredFlatExposure { false }; ISD::CCD::UploadMode rememberUploadMode { ISD::CCD::UPLOAD_CLIENT }; QUrl dirPath; // Misc bool ignoreJobProgress { true }; bool suspendGuideOnDownload { false }; QJsonArray m_SequenceArray; // State CaptureState m_State { CAPTURE_IDLE }; FocusState focusState { FOCUS_IDLE }; GuideState guideState { GUIDE_IDLE }; AlignState alignState { ALIGN_IDLE }; FilterState filterManagerState { FILTER_IDLE }; PauseFunctionPointer pauseFunction; // CCD Chip frame settings QMap frameSettings; // Post capture script QProcess postCaptureScript; // Rotator Settings std::unique_ptr rotatorSettings; // How many images to capture before dithering operation is executed? uint8_t ditherCounter { 0 }; uint8_t inSequenceFocusCounter { 0 }; std::unique_ptr customPropertiesDialog; void createDSLRDialog(); std::unique_ptr dslrInfoDialog; // Filter Manager QSharedPointer filterManager; // DSLR Infos QList> DSLRInfos; // Captured Frames Map SchedulerJob::CapturedFramesMap capturedFramesMap; // Execute the meridian flip void setMeridianFlipStage(MFStage stage); void processFlipCompleted(); // Controls QPointer ISOCombo; QPointer GainSpin; double GainSpinSpecialValue; QList downloadTimes; - QTime downloadTimer; + QElapsedTimer downloadTimer; QTimer downloadProgressTimer; }; } diff --git a/kstars/ekos/scheduler/scheduler.cpp b/kstars/ekos/scheduler/scheduler.cpp index 9f212acd7..feb0fa809 100644 --- a/kstars/ekos/scheduler/scheduler.cpp +++ b/kstars/ekos/scheduler/scheduler.cpp @@ -1,7132 +1,7252 @@ /* Ekos Scheduler Module Copyright (C) 2015 Jasem Mutlaq DBus calls from GSoC 2015 Ekos Scheduler project by Daniel Leu This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "scheduler.h" #include "ksalmanac.h" #include "ksnotification.h" #include "kstars.h" #include "kstarsdata.h" #include "ksutils.h" #include "skymap.h" #include "mosaic.h" #include "Options.h" #include "scheduleradaptor.h" #include "schedulerjob.h" #include "skymapcomposite.h" #include "auxiliary/QProgressIndicator.h" #include "dialogs/finddialog.h" #include "ekos/manager.h" #include "ekos/capture/sequencejob.h" #include "skyobjects/starobject.h" #include #include #include #include #define BAD_SCORE -1000 #define MAX_FAILURE_ATTEMPTS 5 #define UPDATE_PERIOD_MS 1000 #define RESTART_GUIDING_DELAY_MS 5000 #define DEFAULT_CULMINATION_TIME -60 #define DEFAULT_MIN_ALTITUDE 15 #define DEFAULT_MIN_MOON_SEPARATION 0 namespace Ekos { Scheduler::Scheduler() { setupUi(this); qRegisterMetaType("Ekos::SchedulerState"); qDBusRegisterMetaType(); new SchedulerAdaptor(this); QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Scheduler", this); dirPath = QUrl::fromLocalFile(QDir::homePath()); // Get current KStars time and set seconds to zero QDateTime currentDateTime = KStarsData::Instance()->lt(); QTime currentTime = currentDateTime.time(); currentTime.setHMS(currentTime.hour(), currentTime.minute(), 0); currentDateTime.setTime(currentTime); // Set initial time for startup and completion times startupTimeEdit->setDateTime(currentDateTime); completionTimeEdit->setDateTime(currentDateTime); // Set up DBus interfaces QDBusConnection::sessionBus().registerObject("/KStars/Ekos/Scheduler", this); ekosInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", QDBusConnection::sessionBus(), this); // Example of connecting DBus signals //connect(ekosInterface, SIGNAL(indiStatusChanged(Ekos::CommunicationStatus)), this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus))); //connect(ekosInterface, SIGNAL(ekosStatusChanged(Ekos::CommunicationStatus)), this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus))); //connect(ekosInterface, SIGNAL(newModule(QString)), this, SLOT(registerNewModule(QString))); - QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "newModule", this, SLOT(registerNewModule(QString))); - QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "indiStatusChanged", this, SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus))); - QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "ekosStatusChanged", this, SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus))); + QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "newModule", this, + SLOT(registerNewModule(QString))); + QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "indiStatusChanged", this, + SLOT(setINDICommunicationStatus(Ekos::CommunicationStatus))); + QDBusConnection::sessionBus().connect("org.kde.kstars", "/KStars/Ekos", "org.kde.kstars.Ekos", "ekosStatusChanged", this, + SLOT(setEkosCommunicationStatus(Ekos::CommunicationStatus))); sleepLabel->setPixmap( QIcon::fromTheme("chronometer").pixmap(QSize(32, 32))); sleepLabel->hide(); connect(&sleepTimer, &QTimer::timeout, this, &Scheduler::wakeUpScheduler); schedulerTimer.setInterval(UPDATE_PERIOD_MS); jobTimer.setInterval(UPDATE_PERIOD_MS); connect(&schedulerTimer, &QTimer::timeout, this, &Scheduler::checkStatus); connect(&jobTimer, &QTimer::timeout, this, &Scheduler::checkJobStage); restartGuidingTimer.setSingleShot(true); restartGuidingTimer.setInterval(RESTART_GUIDING_DELAY_MS); connect(&restartGuidingTimer, &QTimer::timeout, this, [this]() { startGuiding(true); }); pi = new QProgressIndicator(this); bottomLayout->addWidget(pi, 0, nullptr); geo = KStarsData::Instance()->geo(); raBox->setDegType(false); //RA box should be HMS-style /* FIXME: Find a way to have multi-line tooltips in the .ui file, then move the widget configuration there - what about i18n? */ - queueTable->setToolTip(i18n("Job scheduler list.\nClick to select a job in the list.\nDouble click to edit a job with the left-hand fields.")); + queueTable->setToolTip( + i18n("Job scheduler list.\nClick to select a job in the list.\nDouble click to edit a job with the left-hand fields.")); /* Set first button mode to add observation job from left-hand fields */ setJobAddApply(true); removeFromQueueB->setIcon(QIcon::fromTheme("list-remove")); - removeFromQueueB->setToolTip(i18n("Remove selected job from the observation list.\nJob properties are copied in the edition fields before removal.")); + removeFromQueueB->setToolTip( + i18n("Remove selected job from the observation list.\nJob properties are copied in the edition fields before removal.")); removeFromQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueUpB->setIcon(QIcon::fromTheme("go-up")); queueUpB->setToolTip(i18n("Move selected job one line up in the list.\n" "Order only affect observation jobs that are scheduled to start at the same time.\n" "Not available if option \"Sort jobs by Altitude and Priority\" is set.")); queueUpB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueDownB->setIcon(QIcon::fromTheme("go-down")); queueDownB->setToolTip(i18n("Move selected job one line down in the list.\n" "Order only affect observation jobs that are scheduled to start at the same time.\n" "Not available if option \"Sort jobs by Altitude and Priority\" is set.")); queueDownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); evaluateOnlyB->setIcon(QIcon::fromTheme("system-reboot")); evaluateOnlyB->setToolTip(i18n("Reset state and force reevaluation of all observation jobs.")); evaluateOnlyB->setAttribute(Qt::WA_LayoutUsesWidgetRect); sortJobsB->setIcon(QIcon::fromTheme("transform-move-vertical")); - sortJobsB->setToolTip(i18n("Reset state and sort observation jobs per altitude and movement in sky, using the start time of the first job.\n" - "This action sorts setting targets before rising targets, and may help scheduling when starting your observation.\n" - "Option \"Sort Jobs by Altitude and Priority\" keeps the job list sorted this way, but with current time as reference.\n" - "Note the algorithm first calculates all altitudes using the same time, then evaluates jobs.")); + sortJobsB->setToolTip( + i18n("Reset state and sort observation jobs per altitude and movement in sky, using the start time of the first job.\n" + "This action sorts setting targets before rising targets, and may help scheduling when starting your observation.\n" + "Option \"Sort Jobs by Altitude and Priority\" keeps the job list sorted this way, but with current time as reference.\n" + "Note the algorithm first calculates all altitudes using the same time, then evaluates jobs.")); sortJobsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); mosaicB->setIcon(QIcon::fromTheme("zoom-draw")); mosaicB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveAsB->setIcon(QIcon::fromTheme("document-save-as")); queueSaveAsB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueSaveB->setIcon(QIcon::fromTheme("document-save")); queueSaveB->setAttribute(Qt::WA_LayoutUsesWidgetRect); queueLoadB->setIcon(QIcon::fromTheme("document-open")); queueLoadB->setAttribute(Qt::WA_LayoutUsesWidgetRect); loadSequenceB->setIcon(QIcon::fromTheme("document-open")); loadSequenceB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectStartupScriptB->setIcon(QIcon::fromTheme("document-open")); selectStartupScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectShutdownScriptB->setIcon( QIcon::fromTheme("document-open")); selectShutdownScriptB->setAttribute(Qt::WA_LayoutUsesWidgetRect); selectFITSB->setIcon(QIcon::fromTheme("document-open")); selectFITSB->setAttribute(Qt::WA_LayoutUsesWidgetRect); startupB->setIcon( QIcon::fromTheme("media-playback-start")); startupB->setAttribute(Qt::WA_LayoutUsesWidgetRect); shutdownB->setIcon( QIcon::fromTheme("media-playback-start")); shutdownB->setAttribute(Qt::WA_LayoutUsesWidgetRect); connect(startupB, &QPushButton::clicked, this, &Scheduler::runStartupProcedure); connect(shutdownB, &QPushButton::clicked, this, &Scheduler::runShutdownProcedure); connect(selectObjectB, &QPushButton::clicked, this, &Scheduler::selectObject); connect(selectFITSB, &QPushButton::clicked, this, &Scheduler::selectFITS); connect(loadSequenceB, &QPushButton::clicked, this, &Scheduler::selectSequence); connect(selectStartupScriptB, &QPushButton::clicked, this, &Scheduler::selectStartupScript); connect(selectShutdownScriptB, &QPushButton::clicked, this, &Scheduler::selectShutdownScript); connect(mosaicB, &QPushButton::clicked, this, &Scheduler::startMosaicTool); connect(addToQueueB, &QPushButton::clicked, this, &Scheduler::addJob); connect(removeFromQueueB, &QPushButton::clicked, this, &Scheduler::removeJob); connect(queueUpB, &QPushButton::clicked, this, &Scheduler::moveJobUp); connect(queueDownB, &QPushButton::clicked, this, &Scheduler::moveJobDown); connect(evaluateOnlyB, &QPushButton::clicked, this, &Scheduler::startJobEvaluation); connect(sortJobsB, &QPushButton::clicked, this, &Scheduler::sortJobsPerAltitude); - connect(queueTable->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Scheduler::queueTableSelectionChanged); + connect(queueTable->selectionModel(), &QItemSelectionModel::currentRowChanged, this, + &Scheduler::queueTableSelectionChanged); connect(queueTable, &QAbstractItemView::clicked, this, &Scheduler::clickQueueTable); connect(queueTable, &QAbstractItemView::doubleClicked, this, &Scheduler::loadJob); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setAttribute(Qt::WA_LayoutUsesWidgetRect); pauseB->setIcon(QIcon::fromTheme("media-playback-pause")); pauseB->setAttribute(Qt::WA_LayoutUsesWidgetRect); pauseB->setCheckable(false); connect(startB, &QPushButton::clicked, this, &Scheduler::toggleScheduler); connect(pauseB, &QPushButton::clicked, this, &Scheduler::pause); connect(queueSaveAsB, &QPushButton::clicked, this, &Scheduler::saveAs); connect(queueSaveB, &QPushButton::clicked, this, &Scheduler::save); connect(queueLoadB, &QPushButton::clicked, this, &Scheduler::load); connect(twilightCheck, &QCheckBox::toggled, this, &Scheduler::checkTwilightWarning); // Connect simulation clock scale connect(KStarsData::Instance()->clock(), &SimClock::scaleChanged, this, &Scheduler::simClockScaleChanged); // restore default values for error handling strategy setErrorHandlingStrategy(static_cast(Options::errorHandlingStrategy())); errorHandlingRescheduleErrorsCB->setChecked(Options::rescheduleErrors()); errorHandlingDelaySB->setValue(Options::errorHandlingStrategyDelay()); // save new default values for error handling strategy connect(errorHandlingRescheduleErrorsCB, &QPushButton::clicked, [](bool checked) { Options::setRescheduleErrors(checked); }); - connect(errorHandlingButtonGroup, static_cast(&QButtonGroup::buttonClicked), [this](QAbstractButton * button) + connect(errorHandlingButtonGroup, static_cast + (&QButtonGroup::buttonClicked), [this](QAbstractButton * button) { Q_UNUSED(button) Options::setErrorHandlingStrategy(getErrorHandlingStrategy()); }); connect(errorHandlingDelaySB, static_cast(&QSpinBox::valueChanged), [](int value) { Options::setErrorHandlingStrategyDelay(value); }); connect(copySkyCenterB, &QPushButton::clicked, this, [this]() { SkyPoint center = SkyMap::Instance()->getCenterPoint(); //center.deprecess(KStarsData::Instance()->updateNum()); center.catalogueCoord(KStarsData::Instance()->updateNum()->julianDay()); raBox->setDMS(center.ra0().toHMSString()); decBox->setDMS(center.dec0().toDMSString()); }); connect(KConfigDialog::exists("settings"), &KConfigDialog::settingsChanged, this, &Scheduler::applyConfig); calculateDawnDusk(); loadProfiles(); watchJobChanges(true); } QString Scheduler::getCurrentJobName() { return (currentJob != nullptr ? currentJob->getName() : ""); } void Scheduler::watchJobChanges(bool enable) { /* Don't double watch, this will cause multiple signals to be connected */ if (enable == jobChangesAreWatched) return; /* These are the widgets we want to connect, per signal function, to listen for modifications */ QLineEdit * const lineEdits[] = { nameEdit, raBox, decBox, fitsEdit, sequenceEdit, startupScript, shutdownScript }; QDateTimeEdit * const dateEdits[] = { startupTimeEdit, completionTimeEdit }; QComboBox * const comboBoxes[] = { schedulerProfileCombo }; QButtonGroup * const buttonGroups[] = { stepsButtonGroup, errorHandlingButtonGroup, startupButtonGroup, constraintButtonGroup, completionButtonGroup, startupProcedureButtonGroup, shutdownProcedureGroup }; QAbstractButton * const buttons[] = { errorHandlingRescheduleErrorsCB }; QSpinBox * const spinBoxes[] = { culminationOffset, repeatsSpin, prioritySpin, errorHandlingDelaySB }; QDoubleSpinBox * const dspinBoxes[] = { minMoonSeparation, minAltitude }; if (enable) { /* Connect the relevant signal to setDirty. Note that we are not keeping the connection object: we will * only use that signal once, and there will be no leaks. If we were connecting multiple receiver functions * to the same signal, we would have to be selective when disconnecting. We also use a lambda to absorb the * excess arguments which cannot be passed to setDirty, and limit captured arguments to 'this'. * The main problem with this implementation compared to the macro method is that it is now possible to * stack signal connections. That is, multiple calls to WatchJobChanges will cause multiple signal-to-slot * instances to be registered. As a result, one click will produce N signals, with N*=2 for each call to * WatchJobChanges(true) missing its WatchJobChanges(false) counterpart. */ for (auto * const control : lineEdits) connect(control, &QLineEdit::editingFinished, this, [this]() { setDirty(); }); for (auto * const control : dateEdits) connect(control, &QDateTimeEdit::editingFinished, this, [this]() { setDirty(); }); for (auto * const control : comboBoxes) connect(control, static_cast(&QComboBox::currentIndexChanged), this, [this]() { setDirty(); }); for (auto * const control : buttonGroups) connect(control, static_cast(&QButtonGroup::buttonToggled), this, [this](int, bool) { setDirty(); }); for (auto * const control : buttons) connect(control, static_cast(&QAbstractButton::clicked), this, [this](bool) { setDirty(); }); for (auto * const control : spinBoxes) connect(control, static_cast(&QSpinBox::valueChanged), this, [this]() { setDirty(); }); for (auto * const control : dspinBoxes) connect(control, static_cast(&QDoubleSpinBox::valueChanged), this, [this](double) { setDirty(); }); } else { /* Disconnect the relevant signal from each widget. Actually, this method removes all signals from the widgets, * because we did not take care to keep the connection object when connecting. No problem in our case, we do not * expect other signals to be connected. Because we used a lambda, we cannot use the same function object to * disconnect selectively. */ for (auto * const control : lineEdits) disconnect(control, &QLineEdit::editingFinished, this, nullptr); for (auto * const control : dateEdits) disconnect(control, &QDateTimeEdit::editingFinished, this, nullptr); for (auto * const control : comboBoxes) disconnect(control, static_cast(&QComboBox::currentIndexChanged), this, nullptr); for (auto * const control : buttons) disconnect(control, static_cast(&QAbstractButton::clicked), this, nullptr); for (auto * const control : buttonGroups) disconnect(control, static_cast(&QButtonGroup::buttonToggled), this, nullptr); for (auto * const control : spinBoxes) disconnect(control, static_cast(&QSpinBox::valueChanged), this, nullptr); for (auto * const control : dspinBoxes) disconnect(control, static_cast(&QDoubleSpinBox::valueChanged), this, nullptr); } jobChangesAreWatched = enable; } void Scheduler::appendLogText(const QString &text) { /* FIXME: user settings for log length */ int const max_log_count = 2000; if (m_LogText.size() > max_log_count) m_LogText.removeLast(); m_LogText.prepend(i18nc("log entry; %1 is the date, %2 is the text", "%1 %2", KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text)); qCInfo(KSTARS_EKOS_SCHEDULER) << text; emit newLog(text); } void Scheduler::clearLog() { m_LogText.clear(); emit newLog(QString()); } void Scheduler::applyConfig() { calculateDawnDusk(); if (SCHEDULER_RUNNING != state) { jobEvaluationOnly = true; evaluateJobs(); } } void Scheduler::selectObject() { if (FindDialog::Instance()->exec() == QDialog::Accepted) { SkyObject *object = FindDialog::Instance()->targetObject(); addObject(object); } } void Scheduler::addObject(SkyObject *object) { if (object != nullptr) { QString finalObjectName(object->name()); if (object->name() == "star") { StarObject *s = dynamic_cast(object); if (s->getHDIndex() != 0) finalObjectName = QString("HD %1").arg(s->getHDIndex()); } nameEdit->setText(finalObjectName); raBox->showInHours(object->ra0()); decBox->showInDegrees(object->dec0()); addToQueueB->setEnabled(sequenceEdit->text().isEmpty() == false); mosaicB->setEnabled(sequenceEdit->text().isEmpty() == false); setDirty(); } } void Scheduler::selectFITS() { fitsURL = QFileDialog::getOpenFileUrl(this, i18n("Select FITS Image"), dirPath, "FITS (*.fits *.fit)"); if (fitsURL.isEmpty()) return; dirPath = QUrl(fitsURL.url(QUrl::RemoveFilename)); fitsEdit->setText(fitsURL.toLocalFile()); if (nameEdit->text().isEmpty()) nameEdit->setText(fitsURL.fileName()); addToQueueB->setEnabled(sequenceEdit->text().isEmpty() == false); mosaicB->setEnabled(sequenceEdit->text().isEmpty() == false); processFITSSelection(); setDirty(); } void Scheduler::processFITSSelection() { const QString filename = fitsEdit->text(); int status = 0; double ra = 0, dec = 0; dms raDMS, deDMS; char comment[128], error_status[512]; fitsfile *fptr = nullptr; if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status); return; } status = 0; if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); qCCritical(KSTARS_EKOS_SCHEDULER) << QString::fromUtf8(error_status); return; } status = 0; char objectra_str[32] = {0}; if (fits_read_key(fptr, TSTRING, "OBJCTRA", objectra_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "RA", &ra, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find OBJCTRA (%1).", QString(error_status))); return; } raDMS.setD(ra); } else { raDMS = dms::fromString(objectra_str, false); } status = 0; char objectde_str[32] = {0}; if (fits_read_key(fptr, TSTRING, "OBJCTDEC", objectde_str, comment, &status)) { if (fits_read_key(fptr, TDOUBLE, "DEC", &dec, comment, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); appendLogText(i18n("FITS header: cannot find OBJCTDEC (%1).", QString(error_status))); return; } deDMS.setD(dec); } else { deDMS = dms::fromString(objectde_str, true); } raBox->setDMS(raDMS.toHMSString()); decBox->setDMS(deDMS.toDMSString()); char object_str[256] = {0}; if (fits_read_key(fptr, TSTRING, "OBJECT", object_str, comment, &status)) { QFileInfo info(filename); nameEdit->setText(info.completeBaseName()); } else { nameEdit->setText(object_str); } } void Scheduler::selectSequence() { sequenceURL = QFileDialog::getOpenFileUrl(this, i18n("Select Sequence Queue"), dirPath, i18n("Ekos Sequence Queue (*.esq)")); if (sequenceURL.isEmpty()) return; dirPath = QUrl(sequenceURL.url(QUrl::RemoveFilename)); sequenceEdit->setText(sequenceURL.toLocalFile()); // For object selection, all fields must be filled if ((raBox->isEmpty() == false && decBox->isEmpty() == false && nameEdit->text().isEmpty() == false) // For FITS selection, only the name and fits URL should be filled. || (nameEdit->text().isEmpty() == false && fitsURL.isEmpty() == false)) { addToQueueB->setEnabled(true); mosaicB->setEnabled(true); } setDirty(); } void Scheduler::selectStartupScript() { startupScriptURL = QFileDialog::getOpenFileUrl(this, i18n("Select Startup Script"), dirPath, i18n("Script (*)")); if (startupScriptURL.isEmpty()) return; dirPath = QUrl(startupScriptURL.url(QUrl::RemoveFilename)); mDirty = true; startupScript->setText(startupScriptURL.toLocalFile()); } void Scheduler::selectShutdownScript() { shutdownScriptURL = QFileDialog::getOpenFileUrl(this, i18n("Select Shutdown Script"), dirPath, i18n("Script (*)")); if (shutdownScriptURL.isEmpty()) return; dirPath = QUrl(shutdownScriptURL.url(QUrl::RemoveFilename)); mDirty = true; shutdownScript->setText(shutdownScriptURL.toLocalFile()); } void Scheduler::addJob() { if (0 <= jobUnderEdit) { /* If a job is being edited, reset edition mode as all fields are already transferred to the job */ resetJobEdit(); } else { /* If a job is being added, save fields into a new job */ saveJob(); /* There is now an evaluation for each change, so don't duplicate the evaluation now */ // jobEvaluationOnly = true; // evaluateJobs(); } } void Scheduler::saveJob() { if (state == SCHEDULER_RUNNING) { appendLogText(i18n("Warning: You cannot add or modify a job while the scheduler is running.")); return; } if (nameEdit->text().isEmpty()) { appendLogText(i18n("Warning: Target name is required.")); return; } if (sequenceEdit->text().isEmpty()) { appendLogText(i18n("Warning: Sequence file is required.")); return; } // Coordinates are required unless it is a FITS file if ((raBox->isEmpty() || decBox->isEmpty()) && fitsURL.isEmpty()) { appendLogText(i18n("Warning: Target coordinates are required.")); return; } bool raOk = false, decOk = false; dms /*const*/ ra(raBox->createDms(false, &raOk)); //false means expressed in hours dms /*const*/ dec(decBox->createDms(true, &decOk)); if (raOk == false) { appendLogText(i18n("Warning: RA value %1 is invalid.", raBox->text())); return; } if (decOk == false) { appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text())); return; } watchJobChanges(false); /* Create or Update a scheduler job */ int currentRow = queueTable->currentRow(); SchedulerJob * job = nullptr; /* If no row is selected for insertion, append at end of list. */ if (currentRow < 0) currentRow = queueTable->rowCount(); /* Add job to queue only if it is new, else reuse current row. * Make sure job is added at the right index, now that queueTable may have a line selected without being edited. */ if (0 <= jobUnderEdit) { /* FIXME: jobUnderEdit is a parallel variable that may cause issues if it desyncs from queueTable->currentRow(). */ if (jobUnderEdit != currentRow) qCWarning(KSTARS_EKOS_SCHEDULER) << "BUG: the observation job under edit does not match the selected row in the job table."; /* Use the job in the row currently edited */ job = jobs.at(currentRow); } else { /* Instantiate a new job, insert it in the job list and add a row in the table for it just after the row currently selected. */ job = new SchedulerJob(); jobs.insert(currentRow, job); queueTable->insertRow(currentRow); } /* Configure or reconfigure the observation job */ job->setName(nameEdit->text()); job->setPriority(prioritySpin->value()); job->setTargetCoords(ra, dec); job->setDateTimeDisplayFormat(startupTimeEdit->displayFormat()); /* Consider sequence file is new, and clear captured frames map */ job->setCapturedFramesMap(SchedulerJob::CapturedFramesMap()); job->setSequenceFile(sequenceURL); fitsURL = QUrl::fromLocalFile(fitsEdit->text()); job->setFITSFile(fitsURL); // #1 Startup conditions if (asapConditionR->isChecked()) { job->setStartupCondition(SchedulerJob::START_ASAP); } else if (culminationConditionR->isChecked()) { job->setStartupCondition(SchedulerJob::START_CULMINATION); job->setCulminationOffset(culminationOffset->value()); } else { job->setStartupCondition(SchedulerJob::START_AT); job->setStartupTime(startupTimeEdit->dateTime()); } /* Store the original startup condition */ job->setFileStartupCondition(job->getStartupCondition()); job->setFileStartupTime(job->getStartupTime()); // #2 Constraints // Do we have minimum altitude constraint? if (altConstraintCheck->isChecked()) job->setMinAltitude(minAltitude->value()); else job->setMinAltitude(-90); // Do we have minimum moon separation constraint? if (moonSeparationCheck->isChecked()) job->setMinMoonSeparation(minMoonSeparation->value()); else job->setMinMoonSeparation(-1); // Check enforce weather constraints job->setEnforceWeather(weatherCheck->isChecked()); // twilight constraints job->setEnforceTwilight(twilightCheck->isChecked()); /* Verifications */ /* FIXME: perhaps use a method more visible to the end-user */ if (SchedulerJob::START_AT == job->getFileStartupCondition()) { /* Warn if appending a job which startup time doesn't allow proper score */ if (calculateJobScore(job, job->getStartupTime()) < 0) - appendLogText(i18n("Warning: job '%1' has startup time %2 resulting in a negative score, and will be marked invalid when processed.", - job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); + appendLogText( + i18n("Warning: job '%1' has startup time %2 resulting in a negative score, and will be marked invalid when processed.", + job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()))); } // #3 Completion conditions if (sequenceCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_SEQUENCE); } else if (repeatCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_REPEAT); job->setRepeatsRequired(repeatsSpin->value()); job->setRepeatsRemaining(repeatsSpin->value()); } else if (loopCompletionR->isChecked()) { job->setCompletionCondition(SchedulerJob::FINISH_LOOP); } else { job->setCompletionCondition(SchedulerJob::FINISH_AT); job->setCompletionTime(completionTimeEdit->dateTime()); } // Job steps job->setStepPipeline(SchedulerJob::USE_NONE); if (trackStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_TRACK)); if (focusStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_FOCUS)); if (alignStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_ALIGN)); if (guideStepCheck->isChecked()) job->setStepPipeline(static_cast(job->getStepPipeline() | SchedulerJob::USE_GUIDE)); /* Reset job state to evaluate the changes */ job->reset(); // Warn user if a duplicated job is in the list - same target, same sequence // FIXME: Those duplicated jobs are not necessarily processed in the order they appear in the list! foreach (SchedulerJob *a_job, jobs) { if (a_job == job) { break; } else if (a_job->getName() == job->getName()) { int const a_job_row = a_job->getNameCell() ? a_job->getNameCell()->row() + 1 : 0; /* FIXME: Warning about duplicate jobs only checks the target name, doing it properly would require checking storage for each sequence job of each scheduler job. */ appendLogText(i18n("Warning: job '%1' at row %2 has a duplicate target at row %3, " "the scheduler may consider the same storage for captures.", job->getName(), currentRow, a_job_row)); /* Warn the user in case the two jobs are really identical */ if (a_job->getSequenceFile() == job->getSequenceFile()) { if (a_job->getRepeatsRequired() == job->getRepeatsRequired() && Options::rememberJobProgress()) appendLogText(i18n("Warning: jobs '%1' at row %2 and %3 probably require a different repeat count " "as currently they will complete simultaneously after %4 batches (or disable option 'Remember job progress')", job->getName(), currentRow, a_job_row, job->getRepeatsRequired())); if (a_job->getStartupTime() == a_job->getStartupTime() && a_job->getPriority() == job->getPriority()) appendLogText(i18n("Warning: job '%1' at row %2 might require a specific startup time or a different priority, " "as currently they will start in order of insertion in the table", job->getName(), currentRow)); } } } if (-1 == jobUnderEdit) { QTableWidgetItem *nameCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_NAME), nameCell); nameCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); nameCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *statusCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_STATUS), statusCell); statusCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); statusCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *captureCount = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_CAPTURES), captureCount); captureCount->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); captureCount->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *scoreValue = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_SCORE), scoreValue); scoreValue->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); scoreValue->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *startupCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_STARTTIME), startupCell); startupCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); startupCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *altitudeCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_ALTITUDE), altitudeCell); altitudeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); altitudeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *completionCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_ENDTIME), completionCell); completionCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); completionCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *estimatedTimeCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_DURATION), estimatedTimeCell); estimatedTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); estimatedTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); QTableWidgetItem *leadTimeCell = new QTableWidgetItem(); queueTable->setItem(currentRow, static_cast(SCHEDCOL_LEADTIME), leadTimeCell); leadTimeCell->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); leadTimeCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); } setJobStatusCells(currentRow); /* We just added or saved a job, so we have a job in the list - enable relevant buttons */ queueSaveAsB->setEnabled(true); queueSaveB->setEnabled(true); startB->setEnabled(true); evaluateOnlyB->setEnabled(true); setJobManipulation(!Options::sortSchedulerJobs(), true); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 was saved.").arg(job->getName()).arg(currentRow + 1); watchJobChanges(true); if (SCHEDULER_LOADING != state) { jobEvaluationOnly = true; evaluateJobs(); } } void Scheduler::syncGUIToJob(SchedulerJob *job) { nameEdit->setText(job->getName()); prioritySpin->setValue(job->getPriority()); raBox->showInHours(job->getTargetCoords().ra0()); decBox->showInDegrees(job->getTargetCoords().dec0()); if (job->getFITSFile().isEmpty() == false) fitsEdit->setText(job->getFITSFile().toLocalFile()); else fitsEdit->clear(); sequenceEdit->setText(job->getSequenceFile().toLocalFile()); trackStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_TRACK); focusStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_FOCUS); alignStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_ALIGN); guideStepCheck->setChecked(job->getStepPipeline() & SchedulerJob::USE_GUIDE); switch (job->getFileStartupCondition()) { case SchedulerJob::START_ASAP: asapConditionR->setChecked(true); culminationOffset->setValue(DEFAULT_CULMINATION_TIME); break; case SchedulerJob::START_CULMINATION: culminationConditionR->setChecked(true); culminationOffset->setValue(job->getCulminationOffset()); break; case SchedulerJob::START_AT: startupTimeConditionR->setChecked(true); startupTimeEdit->setDateTime(job->getStartupTime()); culminationOffset->setValue(DEFAULT_CULMINATION_TIME); break; } if (-90 < job->getMinAltitude()) { altConstraintCheck->setChecked(true); minAltitude->setValue(job->getMinAltitude()); } else { altConstraintCheck->setChecked(false); minAltitude->setValue(DEFAULT_MIN_ALTITUDE); } if (job->getMinMoonSeparation() >= 0) { moonSeparationCheck->setChecked(true); minMoonSeparation->setValue(job->getMinMoonSeparation()); } else { moonSeparationCheck->setChecked(false); minMoonSeparation->setValue(DEFAULT_MIN_MOON_SEPARATION); } weatherCheck->setChecked(job->getEnforceWeather()); twilightCheck->blockSignals(true); twilightCheck->setChecked(job->getEnforceTwilight()); twilightCheck->blockSignals(false); switch (job->getCompletionCondition()) { case SchedulerJob::FINISH_SEQUENCE: sequenceCompletionR->setChecked(true); break; case SchedulerJob::FINISH_REPEAT: repeatCompletionR->setChecked(true); repeatsSpin->setValue(job->getRepeatsRequired()); break; case SchedulerJob::FINISH_LOOP: loopCompletionR->setChecked(true); break; case SchedulerJob::FINISH_AT: timeCompletionR->setChecked(true); completionTimeEdit->setDateTime(job->getCompletionTime()); break; } setJobManipulation(!Options::sortSchedulerJobs(), true); } void Scheduler::loadJob(QModelIndex i) { if (jobUnderEdit == i.row()) return; if (state == SCHEDULER_RUNNING) { appendLogText(i18n("Warning: you cannot add or modify a job while the scheduler is running.")); return; } SchedulerJob * const job = jobs.at(i.row()); if (job == nullptr) return; watchJobChanges(false); //job->setState(SchedulerJob::JOB_IDLE); //job->setStage(SchedulerJob::STAGE_IDLE); syncGUIToJob(job); if (job->getFITSFile().isEmpty() == false) fitsURL = job->getFITSFile(); else fitsURL = QUrl(); sequenceURL = job->getSequenceFile(); /* Turn the add button into an apply button */ setJobAddApply(false); /* Disable scheduler start/evaluate buttons */ startB->setEnabled(false); evaluateOnlyB->setEnabled(false); /* Don't let the end-user remove a job being edited */ setJobManipulation(false, false); jobUnderEdit = i.row(); - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is currently edited.").arg(job->getName()).arg(jobUnderEdit + 1); + qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is currently edited.").arg(job->getName()).arg( + jobUnderEdit + 1); watchJobChanges(true); } void Scheduler::queueTableSelectionChanged(QModelIndex current, QModelIndex previous) { Q_UNUSED(previous) // prevent selection when not idle if (state != SCHEDULER_IDLE) return; if (current.row() < 0 || (current.row() + 1) > jobs.size()) return; SchedulerJob * const job = jobs.at(current.row()); if (job == nullptr) return; resetJobEdit(); syncGUIToJob(job); } void Scheduler::clickQueueTable(QModelIndex index) { setJobManipulation(!Options::sortSchedulerJobs() && index.isValid(), index.isValid()); } void Scheduler::setJobAddApply(bool add_mode) { if (add_mode) { addToQueueB->setIcon(QIcon::fromTheme("list-add")); addToQueueB->setToolTip(i18n("Use edition fields to create a new job in the observation list.")); //addToQueueB->setStyleSheet(QString()); addToQueueB->setAttribute(Qt::WA_LayoutUsesWidgetRect); } else { addToQueueB->setIcon(QIcon::fromTheme("dialog-ok-apply")); addToQueueB->setToolTip(i18n("Apply job changes.")); //addToQueueB->setStyleSheet("background-color:orange;}"); addToQueueB->setEnabled(true); } } void Scheduler::setJobManipulation(bool can_reorder, bool can_delete) { bool can_edit = (state == SCHEDULER_IDLE); if (can_reorder) { int const currentRow = queueTable->currentRow(); queueUpB->setEnabled(can_edit && 0 < currentRow); queueDownB->setEnabled(can_edit && currentRow < queueTable->rowCount() - 1); } else { queueUpB->setEnabled(false); queueDownB->setEnabled(false); } sortJobsB->setEnabled(can_edit && can_reorder); removeFromQueueB->setEnabled(can_edit && can_delete); } bool Scheduler::reorderJobs(QList reordered_sublist) { /* Add jobs not reordered at the end of the list, in initial order */ foreach (SchedulerJob* job, jobs) if (!reordered_sublist.contains(job)) reordered_sublist.append(job); if (jobs != reordered_sublist) { /* Remember job currently selected */ int const selectedRow = queueTable->currentRow(); SchedulerJob * const selectedJob = 0 <= selectedRow ? jobs.at(selectedRow) : nullptr; /* Reassign list */ jobs = reordered_sublist; /* Reassign status cells for all jobs, and reset them */ for (int row = 0; row < jobs.size(); row++) setJobStatusCells(row); /* Reselect previously selected job */ if (nullptr != selectedJob) queueTable->selectRow(jobs.indexOf(selectedJob)); return true; } else return false; } void Scheduler::moveJobUp() { /* No move if jobs are sorted automatically */ if (Options::sortSchedulerJobs()) return; int const rowCount = queueTable->rowCount(); int const currentRow = queueTable->currentRow(); int const destinationRow = currentRow - 1; /* No move if no job selected, if table has one line or less or if destination is out of table */ if (currentRow < 0 || rowCount <= 1 || destinationRow < 0) return; /* Swap jobs in the list */ #if QT_VERSION >= QT_VERSION_CHECK(5,13,0) jobs.swapItemsAt(currentRow, destinationRow); #else jobs.swap(currentRow, destinationRow); #endif /* Reassign status cells */ setJobStatusCells(currentRow); setJobStatusCells(destinationRow); /* Move selection to destination row */ queueTable->selectRow(destinationRow); setJobManipulation(!Options::sortSchedulerJobs(), true); /* Jobs are now sorted, so reset all later jobs */ for (int row = destinationRow; row < jobs.size(); row++) jobs.at(row)->reset(); /* Make list modified and evaluate jobs */ mDirty = true; jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::moveJobDown() { /* No move if jobs are sorted automatically */ if (Options::sortSchedulerJobs()) return; int const rowCount = queueTable->rowCount(); int const currentRow = queueTable->currentRow(); int const destinationRow = currentRow + 1; /* No move if no job selected, if table has one line or less or if destination is out of table */ if (currentRow < 0 || rowCount <= 1 || destinationRow == rowCount) return; /* Swap jobs in the list */ #if QT_VERSION >= QT_VERSION_CHECK(5,13,0) jobs.swapItemsAt(currentRow, destinationRow); #else jobs.swap(currentRow, destinationRow); #endif /* Reassign status cells */ setJobStatusCells(currentRow); setJobStatusCells(destinationRow); /* Move selection to destination row */ queueTable->selectRow(destinationRow); setJobManipulation(!Options::sortSchedulerJobs(), true); /* Jobs are now sorted, so reset all later jobs */ for (int row = currentRow; row < jobs.size(); row++) jobs.at(row)->reset(); /* Make list modified and evaluate jobs */ mDirty = true; jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::setJobStatusCells(int row) { if (row < 0 || jobs.size() <= row) return; SchedulerJob * const job = jobs.at(row); job->setNameCell(queueTable->item(row, static_cast(SCHEDCOL_NAME))); job->setStatusCell(queueTable->item(row, static_cast(SCHEDCOL_STATUS))); job->setCaptureCountCell(queueTable->item(row, static_cast(SCHEDCOL_CAPTURES))); job->setScoreCell(queueTable->item(row, static_cast(SCHEDCOL_SCORE))); job->setAltitudeCell(queueTable->item(row, static_cast(SCHEDCOL_ALTITUDE))); job->setStartupCell(queueTable->item(row, static_cast(SCHEDCOL_STARTTIME))); job->setCompletionCell(queueTable->item(row, static_cast(SCHEDCOL_ENDTIME))); job->setEstimatedTimeCell(queueTable->item(row, static_cast(SCHEDCOL_DURATION))); job->setLeadTimeCell(queueTable->item(row, static_cast(SCHEDCOL_LEADTIME))); job->updateJobCells(); } void Scheduler::resetJobEdit() { if (jobUnderEdit < 0) return; SchedulerJob * const job = jobs.at(jobUnderEdit); Q_ASSERT_X(job != nullptr, __FUNCTION__, "Edited job must be valid"); - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is not longer edited.").arg(job->getName()).arg(jobUnderEdit + 1); + qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is not longer edited.").arg(job->getName()).arg( + jobUnderEdit + 1); jobUnderEdit = -1; watchJobChanges(false); /* Revert apply button to add */ setJobAddApply(true); /* Refresh state of job manipulation buttons */ setJobManipulation(!Options::sortSchedulerJobs(), true); /* Restore scheduler operation buttons */ evaluateOnlyB->setEnabled(true); startB->setEnabled(true); Q_ASSERT_X(jobUnderEdit == -1, __FUNCTION__, "No more edited/selected job after exiting edit mode"); } void Scheduler::removeJob() { int currentRow = queueTable->currentRow(); /* Don't remove a row that is not selected */ if (currentRow < 0) return; /* Grab the job currently selected */ SchedulerJob * const job = jobs.at(currentRow); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' at row #%2 is being deleted.").arg(job->getName()).arg(currentRow + 1); /* Remove the job from the table */ queueTable->removeRow(currentRow); /* If there are no job rows left, update UI buttons */ if (queueTable->rowCount() == 0) { setJobManipulation(false, false); evaluateOnlyB->setEnabled(false); queueSaveAsB->setEnabled(false); queueSaveB->setEnabled(false); startB->setEnabled(false); pauseB->setEnabled(false); } /* Else update the selection */ else { if (currentRow > queueTable->rowCount()) currentRow = queueTable->rowCount() - 1; loadJob(queueTable->currentIndex()); queueTable->selectRow(currentRow); } /* If needed, reset edit mode to clean up UI */ if (jobUnderEdit >= 0) resetJobEdit(); /* And remove the job object */ jobs.removeOne(job); delete (job); mDirty = true; jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::toggleScheduler() { if (state == SCHEDULER_RUNNING) { preemptiveShutdown = false; stop(); } else start(); } void Scheduler::stop() { if (state != SCHEDULER_RUNNING) return; qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is stopping..."; // Stop running job and abort all others // in case of soft shutdown we skip this if (preemptiveShutdown == false) { bool wasAborted = false; foreach (SchedulerJob *job, jobs) { if (job == currentJob) stopCurrentJobAction(); if (job->getState() <= SchedulerJob::JOB_BUSY) { appendLogText(i18n("Job '%1' has not been processed upon scheduler stop, marking aborted.", job->getName())); job->setState(SchedulerJob::JOB_ABORTED); wasAborted = true; } } if (wasAborted) KNotification::event(QLatin1String("SchedulerAborted"), i18n("Scheduler aborted.")); } schedulerTimer.stop(); jobTimer.stop(); restartGuidingTimer.stop(); state = SCHEDULER_IDLE; emit newStatus(state); ekosState = EKOS_IDLE; indiState = INDI_IDLE; parkWaitState = PARKWAIT_IDLE; // Only reset startup state to idle if the startup procedure was interrupted before it had the chance to complete. // Or if we're doing a soft shutdown if (startupState != STARTUP_COMPLETE || preemptiveShutdown) { if (startupState == STARTUP_SCRIPT) { scriptProcess.disconnect(); scriptProcess.terminate(); } startupState = STARTUP_IDLE; } // Reset startup state to unparking phase (dome -> mount -> cap) // We do not want to run the startup script again but unparking should be checked // whenever the scheduler is running again. else if (startupState == STARTUP_COMPLETE) { if (unparkDomeCheck->isChecked()) startupState = STARTUP_UNPARK_DOME; else if (unparkMountCheck->isChecked()) startupState = STARTUP_UNPARK_MOUNT; else if (uncapCheck->isChecked()) startupState = STARTUP_UNPARK_CAP; } shutdownState = SHUTDOWN_IDLE; setCurrentJob(nullptr); captureBatch = 0; indiConnectFailureCount = 0; ekosConnectFailureCount = 0; focusFailureCount = 0; guideFailureCount = 0; alignFailureCount = 0; captureFailureCount = 0; jobEvaluationOnly = false; loadAndSlewProgress = false; autofocusCompleted = false; startupB->setEnabled(true); shutdownB->setEnabled(true); // If soft shutdown, we return for now if (preemptiveShutdown) { sleepLabel->setToolTip(i18n("Scheduler is in shutdown until next job is ready")); sleepLabel->show(); return; } // Clear target name in capture interface upon stopping if (captureInterface.isNull() == false) captureInterface->setProperty("targetName", QString()); if (scriptProcess.state() == QProcess::Running) scriptProcess.terminate(); sleepTimer.stop(); //sleepTimer.disconnect(); sleepLabel->hide(); pi->stopAnimation(); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Start Scheduler")); pauseB->setEnabled(false); //startB->setText("Start Scheduler"); queueLoadB->setEnabled(true); addToQueueB->setEnabled(true); setJobManipulation(false, false); mosaicB->setEnabled(true); evaluateOnlyB->setEnabled(true); } void Scheduler::start() { switch (state) { case SCHEDULER_IDLE: /* FIXME: Manage the non-validity of the startup script earlier, and make it a warning only when the scheduler starts */ startupScriptURL = QUrl::fromUserInput(startupScript->text()); if (!startupScript->text().isEmpty() && !startupScriptURL.isValid()) { appendLogText(i18n("Warning: startup script URL %1 is not valid.", startupScript->text())); return; } /* FIXME: Manage the non-validity of the shutdown script earlier, and make it a warning only when the scheduler starts */ shutdownScriptURL = QUrl::fromUserInput(shutdownScript->text()); if (!shutdownScript->text().isEmpty() && !shutdownScriptURL.isValid()) { appendLogText(i18n("Warning: shutdown script URL %1 is not valid.", shutdownScript->text())); return; } qCInfo(KSTARS_EKOS_SCHEDULER) << "Scheduler is starting..."; /* Update UI to reflect startup */ pi->startAnimation(); sleepLabel->hide(); startB->setIcon(QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Scheduler")); pauseB->setEnabled(true); pauseB->setChecked(false); /* Disable edit-related buttons */ queueLoadB->setEnabled(false); addToQueueB->setEnabled(false); setJobManipulation(false, false); mosaicB->setEnabled(false); evaluateOnlyB->setEnabled(false); startupB->setEnabled(false); shutdownB->setEnabled(false); state = SCHEDULER_RUNNING; emit newStatus(state); schedulerTimer.start(); appendLogText(i18n("Scheduler started.")); qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler started."; break; case SCHEDULER_PAUSED: /* Update UI to reflect resume */ startB->setIcon(QIcon::fromTheme("media-playback-stop")); startB->setToolTip(i18n("Stop Scheduler")); pauseB->setEnabled(true); pauseB->setCheckable(false); pauseB->setChecked(false); /* Edit-related buttons are still disabled */ /* The end-user cannot update the schedule, don't re-evaluate jobs. Timer schedulerTimer is already running. */ state = SCHEDULER_RUNNING; emit newStatus(state); schedulerTimer.start(); appendLogText(i18n("Scheduler resuming.")); qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler resuming."; break; default: break; } } void Scheduler::pause() { state = SCHEDULER_PAUSED; emit newStatus(state); appendLogText(i18n("Scheduler pause planned...")); pauseB->setEnabled(false); startB->setIcon(QIcon::fromTheme("media-playback-start")); startB->setToolTip(i18n("Resume Scheduler")); } void Scheduler::setPaused() { pauseB->setCheckable(true); pauseB->setChecked(true); schedulerTimer.stop(); appendLogText(i18n("Scheduler paused.")); } void Scheduler::setCurrentJob(SchedulerJob *job) { /* Reset job widgets */ if (currentJob) { currentJob->setStageLabel(nullptr); } /* Set current job */ currentJob = job; /* Reassign job widgets, or reset to defaults */ if (currentJob) { currentJob->setStageLabel(jobStatus); queueTable->selectRow(jobs.indexOf(currentJob)); } else { jobStatus->setText(i18n("No job running")); //queueTable->clearSelection(); } } void Scheduler::evaluateJobs() { /* Don't evaluate if list is empty */ if (jobs.isEmpty()) return; /* FIXME: it is possible to evaluate jobs while KStars has a time offset, so warn the user about this */ QDateTime const now = KStarsData::Instance()->lt(); /* Start by refreshing the number of captures already present - unneeded if not remembering job progress */ if (Options::rememberJobProgress()) updateCompletedJobsCount(); /* Update dawn and dusk astronomical times - unconditionally in case date changed */ calculateDawnDusk(); /* First, filter out non-schedulable jobs */ /* FIXME: jobs in state JOB_ERROR should not be in the list, reorder states */ QList sortedJobs = jobs; /* Then enumerate SchedulerJobs to consolidate imaging time */ foreach (SchedulerJob *job, sortedJobs) { /* Let aborted jobs be rescheduled later instead of forgetting them */ switch (job->getState()) { case SchedulerJob::JOB_SCHEDULED: /* If job is scheduled, keep it for evaluation against others */ break; case SchedulerJob::JOB_INVALID: case SchedulerJob::JOB_COMPLETE: /* If job is invalid or complete, bypass evaluation */ continue; case SchedulerJob::JOB_BUSY: /* If job is busy, edge case, bypass evaluation */ continue; case SchedulerJob::JOB_ERROR: case SchedulerJob::JOB_ABORTED: /* If job is in error or aborted and we're running, keep its evaluation until there is nothing else to do */ if (state == SCHEDULER_RUNNING) continue; /* Fall through */ case SchedulerJob::JOB_IDLE: case SchedulerJob::JOB_EVALUATION: default: /* If job is idle, re-evaluate completely */ job->setEstimatedTime(-1); break; } switch (job->getCompletionCondition()) { case SchedulerJob::FINISH_AT: /* If planned finishing time has passed, the job is set to IDLE waiting for a next chance to run */ if (job->getCompletionTime().isValid() && job->getCompletionTime() < now) { job->setState(SchedulerJob::JOB_IDLE); continue; } break; case SchedulerJob::FINISH_REPEAT: // In case of a repeating jobs, let's make sure we have more runs left to go // If we don't, re-estimate imaging time for the scheduler job before concluding if (job->getRepeatsRemaining() == 0) { appendLogText(i18n("Job '%1' has no more batches remaining.", job->getName())); if (Options::rememberJobProgress()) { job->setEstimatedTime(-1); } else { job->setState(SchedulerJob::JOB_COMPLETE); job->setEstimatedTime(0); continue; } } break; default: break; } // -1 = Job is not estimated yet // -2 = Job is estimated but time is unknown // > 0 Job is estimated and time is known if (job->getEstimatedTime() == -1) { if (estimateJobTime(job) == false) { job->setState(SchedulerJob::JOB_INVALID); continue; } } if (job->getEstimatedTime() == 0) { job->setRepeatsRemaining(0); job->setState(SchedulerJob::JOB_COMPLETE); continue; } // In any other case, evaluate job->setState(SchedulerJob::JOB_EVALUATION); } /* * At this step, we prepare scheduling of jobs. * We filter out jobs that won't run now, and make sure jobs are not all starting at the same time. */ updatePreDawn(); /* This predicate matches jobs not being evaluated and not aborted */ auto neither_evaluated_nor_aborted = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_EVALUATION != s && SchedulerJob::JOB_ABORTED != s; }; /* This predicate matches jobs neither being evaluated nor aborted nor in error state */ auto neither_evaluated_nor_aborted_nor_error = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_EVALUATION != s && SchedulerJob::JOB_ABORTED != s && SchedulerJob::JOB_ERROR != s; }; /* This predicate matches jobs that aborted, or completed for whatever reason */ auto finished_or_aborted = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_ERROR <= s || SchedulerJob::JOB_ABORTED == s; }; bool nea = std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted); bool neae = std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_evaluated_nor_aborted_nor_error); /* If there are no jobs left to run in the filtered list, stop evaluation */ - if (sortedJobs.isEmpty() || (!errorHandlingRescheduleErrorsCB->isChecked() && nea) || (errorHandlingRescheduleErrorsCB->isChecked() && neae)) + if (sortedJobs.isEmpty() || (!errorHandlingRescheduleErrorsCB->isChecked() && nea) + || (errorHandlingRescheduleErrorsCB->isChecked() && neae)) { appendLogText(i18n("No jobs left in the scheduler queue.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } /* If there are only aborted jobs that can run, reschedule those */ if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted) && errorHandlingDontRestartButton->isChecked() == false) { appendLogText(i18n("Only %1 jobs left in the scheduler queue, rescheduling those.", errorHandlingRescheduleErrorsCB->isChecked() ? "aborted or error" : "aborted")); // set aborted and error jobs to evaluation state for (int index = 0; index < sortedJobs.size(); index++) { SchedulerJob * const job = sortedJobs.at(index); if (SchedulerJob::JOB_ABORTED == job->getState() || (errorHandlingRescheduleErrorsCB->isChecked() && SchedulerJob::JOB_ERROR == job->getState())) job->setState(SchedulerJob::JOB_EVALUATION); } if (errorHandlingRestartAfterAllButton->isChecked()) { // interrupt regular status checks during the sleep time schedulerTimer.stop(); // but before we restart them, we wait for the given delay. appendLogText(i18n("All jobs aborted. Waiting %1 seconds to re-schedule.", errorHandlingDelaySB->value())); // wait the given delay until the jobs will be evaluated again sleepTimer.setInterval(std::lround((errorHandlingDelaySB->value() * 1000) / KStarsData::Instance()->clock()->scale())); sleepTimer.start(); sleepLabel->setToolTip(i18n("Scheduler waits for a retry.")); sleepLabel->show(); // we continue to determine which job should be running, when the delay is over } } /* If option says so, reorder by altitude and priority before sequencing */ /* FIXME: refactor so all sorts are using the same predicates */ /* FIXME: use std::stable_sort as qStableSort is deprecated */ /* FIXME: dissociate altitude and priority, it's difficult to choose which predicate to use first */ qCInfo(KSTARS_EKOS_SCHEDULER) << "Option to sort jobs based on priority and altitude is" << Options::sortSchedulerJobs(); if (Options::sortSchedulerJobs()) { using namespace std::placeholders; std::stable_sort(sortedJobs.begin(), sortedJobs.end(), std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, KStarsData::Instance()->lt())); std::stable_sort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder); } /* The first reordered job has no lead time - this could also be the delay from now to startup */ sortedJobs.first()->setLeadTime(0); /* The objective of the following block is to make sure jobs are sequential in the list filtered previously. * * The algorithm manages overlap between jobs by stating that scheduled jobs that start sooner are non-movable. * If the completion time of the previous job overlaps the current job, we offset the startup of the current job. * Jobs that have no valid startup time when evaluated (ASAP jobs) are assigned an immediate startup time. * The lead time from the Options registry is used as a buffer between jobs. * * Note about the situation where the current job overlaps the next job, and the next job is not movable: * - If we mark the current job invalid, it will not be processed at all. Dropping is not satisfactory. * - If we move the current job after the fixed job, we need to restart evaluation with a new list, and risk an * infinite loop eventually. This means swapping schedules, and is incompatible with altitude/priority sort. * - If we mark the current job aborted, it will be re-evaluated each time a job is complete to see if it can fit. * Although puzzling for the end-user, this solution is dynamic: the aborted job might or might not be scheduled * at the planned time slot. But as the end-user did not enforce the start time, this is acceptable. Moreover, the * schedule will be altered by external events during the execution. * * Here are the constraints that have an effect on the job being examined, and indirectly on all subsequent jobs: * - Twilight constraint moves jobs to the next dark sky interval. * - Altitude constraint, currently linked with Moon separation, moves jobs to the next acceptable altitude time. * - Culmination constraint moves jobs to the next transit time, with arbitrary offset. * - Fixed startup time moves jobs to a fixed time, essentially making them non-movable, or invalid if in the past. * * Here are the constraints that have an effect on jobs following the job being examined: * - Repeats requirement increases the duration of the current job, pushing subsequent jobs. * - Looping requirement causes subsequent jobs to become invalid (until dynamic priority is implemented). * - Fixed completion makes subsequent jobs start after that boundary time. * * However, we need a way to inform the end-user about failed schedules clearly in the UI. * The message to get through is that if jobs are not sorted by altitude/priority, the aborted or invalid jobs * should be modified or manually moved to a better position. If jobs are sorted automatically, aborted jobs will * be processed when possible, probably not at the expected moment. */ // Make sure no two jobs have the same scheduled time or overlap with other jobs for (int index = 0; index < sortedJobs.size(); index++) { SchedulerJob * const currentJob = sortedJobs.at(index); // Bypass jobs that are not marked for evaluation - we did not remove them to preserve schedule order if (SchedulerJob::JOB_EVALUATION != currentJob->getState()) continue; // At this point, a job with no valid start date is a problem, so consider invalid startup time is now if (!currentJob->getStartupTime().isValid()) currentJob->setStartupTime(now); // Locate the previous scheduled job, so that a full schedule plan may be actually consolidated SchedulerJob const * previousJob = nullptr; for (int i = index - 1; 0 <= i; i--) { SchedulerJob const * const a_job = sortedJobs.at(i); if (SchedulerJob::JOB_SCHEDULED == a_job->getState()) { previousJob = a_job; break; } } - Q_ASSERT_X(nullptr == previousJob || previousJob != currentJob, __FUNCTION__, "Previous job considered for schedule is either undefined or not equal to current."); + Q_ASSERT_X(nullptr == previousJob + || previousJob != currentJob, __FUNCTION__, + "Previous job considered for schedule is either undefined or not equal to current."); // Locate the next job - nothing special required except end of list check SchedulerJob const * const nextJob = index + 1 < sortedJobs.size() ? sortedJobs.at(index + 1) : nullptr; - Q_ASSERT_X(nullptr == nextJob || nextJob != currentJob, __FUNCTION__, "Next job considered for schedule is either undefined or not equal to current."); + Q_ASSERT_X(nullptr == nextJob + || nextJob != currentJob, __FUNCTION__, "Next job considered for schedule is either undefined or not equal to current."); // We're attempting to schedule the job 10 times before making it invalid for (int attempt = 1; attempt < 11; attempt++) { - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Schedule attempt #%1 for %2-second job '%3' on row #%4 starting at %5, completing at %6.") + qCDebug(KSTARS_EKOS_SCHEDULER) << + QString("Schedule attempt #%1 for %2-second job '%3' on row #%4 starting at %5, completing at %6.") .arg(attempt) .arg(static_cast(currentJob->getEstimatedTime())) .arg(currentJob->getName()) .arg(index + 1) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) .arg(currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat())); // ----- #1 Should we reject the current job because of its fixed startup time? // // A job with fixed startup time must be processed at the time of startup, and may be late up to leadTime. // When such a job repeats, its startup time is reinitialized to prevent abort - see completion algorithm. // If such a job requires night time, minimum altitude or Moon separation, the consolidated startup time is checked for errors. // If all restrictions are complied with, we bypass the rest of the verifications as the job cannot be moved. if (SchedulerJob::START_AT == currentJob->getFileStartupCondition()) { // Check whether the current job is too far in the past to be processed - if job is repeating, its startup time is already now if (currentJob->getStartupTime().addSecs(static_cast (ceil(Options::leadTime() * 60))) < now) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has fixed startup time %2 set in the past, marking invalid.", currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); break; } // Check whether the current job has a positive dark sky score at the time of startup else if (true == currentJob->getEnforceTwilight() && getDarkSkyScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its twilight restriction, marking invalid.", currentJob->getName())); break; } // Check whether the current job has a positive altitude score at the time of startup else if (-90 < currentJob->getMinAltitude() && currentJob->getAltitudeScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its altitude restriction, marking invalid.", currentJob->getName())); break; } // Check whether the current job has a positive Moon separation score at the time of startup else if (0 < currentJob->getMinMoonSeparation() && currentJob->getMoonSeparationScore(currentJob->getStartupTime()) < 0) { currentJob->setState(SchedulerJob::JOB_INVALID); - appendLogText(i18n("Warning: job '%1' has a fixed start time incompatible with its Moon separation restriction, marking invalid.", - currentJob->getName())); + appendLogText( + i18n("Warning: job '%1' has a fixed start time incompatible with its Moon separation restriction, marking invalid.", + currentJob->getName())); break; } // Check whether a previous job overlaps the current job if (nullptr != previousJob && previousJob->getCompletionTime().isValid()) { // Calculate time we should be at after finishing the previous job - QDateTime const previousCompletionTime = previousJob->getCompletionTime().addSecs(static_cast (ceil(Options::leadTime() * 60.0))); + QDateTime const previousCompletionTime = previousJob->getCompletionTime().addSecs(static_cast (ceil( + Options::leadTime() * 60.0))); // Make this job invalid if startup time is not achievable because a START_AT job is non-movable if (currentJob->getStartupTime() < previousCompletionTime) { currentJob->setState(SchedulerJob::JOB_INVALID); - appendLogText(i18n("Warning: job '%1' has fixed startup time %2 unachievable due to the completion time of its previous sibling, marking invalid.", - currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); + appendLogText( + i18n("Warning: job '%1' has fixed startup time %2 unachievable due to the completion time of its previous sibling, marking invalid.", + currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); break; } currentJob->setLeadTime(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())); } // This job is non-movable, we're done currentJob->setScore(calculateJobScore(currentJob, now)); currentJob->setState(SchedulerJob::JOB_SCHEDULED); - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with fixed startup time requirement.") + qCDebug(KSTARS_EKOS_SCHEDULER) << + QString("Job '%1' is scheduled to start at %2, in compliance with fixed startup time requirement.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); break; } // ----- #2 Should we delay the current job because it overlaps the previous job? // // The previous job is considered non-movable, and its completion, plus lead time, is the origin for the current job. // If no previous job exists, or if all prior jobs in the list are rejected, there is no overlap. // If there is a previous job, the current job is simply delayed to avoid an eventual overlap. // IF there is a previous job but it never finishes, the current job is rejected. // This scheduling obviously relies on imaging time estimation: because errors stack up, future startup times are less and less reliable. if (nullptr != previousJob) { if (previousJob->getCompletionTime().isValid()) { // Calculate time we should be at after finishing the previous job - QDateTime const previousCompletionTime = previousJob->getCompletionTime().addSecs(static_cast (ceil(Options::leadTime() * 60.0))); + QDateTime const previousCompletionTime = previousJob->getCompletionTime().addSecs(static_cast (ceil( + Options::leadTime() * 60.0))); // Delay the current job to completion of its previous sibling if needed - this updates the completion time automatically if (currentJob->getStartupTime() < previousCompletionTime) { currentJob->setStartupTime(previousCompletionTime); - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, %3 seconds after %4, in compliance with previous job completion requirement.") + qCDebug(KSTARS_EKOS_SCHEDULER) << + QString("Job '%1' is scheduled to start at %2, %3 seconds after %4, in compliance with previous job completion requirement.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) .arg(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())) .arg(previousJob->getCompletionTime().toString(previousJob->getDateTimeDisplayFormat())); // If the job is repeating or looping, re-estimate imaging duration - error case may be a bug if (SchedulerJob::FINISH_SEQUENCE != currentJob->getCompletionCondition()) if (false == estimateJobTime(currentJob)) currentJob->setState(SchedulerJob::JOB_INVALID); continue; } } else { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: Job '%1' cannot start because its previous sibling has no completion time, marking invalid.", currentJob->getName())); break; } currentJob->setLeadTime(previousJob->getCompletionTime().secsTo(currentJob->getStartupTime())); // Lead time can be zero, so completion may equal startup - Q_ASSERT_X(previousJob->getCompletionTime() <= currentJob->getStartupTime(), __FUNCTION__, "Previous and current jobs do not overlap."); + Q_ASSERT_X(previousJob->getCompletionTime() <= currentJob->getStartupTime(), __FUNCTION__, + "Previous and current jobs do not overlap."); } // ----- #3 Should we delay the current job because it overlaps daylight? // // Pre-dawn time rules whether a job may be started before dawn, or delayed to next night. // Note that the case of START_AT jobs is considered earlier in the algorithm, thus may be omitted here. // In addition to be hardcoded currently, the imaging duration is not reliable enough to start a short job during pre-dawn. // However, completion time during daylight only causes a warning, as this case will be processed as the job runs. if (currentJob->getEnforceTwilight()) { // During that check, we don't verify the current job can actually complete before dawn. // If the job is interrupted while running, it will be aborted and rescheduled at a later time. // We wouldn't start observation 30 mins (default) before dawn. // FIXME: Refactor duplicated dawn/dusk calculations double const earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); // Compute dawn time for the startup date of the job // FIXME: Use KAlmanac to find the real dawn/dusk time for the day the job is supposed to be processed QDateTime const dawnDateTime(currentJob->getStartupTime().date(), QTime(0, 0).addSecs(earlyDawn * 24 * 3600)); // Check if the job starts after dawn if (dawnDateTime < currentJob->getStartupTime()) { // Compute dusk time for the startup date of the job - no lead time on dusk QDateTime duskDateTime(currentJob->getStartupTime().date(), QTime(0, 0).addSecs(Dusk * 24 * 3600)); // Near summer solstice, dusk may happen before dawn on the same day, shift dusk by one day in that case if (duskDateTime < dawnDateTime) duskDateTime = duskDateTime.addDays(1); // Check if the job starts before dusk if (currentJob->getStartupTime() < duskDateTime) { // Delay job to next dusk - we will check other requirements later on currentJob->setStartupTime(duskDateTime); - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with night time requirement.") + qCDebug(KSTARS_EKOS_SCHEDULER) << + QString("Job '%1' is scheduled to start at %2, in compliance with night time requirement.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); continue; } } // Compute dawn time for the day following the startup time, but disregard the pre-dawn offset as we'll consider completion // FIXME: Use KAlmanac to find the real dawn/dusk time for the day next to the day the job is supposed to be processed QDateTime const nextDawnDateTime(currentJob->getStartupTime().date().addDays(1), QTime(0, 0).addSecs(Dawn * 24 * 3600)); // Check if the completion date overlaps the next dawn, and issue a warning if so if (nextDawnDateTime < currentJob->getCompletionTime()) { - appendLogText(i18n("Warning: job '%1' execution overlaps daylight, it will be interrupted at dawn and rescheduled on next night time.", - currentJob->getName())); + appendLogText( + i18n("Warning: job '%1' execution overlaps daylight, it will be interrupted at dawn and rescheduled on next night time.", + currentJob->getName())); } - Q_ASSERT_X(0 <= getDarkSkyScore(currentJob->getStartupTime()), __FUNCTION__, "Consolidated startup time results in a positive dark sky score."); + Q_ASSERT_X(0 <= getDarkSkyScore(currentJob->getStartupTime()), __FUNCTION__, + "Consolidated startup time results in a positive dark sky score."); } // ----- #4 Should we delay the current job because of its target culmination? // // Culmination uses the transit time, and fixes the startup time of the job to a particular offset around this transit time. // This restriction may be used to start a job at the least air mass, or after a meridian flip. // Culmination is scheduled before altitude restriction because it is normally more restrictive for the resulting startup time. // It may happen that a target cannot rise enough to comply with the altitude restriction, but a culmination time is always valid. if (SchedulerJob::START_CULMINATION == currentJob->getFileStartupCondition()) { // Consolidate the culmination time, with offset, of the current job QDateTime const nextCulminationTime = currentJob->calculateCulmination(currentJob->getStartupTime()); if (nextCulminationTime.isValid()) // Guaranteed { if (currentJob->getStartupTime() < nextCulminationTime) { currentJob->setStartupTime(nextCulminationTime); - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with culmination requirements.") + qCDebug(KSTARS_EKOS_SCHEDULER) << + QString("Job '%1' is scheduled to start at %2, in compliance with culmination requirements.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); continue; } } else { currentJob->setState(SchedulerJob::JOB_INVALID); appendLogText(i18n("Warning: job '%1' requires culmination offset of %2 minutes, not achievable, marking invalid.", currentJob->getName(), QString("%L1").arg(currentJob->getCulminationOffset()))); break; } // Don't test altitude here, because we will push the job during the next check step // Q_ASSERT_X(0 <= getAltitudeScore(currentJob, currentJob->getStartupTime()), __FUNCTION__, "Consolidated altitude time results in a positive altitude score."); } // ----- #5 Should we delay the current job because its altitude is incorrect? // // Altitude time ensures the job is assigned a startup time when its target is high enough. // As other restrictions, the altitude is only considered for startup time, completion time is managed while the job is running. // Because a target setting down is a problem for the schedule, a cutoff altitude is added in the case the job target is past the meridian at startup time. // FIXME: though arguable, Moon separation is also considered in that restriction check - move it to a separate case. if (-90 < currentJob->getMinAltitude()) { // Consolidate a new altitude time from the startup time of the current job QDateTime const nextAltitudeTime = currentJob->calculateAltitudeTime(currentJob->getStartupTime()); if (nextAltitudeTime.isValid()) { if (currentJob->getStartupTime() < nextAltitudeTime) { currentJob->setStartupTime(nextAltitudeTime); - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is scheduled to start at %2, in compliance with altitude and Moon separation requirements.") + qCDebug(KSTARS_EKOS_SCHEDULER) << + QString("Job '%1' is scheduled to start at %2, in compliance with altitude and Moon separation requirements.") .arg(currentJob->getName()) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())); continue; } } else { currentJob->setState(SchedulerJob::JOB_INVALID); - appendLogText(i18n("Warning: job '%1' requires minimum altitude %2 and Moon separation %3, not achievable, marking invalid.", - currentJob->getName(), - QString("%L1").arg(static_cast(currentJob->getMinAltitude()), 0, 'f', minAltitude->decimals()), - 0.0 < currentJob->getMinMoonSeparation() ? - QString("%L1").arg(static_cast(currentJob->getMinMoonSeparation()), 0, 'f', minMoonSeparation->decimals()) : - QString("-"))); + appendLogText( + i18n("Warning: job '%1' requires minimum altitude %2 and Moon separation %3, not achievable, marking invalid.", + currentJob->getName(), + QString("%L1").arg(static_cast(currentJob->getMinAltitude()), 0, 'f', minAltitude->decimals()), + 0.0 < currentJob->getMinMoonSeparation() ? + QString("%L1").arg(static_cast(currentJob->getMinMoonSeparation()), 0, 'f', minMoonSeparation->decimals()) : + QString("-"))); break; } - Q_ASSERT_X(0 <= currentJob->getAltitudeScore(currentJob->getStartupTime()), __FUNCTION__, "Consolidated altitude time results in a positive altitude score."); + Q_ASSERT_X(0 <= currentJob->getAltitudeScore(currentJob->getStartupTime()), __FUNCTION__, + "Consolidated altitude time results in a positive altitude score."); } // ----- #6 Should we reject the current job because it overlaps the next job and that next job is not movable? // // If we have a blocker next to the current job, we compare the completion time of the current job and the startup time of this next job, taking lead time into account. // This verification obviously relies on the imaging time to be reliable, but there's not much we can do at this stage of the implementation. if (nullptr != nextJob && SchedulerJob::START_AT == nextJob->getFileStartupCondition()) { // In the current implementation, it is not possible to abort a running job when the next job is supposed to start. // Movable jobs after this one will be delayed, but non-movable jobs are considered blockers. // Calculate time we have between the end of the current job and the next job double const timeToNext = static_cast (currentJob->getCompletionTime().secsTo(nextJob->getStartupTime())); // If that time is overlapping the next job, abort the current job if (timeToNext < Options::leadTime() * 60) { currentJob->setState(SchedulerJob::JOB_ABORTED); - appendLogText(i18n("Warning: job '%1' is constrained by the start time of the next job, and cannot finish in time, marking aborted.", - currentJob->getName())); + appendLogText( + i18n("Warning: job '%1' is constrained by the start time of the next job, and cannot finish in time, marking aborted.", + currentJob->getName())); break; } - Q_ASSERT_X(currentJob->getCompletionTime().addSecs(Options::leadTime() * 60) < nextJob->getStartupTime(), __FUNCTION__, "No overlap "); + Q_ASSERT_X(currentJob->getCompletionTime().addSecs(Options::leadTime() * 60) < nextJob->getStartupTime(), __FUNCTION__, + "No overlap "); } // ----- #7 Should we reject the current job because it exceeded its fixed completion time? // // This verification simply checks that because of previous jobs, the startup time of the current job doesn't exceed its fixed completion time. // Its main objective is to catch wrong dates in the FINISH_AT configuration. if (SchedulerJob::FINISH_AT == currentJob->getCompletionCondition()) { if (currentJob->getCompletionTime() < currentJob->getStartupTime()) { appendLogText(i18n("Job '%1' completion time (%2) could not be achieved before start up time (%3)", currentJob->getName(), currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); currentJob->setState(SchedulerJob::JOB_INVALID); break; } } // ----- #8 Should we reject the current job because of weather? // // That verification is left for runtime // // if (false == isWeatherOK(currentJob)) //{ // currentJob->setState(SchedulerJob::JOB_ABORTED); // // appendLogText(i18n("Job '%1' cannot run now because of bad weather, marking aborted.", currentJob->getName())); //} // ----- #9 Update score for current time and mark evaluating jobs as scheduled currentJob->setScore(calculateJobScore(currentJob, now)); currentJob->setState(SchedulerJob::JOB_SCHEDULED); - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' on row #%2 passed all checks after %3 attempts, will proceed at %4 for approximately %5 seconds, marking scheduled") + qCDebug(KSTARS_EKOS_SCHEDULER) << + QString("Job '%1' on row #%2 passed all checks after %3 attempts, will proceed at %4 for approximately %5 seconds, marking scheduled") .arg(currentJob->getName()) .arg(index + 1) .arg(attempt) .arg(currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat())) .arg(currentJob->getEstimatedTime()); break; } // Check if job was successfully scheduled, else reject it if (SchedulerJob::JOB_EVALUATION == currentJob->getState()) { currentJob->setState(SchedulerJob::JOB_INVALID); //appendLogText(i18n("Warning: job '%1' on row #%2 could not be scheduled during evaluation and is marked invalid, please review your plan.", // currentJob->getName(), // index + 1)); } } /* Apply sorting to queue table, and mark it for saving if it changes */ mDirty = reorderJobs(sortedJobs) | mDirty; if (jobEvaluationOnly || state != SCHEDULER_RUNNING) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos finished evaluating jobs, no job selection required."; jobEvaluationOnly = false; return; } /* * At this step, we finished evaluating jobs. * We select the first job that has to be run, per schedule. */ /* This predicate matches jobs that are neither scheduled to run nor aborted */ auto neither_scheduled_nor_aborted = [](SchedulerJob const * const job) { SchedulerJob::JOBStatus const s = job->getState(); return SchedulerJob::JOB_SCHEDULED != s && SchedulerJob::JOB_ABORTED != s; }; /* If there are no jobs left to run in the filtered list, stop evaluation */ if (sortedJobs.isEmpty() || std::all_of(sortedJobs.begin(), sortedJobs.end(), neither_scheduled_nor_aborted)) { appendLogText(i18n("No jobs left in the scheduler queue after evaluating.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } /* If there are only aborted jobs that can run, reschedule those and let Scheduler restart one loop */ else if (std::all_of(sortedJobs.begin(), sortedJobs.end(), finished_or_aborted) && errorHandlingDontRestartButton->isChecked() == false) { appendLogText(i18n("Only aborted jobs left in the scheduler queue after evaluating, rescheduling those.")); std::for_each(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * job) { if (SchedulerJob::JOB_ABORTED == job->getState()) job->setState(SchedulerJob::JOB_EVALUATION); }); jobEvaluationOnly = false; return; } /* The job to run is the first scheduled, locate it in the list */ - QList::iterator job_to_execute_iterator = std::find_if(sortedJobs.begin(), sortedJobs.end(), [](SchedulerJob * const job) + QList::iterator job_to_execute_iterator = std::find_if(sortedJobs.begin(), + sortedJobs.end(), [](SchedulerJob * const job) { return SchedulerJob::JOB_SCHEDULED == job->getState(); }); /* If there is no scheduled job anymore (because the restriction loop made them invalid, for instance), bail out */ if (sortedJobs.end() == job_to_execute_iterator) { appendLogText(i18n("No jobs left in the scheduler queue after schedule cleanup.")); setCurrentJob(nullptr); jobEvaluationOnly = false; return; } /* Check if job can be processed right now */ SchedulerJob * const job_to_execute = *job_to_execute_iterator; if (job_to_execute->getFileStartupCondition() == SchedulerJob::START_ASAP) if( 0 <= calculateJobScore(job_to_execute, now)) job_to_execute->setStartupTime(now); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is selected for next observation with priority #%2 and score %3.") .arg(job_to_execute->getName()) .arg(job_to_execute->getPriority()) .arg(job_to_execute->getScore()); // Set the current job, and let the status timer execute it when ready setCurrentJob(job_to_execute); } void Scheduler::wakeUpScheduler() { sleepLabel->hide(); sleepTimer.stop(); if (preemptiveShutdown) { preemptiveShutdown = false; appendLogText(i18n("Scheduler is awake.")); start(); } else { if (state == SCHEDULER_RUNNING) appendLogText(i18n("Scheduler is awake. Jobs shall be started when ready...")); else appendLogText(i18n("Scheduler is awake. Jobs shall be started when scheduler is resumed.")); schedulerTimer.start(); } } int16_t Scheduler::getWeatherScore() const { if (weatherCheck->isEnabled() == false || weatherCheck->isChecked() == false) return 0; if (weatherStatus == ISD::Weather::WEATHER_WARNING) return BAD_SCORE / 2; else if (weatherStatus == ISD::Weather::WEATHER_ALERT) return BAD_SCORE; return 0; } int16_t Scheduler::getDarkSkyScore(QDateTime const &when) const { double const secsPerDay = 24.0 * 3600.0; double const minsPerDay = 24.0 * 60.0; // Dark sky score is calculated based on distance to today's dawn and next dusk. // Option "Pre-dawn Time" avoids executing a job when dawn is approaching, and is a value in minutes. // - If observation is between option "Pre-dawn Time" and dawn, score is BAD_SCORE/50. // - If observation is before dawn today, score is fraction of the day from beginning of observation to dawn time, as percentage. // - If observation is after dusk, score is fraction of the day from dusk to beginning of observation, as percentage. // - If observation is between dawn and dusk, score is BAD_SCORE. // // If observation time is invalid, the score is calculated for the current day time. // Note exact dusk time is considered valid in terms of night time, and will return a positive, albeit null, score. // FIXME: Dark sky score should consider the middle of the local night as best value. // FIXME: Current algorithm uses the dawn and dusk of today, instead of the day of the observation. - int const earlyDawnSecs = static_cast ((Dawn - static_cast (Options::preDawnTime()) / minsPerDay) * secsPerDay); + int const earlyDawnSecs = static_cast ((Dawn - static_cast (Options::preDawnTime()) / minsPerDay) * + secsPerDay); int const dawnSecs = static_cast (Dawn * secsPerDay); int const duskSecs = static_cast (Dusk * secsPerDay); int const obsSecs = (when.isValid() ? when : KStarsData::Instance()->lt()).time().msecsSinceStartOfDay() / 1000; int16_t score = 0; if (earlyDawnSecs <= obsSecs && obsSecs < dawnSecs) { score = BAD_SCORE / 50; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (between pre-dawn and dawn).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } else if (obsSecs < dawnSecs) { score = static_cast ((dawnSecs - obsSecs) / secsPerDay) * 100; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (before dawn).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } else if (duskSecs <= obsSecs) { score = static_cast ((obsSecs - duskSecs) / secsPerDay) * 100; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (after dusk).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } else { score = BAD_SCORE; //qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Dark sky score at %1 is %2 (during daylight).") // .arg(observationDateTime.toString()) // .arg(QString::asprintf("%+d", score)); } return score; } int16_t Scheduler::calculateJobScore(SchedulerJob const *job, QDateTime const &when) const { if (nullptr == job) return BAD_SCORE; /* Only consolidate the score if light frames are required, calibration frames can run whenever needed */ if (!job->getLightFramesRequired()) return 1000; int16_t total = 0; /* As soon as one score is negative, it's a no-go and other scores are unneeded */ if (job->getEnforceTwilight()) { int16_t const darkSkyScore = getDarkSkyScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' dark sky score is %2 at %3") .arg(job->getName()) .arg(QString::asprintf("%+d", darkSkyScore)) .arg(when.toString(job->getDateTimeDisplayFormat())); total += darkSkyScore; } /* We still enforce altitude if the job is neither required to track nor guide, because this is too confusing for the end-user. * If we bypass calculation here, it must also be bypassed when checking job constraints in checkJobStage. */ if (0 <= total /*&& ((job->getStepPipeline() & SchedulerJob::USE_TRACK) || (job->getStepPipeline() & SchedulerJob::USE_GUIDE))*/) { int16_t const altitudeScore = job->getAltitudeScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' altitude score is %2 at %3") .arg(job->getName()) .arg(QString::asprintf("%+d", altitudeScore)) .arg(when.toString(job->getDateTimeDisplayFormat())); total += altitudeScore; } if (0 <= total) { int16_t const moonSeparationScore = job->getMoonSeparationScore(when); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' Moon separation score is %2 at %3") .arg(job->getName()) .arg(QString::asprintf("%+d", moonSeparationScore)) .arg(when.toString(job->getDateTimeDisplayFormat())); total += moonSeparationScore; } qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a total score of %2 at %3.") .arg(job->getName()) .arg(QString::asprintf("%+d", total)) .arg(when.toString(job->getDateTimeDisplayFormat())); return total; } void Scheduler::calculateDawnDusk() { KSAlmanac ksal; Dawn = ksal.getDawnAstronomicalTwilight() + Options::dawnOffset() / 24.0; Dusk = ksal.getDuskAstronomicalTwilight() + Options::duskOffset() / 24.0; QTime const dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); QTime const dusk = QTime(0, 0, 0).addSecs(Dusk * 24 * 3600); duskDateTime.setDate(KStars::Instance()->data()->lt().date()); duskDateTime.setTime(dusk); nightTime->setText(i18n("%1 - %2", dusk.toString("hh:mm"), dawn.toString("hh:mm"))); } void Scheduler::executeJob(SchedulerJob *job) { // Some states have executeJob called after current job is cancelled - checkStatus does this if (job == nullptr) return; // Don't execute the current job if it is already busy if (currentJob == job && SchedulerJob::JOB_BUSY == currentJob->getState()) return; setCurrentJob(job); int index = jobs.indexOf(job); if (index >= 0) queueTable->selectRow(index); QDateTime const now = KStarsData::Instance()->lt(); // If we already started, we check when the next object is scheduled at. // If it is more than 30 minutes in the future, we park the mount if that is supported // and we unpark when it is due to start. //int const nextObservationTime = now.secsTo(currentJob->getStartupTime()); // If the time to wait is greater than the lead time (5 minutes by default) // then we sleep, otherwise we wait. It's the same thing, just different labels. if (shouldSchedulerSleep(currentJob)) return; // If job schedule isn't now, wait - continuing to execute would cancel a parking attempt else if (0 < KStarsData::Instance()->lt().secsTo(currentJob->getStartupTime())) return; // From this point job can be executed now if (job->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE && Options::rememberJobProgress()) { QString sanitized = job->getName(); sanitized = sanitized.replace( QRegularExpression("\\s|/|\\(|\\)|:|\\*|~|\"" ), "_" ) // Remove any two or more __ .replace( QRegularExpression("_{2,}"), "_") // Remove any _ at the end .replace( QRegularExpression("_$"), ""); captureInterface->setProperty("targetName", sanitized); } updatePreDawn(); // Reset autofocus so that focus step is applied properly when checked // When the focus step is not checked, the capture module will eventually run focus periodically autofocusCompleted = false; qCInfo(KSTARS_EKOS_SCHEDULER) << "Executing Job " << currentJob->getName(); currentJob->setState(SchedulerJob::JOB_BUSY); KNotification::event(QLatin1String("EkosSchedulerJobStart"), i18n("Ekos job started (%1)", currentJob->getName())); // No need to continue evaluating jobs as we already have one. schedulerTimer.stop(); jobTimer.start(); } bool Scheduler::checkEkosState() { if (state == SCHEDULER_PAUSED) return false; switch (ekosState) { case EKOS_IDLE: { if (m_EkosCommunicationStatus == Ekos::Success) { ekosState = EKOS_READY; return true; } else { ekosInterface->call(QDBus::AutoDetect, "start"); ekosState = EKOS_STARTING; currentOperationTime.start(); qCInfo(KSTARS_EKOS_SCHEDULER) << "Ekos communication status is" << m_EkosCommunicationStatus << "Starting Ekos..."; return false; } } case EKOS_STARTING: { if (m_EkosCommunicationStatus == Ekos::Success) { appendLogText(i18n("Ekos started.")); ekosConnectFailureCount = 0; ekosState = EKOS_READY; return true; } else if (m_EkosCommunicationStatus == Ekos::Error) { if (ekosConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Starting Ekos failed. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "start"); return false; } appendLogText(i18n("Starting Ekos failed.")); stop(); return false; } else if (m_EkosCommunicationStatus == Ekos::Idle) return false; // If a minute passed, give up else if (currentOperationTime.elapsed() > (60 * 1000)) { if (ekosConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Starting Ekos timed out. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "stop"); QTimer::singleShot(1000, this, [&]() { ekosInterface->call(QDBus::AutoDetect, "start"); currentOperationTime.restart(); }); return false; } appendLogText(i18n("Starting Ekos timed out.")); stop(); return false; } } break; case EKOS_STOPPING: { if (m_EkosCommunicationStatus == Ekos::Idle) { appendLogText(i18n("Ekos stopped.")); ekosState = EKOS_IDLE; return true; } } break; case EKOS_READY: return true; } return false; } bool Scheduler::isINDIConnected() { return (m_INDICommunicationStatus == Ekos::Success); } bool Scheduler::checkINDIState() { if (state == SCHEDULER_PAUSED) return false; //qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI State" << indiState; switch (indiState) { case INDI_IDLE: { if (m_INDICommunicationStatus == Ekos::Success) { indiState = INDI_PROPERTY_CHECK; indiConnectFailureCount = 0; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI Properties..."; } else { qCDebug(KSTARS_EKOS_SCHEDULER) << "Connecting INDI devices..."; ekosInterface->call(QDBus::AutoDetect, "connectDevices"); indiState = INDI_CONNECTING; currentOperationTime.start(); } } break; case INDI_CONNECTING: { if (m_INDICommunicationStatus == Ekos::Success) { appendLogText(i18n("INDI devices connected.")); indiState = INDI_PROPERTY_CHECK; } else if (m_INDICommunicationStatus == Ekos::Error) { if (indiConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("One or more INDI devices failed to connect. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "connectDevices"); } else { appendLogText(i18n("One or more INDI devices failed to connect. Check INDI control panel for details.")); stop(); } } // If 30 seconds passed, we retry else if (currentOperationTime.elapsed() > (30 * 1000)) { if (indiConnectFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("One or more INDI devices timed out. Retrying...")); ekosInterface->call(QDBus::AutoDetect, "connectDevices"); currentOperationTime.restart(); } else { appendLogText(i18n("One or more INDI devices timed out. Check INDI control panel for details.")); stop(); } } } break; case INDI_DISCONNECTING: { if (m_INDICommunicationStatus == Ekos::Idle) { appendLogText(i18n("INDI devices disconnected.")); indiState = INDI_IDLE; return true; } } break; case INDI_PROPERTY_CHECK: { qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking INDI properties."; // If dome unparking is required then we wait for dome interface if (unparkDomeCheck->isChecked() && m_DomeReady == false) { if (currentOperationTime.elapsed() > (30 * 1000)) { currentOperationTime.restart(); appendLogText(i18n("Warning: dome device not ready after timeout, attempting to recover...")); disconnectINDI(); stopEkos(); } qCDebug(KSTARS_EKOS_SCHEDULER) << "Dome unpark required but dome is not yet ready."; return false; } // If mount unparking is required then we wait for mount interface if (unparkMountCheck->isChecked() && m_MountReady == false) { if (currentOperationTime.elapsed() > (30 * 1000)) { currentOperationTime.restart(); appendLogText(i18n("Warning: mount device not ready after timeout, attempting to recover...")); disconnectINDI(); stopEkos(); } qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount unpark required but mount is not yet ready."; return false; } // If cap unparking is required then we wait for cap interface if (uncapCheck->isChecked() && m_CapReady == false) { if (currentOperationTime.elapsed() > (30 * 1000)) { currentOperationTime.restart(); appendLogText(i18n("Warning: cap device not ready after timeout, attempting to recover...")); disconnectINDI(); stopEkos(); } qCDebug(KSTARS_EKOS_SCHEDULER) << "Cap unpark required but cap is not yet ready."; return false; } // capture interface is required at all times to proceed. if (captureInterface.isNull()) return false; if (m_CaptureReady == false) { QVariant hasCoolerControl = captureInterface->property("coolerControl"); if (hasCoolerControl.isValid()) { warmCCDCheck->setEnabled(hasCoolerControl.toBool()); m_CaptureReady = true; } else qCWarning(KSTARS_EKOS_SCHEDULER) << "Capture module is not ready yet..."; } indiState = INDI_READY; indiConnectFailureCount = 0; return true; } case INDI_READY: return true; } return false; } bool Scheduler::checkStartupState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Checking Startup State (%1)...").arg(startupState); switch (startupState) { case STARTUP_IDLE: { KNotification::event(QLatin1String("ObservatoryStartup"), i18n("Observatory is in the startup process")); qCDebug(KSTARS_EKOS_SCHEDULER) << "Startup Idle. Starting startup process..."; // If Ekos is already started, we skip the script and move on to dome unpark step // unless we do not have light frames, then we skip all //QDBusReply isEkosStarted; //isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus"); //if (isEkosStarted.value() == Ekos::Success) if (m_EkosCommunicationStatus == Ekos::Success) { if (startupScriptURL.isEmpty() == false) appendLogText(i18n("Ekos is already started, skipping startup script...")); if (currentJob->getLightFramesRequired()) startupState = STARTUP_UNPARK_DOME; else startupState = STARTUP_COMPLETE; return true; } if (schedulerProfileCombo->currentText() != i18n("Default")) { QList profile; profile.append(schedulerProfileCombo->currentText()); ekosInterface->callWithArgumentList(QDBus::AutoDetect, "setProfile", profile); } if (startupScriptURL.isEmpty() == false) { startupState = STARTUP_SCRIPT; executeScript(startupScriptURL.toString(QUrl::PreferLocalFile)); return false; } startupState = STARTUP_UNPARK_DOME; return false; } case STARTUP_SCRIPT: return false; case STARTUP_UNPARK_DOME: // If there is no job in case of manual startup procedure, // or if the job requires light frames, let's proceed with // unparking the dome, otherwise startup process is complete. if (currentJob == nullptr || currentJob->getLightFramesRequired()) { if (unparkDomeCheck->isEnabled() && unparkDomeCheck->isChecked()) unParkDome(); else startupState = STARTUP_UNPARK_MOUNT; } else { startupState = STARTUP_COMPLETE; return true; } break; case STARTUP_UNPARKING_DOME: checkDomeParkingStatus(); break; case STARTUP_UNPARK_MOUNT: if (unparkMountCheck->isEnabled() && unparkMountCheck->isChecked()) unParkMount(); else startupState = STARTUP_UNPARK_CAP; break; case STARTUP_UNPARKING_MOUNT: checkMountParkingStatus(); break; case STARTUP_UNPARK_CAP: if (uncapCheck->isEnabled() && uncapCheck->isChecked()) unParkCap(); else startupState = STARTUP_COMPLETE; break; case STARTUP_UNPARKING_CAP: checkCapParkingStatus(); break; case STARTUP_COMPLETE: return true; case STARTUP_ERROR: stop(); return true; } return false; } bool Scheduler::checkShutdownState() { if (state == SCHEDULER_PAUSED) return false; qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking shutdown state..."; switch (shutdownState) { case SHUTDOWN_IDLE: KNotification::event(QLatin1String("ObservatoryShutdown"), i18n("Observatory is in the shutdown process")); qCInfo(KSTARS_EKOS_SCHEDULER) << "Starting shutdown process..."; // weatherTimer.stop(); // weatherTimer.disconnect(); weatherLabel->hide(); jobTimer.stop(); setCurrentJob(nullptr); if (state == SCHEDULER_RUNNING) schedulerTimer.start(); if (preemptiveShutdown == false) { sleepTimer.stop(); //sleepTimer.disconnect(); } if (warmCCDCheck->isEnabled() && warmCCDCheck->isChecked()) { appendLogText(i18n("Warming up CCD...")); // Turn it off //QVariant arg(false); //captureInterface->call(QDBus::AutoDetect, "setCoolerControl", arg); captureInterface->setProperty("coolerControl", false); } // The following steps require a connection to the INDI server if (isINDIConnected()) { if (capCheck->isEnabled() && capCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_CAP; return false; } if (parkMountCheck->isEnabled() && parkMountCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_MOUNT; return false; } if (parkDomeCheck->isEnabled() && parkDomeCheck->isChecked()) { shutdownState = SHUTDOWN_PARK_DOME; return false; } } else appendLogText(i18n("Warning: Bypassing parking procedures, no INDI connection.")); if (shutdownScriptURL.isEmpty() == false) { shutdownState = SHUTDOWN_SCRIPT; return false; } shutdownState = SHUTDOWN_COMPLETE; return true; case SHUTDOWN_PARK_CAP: if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; } else if (capCheck->isEnabled() && capCheck->isChecked()) parkCap(); else shutdownState = SHUTDOWN_PARK_MOUNT; break; case SHUTDOWN_PARKING_CAP: checkCapParkingStatus(); break; case SHUTDOWN_PARK_MOUNT: if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; } else if (parkMountCheck->isEnabled() && parkMountCheck->isChecked()) parkMount(); else shutdownState = SHUTDOWN_PARK_DOME; break; case SHUTDOWN_PARKING_MOUNT: checkMountParkingStatus(); break; case SHUTDOWN_PARK_DOME: if (!isINDIConnected()) { qCInfo(KSTARS_EKOS_SCHEDULER) << "Bypassing shutdown step 'park cap', no INDI connection."; shutdownState = SHUTDOWN_SCRIPT; } else if (parkDomeCheck->isEnabled() && parkDomeCheck->isChecked()) parkDome(); else shutdownState = SHUTDOWN_SCRIPT; break; case SHUTDOWN_PARKING_DOME: checkDomeParkingStatus(); break; case SHUTDOWN_SCRIPT: if (shutdownScriptURL.isEmpty() == false) { // Need to stop Ekos now before executing script if it happens to stop INDI if (ekosState != EKOS_IDLE && Options::shutdownScriptTerminatesINDI()) { stopEkos(); return false; } shutdownState = SHUTDOWN_SCRIPT_RUNNING; executeScript(shutdownScriptURL.toString(QUrl::PreferLocalFile)); } else shutdownState = SHUTDOWN_COMPLETE; break; case SHUTDOWN_SCRIPT_RUNNING: return false; case SHUTDOWN_COMPLETE: return true; case SHUTDOWN_ERROR: stop(); return true; } return false; } bool Scheduler::checkParkWaitState() { if (state == SCHEDULER_PAUSED) return false; if (parkWaitState == PARKWAIT_IDLE) return true; // qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking Park Wait State..."; switch (parkWaitState) { case PARKWAIT_PARK: parkMount(); break; case PARKWAIT_PARKING: checkMountParkingStatus(); break; case PARKWAIT_UNPARK: unParkMount(); break; case PARKWAIT_UNPARKING: checkMountParkingStatus(); break; case PARKWAIT_IDLE: case PARKWAIT_PARKED: case PARKWAIT_UNPARKED: return true; case PARKWAIT_ERROR: appendLogText(i18n("park/unpark wait procedure failed, aborting...")); stop(); return true; } return false; } void Scheduler::executeScript(const QString &filename) { appendLogText(i18n("Executing script %1...", filename)); connect(&scriptProcess, &QProcess::readyReadStandardOutput, this, &Scheduler::readProcessOutput); - connect(&scriptProcess, static_cast(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus) + connect(&scriptProcess, static_cast(&QProcess::finished), + this, [this](int exitCode, QProcess::ExitStatus) { checkProcessExit(exitCode); }); scriptProcess.start(filename); } void Scheduler::readProcessOutput() { appendLogText(scriptProcess.readAllStandardOutput().simplified()); } void Scheduler::checkProcessExit(int exitCode) { scriptProcess.disconnect(); if (exitCode == 0) { if (startupState == STARTUP_SCRIPT) startupState = STARTUP_UNPARK_DOME; else if (shutdownState == SHUTDOWN_SCRIPT_RUNNING) shutdownState = SHUTDOWN_COMPLETE; return; } if (startupState == STARTUP_SCRIPT) { appendLogText(i18n("Startup script failed, aborting...")); startupState = STARTUP_ERROR; } else if (shutdownState == SHUTDOWN_SCRIPT_RUNNING) { appendLogText(i18n("Shutdown script failed, aborting...")); shutdownState = SHUTDOWN_ERROR; } } bool Scheduler::checkStatus() { for (auto job : jobs) job->updateJobCells(); if (state == SCHEDULER_PAUSED) { if (currentJob == nullptr) { setPaused(); return false; } switch (currentJob->getState()) { case SchedulerJob::JOB_BUSY: // do nothing break; case SchedulerJob::JOB_COMPLETE: // start finding next job before pausing break; default: // in all other cases pause setPaused(); break; } } // #1 If no current job selected, let's check if we need to shutdown or evaluate jobs if (currentJob == nullptr) { // #2.1 If shutdown is already complete or in error, we need to stop if (shutdownState == SHUTDOWN_COMPLETE || shutdownState == SHUTDOWN_ERROR) { // If INDI is not done disconnecting, try again later if (indiState == INDI_DISCONNECTING && checkINDIState() == false) return false; // Disconnect INDI if required first if (indiState != INDI_IDLE && Options::stopEkosAfterShutdown()) { disconnectINDI(); return false; } // If Ekos is not done stopping, try again later if (ekosState == EKOS_STOPPING && checkEkosState() == false) return false; // Stop Ekos if required. if (ekosState != EKOS_IDLE && Options::stopEkosAfterShutdown()) { stopEkos(); return false; } if (shutdownState == SHUTDOWN_COMPLETE) appendLogText(i18n("Shutdown complete.")); else appendLogText(i18n("Shutdown procedure failed, aborting...")); // Stop Scheduler stop(); return true; } // #2.2 Check if shutdown is in progress if (shutdownState > SHUTDOWN_IDLE) { // If Ekos is not done stopping, try again later if (ekosState == EKOS_STOPPING && checkEkosState() == false) return false; checkShutdownState(); return false; } // #2.3 Check if park wait procedure is in progress if (checkParkWaitState() == false) return false; // #2.4 If not in shutdown state, evaluate the jobs evaluateJobs(); // #2.5 If there is no current job after evaluation, shutdown if (nullptr == currentJob) { checkShutdownState(); return false; } } // JM 2018-12-07: Check if we need to sleep else if (shouldSchedulerSleep(currentJob) == false) { // #3 Check if startup procedure has failed. if (startupState == STARTUP_ERROR) { // Stop Scheduler stop(); return true; } // #4 Check if startup procedure Phase #1 is complete (Startup script) if ((startupState == STARTUP_IDLE && checkStartupState() == false) || startupState == STARTUP_SCRIPT) return false; // #5 Check if Ekos is started if (checkEkosState() == false) return false; // #6 Check if INDI devices are connected. if (checkINDIState() == false) return false; // #6.1 Check if park wait procedure is in progress - in the case we're waiting for a distant job if (checkParkWaitState() == false) return false; // #7 Check if startup procedure Phase #2 is complete (Unparking phase) if (startupState > STARTUP_SCRIPT && startupState < STARTUP_ERROR && checkStartupState() == false) return false; // #8 Check it it already completed (should only happen starting a paused job) // Find the next job in this case, otherwise execute the current one if (currentJob->getState() == SchedulerJob::JOB_COMPLETE) findNextJob(); else executeJob(currentJob); } return true; } void Scheduler::checkJobStage() { Q_ASSERT_X(currentJob, __FUNCTION__, "Actual current job is required to check job stage"); if (!currentJob) return; if (checkJobStageCounter == 0) { - qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking job stage for" << currentJob->getName() << "startup" << currentJob->getStartupCondition() << currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()) << "state" << currentJob->getState(); + qCDebug(KSTARS_EKOS_SCHEDULER) << "Checking job stage for" << currentJob->getName() << "startup" << + currentJob->getStartupCondition() << currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()) << + "state" << currentJob->getState(); if (checkJobStageCounter++ == 30) checkJobStageCounter = 0; } QDateTime const now = KStarsData::Instance()->lt(); /* Refresh the score of the current job */ /* currentJob->setScore(calculateJobScore(currentJob, now)); */ /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) if (now < currentJob->getStartupTime()) return; // #1 Check if we need to stop at some point if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_AT && currentJob->getState() == SchedulerJob::JOB_BUSY) { // If the job reached it COMPLETION time, we stop it. if (now.secsTo(currentJob->getCompletionTime()) <= 0) { appendLogText(i18n("Job '%1' reached completion time %2, stopping.", currentJob->getName(), currentJob->getCompletionTime().toString(currentJob->getDateTimeDisplayFormat()))); currentJob->setState(SchedulerJob::JOB_COMPLETE); stopCurrentJobAction(); findNextJob(); return; } } // #2 Check if altitude restriction still holds true if (-90 < currentJob->getMinAltitude()) { SkyPoint p = currentJob->getTargetCoords(); p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); /* FIXME: find a way to use altitude cutoff here, because the job can be scheduled when evaluating, then aborted when running */ if (p.alt().Degrees() < currentJob->getMinAltitude()) { // Only terminate job due to altitude limitation if mount is NOT parked. if (isMountParked() == false) { appendLogText(i18n("Job '%1' current altitude (%2 degrees) crossed minimum constraint altitude (%3 degrees), " "marking idle.", currentJob->getName(), QString("%L1").arg(p.alt().Degrees(), 0, 'f', minAltitude->decimals()), QString("%L1").arg(currentJob->getMinAltitude(), 0, 'f', minAltitude->decimals()))); currentJob->setState(SchedulerJob::JOB_IDLE); stopCurrentJobAction(); findNextJob(); return; } } } // #3 Check if moon separation is still valid if (currentJob->getMinMoonSeparation() > 0) { SkyPoint p = currentJob->getTargetCoords(); p.EquatorialToHorizontal(KStarsData::Instance()->lst(), geo->lat()); double moonSeparation = currentJob->getCurrentMoonSeparation(); if (moonSeparation < currentJob->getMinMoonSeparation()) { // Only terminate job due to moon separation limitation if mount is NOT parked. if (isMountParked() == false) { appendLogText(i18n("Job '%2' current moon separation (%1 degrees) is lower than minimum constraint (%3 " "degrees), marking idle.", moonSeparation, currentJob->getName(), currentJob->getMinMoonSeparation())); currentJob->setState(SchedulerJob::JOB_IDLE); stopCurrentJobAction(); findNextJob(); return; } } } // #4 Check if we're not at dawn if (currentJob->getEnforceTwilight() && now > KStarsDateTime(preDawnDateTime)) { // If either mount or dome are not parked, we shutdown if we approach dawn if (isMountParked() == false || (parkDomeCheck->isEnabled() && isDomeParked() == false)) { // Minute is a DOUBLE value, do not use i18np appendLogText(i18n( "Job '%3' is now approaching astronomical twilight rise limit at %1 (%2 minutes safety margin), marking idle.", preDawnDateTime.toString(), Options::preDawnTime(), currentJob->getName())); currentJob->setState(SchedulerJob::JOB_IDLE); stopCurrentJobAction(); findNextJob(); return; } } // #5 Check system status to improve robustness // This handles external events such as disconnections or end-user manipulating INDI panel if (!checkStatus()) return; // #6 Check each stage is processing properly // FIXME: Vanishing property should trigger a call to its event callback switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: getNextAction(); break; case SchedulerJob::STAGE_ALIGNING: // Let's make sure align module does not become unresponsive if (currentOperationTime.elapsed() > static_cast(ALIGN_INACTIVITY_TIMEOUT)) { QVariant const status = alignInterface->property("status"); Ekos::AlignState alignStatus = static_cast(status.toInt()); if (alignStatus == Ekos::ALIGN_IDLE) { if (alignFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Align module timed out. Restarting request..."; startAstrometry(); } else { appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_CAPTURING: // Let's make sure capture module does not become unresponsive if (currentOperationTime.elapsed() > static_cast(CAPTURE_INACTIVITY_TIMEOUT)) { QVariant const status = captureInterface->property("status"); Ekos::CaptureState captureStatus = static_cast(status.toInt()); if (captureStatus == Ekos::CAPTURE_IDLE) { if (captureFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "capture module timed out. Restarting request..."; startCapture(); } else { appendLogText(i18n("Warning: job '%1' capture procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_FOCUSING: // Let's make sure focus module does not become unresponsive if (currentOperationTime.elapsed() > static_cast(FOCUS_INACTIVITY_TIMEOUT)) { QVariant const status = focusInterface->property("status"); Ekos::FocusState focusStatus = static_cast(status.toInt()); if (focusStatus == Ekos::FOCUS_IDLE || focusStatus == Ekos::FOCUS_WAITING) { if (focusFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus module timed out. Restarting request..."; startFocusing(); } else { appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_GUIDING: // Let's make sure guide module does not become unresponsive if (currentOperationTime.elapsed() > GUIDE_INACTIVITY_TIMEOUT) { GuideState guideStatus = getGuidingStatus(); if (guideStatus == Ekos::GUIDE_IDLE || guideStatus == Ekos::GUIDE_CONNECTED || guideStatus == Ekos::GUIDE_DISCONNECTED) { if (guideFailureCount++ < MAX_FAILURE_ATTEMPTS) { qCDebug(KSTARS_EKOS_SCHEDULER) << "guide module timed out. Restarting request..."; startGuiding(); } else { appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else currentOperationTime.restart(); } break; case SchedulerJob::STAGE_SLEWING: case SchedulerJob::STAGE_RESLEWING: // While slewing or re-slewing, check slew status can still be obtained { QVariant const slewStatus = mountInterface->property("status"); if (slewStatus.isValid()) { // Send the slew status periodically to avoid the situation where the mount is already at location and does not send any event // FIXME: in that case, filter TRACKING events only? ISD::Telescope::Status const status = static_cast(slewStatus.toInt()); setMountStatus(status); } else { appendLogText(i18n("Warning: job '%1' lost connection to the mount, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } } break; case SchedulerJob::STAGE_SLEW_COMPLETE: case SchedulerJob::STAGE_RESLEWING_COMPLETE: // When done slewing or re-slewing and we use a dome, only shift to the next action when the dome is done moving if (m_DomeReady) { QVariant const isDomeMoving = domeInterface->property("isMoving"); if (!isDomeMoving.isValid()) { appendLogText(i18n("Warning: job '%1' lost connection to the dome, attempting to reconnect.", currentJob->getName())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } if (!isDomeMoving.value()) getNextAction(); } else getNextAction(); break; default: break; } } void Scheduler::getNextAction() { qCDebug(KSTARS_EKOS_SCHEDULER) << "Get next action..."; switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: if (currentJob->getLightFramesRequired()) { if (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) startSlew(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS && autofocusCompleted == false) - { + { qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3485"; startFocusing(); - } + } else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) if (getGuidingStatus() == GUIDE_GUIDING) { appendLogText(i18n("Guiding already running, directly start capturing.")); startCapture(); } else startGuiding(); else startCapture(); } else { if (currentJob->getStepPipeline()) appendLogText( - i18n("Job '%1' is proceeding directly to capture stage because only calibration frames are pending.", currentJob->getName())); + i18n("Job '%1' is proceeding directly to capture stage because only calibration frames are pending.", + currentJob->getName())); startCapture(); } break; case SchedulerJob::STAGE_SLEW_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS && autofocusCompleted == false) - { + { qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3514"; startFocusing(); - } + } else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_FOCUS_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) startAstrometry(); else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_ALIGN_COMPLETE: currentJob->setStage(SchedulerJob::STAGE_RESLEWING); break; case SchedulerJob::STAGE_RESLEWING_COMPLETE: // If we have in-sequence-focus in the sequence file then we perform post alignment focusing so that the focus // frame is ready for the capture module in-sequence-focus procedure. if ((currentJob->getStepPipeline() & SchedulerJob::USE_FOCUS) && currentJob->getInSequenceFocus()) // Post alignment re-focusing - { + { qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 3544"; startFocusing(); - } + } else if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE: if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) startGuiding(); else startCapture(); break; case SchedulerJob::STAGE_GUIDING_COMPLETE: startCapture(); break; default: break; } } void Scheduler::stopCurrentJobAction() { if (nullptr != currentJob) { - qCDebug(KSTARS_EKOS_SCHEDULER) << "Job '" << currentJob->getName() << "' is stopping current action..." << currentJob->getStage(); + qCDebug(KSTARS_EKOS_SCHEDULER) << "Job '" << currentJob->getName() << "' is stopping current action..." << + currentJob->getStage(); switch (currentJob->getStage()) { case SchedulerJob::STAGE_IDLE: break; case SchedulerJob::STAGE_SLEWING: mountInterface->call(QDBus::AutoDetect, "abort"); break; case SchedulerJob::STAGE_FOCUSING: focusInterface->call(QDBus::AutoDetect, "abort"); break; case SchedulerJob::STAGE_ALIGNING: alignInterface->call(QDBus::AutoDetect, "abort"); break; case SchedulerJob::STAGE_CAPTURING: captureInterface->call(QDBus::AutoDetect, "abort"); break; default: break; } /* Reset interrupted job stage */ currentJob->setStage(SchedulerJob::STAGE_IDLE); } /* Guiding being a parallel process, check to stop it */ stopGuiding(); } bool Scheduler::manageConnectionLoss() { if (SCHEDULER_RUNNING != state) return false; // Don't manage loss if Ekos is actually down in the state machine switch (ekosState) { case EKOS_IDLE: case EKOS_STOPPING: return false; default: break; } // Don't manage loss if INDI is actually down in the state machine switch (indiState) { case INDI_IDLE: case INDI_DISCONNECTING: return false; default: break; } // If Ekos is assumed to be up, check its state //QDBusReply const isEkosStarted = ekosInterface->call(QDBus::AutoDetect, "getEkosStartingStatus"); if (m_EkosCommunicationStatus == Ekos::Success) { qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Ekos is currently connected, checking INDI before mitigating connection loss."); // If INDI is assumed to be up, check its state if (isINDIConnected()) { // If both Ekos and INDI are assumed up, and are actually up, no mitigation needed, this is a DBus interface error qCDebug(KSTARS_EKOS_SCHEDULER) << QString("INDI is currently connected, no connection loss mitigation needed."); return false; } } // Stop actions of the current job stopCurrentJobAction(); // Acknowledge INDI and Ekos disconnections disconnectINDI(); stopEkos(); // Let the Scheduler attempt to connect INDI again return true; } void Scheduler::load() { QUrl fileURL = QFileDialog::getOpenFileUrl(this, i18n("Open Ekos Scheduler List"), dirPath, "Ekos Scheduler List (*.esl)"); if (fileURL.isEmpty()) return; if (fileURL.isValid() == false) { QString message = i18n("Invalid URL: %1", fileURL.toLocalFile()); KSNotification::sorry(message, i18n("Invalid URL")); return; } dirPath = QUrl(fileURL.url(QUrl::RemoveFilename)); /* Run a job idle evaluation after a successful load */ if (loadScheduler(fileURL.toLocalFile())) startJobEvaluation(); } bool Scheduler::loadScheduler(const QString &fileURL) { SchedulerState const old_state = state; state = SCHEDULER_LOADING; QFile sFile; sFile.setFileName(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open file %1", fileURL); KSNotification::sorry(message, i18n("Could Not Open File")); state = old_state; return false; } if (jobUnderEdit >= 0) resetJobEdit(); while (queueTable->rowCount() > 0) queueTable->removeRow(0); qDeleteAll(jobs); jobs.clear(); LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; XMLEle *ep = nullptr; XMLEle *subEP = nullptr; char c; // We expect all data read from the XML to be in the C locale - QLocale::c() QLocale cLocale = QLocale::c(); while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { const char *tag = tagXMLEle(ep); if (!strcmp(tag, "Job")) processJobInfo(ep); else if (!strcmp(tag, "Profile")) { schedulerProfileCombo->setCurrentText(pcdataXMLEle(ep)); } else if (!strcmp(tag, "ErrorHandlingStrategy")) { setErrorHandlingStrategy(static_cast(cLocale.toInt(findXMLAttValu(ep, "value")))); subEP = findXMLEle(ep, "delay"); if (subEP) { errorHandlingDelaySB->setValue(cLocale.toInt(pcdataXMLEle(subEP))); } subEP = findXMLEle(ep, "RescheduleErrors"); errorHandlingRescheduleErrorsCB->setChecked(subEP != nullptr); } else if (!strcmp(tag, "StartupProcedure")) { XMLEle *procedure; startupScript->clear(); unparkDomeCheck->setChecked(false); unparkMountCheck->setChecked(false); uncapCheck->setChecked(false); for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(procedure); if (!strcmp(proc, "StartupScript")) { startupScript->setText(findXMLAttValu(procedure, "value")); startupScriptURL = QUrl::fromUserInput(startupScript->text()); } else if (!strcmp(proc, "UnparkDome")) unparkDomeCheck->setChecked(true); else if (!strcmp(proc, "UnparkMount")) unparkMountCheck->setChecked(true); else if (!strcmp(proc, "UnparkCap")) uncapCheck->setChecked(true); } } else if (!strcmp(tag, "ShutdownProcedure")) { XMLEle *procedure; shutdownScript->clear(); warmCCDCheck->setChecked(false); parkDomeCheck->setChecked(false); parkMountCheck->setChecked(false); capCheck->setChecked(false); for (procedure = nextXMLEle(ep, 1); procedure != nullptr; procedure = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(procedure); if (!strcmp(proc, "ShutdownScript")) { shutdownScript->setText(findXMLAttValu(procedure, "value")); shutdownScriptURL = QUrl::fromUserInput(shutdownScript->text()); } else if (!strcmp(proc, "ParkDome")) parkDomeCheck->setChecked(true); else if (!strcmp(proc, "ParkMount")) parkMountCheck->setChecked(true); else if (!strcmp(proc, "ParkCap")) capCheck->setChecked(true); else if (!strcmp(proc, "WarmCCD")) warmCCDCheck->setChecked(true); } } } delXMLEle(root); } else if (errmsg[0]) { appendLogText(QString(errmsg)); delLilXML(xmlParser); state = old_state; return false; } } schedulerURL = QUrl::fromLocalFile(fileURL); mosaicB->setEnabled(true); mDirty = false; delLilXML(xmlParser); // update save button tool tip queueSaveB->setToolTip("Save schedule to " + schedulerURL.fileName()); state = old_state; return true; } bool Scheduler::processJobInfo(XMLEle *root) { XMLEle *ep; XMLEle *subEP; altConstraintCheck->setChecked(false); moonSeparationCheck->setChecked(false); weatherCheck->setChecked(false); twilightCheck->blockSignals(true); twilightCheck->setChecked(false); twilightCheck->blockSignals(false); minAltitude->setValue(minAltitude->minimum()); minMoonSeparation->setValue(minMoonSeparation->minimum()); // We expect all data read from the XML to be in the C locale - QLocale::c() QLocale cLocale = QLocale::c(); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Name")) nameEdit->setText(pcdataXMLEle(ep)); else if (!strcmp(tagXMLEle(ep), "Priority")) prioritySpin->setValue(atoi(pcdataXMLEle(ep))); else if (!strcmp(tagXMLEle(ep), "Coordinates")) { subEP = findXMLEle(ep, "J2000RA"); if (subEP) { dms ra; ra.setH(cLocale.toDouble(pcdataXMLEle(subEP))); raBox->showInHours(ra); } subEP = findXMLEle(ep, "J2000DE"); if (subEP) { dms de; de.setD(cLocale.toDouble(pcdataXMLEle(subEP))); decBox->showInDegrees(de); } } else if (!strcmp(tagXMLEle(ep), "Sequence")) { sequenceEdit->setText(pcdataXMLEle(ep)); sequenceURL = QUrl::fromUserInput(sequenceEdit->text()); } else if (!strcmp(tagXMLEle(ep), "FITS")) { fitsEdit->setText(pcdataXMLEle(ep)); fitsURL.setPath(fitsEdit->text()); } else if (!strcmp(tagXMLEle(ep), "StartupCondition")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("ASAP", pcdataXMLEle(subEP))) asapConditionR->setChecked(true); else if (!strcmp("Culmination", pcdataXMLEle(subEP))) { culminationConditionR->setChecked(true); culminationOffset->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value"))); } else if (!strcmp("At", pcdataXMLEle(subEP))) { startupTimeConditionR->setChecked(true); startupTimeEdit->setDateTime(QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate)); } } } else if (!strcmp(tagXMLEle(ep), "Constraints")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("MinimumAltitude", pcdataXMLEle(subEP))) { altConstraintCheck->setChecked(true); minAltitude->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value"))); } else if (!strcmp("MoonSeparation", pcdataXMLEle(subEP))) { moonSeparationCheck->setChecked(true); minMoonSeparation->setValue(cLocale.toDouble(findXMLAttValu(subEP, "value"))); } else if (!strcmp("EnforceWeather", pcdataXMLEle(subEP))) weatherCheck->setChecked(true); else if (!strcmp("EnforceTwilight", pcdataXMLEle(subEP))) twilightCheck->setChecked(true); } } else if (!strcmp(tagXMLEle(ep), "CompletionCondition")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp("Sequence", pcdataXMLEle(subEP))) sequenceCompletionR->setChecked(true); else if (!strcmp("Repeat", pcdataXMLEle(subEP))) { repeatCompletionR->setChecked(true); repeatsSpin->setValue(cLocale.toInt(findXMLAttValu(subEP, "value"))); } else if (!strcmp("Loop", pcdataXMLEle(subEP))) loopCompletionR->setChecked(true); else if (!strcmp("At", pcdataXMLEle(subEP))) { timeCompletionR->setChecked(true); completionTimeEdit->setDateTime(QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate)); } } } else if (!strcmp(tagXMLEle(ep), "Steps")) { XMLEle *module; trackStepCheck->setChecked(false); focusStepCheck->setChecked(false); alignStepCheck->setChecked(false); guideStepCheck->setChecked(false); for (module = nextXMLEle(ep, 1); module != nullptr; module = nextXMLEle(ep, 0)) { const char *proc = pcdataXMLEle(module); if (!strcmp(proc, "Track")) trackStepCheck->setChecked(true); else if (!strcmp(proc, "Focus")) focusStepCheck->setChecked(true); else if (!strcmp(proc, "Align")) alignStepCheck->setChecked(true); else if (!strcmp(proc, "Guide")) guideStepCheck->setChecked(true); } } } addToQueueB->setEnabled(true); saveJob(); return true; } void Scheduler::saveAs() { schedulerURL.clear(); save(); } void Scheduler::save() { QUrl backupCurrent = schedulerURL; if (schedulerURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || schedulerURL.toLocalFile().contains("/Temp")) schedulerURL.clear(); // If no changes made, return. if (mDirty == false && !schedulerURL.isEmpty()) return; if (schedulerURL.isEmpty()) { schedulerURL = QFileDialog::getSaveFileUrl(this, i18n("Save Ekos Scheduler List"), dirPath, "Ekos Scheduler List (*.esl)"); // if user presses cancel if (schedulerURL.isEmpty()) { schedulerURL = backupCurrent; return; } dirPath = QUrl(schedulerURL.url(QUrl::RemoveFilename)); if (schedulerURL.toLocalFile().contains('.') == 0) schedulerURL.setPath(schedulerURL.toLocalFile() + ".esl"); } if (schedulerURL.isValid()) { if ((saveScheduler(schedulerURL)) == false) { KSNotification::error(i18n("Failed to save scheduler list"), i18n("Save")); return; } mDirty = false; // update save button tool tip queueSaveB->setToolTip("Save schedule to " + schedulerURL.fileName()); } else { QString message = i18n("Invalid URL: %1", schedulerURL.url()); KSNotification::sorry(message, i18n("Invalid URL")); } } bool Scheduler::saveScheduler(const QUrl &fileURL) { QFile file; file.setFileName(fileURL.toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { QString message = i18n("Unable to write to file %1", fileURL.toLocalFile()); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } QTextStream outstream(&file); // We serialize sequence data to XML using the C locale QLocale cLocale = QLocale::c(); outstream << "" << endl; outstream << "" << endl; outstream << "" << schedulerProfileCombo->currentText() << "" << endl; foreach (SchedulerJob *job, jobs) { outstream << "" << endl; outstream << "" << job->getName() << "" << endl; outstream << "" << job->getPriority() << "" << endl; outstream << "" << endl; outstream << "" << cLocale.toString(job->getTargetCoords().ra0().Hours()) << "" << endl; outstream << "" << cLocale.toString(job->getTargetCoords().dec0().Degrees()) << "" << endl; outstream << "" << endl; if (job->getFITSFile().isValid() && job->getFITSFile().isEmpty() == false) outstream << "" << job->getFITSFile().toLocalFile() << "" << endl; outstream << "" << job->getSequenceFile().toLocalFile() << "" << endl; outstream << "" << endl; if (job->getFileStartupCondition() == SchedulerJob::START_ASAP) outstream << "ASAP" << endl; else if (job->getFileStartupCondition() == SchedulerJob::START_CULMINATION) outstream << "Culmination" << endl; else if (job->getFileStartupCondition() == SchedulerJob::START_AT) outstream << "At" << endl; outstream << "" << endl; outstream << "" << endl; if (-90 < job->getMinAltitude()) outstream << "MinimumAltitude" << endl; if (job->getMinMoonSeparation() > 0) outstream << "MoonSeparation" << endl; if (job->getEnforceWeather()) outstream << "EnforceWeather" << endl; if (job->getEnforceTwilight()) outstream << "EnforceTwilight" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE) outstream << "Sequence" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) outstream << "Repeat" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_LOOP) outstream << "Loop" << endl; else if (job->getCompletionCondition() == SchedulerJob::FINISH_AT) outstream << "At" << endl; outstream << "" << endl; outstream << "" << endl; if (job->getStepPipeline() & SchedulerJob::USE_TRACK) outstream << "Track" << endl; if (job->getStepPipeline() & SchedulerJob::USE_FOCUS) outstream << "Focus" << endl; if (job->getStepPipeline() & SchedulerJob::USE_ALIGN) outstream << "Align" << endl; if (job->getStepPipeline() & SchedulerJob::USE_GUIDE) outstream << "Guide" << endl; outstream << "" << endl; outstream << "" << endl; } outstream << "" << endl; if (errorHandlingRescheduleErrorsCB->isChecked()) outstream << "" << endl; outstream << "" << errorHandlingDelaySB->value() << "" << endl; outstream << "" << endl; outstream << "" << endl; if (startupScript->text().isEmpty() == false) outstream << "StartupScript" << endl; if (unparkDomeCheck->isChecked()) outstream << "UnparkDome" << endl; if (unparkMountCheck->isChecked()) outstream << "UnparkMount" << endl; if (uncapCheck->isChecked()) outstream << "UnparkCap" << endl; outstream << "" << endl; outstream << "" << endl; if (warmCCDCheck->isChecked()) outstream << "WarmCCD" << endl; if (capCheck->isChecked()) outstream << "ParkCap" << endl; if (parkMountCheck->isChecked()) outstream << "ParkMount" << endl; if (parkDomeCheck->isChecked()) outstream << "ParkDome" << endl; if (shutdownScript->text().isEmpty() == false) outstream << "ShutdownScript" << endl; outstream << "" << endl; outstream << "" << endl; appendLogText(i18n("Scheduler list saved to %1", fileURL.toLocalFile())); file.close(); return true; } void Scheduler::startSlew() { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting slewing must be valid"); // If the mount was parked by a pause or the end-user, unpark if (isMountParked()) { parkWaitState = PARKWAIT_UNPARK; return; } if (Options::resetMountModelBeforeJob()) mountInterface->call(QDBus::AutoDetect, "resetModel"); SkyPoint target = currentJob->getTargetCoords(); QList telescopeSlew; telescopeSlew.append(target.ra().Hours()); telescopeSlew.append(target.dec().Degrees()); QDBusReply const slewModeReply = mountInterface->callWithArgumentList(QDBus::AutoDetect, "slew", telescopeSlew); if (slewModeReply.error().type() != QDBusError::NoError) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' slew request received DBUS error: %2").arg(currentJob->getName(), QDBusError::errorString(slewModeReply.error().type())); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' slew request received DBUS error: %2").arg( + currentJob->getName(), QDBusError::errorString(slewModeReply.error().type())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); } else { currentJob->setStage(SchedulerJob::STAGE_SLEWING); appendLogText(i18n("Job '%1' is slewing to target.", currentJob->getName())); } } void Scheduler::startFocusing() { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting focusing must be valid"); // 2017-09-30 Jasem: We're skipping post align focusing now as it can be performed // when first focus request is made in capture module if (currentJob->getStage() == SchedulerJob::STAGE_RESLEWING_COMPLETE || currentJob->getStage() == SchedulerJob::STAGE_POSTALIGN_FOCUSING) { // Clear the HFR limit value set in the capture module captureInterface->call(QDBus::AutoDetect, "clearAutoFocusHFR"); // Reset Focus frame so that next frame take a full-resolution capture first. focusInterface->call(QDBus::AutoDetect, "resetFrame"); currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING_COMPLETE); getNextAction(); return; } // Check if autofocus is supported QDBusReply focusModeReply; focusModeReply = focusInterface->call(QDBus::AutoDetect, "canAutoFocus"); if (focusModeReply.error().type() != QDBusError::NoError) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' canAutoFocus request received DBUS error: %2").arg(currentJob->getName(), QDBusError::errorString(focusModeReply.error().type())); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' canAutoFocus request received DBUS error: %2").arg( + currentJob->getName(), QDBusError::errorString(focusModeReply.error().type())); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } if (focusModeReply.value() == false) { appendLogText(i18n("Warning: job '%1' is unable to proceed with autofocus, not supported.", currentJob->getName())); currentJob->setStepPipeline( static_cast(currentJob->getStepPipeline() & ~SchedulerJob::USE_FOCUS)); currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE); getNextAction(); return; } // Clear the HFR limit value set in the capture module captureInterface->call(QDBus::AutoDetect, "clearAutoFocusHFR"); QDBusMessage reply; // We always need to reset frame first if ((reply = focusInterface->call(QDBus::AutoDetect, "resetFrame")).type() == QDBusMessage::ErrorMessage) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' resetFrame request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' resetFrame request received DBUS error: %2").arg( + currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } // Set autostar if full field option is false if (Options::focusUseFullField() == false) { QList autoStar; autoStar.append(true); if ((reply = focusInterface->callWithArgumentList(QDBus::AutoDetect, "setAutoStarEnabled", autoStar)).type() == QDBusMessage::ErrorMessage) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setAutoFocusStar request received DBUS error: %1").arg(currentJob->getName(), reply.errorMessage()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setAutoFocusStar request received DBUS error: %1").arg( + currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } } // Start auto-focus if ((reply = focusInterface->call(QDBus::AutoDetect, "start")).type() == QDBusMessage::ErrorMessage) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' startFocus request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' startFocus request received DBUS error: %2").arg( + currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } /*if (currentJob->getStage() == SchedulerJob::STAGE_RESLEWING_COMPLETE || currentJob->getStage() == SchedulerJob::STAGE_POSTALIGN_FOCUSING) { currentJob->setStage(SchedulerJob::STAGE_POSTALIGN_FOCUSING); appendLogText(i18n("Post-alignment focusing for %1 ...", currentJob->getName())); } else { currentJob->setStage(SchedulerJob::STAGE_FOCUSING); appendLogText(i18n("Focusing %1 ...", currentJob->getName())); }*/ currentJob->setStage(SchedulerJob::STAGE_FOCUSING); appendLogText(i18n("Job '%1' is focusing.", currentJob->getName())); currentOperationTime.restart(); } void Scheduler::findNextJob() { if (state == SCHEDULER_PAUSED) { // everything finished, we can pause setPaused(); return; } Q_ASSERT_X(currentJob->getState() == SchedulerJob::JOB_ERROR || currentJob->getState() == SchedulerJob::JOB_ABORTED || currentJob->getState() == SchedulerJob::JOB_COMPLETE || currentJob->getState() == SchedulerJob::JOB_IDLE, __FUNCTION__, "Finding next job requires current to be in error, aborted, idle or complete"); jobTimer.stop(); // Reset failed count alignFailureCount = guideFailureCount = focusFailureCount = captureFailureCount = 0; /* FIXME: Other debug logs in that function probably */ qCDebug(KSTARS_EKOS_SCHEDULER) << "Find next job..."; if (currentJob->getState() == SchedulerJob::JOB_ERROR || currentJob->getState() == SchedulerJob::JOB_ABORTED) { captureBatch = 0; // Stop Guiding if it was used stopGuiding(); if (currentJob->getState() == SchedulerJob::JOB_ERROR) appendLogText(i18n("Job '%1' is terminated due to errors.", currentJob->getName())); else appendLogText(i18n("Job '%1' is aborted.", currentJob->getName())); // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); // restart aborted jobs immediately, if error handling strategy is set to "restart immediately" if (errorHandlingRestartImmediatelyButton->isChecked() && (currentJob->getState() == SchedulerJob::JOB_ABORTED || (currentJob->getState() == SchedulerJob::JOB_ERROR && errorHandlingRescheduleErrorsCB->isChecked()))) { // reset the state so that it will be restarted currentJob->setState(SchedulerJob::JOB_SCHEDULED); appendLogText(i18n("Waiting %1 seconds to restart job '%2'.", errorHandlingDelaySB->value(), currentJob->getName())); // wait the given delay until the jobs will be evaluated again sleepTimer.setInterval(std::lround((errorHandlingDelaySB->value() * 1000) / KStarsData::Instance()->clock()->scale())); sleepTimer.start(); sleepLabel->setToolTip(i18n("Scheduler waits for a retry.")); sleepLabel->show(); return; } // otherwise start re-evaluation setCurrentJob(nullptr); schedulerTimer.start(); } else if (currentJob->getState() == SchedulerJob::JOB_IDLE) { // job constraints no longer valid, start re-evaluation setCurrentJob(nullptr); schedulerTimer.start(); } // Job is complete, so check completion criteria to optimize processing // In any case, we're done whether the job completed successfully or not. else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE) { /* If we remember job progress, mark the job idle as well as all its duplicates for re-evaluation */ if (Options::rememberJobProgress()) { foreach(SchedulerJob *a_job, jobs) if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) a_job->setState(SchedulerJob::JOB_IDLE); } captureBatch = 0; // Stop Guiding if it was used stopGuiding(); appendLogText(i18n("Job '%1' is complete.", currentJob->getName())); // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); setCurrentJob(nullptr); schedulerTimer.start(); } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_REPEAT) { /* If the job is about to repeat, decrease its repeat count and reset its start time */ if (0 < currentJob->getRepeatsRemaining()) { currentJob->setRepeatsRemaining(currentJob->getRepeatsRemaining() - 1); currentJob->setStartupTime(QDateTime()); } /* Mark the job idle as well as all its duplicates for re-evaluation */ foreach(SchedulerJob *a_job, jobs) if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) a_job->setState(SchedulerJob::JOB_IDLE); /* Re-evaluate all jobs, without selecting a new job */ jobEvaluationOnly = true; evaluateJobs(); /* If current job is actually complete because of previous duplicates, prepare for next job */ if (currentJob == nullptr || currentJob->getRepeatsRemaining() == 0) { stopCurrentJobAction(); if (currentJob != nullptr) { appendLogText(i18np("Job '%1' is complete after #%2 batch.", "Job '%1' is complete after #%2 batches.", currentJob->getName(), currentJob->getRepeatsRequired())); setCurrentJob(nullptr); } schedulerTimer.start(); } /* If job requires more work, continue current observation */ else { /* FIXME: raise priority to allow other jobs to schedule in-between */ executeJob(currentJob); /* If we are guiding, continue capturing */ if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { currentJob->setStage(SchedulerJob::STAGE_CAPTURING); startCapture(); } /* If we are not guiding, but using alignment, realign */ else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN) { currentJob->setStage(SchedulerJob::STAGE_ALIGNING); startAstrometry(); } /* Else if we are neither guiding nor using alignment, slew back to target */ else if (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) { currentJob->setStage(SchedulerJob::STAGE_SLEWING); startSlew(); } /* Else just start capturing */ else { currentJob->setStage(SchedulerJob::STAGE_CAPTURING); startCapture(); } appendLogText(i18np("Job '%1' is repeating, #%2 batch remaining.", "Job '%1' is repeating, #%2 batches remaining.", currentJob->getName(), currentJob->getRepeatsRemaining())); /* currentJob remains the same */ jobTimer.start(); } } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP) { executeJob(currentJob); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); captureBatch++; startCapture(); appendLogText(i18n("Job '%1' is repeating, looping indefinitely.", currentJob->getName())); /* currentJob remains the same */ jobTimer.start(); } else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { if (KStarsData::Instance()->lt().secsTo(currentJob->getCompletionTime()) <= 0) { /* Mark the job idle as well as all its duplicates for re-evaluation */ foreach(SchedulerJob *a_job, jobs) if (a_job == currentJob || a_job->isDuplicateOf(currentJob)) a_job->setState(SchedulerJob::JOB_IDLE); stopCurrentJobAction(); captureBatch = 0; appendLogText(i18np("Job '%1' stopping, reached completion time with #%2 batch done.", "Job '%1' stopping, reached completion time with #%2 batches done.", currentJob->getName(), captureBatch + 1)); // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); setCurrentJob(nullptr); schedulerTimer.start(); } else { executeJob(currentJob); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); captureBatch++; startCapture(); appendLogText(i18np("Job '%1' completed #%2 batch before completion time, restarted.", "Job '%1' completed #%2 batches before completion time, restarted.", currentJob->getName(), captureBatch)); /* currentJob remains the same */ jobTimer.start(); } } else { /* Unexpected situation, mitigate by resetting the job and restarting the scheduler timer */ qCDebug(KSTARS_EKOS_SCHEDULER) << "BUGBUG! Job '" << currentJob->getName() << "' timer elapsed, but no action to be taken."; // Always reset job stage currentJob->setStage(SchedulerJob::STAGE_IDLE); setCurrentJob(nullptr); schedulerTimer.start(); } } void Scheduler::startAstrometry() { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting aligning must be valid"); QDBusMessage reply; setSolverAction(Align::GOTO_SLEW); // Always turn update coords on //QVariant arg(true); //alignInterface->call(QDBus::AutoDetect, "setUpdateCoords", arg); // If FITS file is specified, then we use load and slew if (currentJob->getFITSFile().isEmpty() == false) { QList solveArgs; solveArgs.append(currentJob->getFITSFile().toString(QUrl::PreferLocalFile)); if ((reply = alignInterface->callWithArgumentList(QDBus::AutoDetect, "loadAndSlew", solveArgs)).type() == QDBusMessage::ErrorMessage) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' loadAndSlew request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' loadAndSlew request received DBUS error: %2").arg( + currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } loadAndSlewProgress = true; appendLogText(i18n("Job '%1' is plate solving %2.", currentJob->getName(), currentJob->getFITSFile().fileName())); } else { if ((reply = alignInterface->call(QDBus::AutoDetect, "captureAndSolve")).type() == QDBusMessage::ErrorMessage) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' captureAndSolve request received DBUS error: %2").arg(currentJob->getName(), reply.errorMessage()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' captureAndSolve request received DBUS error: %2").arg( + currentJob->getName(), reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } appendLogText(i18n("Job '%1' is capturing and plate solving.", currentJob->getName())); } /* FIXME: not supposed to modify the job */ currentJob->setStage(SchedulerJob::STAGE_ALIGNING); currentOperationTime.restart(); } void Scheduler::startGuiding(bool resetCalibration) { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting guiding must be valid"); // avoid starting the guider twice if (resetCalibration == false && getGuidingStatus() == GUIDE_GUIDING) { appendLogText(i18n("Guiding already running for %1 ...", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_GUIDING); currentOperationTime.restart(); return; } // Connect Guider guideInterface->call(QDBus::AutoDetect, "connectGuider"); // Set Auto Star to true QVariant arg(true); guideInterface->call(QDBus::AutoDetect, "setCalibrationAutoStar", arg); // Only reset calibration on trouble // and if we are allowed to reset calibration (true by default) if (resetCalibration && Options::resetGuideCalibration()) guideInterface->call(QDBus::AutoDetect, "clearCalibration"); guideInterface->call(QDBus::AutoDetect, "guide"); currentJob->setStage(SchedulerJob::STAGE_GUIDING); appendLogText(i18n("Starting guiding procedure for %1 ...", currentJob->getName())); currentOperationTime.restart(); } void Scheduler::startCapture(bool restart) { Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "Job starting capturing must be valid"); // ensure that guiding is running before we start capturing if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE && getGuidingStatus() != GUIDE_GUIDING) { // guiding should run, but it doesn't. So start guiding first currentJob->setStage(SchedulerJob::STAGE_GUIDING); startGuiding(); return; } QString sanitized = currentJob->getName(); sanitized = sanitized.replace( QRegularExpression("\\s|/|\\(|\\)|:|\\*|~|\"" ), "_" ) // Remove any two or more __ .replace( QRegularExpression("_{2,}"), "_") // Remove any _ at the end .replace( QRegularExpression("_$"), ""); captureInterface->setProperty("targetName", sanitized); QString url = currentJob->getSequenceFile().toLocalFile(); if (restart == false) { QList dbusargs; dbusargs.append(url); captureInterface->callWithArgumentList(QDBus::AutoDetect, "loadSequenceQueue", dbusargs); } switch (currentJob->getCompletionCondition()) { case SchedulerJob::FINISH_LOOP: case SchedulerJob::FINISH_AT: // In these cases, we leave the captured frames map empty // to ensure, that the capture sequence is executed in any case. break; default: // Scheduler always sets captured frame map when starting a sequence - count may be different, robustness, dynamic priority // hand over the map of captured frames so that the capture // process knows about existing frames SchedulerJob::CapturedFramesMap fMap = currentJob->getCapturedFramesMap(); for (auto &e : fMap.keys()) { QList dbusargs; QDBusMessage reply; dbusargs.append(e); dbusargs.append(fMap.value(e)); if ((reply = captureInterface->callWithArgumentList(QDBus::AutoDetect, "setCapturedFramesMap", dbusargs)).type() == QDBusMessage::ErrorMessage) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: job '%1' setCapturedFramesCount request received DBUS error: %1").arg(currentJob->getName()).arg(reply.errorMessage()); + qCCritical(KSTARS_EKOS_SCHEDULER) << + QString("Warning: job '%1' setCapturedFramesCount request received DBUS error: %1").arg(currentJob->getName()).arg( + reply.errorMessage()); if (!manageConnectionLoss()) currentJob->setState(SchedulerJob::JOB_ERROR); return; } } break; } // Start capture process captureInterface->call(QDBus::AutoDetect, "start"); currentJob->setStage(SchedulerJob::STAGE_CAPTURING); KNotification::event(QLatin1String("EkosScheduledImagingStart"), i18n("Ekos job (%1) - Capture started", currentJob->getName())); if (captureBatch > 0) appendLogText(i18n("Job '%1' capture is in progress (batch #%2)...", currentJob->getName(), captureBatch + 1)); else appendLogText(i18n("Job '%1' capture is in progress...", currentJob->getName())); currentOperationTime.restart(); } void Scheduler::stopGuiding() { if (!guideInterface) return; // Tell guider to abort if the current job requires guiding - end-user may enable guiding manually before observation if (nullptr != currentJob && (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE)) { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is stopping guiding...").arg(currentJob->getName()); guideInterface->call(QDBus::AutoDetect, "abort"); guideFailureCount = 0; } // In any case, stop the automatic guider restart if (restartGuidingTimer.isActive()) restartGuidingTimer.stop(); } void Scheduler::setSolverAction(Align::GotoMode mode) { QVariant gotoMode(static_cast(mode)); alignInterface->call(QDBus::AutoDetect, "setSolverAction", gotoMode); } void Scheduler::disconnectINDI() { qCInfo(KSTARS_EKOS_SCHEDULER) << "Disconnecting INDI..."; indiState = INDI_DISCONNECTING; ekosInterface->call(QDBus::AutoDetect, "disconnectDevices"); } void Scheduler::stopEkos() { qCInfo(KSTARS_EKOS_SCHEDULER) << "Stopping Ekos..."; ekosState = EKOS_STOPPING; ekosConnectFailureCount = 0; ekosInterface->call(QDBus::AutoDetect, "stop"); m_MountReady = m_CapReady = m_CaptureReady = m_DomeReady = false; } void Scheduler::setDirty() { mDirty = true; if (sender() == startupProcedureButtonGroup || sender() == shutdownProcedureGroup) return; if (0 <= jobUnderEdit && state != SCHEDULER_RUNNING && 0 <= queueTable->currentRow()) { // Now that jobs are sorted, reset jobs that are later than the edited one for re-evaluation for (int row = jobUnderEdit; row < jobs.size(); row++) jobs.at(row)->reset(); saveJob(); } // For object selection, all fields must be filled bool const nameSelectionOK = !raBox->isEmpty() && !decBox->isEmpty() && !nameEdit->text().isEmpty(); // For FITS selection, only the name and fits URL should be filled. bool const fitsSelectionOK = !nameEdit->text().isEmpty() && !fitsURL.isEmpty(); // Sequence selection is required bool const seqSelectionOK = !sequenceEdit->text().isEmpty(); // Finally, adding is allowed upon object/FITS and sequence selection bool const addingOK = (nameSelectionOK || fitsSelectionOK) && seqSelectionOK; addToQueueB->setEnabled(addingOK); mosaicB->setEnabled(addingOK); } void Scheduler::updateCompletedJobsCount(bool forced) { /* Use a temporary map in order to limit the number of file searches */ SchedulerJob::CapturedFramesMap newFramesCount; /* FIXME: Capture storage cache is refreshed too often, feature requires rework. */ /* Check if one job is idle or requires evaluation - if so, force refresh */ forced |= std::any_of(jobs.begin(), jobs.end(), [](SchedulerJob * oneJob) -> bool { SchedulerJob::JOBStatus const state = oneJob->getState(); return state == SchedulerJob::JOB_IDLE || state == SchedulerJob::JOB_EVALUATION;}); /* If update is forced, clear the frame map */ if (forced) capturedFramesCount.clear(); /* Enumerate SchedulerJobs to count captures that are already stored */ for (SchedulerJob *oneJob : jobs) { QList seqjobs; bool hasAutoFocus = false; //oneJob->setLightFramesRequired(false); /* Look into the sequence requirements, bypass if invalid */ if (loadSequenceQueue(oneJob->getSequenceFile().toLocalFile(), oneJob, seqjobs, hasAutoFocus) == false) { - appendLogText(i18n("Warning: job '%1' has inaccessible sequence '%2', marking invalid.", oneJob->getName(), oneJob->getSequenceFile().toLocalFile())); + appendLogText(i18n("Warning: job '%1' has inaccessible sequence '%2', marking invalid.", oneJob->getName(), + oneJob->getSequenceFile().toLocalFile())); oneJob->setState(SchedulerJob::JOB_INVALID); continue; } /* Enumerate the SchedulerJob's SequenceJobs to count captures stored for each */ for (SequenceJob *oneSeqJob : seqjobs) { /* Only consider captures stored on client (Ekos) side */ /* FIXME: ask the remote for the file count */ if (oneSeqJob->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) continue; /* FIXME: this signature path is incoherent when there is no filter wheel on the setup - bugfix should be elsewhere though */ QString const signature = oneSeqJob->getSignature(); /* If signature was processed during this run, keep it */ if (newFramesCount.constEnd() != newFramesCount.constFind(signature)) continue; /* If signature was processed during an earlier run, use the earlier count */ QMap::const_iterator const earlierRunIterator = capturedFramesCount.constFind(signature); if (capturedFramesCount.constEnd() != earlierRunIterator) { newFramesCount[signature] = earlierRunIterator.value(); continue; } /* Else recount captures already stored */ newFramesCount[signature] = getCompletedFiles(signature, oneSeqJob->getFullPrefix()); } // determine whether we need to continue capturing, depending on captured frames bool lightFramesRequired = false; switch (oneJob->getCompletionCondition()) { case SchedulerJob::FINISH_SEQUENCE: case SchedulerJob::FINISH_REPEAT: for (SequenceJob *oneSeqJob : seqjobs) { QString const signature = oneSeqJob->getSignature(); /* If frame is LIGHT, how hany do we have left? */ - if (oneSeqJob->getFrameType() == FRAME_LIGHT && oneSeqJob->getCount()*oneJob->getRepeatsRequired() > newFramesCount[signature]) + if (oneSeqJob->getFrameType() == FRAME_LIGHT + && oneSeqJob->getCount()*oneJob->getRepeatsRequired() > newFramesCount[signature]) lightFramesRequired = true; } break; default: // in all other cases it does not depend on the number of captured frames lightFramesRequired = true; } oneJob->setLightFramesRequired(lightFramesRequired); } capturedFramesCount = newFramesCount; //if (forced) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Frame map summary:"; QMap::const_iterator it = capturedFramesCount.constBegin(); for (; it != capturedFramesCount.constEnd(); it++) qCDebug(KSTARS_EKOS_SCHEDULER) << " " << it.key() << ':' << it.value(); } } bool Scheduler::estimateJobTime(SchedulerJob *schedJob) { static SchedulerJob *jobWarned = nullptr; /* updateCompletedJobsCount(); */ // Load the sequence job associated with the argument scheduler job. QList seqJobs; bool hasAutoFocus = false; if (loadSequenceQueue(schedJob->getSequenceFile().toLocalFile(), schedJob, seqJobs, hasAutoFocus) == false) { - qCWarning(KSTARS_EKOS_SCHEDULER) << QString("Warning: Failed estimating the duration of job '%1', its sequence file is invalid.").arg(schedJob->getSequenceFile().toLocalFile()); + qCWarning(KSTARS_EKOS_SCHEDULER) << + QString("Warning: Failed estimating the duration of job '%1', its sequence file is invalid.").arg( + schedJob->getSequenceFile().toLocalFile()); return false; } // FIXME: setting in-sequence focus should be done in XML processing. schedJob->setInSequenceFocus(hasAutoFocus); // Stop spam of log on re-evaluation. If we display the warning once, then that's it. if (schedJob != jobWarned && hasAutoFocus && !(schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS)) { - appendLogText(i18n("Warning: Job '%1' has its focus step disabled, periodic and/or HFR procedures currently set in its sequence will not occur.", schedJob->getName())); + appendLogText( + i18n("Warning: Job '%1' has its focus step disabled, periodic and/or HFR procedures currently set in its sequence will not occur.", + schedJob->getName())); jobWarned = schedJob; } /* This is the map of captured frames for this scheduler job, keyed per storage signature. * It will be forwarded to the Capture module in order to capture only what frames are required. * If option "Remember Job Progress" is disabled, this map will be empty, and the Capture module will process all requested captures unconditionally. */ SchedulerJob::CapturedFramesMap capture_map; bool const rememberJobProgress = Options::rememberJobProgress(); int totalSequenceCount = 0, totalCompletedCount = 0; double totalImagingTime = 0; // Determine number of captures in the scheduler job int capturesPerRepeat = 0; foreach (SequenceJob *seqJob, seqJobs) capturesPerRepeat += seqJob->getCount(); // Loop through sequence jobs to calculate the number of required frames and estimate duration. foreach (SequenceJob *seqJob, seqJobs) { // FIXME: find a way to actually display the filter name. - QString seqName = i18n("Job '%1' %2x%3\" %4", schedJob->getName(), seqJob->getCount(), seqJob->getExposure(), seqJob->getFilterName()); + QString seqName = i18n("Job '%1' %2x%3\" %4", schedJob->getName(), seqJob->getCount(), seqJob->getExposure(), + seqJob->getFilterName()); if (seqJob->getUploadMode() == ISD::CCD::UPLOAD_LOCAL) { - qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 duration cannot be estimated time since the sequence saves the files remotely.").arg(seqName); + qCInfo(KSTARS_EKOS_SCHEDULER) << + QString("%1 duration cannot be estimated time since the sequence saves the files remotely.").arg(seqName); schedJob->setEstimatedTime(-2); qDeleteAll(seqJobs); return true; } // Note that looping jobs will have zero repeats required. int const captures_required = seqJob->getCount() * schedJob->getRepeatsRequired(); int captures_completed = 0; if (rememberJobProgress) { /* Enumerate sequence jobs associated to this scheduler job, and assign them a completed count. * * The objective of this block is to fill the storage map of the scheduler job with completed counts for each capture storage. * * Sequence jobs capture to a storage folder, and are given a count of captures to store at that location. * The tricky part is to make sure the repeat count of the scheduler job is properly transferred to each sequence job. * * For instance, a scheduler job repeated three times must execute the full list of sequence jobs three times, thus * has to tell each sequence job it misses all captures, three times. It cannot tell the sequence job three captures are * missing, first because that's not how the sequence job is designed (completed count, not required count), and second * because this would make the single sequence job repeat three times, instead of repeating the full list of sequence * jobs three times. * * The consolidated storage map will be assigned to each sequence job based on their signature when the scheduler job executes them. * * For instance, consider a RGBL sequence of single captures. The map will store completed captures for R, G, B and L storages. * If R and G have 1 file each, and B and L have no files, map[storage(R)] = map[storage(G)] = 1 and map[storage(B)] = map[storage(L)] = 0. * When that scheduler job executes, only B and L captures will be processed. * * In the case of a RGBLRGB sequence of single captures, the second R, G and B map items will count one less capture than what is really in storage. * If R and G have 1 file each, and B and L have no files, map[storage(R1)] = map[storage(B1)] = 1, and all others will be 0. * When that scheduler job executes, B1, L, R2, G2 and B2 will be processed. * * This doesn't handle the case of duplicated scheduler jobs, that is, scheduler jobs with the same storage for capture sets. * Those scheduler jobs will all change state to completion at the same moment as they all target the same storage. * This is why it is important to manage the repeat count of the scheduler job, as stated earlier. */ // Retrieve cached count of completed captures for the output folder of this seqJob QString const signature = seqJob->getSignature(); QString const signature_path = QFileInfo(signature).path(); captures_completed = capturedFramesCount[signature]; - qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 sees %2 captures in output folder '%3'.").arg(seqName).arg(captures_completed).arg(signature_path); + qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 sees %2 captures in output folder '%3'.").arg(seqName).arg( + captures_completed).arg(signature_path); // Enumerate sequence jobs to check how many captures are completed overall in the same storage as the current one foreach (SequenceJob *prevSeqJob, seqJobs) { // Enumerate seqJobs up to the current one if (seqJob == prevSeqJob) break; // If the previous sequence signature matches the current, reduce completion count to take duplicates into account if (!signature.compare(prevSeqJob->getLocalDir() + prevSeqJob->getDirectoryPostfix())) { // Note that looping jobs will have zero repeats required. int const previous_captures_required = prevSeqJob->getCount() * schedJob->getRepeatsRequired(); - qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 has a previous duplicate sequence job requiring %2 captures.").arg(seqName).arg(previous_captures_required); + qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 has a previous duplicate sequence job requiring %2 captures.").arg( + seqName).arg(previous_captures_required); captures_completed -= previous_captures_required; } // Now completed count can be needlessly negative for this job, so clamp to zero if (captures_completed < 0) captures_completed = 0; // And break if no captures remain, this job has to execute if (captures_completed == 0) break; } // Finally we're only interested in the number of captures required for this sequence item if (0 < captures_required && captures_required < captures_completed) captures_completed = captures_required; - qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 has completed %2/%3 of its required captures in output folder '%4'.").arg(seqName).arg(captures_completed).arg(captures_required).arg(signature_path); + qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 has completed %2/%3 of its required captures in output folder '%4'.").arg( + seqName).arg(captures_completed).arg(captures_required).arg(signature_path); // Update the completion count for this signature in the frame map if we still have captures to take. // That frame map will be transferred to the Capture module, for which the sequence is a single batch of the scheduler job. // For instance, consider a scheduler job repeated 3 times and using a 3xLum sequence, so we want 9xLum in the end. // - If no captures are already processed, the frame map contains Lum=0 // - If 1xLum are already processed, the frame map contains Lum=0 when the batch executes, so that 3xLum may be taken. // - If 3xLum are already processed, the frame map contains Lum=0 when the batch executes, as we still need more than what the sequence provides. // - If 7xLum are already processed, the frame map contains Lum=1 when the batch executes, because we now only need 2xLum to finish the job. // Therefore we need to specify a number of existing captures only for the last batch of the scheduler job. // In the last batch, we only need the remainder of frames to get to the required total. if (captures_completed < captures_required) { if (captures_required - captures_completed < seqJob->getCount()) capture_map[signature] = captures_completed % seqJob->getCount(); else capture_map[signature] = 0; } else capture_map[signature] = captures_required; // From now on, 'captures_completed' is the number of frames completed for the *current* sequence job } // Else rely on the captures done during this session else captures_completed = schedJob->getCompletedCount() / capturesPerRepeat * seqJob->getCount(); // Check if we still need any light frames. Because light frames changes the flow of the observatory startup // Without light frames, there is no need to do focusing, alignment, guiding...etc // We check if the frame type is LIGHT and if either the number of captures_completed frames is less than required // OR if the completion condition is set to LOOP so it is never complete due to looping. // Note that looping jobs will have zero repeats required. // FIXME: As it is implemented now, FINISH_LOOP may loop over a capture-complete, therefore inoperant, scheduler job. bool const areJobCapturesComplete = !(captures_completed < captures_required || 0 == captures_required); if (seqJob->getFrameType() == FRAME_LIGHT) { if(areJobCapturesComplete) { - qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 completed its sequence of %2 light frames.").arg(seqName).arg(captures_required); + qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 completed its sequence of %2 light frames.").arg(seqName).arg( + captures_required); } } else { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 captures calibration frames.").arg(seqName); } totalSequenceCount += captures_required; totalCompletedCount += captures_completed; /* If captures are not complete, we have imaging time left */ if (!areJobCapturesComplete) { /* if looping, consider we always have one capture left */ unsigned int const captures_to_go = 0 < captures_required ? captures_required - captures_completed : 1; totalImagingTime += fabs((seqJob->getExposure() + seqJob->getDelay()) * captures_to_go); /* If we have light frames to process, add focus/dithering delay */ if (seqJob->getFrameType() == FRAME_LIGHT) { // If inSequenceFocus is true if (hasAutoFocus) { // Wild guess that each in sequence auto focus takes an average of 30 seconds. It can take any where from 2 seconds to 2+ minutes. // FIXME: estimating one focus per capture is probably not realistic. qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a focus procedure.").arg(seqName); totalImagingTime += captures_to_go * 30; } // If we're dithering after each exposure, that's another 10-20 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE && Options::ditherEnabled()) { qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a dither procedure.").arg(seqName); totalImagingTime += (captures_to_go * 15) / Options::ditherFrames(); } } } } schedJob->setCapturedFramesMap(capture_map); schedJob->setSequenceCount(totalSequenceCount); // only in case we remember the job progress, we change the completion count if (rememberJobProgress) schedJob->setCompletedCount(totalCompletedCount); qDeleteAll(seqJobs); // FIXME: Move those ifs away to the caller in order to avoid estimating in those situations! // We can't estimate times that do not finish when sequence is done if (schedJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP) { // We can't know estimated time if it is looping indefinitely schedJob->setEstimatedTime(-2); - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' is configured to loop until Scheduler is stopped manually, has undefined imaging time.") + qCDebug(KSTARS_EKOS_SCHEDULER) << + QString("Job '%1' is configured to loop until Scheduler is stopped manually, has undefined imaging time.") .arg(schedJob->getName()); } // If we know startup and finish times, we can estimate time right away else if (schedJob->getStartupCondition() == SchedulerJob::START_AT && schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { // FIXME: SchedulerJob is probably doing this already qint64 const diff = schedJob->getStartupTime().secsTo(schedJob->getCompletionTime()); schedJob->setEstimatedTime(diff); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a startup time and fixed completion time, will run for %2.") .arg(schedJob->getName()) .arg(dms(diff * 15.0 / 3600.0f).toHMSString()); } // If we know finish time only, we can roughly estimate the time considering the job starts now else if (schedJob->getStartupCondition() != SchedulerJob::START_AT && schedJob->getCompletionCondition() == SchedulerJob::FINISH_AT) { qint64 const diff = KStarsData::Instance()->lt().secsTo(schedJob->getCompletionTime()); schedJob->setEstimatedTime(diff); - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has no startup time but fixed completion time, will run for %2 if started now.") + qCDebug(KSTARS_EKOS_SCHEDULER) << + QString("Job '%1' has no startup time but fixed completion time, will run for %2 if started now.") .arg(schedJob->getName()) .arg(dms(diff * 15.0 / 3600.0f).toHMSString()); } // Rely on the estimated imaging time to determine whether this job is complete or not - this makes the estimated time null else if (totalImagingTime <= 0) { schedJob->setEstimatedTime(0); qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' will not run, complete with %2/%3 captures.") .arg(schedJob->getName()).arg(totalCompletedCount).arg(totalSequenceCount); } // Else consolidate with step durations else { if (schedJob->getLightFramesRequired()) { /* FIXME: estimation should base on actual measure of each step, eventually with preliminary data as what it used now */ // Are we doing tracking? It takes about 30 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_TRACK) totalImagingTime += 30; // Are we doing initial focusing? That can take about 2 minutes if (schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS) totalImagingTime += 120; // Are we doing astrometry? That can take about 60 seconds if (schedJob->getStepPipeline() & SchedulerJob::USE_ALIGN) { totalImagingTime += 60; } // Are we doing guiding? if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { // Looping, finding guide star, settling takes 15 sec totalImagingTime += 15; // Add guiding settle time from dither setting (used by phd2::guide()) totalImagingTime += Options::ditherSettle(); // Add guiding settle time from ekos sccheduler setting totalImagingTime += Options::guidingSettle(); // If calibration always cleared // then calibration process can take about 2 mins if(Options::resetGuideCalibration()) totalImagingTime += 120; } } dms const estimatedTime(totalImagingTime * 15.0 / 3600.0); schedJob->setEstimatedTime(totalImagingTime); - qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' estimated to take %2 to complete.").arg(schedJob->getName(), estimatedTime.toHMSString()); + qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' estimated to take %2 to complete.").arg(schedJob->getName(), + estimatedTime.toHMSString()); } return true; } void Scheduler::parkMount() { QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg( + mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { case ISD::PARK_PARKED: if (shutdownState == SHUTDOWN_PARK_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; parkWaitState = PARKWAIT_PARKED; appendLogText(i18n("Mount already parked.")); break; case ISD::PARK_UNPARKING: //case Mount::UNPARKING_BUSY: /* FIXME: Handle the situation where we request parking but an unparking procedure is running. */ // case Mount::PARKING_IDLE: // case Mount::UNPARKING_OK: case ISD::PARK_ERROR: case ISD::PARK_UNKNOWN: case ISD::PARK_UNPARKED: { QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "park"); if (mountReply.error().type() != QDBusError::NoError) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount park request received DBUS error: %1").arg(QDBusError::errorString(mountReply.error().type())); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount park request received DBUS error: %1").arg( + QDBusError::errorString(mountReply.error().type())); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } else currentOperationTime.start(); } // Fall through case ISD::PARK_PARKING: //case Mount::PARKING_BUSY: if (shutdownState == SHUTDOWN_PARK_MOUNT) shutdownState = SHUTDOWN_PARKING_MOUNT; parkWaitState = PARKWAIT_PARKING; appendLogText(i18n("Parking mount in progress...")); break; // All cases covered above so no need for default //default: // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while parking mount.").arg(mountReply.value()); } } void Scheduler::unParkMount() { if (mountInterface.isNull()) return; QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg( + mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { //case Mount::UNPARKING_OK: case ISD::PARK_UNPARKED: if (startupState == STARTUP_UNPARK_MOUNT) startupState = STARTUP_UNPARK_CAP; parkWaitState = PARKWAIT_UNPARKED; appendLogText(i18n("Mount already unparked.")); break; //case Mount::PARKING_BUSY: case ISD::PARK_PARKING: /* FIXME: Handle the situation where we request unparking but a parking procedure is running. */ // case Mount::PARKING_IDLE: // case Mount::PARKING_OK: // case Mount::PARKING_ERROR: case ISD::PARK_ERROR: case ISD::PARK_UNKNOWN: case ISD::PARK_PARKED: { QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "unpark"); if (mountReply.error().type() != QDBusError::NoError) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount unpark request received DBUS error: %1").arg(QDBusError::errorString(mountReply.error().type())); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount unpark request received DBUS error: %1").arg( + QDBusError::errorString(mountReply.error().type())); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } else currentOperationTime.start(); } // Fall through //case Mount::UNPARKING_BUSY: case ISD::PARK_UNPARKING: if (startupState == STARTUP_UNPARK_MOUNT) startupState = STARTUP_UNPARKING_MOUNT; parkWaitState = PARKWAIT_UNPARKING; qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress..."; break; // All cases covered above //default: // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while unparking mount.").arg(mountReply.value()); } } void Scheduler::checkMountParkingStatus() { if (mountInterface.isNull()) return; static int parkingFailureCount = 0; QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parkStatus request received DBUS error: %1").arg( + mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { //case Mount::PARKING_OK: case ISD::PARK_PARKED: // If we are starting up, we will unpark the mount in checkParkWaitState soon // If we are shutting down and mount is parked, proceed to next step if (shutdownState == SHUTDOWN_PARKING_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; // Update parking engine state if (parkWaitState == PARKWAIT_PARKING) parkWaitState = PARKWAIT_PARKED; appendLogText(i18n("Mount parked.")); parkingFailureCount = 0; break; //case Mount::UNPARKING_OK: case ISD::PARK_UNPARKED: // If we are starting up and mount is unparked, proceed to next step // If we are shutting down, we will park the mount in checkParkWaitState soon if (startupState == STARTUP_UNPARKING_MOUNT) startupState = STARTUP_UNPARK_CAP; // Update parking engine state if (parkWaitState == PARKWAIT_UNPARKING) parkWaitState = PARKWAIT_UNPARKED; appendLogText(i18n("Mount unparked.")); parkingFailureCount = 0; break; // FIXME: Create an option for the parking/unparking timeout. //case Mount::UNPARKING_BUSY: case ISD::PARK_UNPARKING: if (currentOperationTime.elapsed() > (60 * 1000)) { if (++parkingFailureCount < MAX_FAILURE_ATTEMPTS) { - appendLogText(i18n("Warning: mount unpark operation timed out on attempt %1/%2. Restarting operation...", parkingFailureCount, MAX_FAILURE_ATTEMPTS)); + appendLogText(i18n("Warning: mount unpark operation timed out on attempt %1/%2. Restarting operation...", + parkingFailureCount, MAX_FAILURE_ATTEMPTS)); unParkMount(); } else { appendLogText(i18n("Warning: mount unpark operation timed out on last attempt.")); parkWaitState = PARKWAIT_ERROR; } } else qCInfo(KSTARS_EKOS_SCHEDULER) << "Unparking mount in progress..."; break; //case Mount::PARKING_BUSY: case ISD::PARK_PARKING: if (currentOperationTime.elapsed() > (60 * 1000)) { if (++parkingFailureCount < MAX_FAILURE_ATTEMPTS) { - appendLogText(i18n("Warning: mount park operation timed out on attempt %1/%2. Restarting operation...", parkingFailureCount, MAX_FAILURE_ATTEMPTS)); + appendLogText(i18n("Warning: mount park operation timed out on attempt %1/%2. Restarting operation...", parkingFailureCount, + MAX_FAILURE_ATTEMPTS)); parkMount(); } else { appendLogText(i18n("Warning: mount park operation timed out on last attempt.")); parkWaitState = PARKWAIT_ERROR; } } else qCInfo(KSTARS_EKOS_SCHEDULER) << "Parking mount in progress..."; break; //case Mount::PARKING_ERROR: case ISD::PARK_ERROR: if (startupState == STARTUP_UNPARKING_MOUNT) { appendLogText(i18n("Mount unparking error.")); startupState = STARTUP_ERROR; + parkingFailureCount = 0; } else if (shutdownState == SHUTDOWN_PARKING_MOUNT) { - appendLogText(i18n("Mount parking error.")); - shutdownState = SHUTDOWN_ERROR; + if (++parkingFailureCount < MAX_FAILURE_ATTEMPTS) + { + appendLogText(i18n("Warning: mount park operation failed on attempt %1/%2. Restarting operation...", parkingFailureCount, + MAX_FAILURE_ATTEMPTS)); + parkMount(); + } + else + { + appendLogText(i18n("Mount parking error.")); + shutdownState = SHUTDOWN_ERROR; + parkingFailureCount = 0; + } + } else if (parkWaitState == PARKWAIT_PARKING) { appendLogText(i18n("Mount parking error.")); parkWaitState = PARKWAIT_ERROR; + parkingFailureCount = 0; } else if (parkWaitState == PARKWAIT_UNPARKING) { appendLogText(i18n("Mount unparking error.")); parkWaitState = PARKWAIT_ERROR; + parkingFailureCount = 0; } - - parkingFailureCount = 0; break; //case Mount::PARKING_IDLE: // FIXME Does this work as intended? check! case ISD::PARK_UNKNOWN: // Last parking action did not result in an action, so proceed to next step if (shutdownState == SHUTDOWN_PARKING_MOUNT) shutdownState = SHUTDOWN_PARK_DOME; // Last unparking action did not result in an action, so proceed to next step if (startupState == STARTUP_UNPARKING_MOUNT) startupState = STARTUP_UNPARK_CAP; // Update parking engine state if (parkWaitState == PARKWAIT_PARKING) parkWaitState = PARKWAIT_PARKED; else if (parkWaitState == PARKWAIT_UNPARKING) parkWaitState = PARKWAIT_UNPARKED; parkingFailureCount = 0; break; // All cases covered above //default: // qCWarning(KSTARS_EKOS_SCHEDULER) << QString("BUG: Parking state %1 not managed while checking progress.").arg(mountReply.value()); } } bool Scheduler::isMountParked() { if (mountInterface.isNull()) return false; // First check if the mount is able to park - if it isn't, getParkingStatus will reply PARKING_ERROR and status won't be clear //QDBusReply const parkCapableReply = mountInterface->call(QDBus::AutoDetect, "canPark"); QVariant canPark = mountInterface->property("canPark"); if (canPark.isValid() == false) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount canPark request received DBUS error: %1").arg(mountInterface->lastError().type()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount canPark request received DBUS error: %1").arg( + mountInterface->lastError().type()); manageConnectionLoss(); return false; } else if (canPark.toBool() == true) { // If it is able to park, obtain its current status //QDBusReply const mountReply = mountInterface->call(QDBus::AutoDetect, "getParkingStatus"); QVariant parkingStatus = mountInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parking status property is invalid %1.").arg(mountInterface->lastError().type()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: mount parking status property is invalid %1.").arg( + mountInterface->lastError().type()); manageConnectionLoss(); return false; } // Deduce state of mount - see getParkingStatus in mount.cpp switch (static_cast(parkingStatus.toInt())) { // case Mount::PARKING_OK: // INDI switch ok, and parked // case Mount::PARKING_IDLE: // INDI switch idle, and parked case ISD::PARK_PARKED: return true; // case Mount::UNPARKING_OK: // INDI switch idle or ok, and unparked // case Mount::PARKING_ERROR: // INDI switch error // case Mount::PARKING_BUSY: // INDI switch busy // case Mount::UNPARKING_BUSY: // INDI switch busy default: return false; } } // If the mount is not able to park, consider it not parked return false; } void Scheduler::parkDome() { if (domeInterface.isNull()) return; //QDBusReply const domeReply = domeInterface->call(QDBus::AutoDetect, "getParkingStatus"); //Dome::ParkingStatus status = static_cast(domeReply.value()); QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg( + mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); if (status != ISD::PARK_PARKED) { shutdownState = SHUTDOWN_PARKING_DOME; domeInterface->call(QDBus::AutoDetect, "park"); appendLogText(i18n("Parking dome...")); currentOperationTime.start(); } else { appendLogText(i18n("Dome already parked.")); shutdownState = SHUTDOWN_SCRIPT; } } void Scheduler::unParkDome() { if (domeInterface.isNull()) return; QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg( + mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } if (static_cast(parkingStatus.toInt()) != ISD::PARK_UNPARKED) { startupState = STARTUP_UNPARKING_DOME; domeInterface->call(QDBus::AutoDetect, "unpark"); appendLogText(i18n("Unparking dome...")); currentOperationTime.start(); } else { appendLogText(i18n("Dome already unparked.")); startupState = STARTUP_UNPARK_MOUNT; } } void Scheduler::checkDomeParkingStatus() { if (domeInterface.isNull()) return; /* FIXME: move this elsewhere */ static int parkingFailureCount = 0; QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg( + mountInterface->lastError().type()); if (!manageConnectionLoss()) parkWaitState = PARKWAIT_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { case ISD::PARK_PARKED: if (shutdownState == SHUTDOWN_PARKING_DOME) { appendLogText(i18n("Dome parked.")); shutdownState = SHUTDOWN_SCRIPT; } parkingFailureCount = 0; break; case ISD::PARK_UNPARKED: if (startupState == STARTUP_UNPARKING_DOME) { startupState = STARTUP_UNPARK_MOUNT; appendLogText(i18n("Dome unparked.")); } parkingFailureCount = 0; break; case ISD::PARK_PARKING: case ISD::PARK_UNPARKING: // TODO make the timeouts configurable by the user if (currentOperationTime.elapsed() > (120 * 1000)) { if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Operation timeout. Restarting operation...")); if (status == ISD::PARK_PARKING) parkDome(); else unParkDome(); break; } } break; case ISD::PARK_ERROR: if (shutdownState == SHUTDOWN_PARKING_DOME) { - appendLogText(i18n("Dome parking error.")); - shutdownState = SHUTDOWN_ERROR; + if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS) + { + appendLogText(i18n("Dome parking failed. Restarting operation...")); + parkDome(); + } + else + { + appendLogText(i18n("Dome parking error.")); + shutdownState = SHUTDOWN_ERROR; + parkingFailureCount = 0; + } } else if (startupState == STARTUP_UNPARKING_DOME) { - appendLogText(i18n("Dome unparking error.")); - startupState = STARTUP_ERROR; + if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS) + { + appendLogText(i18n("Dome unparking failed. Restarting operation...")); + unParkDome(); + } + else + { + appendLogText(i18n("Dome unparking error.")); + startupState = STARTUP_ERROR; + parkingFailureCount = 0; + } } - parkingFailureCount = 0; break; default: break; } } bool Scheduler::isDomeParked() { if (domeInterface.isNull()) return false; QVariant parkingStatus = domeInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: dome parkStatus request received DBUS error: %1").arg( + mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); return status == ISD::PARK_PARKED; } void Scheduler::parkCap() { if (capInterface.isNull()) return; QVariant parkingStatus = capInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg( + mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); if (status != ISD::PARK_PARKED) { shutdownState = SHUTDOWN_PARKING_CAP; capInterface->call(QDBus::AutoDetect, "park"); appendLogText(i18n("Parking Cap...")); currentOperationTime.start(); } else { appendLogText(i18n("Cap already parked.")); shutdownState = SHUTDOWN_PARK_MOUNT; } } void Scheduler::unParkCap() { if (capInterface.isNull()) return; QVariant parkingStatus = capInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg( + mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); if (status != ISD::PARK_UNPARKED) { startupState = STARTUP_UNPARKING_CAP; capInterface->call(QDBus::AutoDetect, "unpark"); appendLogText(i18n("Unparking cap...")); currentOperationTime.start(); } else { appendLogText(i18n("Cap already unparked.")); startupState = STARTUP_COMPLETE; } } void Scheduler::checkCapParkingStatus() { if (capInterface.isNull()) return; /* FIXME: move this elsewhere */ static int parkingFailureCount = 0; QVariant parkingStatus = capInterface->property("parkStatus"); if (parkingStatus.isValid() == false) { - qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg(mountInterface->lastError().type()); + qCCritical(KSTARS_EKOS_SCHEDULER) << QString("Warning: cap parkStatus request received DBUS error: %1").arg( + mountInterface->lastError().type()); if (!manageConnectionLoss()) parkingStatus = ISD::PARK_ERROR; } ISD::ParkStatus status = static_cast(parkingStatus.toInt()); switch (status) { case ISD::PARK_PARKED: if (shutdownState == SHUTDOWN_PARKING_CAP) { appendLogText(i18n("Cap parked.")); shutdownState = SHUTDOWN_PARK_MOUNT; } parkingFailureCount = 0; break; case ISD::PARK_UNPARKED: if (startupState == STARTUP_UNPARKING_CAP) { startupState = STARTUP_COMPLETE; appendLogText(i18n("Cap unparked.")); } parkingFailureCount = 0; break; case ISD::PARK_PARKING: case ISD::PARK_UNPARKING: // TODO make the timeouts configurable by the user if (currentOperationTime.elapsed() > (60 * 1000)) { if (parkingFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Operation timeout. Restarting operation...")); if (status == ISD::PARK_PARKING) parkCap(); else unParkCap(); break; } } break; case ISD::PARK_ERROR: if (shutdownState == SHUTDOWN_PARKING_CAP) { appendLogText(i18n("Cap parking error.")); shutdownState = SHUTDOWN_ERROR; } else if (startupState == STARTUP_UNPARKING_CAP) { appendLogText(i18n("Cap unparking error.")); startupState = STARTUP_ERROR; } parkingFailureCount = 0; break; default: break; } } void Scheduler::startJobEvaluation() { // Reset current job setCurrentJob(nullptr); // Reset ALL scheduler jobs to IDLE and force-reset their completed count - no effect when progress is kept for (SchedulerJob * job : jobs) { job->reset(); job->setCompletedCount(0); } // Unconditionally update the capture storage updateCompletedJobsCount(true); // And evaluate all pending jobs per the conditions set in each jobEvaluationOnly = true; evaluateJobs(); } void Scheduler::sortJobsPerAltitude() { // We require a first job to sort, so bail out if list is empty if (jobs.isEmpty()) return; // Don't reset current job // setCurrentJob(nullptr); // Don't reset scheduler jobs startup times before sorting - we need the first job startup time // Sort by startup time, using the first job time as reference for altitude calculations using namespace std::placeholders; QList sortedJobs = jobs; std::stable_sort(sortedJobs.begin() + 1, sortedJobs.end(), std::bind(SchedulerJob::decreasingAltitudeOrder, _1, _2, jobs.first()->getStartupTime())); // If order changed, reset and re-evaluate if (reorderJobs(sortedJobs)) { for (SchedulerJob * job : jobs) job->reset(); jobEvaluationOnly = true; evaluateJobs(); } } void Scheduler::updatePreDawn() { double earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0); int dayOffset = 0; QTime dawn = QTime(0, 0, 0).addSecs(Dawn * 24 * 3600); if (KStarsData::Instance()->lt().time() >= dawn) dayOffset = 1; preDawnDateTime.setDate(KStarsData::Instance()->lt().date().addDays(dayOffset)); preDawnDateTime.setTime(QTime::fromMSecsSinceStartOfDay(earlyDawn * 24 * 3600 * 1000)); } bool Scheduler::isWeatherOK(SchedulerJob *job) { if (weatherStatus == ISD::Weather::WEATHER_OK || weatherCheck->isChecked() == false) return true; else if (weatherStatus == ISD::Weather::WEATHER_IDLE) { if (indiState == INDI_READY) appendLogText(i18n("Weather information is pending...")); return true; } // Temporary BUSY is ALSO accepted for now // TODO Figure out how to exactly handle this if (weatherStatus == ISD::Weather::WEATHER_WARNING) return true; if (weatherStatus == ISD::Weather::WEATHER_ALERT) { job->setState(SchedulerJob::JOB_ABORTED); appendLogText(i18n("Job '%1' suffers from bad weather, marking aborted.", job->getName())); } /*else if (weatherStatus == IPS_BUSY) { appendLogText(i18n("%1 observation job delayed due to bad weather.", job->getName())); schedulerTimer.stop(); connect(this, &Scheduler::weatherChanged, this, &Scheduler::resumeCheckStatus); }*/ return false; } void Scheduler::resumeCheckStatus() { disconnect(this, &Scheduler::weatherChanged, this, &Scheduler::resumeCheckStatus); schedulerTimer.start(); } Scheduler::ErrorHandlingStrategy Scheduler::getErrorHandlingStrategy() { // The UI holds the state if (errorHandlingRestartAfterAllButton->isChecked()) return ERROR_RESTART_AFTER_TERMINATION; else if (errorHandlingRestartImmediatelyButton->isChecked()) return ERROR_RESTART_IMMEDIATELY; else return ERROR_DONT_RESTART; } void Scheduler::setErrorHandlingStrategy(Scheduler::ErrorHandlingStrategy strategy) { errorHandlingDelaySB->setEnabled(strategy != ERROR_DONT_RESTART); switch (strategy) { case ERROR_RESTART_AFTER_TERMINATION: errorHandlingRestartAfterAllButton->setChecked(true); break; case ERROR_RESTART_IMMEDIATELY: errorHandlingRestartImmediatelyButton->setChecked(true); break; default: errorHandlingDontRestartButton->setChecked(true); break; } } void Scheduler::startMosaicTool() { bool raOk = false, decOk = false; dms ra(raBox->createDms(false, &raOk)); //false means expressed in hours dms dec(decBox->createDms(true, &decOk)); if (raOk == false) { appendLogText(i18n("Warning: RA value %1 is invalid.", raBox->text())); return; } if (decOk == false) { appendLogText(i18n("Warning: DEC value %1 is invalid.", decBox->text())); return; } Mosaic mosaicTool; SkyPoint center; center.setRA0(ra); center.setDec0(dec); mosaicTool.setCenter(center); mosaicTool.calculateFOV(); mosaicTool.adjustSize(); if (mosaicTool.exec() == QDialog::Accepted) { // #1 Edit Sequence File ---> Not needed as of 2016-09-12 since Scheduler can send Target Name to Capture module it will append it to root dir // #1.1 Set prefix to Target-Part# // #1.2 Set directory to output/Target-Part# // #2 Save all sequence files in Jobs dir // #3 Set as current Sequence file // #4 Change Target name to Target-Part# // #5 Update J2000 coords // #6 Repeat and save Ekos Scheduler List in the output directory qCDebug(KSTARS_EKOS_SCHEDULER) << "Job accepted with # " << mosaicTool.getJobs().size() << " jobs and fits dir " << mosaicTool.getJobsDir(); QString outputDir = mosaicTool.getJobsDir(); QString targetName = nameEdit->text().simplified(); // Sanitize name targetName = targetName.replace( QRegularExpression("\\s|/|\\(|\\)|:|\\*|~|\"" ), "_" ) // Remove any two or more __ .replace( QRegularExpression("_{2,}"), "_") // Remove any _ at the end .replace( QRegularExpression("_$"), ""); int batchCount = 1; XMLEle *root = getSequenceJobRoot(); if (root == nullptr) return; // Delete any prior jobs before saving if (!jobs.empty()) { - if (KMessageBox::questionYesNo(nullptr, i18n("Do you want to keep the existing jobs in the mosaic schedule?")) == KMessageBox::No) + if (KMessageBox::questionYesNo(nullptr, + i18n("Do you want to keep the existing jobs in the mosaic schedule?")) == KMessageBox::No) { qDeleteAll(jobs); jobs.clear(); while (queueTable->rowCount() > 0) queueTable->removeRow(0); } } // We do not want FITS image for mosaic job since each job has its own calculated center QString fitsFileBackup = fitsEdit->text(); fitsEdit->clear(); foreach (OneTile *oneJob, mosaicTool.getJobs()) { QString prefix = QString("%1-Part%2").arg(targetName).arg(batchCount++); prefix.replace(' ', '-'); nameEdit->setText(prefix); if (createJobSequence(root, prefix, outputDir) == false) return; QString filename = QString("%1/%2.esq").arg(outputDir, prefix); sequenceEdit->setText(filename); sequenceURL = QUrl::fromLocalFile(filename); raBox->showInHours(oneJob->skyCenter.ra0()); decBox->showInDegrees(oneJob->skyCenter.dec0()); saveJob(); } delXMLEle(root); QUrl mosaicURL = QUrl::fromLocalFile((QString("%1/%2_mosaic.esl").arg(outputDir, targetName))); if (saveScheduler(mosaicURL)) { appendLogText(i18n("Mosaic file %1 saved successfully.", mosaicURL.toLocalFile())); } else { appendLogText(i18n("Error saving mosaic file %1. Please reload job.", mosaicURL.toLocalFile())); } fitsEdit->setText(fitsFileBackup); } } XMLEle *Scheduler::getSequenceJobRoot() { QFile sFile; sFile.setFileName(sequenceURL.toLocalFile()); if (!sFile.open(QIODevice::ReadOnly)) { KSNotification::sorry(i18n("Unable to open file %1", sFile.fileName()), i18n("Could Not Open File")); return nullptr; } LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) break; } delLilXML(xmlParser); sFile.close(); return root; } bool Scheduler::createJobSequence(XMLEle *root, const QString &prefix, const QString &outputDir) { QFile sFile; sFile.setFileName(sequenceURL.toLocalFile()); if (!sFile.open(QIODevice::ReadOnly)) { KSNotification::sorry(i18n("Unable to open sequence file %1", sFile.fileName()), i18n("Could Not Open File")); return false; } XMLEle *ep = nullptr; XMLEle *subEP = nullptr; for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Job")) { for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0)) { if (!strcmp(tagXMLEle(subEP), "Prefix")) { XMLEle *rawPrefix = findXMLEle(subEP, "RawPrefix"); if (rawPrefix) { editXMLEle(rawPrefix, prefix.toLatin1().constData()); } } else if (!strcmp(tagXMLEle(subEP), "FITSDirectory")) { editXMLEle(subEP, QString("%1/%2").arg(outputDir, prefix).toLatin1().constData()); } } } } QDir().mkpath(outputDir); QString filename = QString("%1/%2.esq").arg(outputDir, prefix); FILE *outputFile = fopen(filename.toLatin1().constData(), "w"); if (outputFile == nullptr) { QString message = i18n("Unable to write to file %1", filename); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } fprintf(outputFile, ""); prXMLEle(outputFile, root, 0); fclose(outputFile); return true; } void Scheduler::resetAllJobs() { if (state == SCHEDULER_RUNNING) return; // Reset capture count of all jobs before re-evaluating foreach (SchedulerJob *job, jobs) job->setCompletedCount(0); // Evaluate all jobs, this refreshes storage and resets job states startJobEvaluation(); } void Scheduler::checkTwilightWarning(bool enabled) { if (enabled) return; if (KMessageBox::warningContinueCancel( nullptr, i18n("Turning off astronomial twilight check may cause the observatory " "to run during daylight. This can cause irreversible damage to your equipment!"), i18n("Astronomial Twilight Warning"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "astronomical_twilight_warning") == KMessageBox::Cancel) { twilightCheck->setChecked(true); } } void Scheduler::checkStartupProcedure() { if (checkStartupState() == false) QTimer::singleShot(1000, this, SLOT(checkStartupProcedure())); else { if (startupState == STARTUP_COMPLETE) appendLogText(i18n("Manual startup procedure completed successfully.")); else if (startupState == STARTUP_ERROR) appendLogText(i18n("Manual startup procedure terminated due to errors.")); startupB->setIcon( QIcon::fromTheme("media-playback-start")); } } void Scheduler::runStartupProcedure() { if (startupState == STARTUP_IDLE || startupState == STARTUP_ERROR || startupState == STARTUP_COMPLETE) { /* FIXME: Probably issue a warning only, in case the user wants to run the startup script alone */ if (indiState == INDI_IDLE) { KSNotification::sorry(i18n("Cannot run startup procedure while INDI devices are not online.")); return; } if (KMessageBox::questionYesNo( nullptr, i18n("Are you sure you want to execute the startup procedure manually?")) == KMessageBox::Yes) { appendLogText(i18n("Warning: executing startup procedure manually...")); startupB->setIcon( QIcon::fromTheme("media-playback-stop")); startupState = STARTUP_IDLE; checkStartupState(); QTimer::singleShot(1000, this, SLOT(checkStartupProcedure())); } } else { switch (startupState) { case STARTUP_IDLE: break; case STARTUP_SCRIPT: scriptProcess.terminate(); break; case STARTUP_UNPARK_DOME: break; case STARTUP_UNPARKING_DOME: domeInterface->call(QDBus::AutoDetect, "abort"); break; case STARTUP_UNPARK_MOUNT: break; case STARTUP_UNPARKING_MOUNT: mountInterface->call(QDBus::AutoDetect, "abort"); break; case STARTUP_UNPARK_CAP: break; case STARTUP_UNPARKING_CAP: break; case STARTUP_COMPLETE: break; case STARTUP_ERROR: break; } startupState = STARTUP_IDLE; appendLogText(i18n("Startup procedure terminated.")); } } void Scheduler::checkShutdownProcedure() { // If shutdown procedure is not finished yet, let's check again in 1 second. if (checkShutdownState() == false) QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure())); else { if (shutdownState == SHUTDOWN_COMPLETE) { appendLogText(i18n("Manual shutdown procedure completed successfully.")); // Stop Ekos if (Options::stopEkosAfterShutdown()) stopEkos(); } else if (shutdownState == SHUTDOWN_ERROR) appendLogText(i18n("Manual shutdown procedure terminated due to errors.")); shutdownState = SHUTDOWN_IDLE; shutdownB->setIcon( QIcon::fromTheme("media-playback-start")); } } void Scheduler::runShutdownProcedure() { if (shutdownState == SHUTDOWN_IDLE || shutdownState == SHUTDOWN_ERROR || shutdownState == SHUTDOWN_COMPLETE) { if (KMessageBox::questionYesNo( nullptr, i18n("Are you sure you want to execute the shutdown procedure manually?")) == KMessageBox::Yes) { appendLogText(i18n("Warning: executing shutdown procedure manually...")); shutdownB->setIcon( QIcon::fromTheme("media-playback-stop")); shutdownState = SHUTDOWN_IDLE; checkShutdownState(); QTimer::singleShot(1000, this, SLOT(checkShutdownProcedure())); } } else { switch (shutdownState) { case SHUTDOWN_IDLE: break; case SHUTDOWN_SCRIPT: break; case SHUTDOWN_SCRIPT_RUNNING: scriptProcess.terminate(); break; case SHUTDOWN_PARK_DOME: break; case SHUTDOWN_PARKING_DOME: domeInterface->call(QDBus::AutoDetect, "abort"); break; case SHUTDOWN_PARK_MOUNT: break; case SHUTDOWN_PARKING_MOUNT: mountInterface->call(QDBus::AutoDetect, "abort"); break; case SHUTDOWN_PARK_CAP: break; case SHUTDOWN_PARKING_CAP: break; case SHUTDOWN_COMPLETE: break; case SHUTDOWN_ERROR: break; } shutdownState = SHUTDOWN_IDLE; appendLogText(i18n("Shutdown procedure terminated.")); } } void Scheduler::loadProfiles() { QString currentProfile = schedulerProfileCombo->currentText(); QDBusReply profiles = ekosInterface->call(QDBus::AutoDetect, "getProfiles"); if (profiles.error().type() == QDBusError::NoError) { schedulerProfileCombo->blockSignals(true); schedulerProfileCombo->clear(); schedulerProfileCombo->addItem(i18n("Default")); schedulerProfileCombo->addItems(profiles); schedulerProfileCombo->setCurrentText(currentProfile); schedulerProfileCombo->blockSignals(false); } } bool Scheduler::loadSequenceQueue(const QString &fileURL, SchedulerJob *schedJob, QList &jobs, bool &hasAutoFocus) { QFile sFile; sFile.setFileName(fileURL); if (!sFile.open(QIODevice::ReadOnly)) { QString message = i18n("Unable to open sequence queue file '%1'", fileURL); KSNotification::sorry(message, i18n("Could Not Open File")); return false; } LilXML *xmlParser = newLilXML(); char errmsg[MAXRBUF]; XMLEle *root = nullptr; XMLEle *ep = nullptr; char c; while (sFile.getChar(&c)) { root = readXMLEle(xmlParser, c, errmsg); if (root) { for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Autofocus")) hasAutoFocus = (!strcmp(findXMLAttValu(ep, "enabled"), "true")); else if (!strcmp(tagXMLEle(ep), "Job")) jobs.append(processJobInfo(ep, schedJob)); } delXMLEle(root); } else if (errmsg[0]) { appendLogText(QString(errmsg)); delLilXML(xmlParser); qDeleteAll(jobs); return false; } } return true; } SequenceJob *Scheduler::processJobInfo(XMLEle *root, SchedulerJob *schedJob) { XMLEle *ep = nullptr; XMLEle *subEP = nullptr; const QMap frameTypes = { { "Light", FRAME_LIGHT }, { "Dark", FRAME_DARK }, { "Bias", FRAME_BIAS }, { "Flat", FRAME_FLAT } }; SequenceJob *job = new SequenceJob(); QString rawPrefix, frameType, filterType; double exposure = 0; bool filterEnabled = false, expEnabled = false, tsEnabled = false; /* Reset light frame presence flag before enumerating */ // JM 2018-09-14: If last sequence job is not LIGHT // then scheduler job light frame is set to whatever last sequence job is // so if it was non-LIGHT, this value is set to false which is wrong. //if (nullptr != schedJob) // schedJob->setLightFramesRequired(false); for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0)) { if (!strcmp(tagXMLEle(ep), "Exposure")) { exposure = atof(pcdataXMLEle(ep)); job->setExposure(exposure); } else if (!strcmp(tagXMLEle(ep), "Filter")) { filterType = QString(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "Type")) { frameType = QString(pcdataXMLEle(ep)); /* Record frame type and mark presence of light frames for this sequence */ CCDFrameType const frameEnum = frameTypes[frameType]; job->setFrameType(frameEnum); if (FRAME_LIGHT == frameEnum && nullptr != schedJob) schedJob->setLightFramesRequired(true); } else if (!strcmp(tagXMLEle(ep), "Prefix")) { subEP = findXMLEle(ep, "RawPrefix"); if (subEP) rawPrefix = QString(pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "FilterEnabled"); if (subEP) filterEnabled = !strcmp("1", pcdataXMLEle(subEP)); subEP = findXMLEle(ep, "ExpEnabled"); if (subEP) expEnabled = (!strcmp("1", pcdataXMLEle(subEP))); subEP = findXMLEle(ep, "TimeStampEnabled"); if (subEP) tsEnabled = (!strcmp("1", pcdataXMLEle(subEP))); job->setPrefixSettings(rawPrefix, filterEnabled, expEnabled, tsEnabled); } else if (!strcmp(tagXMLEle(ep), "Count")) { job->setCount(atoi(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "Delay")) { job->setDelay(atoi(pcdataXMLEle(ep))); } else if (!strcmp(tagXMLEle(ep), "FITSDirectory")) { job->setLocalDir(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "RemoteDirectory")) { job->setRemoteDir(pcdataXMLEle(ep)); } else if (!strcmp(tagXMLEle(ep), "UploadMode")) { job->setUploadMode(static_cast(atoi(pcdataXMLEle(ep)))); } } // Sanitize name QString targetName = schedJob->getName(); targetName = targetName.replace( QRegularExpression("\\s|/|\\(|\\)|:|\\*|~|\"" ), "_" ) // Remove any two or more __ .replace( QRegularExpression("_{2,}"), "_") // Remove any _ at the end .replace( QRegularExpression("_$"), ""); // Because scheduler sets the target name in capture module // it would be the same as the raw prefix if (targetName.isEmpty() == false && rawPrefix.isEmpty()) rawPrefix = targetName; // Make full prefix QString imagePrefix = rawPrefix; if (imagePrefix.isEmpty() == false) imagePrefix += '_'; imagePrefix += frameType; if (filterEnabled && filterType.isEmpty() == false && (job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT)) { imagePrefix += '_'; imagePrefix += filterType; } if (expEnabled) { imagePrefix += '_'; if (exposure == static_cast(exposure)) // Whole number imagePrefix += QString::number(exposure, 'd', 0) + QString("_secs"); else { // Decimal if (exposure >= 0.001) imagePrefix += QString::number(exposure, 'f', 3) + QString("_secs"); else imagePrefix += QString::number(exposure, 'f', 6) + QString("_secs"); } } job->setFullPrefix(imagePrefix); // Directory postfix QString directoryPostfix; /* FIXME: Refactor directoryPostfix assignment, whose code is duplicated in capture.cpp */ if (targetName.isEmpty()) directoryPostfix = QLatin1String("/") + frameType; else directoryPostfix = QLatin1String("/") + targetName + QLatin1String("/") + frameType; if ((job->getFrameType() == FRAME_LIGHT || job->getFrameType() == FRAME_FLAT) && filterType.isEmpty() == false) directoryPostfix += QLatin1String("/") + filterType; job->setDirectoryPostfix(directoryPostfix); return job; } int Scheduler::getCompletedFiles(const QString &path, const QString &seqPrefix) { int seqFileCount = 0; QFileInfo const path_info(path); QString const sig_dir(path_info.dir().path()); QString const sig_file(path_info.completeBaseName()); - qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Searching in path '%1', files '%2*' for prefix '%3'...").arg(sig_dir, sig_file, seqPrefix); + qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Searching in path '%1', files '%2*' for prefix '%3'...").arg(sig_dir, sig_file, + seqPrefix); QDirIterator it(sig_dir, QDir::Files); /* FIXME: this counts all files with prefix in the storage location, not just captures. DSS analysis files are counted in, for instance. */ while (it.hasNext()) { QString const fileName = QFileInfo(it.next()).completeBaseName(); if (fileName.startsWith(seqPrefix)) { qCDebug(KSTARS_EKOS_SCHEDULER) << QString("> Found '%1'").arg(fileName); seqFileCount++; } } return seqFileCount; } void Scheduler::setINDICommunicationStatus(Ekos::CommunicationStatus status) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler INDI status is" << status; m_INDICommunicationStatus = status; } void Scheduler::setEkosCommunicationStatus(Ekos::CommunicationStatus status) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Scheduler Ekos status is" << status; m_EkosCommunicationStatus = status; } void Scheduler::simClockScaleChanged(float newScale) { if (sleepTimer.isActive()) { QTime const remainingTimeMs = QTime::fromMSecsSinceStartOfDay(std::lround(static_cast(sleepTimer.remainingTime()) * KStarsData::Instance()->clock()->scale() / newScale)); appendLogText(i18n("Sleeping for %1 on simulation clock update until next observation job is ready...", remainingTimeMs.toString("hh:mm:ss"))); sleepTimer.stop(); sleepTimer.start(remainingTimeMs.msecsSinceStartOfDay()); } } void Scheduler::registerNewModule(const QString &name) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Registering new Module (" << name << ")"; if (name == "Focus") { delete focusInterface; focusInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Focus", "org.kde.kstars.Ekos.Focus", QDBusConnection::sessionBus(), this); - connect(focusInterface, SIGNAL(newStatus(Ekos::FocusState)), this, SLOT(setFocusStatus(Ekos::FocusState)), Qt::UniqueConnection); + connect(focusInterface, SIGNAL(newStatus(Ekos::FocusState)), this, SLOT(setFocusStatus(Ekos::FocusState)), + Qt::UniqueConnection); } else if (name == "Capture") { delete captureInterface; captureInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Capture", "org.kde.kstars.Ekos.Capture", QDBusConnection::sessionBus(), this); connect(captureInterface, SIGNAL(ready()), this, SLOT(syncProperties())); - connect(captureInterface, SIGNAL(newStatus(Ekos::CaptureState)), this, SLOT(setCaptureStatus(Ekos::CaptureState)), Qt::UniqueConnection); + connect(captureInterface, SIGNAL(newStatus(Ekos::CaptureState)), this, SLOT(setCaptureStatus(Ekos::CaptureState)), + Qt::UniqueConnection); } else if (name == "Mount") { delete mountInterface; mountInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Mount", "org.kde.kstars.Ekos.Mount", QDBusConnection::sessionBus(), this); connect(mountInterface, SIGNAL(ready()), this, SLOT(syncProperties())); - connect(mountInterface, SIGNAL(newStatus(ISD::Telescope::Status)), this, SLOT(setMountStatus(ISD::Telescope::Status)), Qt::UniqueConnection); + connect(mountInterface, SIGNAL(newStatus(ISD::Telescope::Status)), this, SLOT(setMountStatus(ISD::Telescope::Status)), + Qt::UniqueConnection); } else if (name == "Align") { delete alignInterface; alignInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Align", "org.kde.kstars.Ekos.Align", QDBusConnection::sessionBus(), this); - connect(alignInterface, SIGNAL(newStatus(Ekos::AlignState)), this, SLOT(setAlignStatus(Ekos::AlignState)), Qt::UniqueConnection); + connect(alignInterface, SIGNAL(newStatus(Ekos::AlignState)), this, SLOT(setAlignStatus(Ekos::AlignState)), + Qt::UniqueConnection); } else if (name == "Guide") { delete guideInterface; guideInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Guide", "org.kde.kstars.Ekos.Guide", QDBusConnection::sessionBus(), this); - connect(guideInterface, SIGNAL(newStatus(Ekos::GuideState)), this, SLOT(setGuideStatus(Ekos::GuideState)), Qt::UniqueConnection); + connect(guideInterface, SIGNAL(newStatus(Ekos::GuideState)), this, SLOT(setGuideStatus(Ekos::GuideState)), + Qt::UniqueConnection); } else if (name == "Dome") { delete domeInterface; domeInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Dome", "org.kde.kstars.Ekos.Dome", QDBusConnection::sessionBus(), this); connect(domeInterface, SIGNAL(ready()), this, SLOT(syncProperties())); } else if (name == "Weather") { delete weatherInterface; weatherInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/Weather", "org.kde.kstars.Ekos.Weather", QDBusConnection::sessionBus(), this); connect(weatherInterface, SIGNAL(ready()), this, SLOT(syncProperties())); connect(weatherInterface, SIGNAL(newStatus(ISD::Weather::Status)), this, SLOT(setWeatherStatus(ISD::Weather::Status))); } else if (name == "DustCap") { delete capInterface; capInterface = new QDBusInterface("org.kde.kstars", "/KStars/Ekos/DustCap", "org.kde.kstars.Ekos.DustCap", QDBusConnection::sessionBus(), this); connect(capInterface, SIGNAL(ready()), this, SLOT(syncProperties()), Qt::UniqueConnection); } } void Scheduler::syncProperties() { QDBusInterface *iface = qobject_cast(sender()); if (iface == mountInterface) { QVariant canMountPark = mountInterface->property("canPark"); unparkMountCheck->setEnabled(canMountPark.toBool()); parkMountCheck->setEnabled(canMountPark.toBool()); m_MountReady = true; } else if (iface == capInterface) { QVariant canCapPark = capInterface->property("canPark"); if (canCapPark.isValid()) { capCheck->setEnabled(canCapPark.toBool()); uncapCheck->setEnabled(canCapPark.toBool()); m_CapReady = true; } else { capCheck->setEnabled(false); uncapCheck->setEnabled(false); } } else if (iface == weatherInterface) { QVariant updatePeriod = weatherInterface->property("updatePeriod"); if (updatePeriod.isValid()) { weatherCheck->setEnabled(true); QVariant status = weatherInterface->property("status"); setWeatherStatus(static_cast(status.toInt())); // if (updatePeriod.toInt() > 0) // { // weatherTimer.setInterval(updatePeriod.toInt() * 1000); // connect(&weatherTimer, &QTimer::timeout, this, &Scheduler::checkWeather, Qt::UniqueConnection); // weatherTimer.start(); // // Check weather initially // checkWeather(); // } } else weatherCheck->setEnabled(true); } else if (iface == domeInterface) { QVariant canDomePark = domeInterface->property("canPark"); unparkDomeCheck->setEnabled(canDomePark.toBool()); parkDomeCheck->setEnabled(canDomePark.toBool()); m_DomeReady = true; } else if (iface == captureInterface) { QVariant hasCoolerControl = captureInterface->property("coolerControl"); warmCCDCheck->setEnabled(hasCoolerControl.toBool()); m_CaptureReady = true; } } void Scheduler::setAlignStatus(Ekos::AlignState status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Align State" << Ekos::getAlignStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_ALIGNING) { // Is solver complete? if (status == Ekos::ALIGN_COMPLETE) { appendLogText(i18n("Job '%1' alignment is complete.", currentJob->getName())); alignFailureCount = 0; currentJob->setStage(SchedulerJob::STAGE_ALIGN_COMPLETE); getNextAction(); } else if (status == Ekos::ALIGN_FAILED || status == Ekos::ALIGN_ABORTED) { appendLogText(i18n("Warning: job '%1' alignment failed.", currentJob->getName())); if (alignFailureCount++ < MAX_FAILURE_ATTEMPTS) { if (Options::resetMountModelOnAlignFail() && MAX_FAILURE_ATTEMPTS - 1 < alignFailureCount) { - appendLogText(i18n("Warning: job '%1' forcing mount model reset after failing alignment #%2.", currentJob->getName(), alignFailureCount)); + appendLogText(i18n("Warning: job '%1' forcing mount model reset after failing alignment #%2.", currentJob->getName(), + alignFailureCount)); mountInterface->call(QDBus::AutoDetect, "resetModel"); } appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName())); startAstrometry(); } else { appendLogText(i18n("Warning: job '%1' alignment procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } } void Scheduler::setGuideStatus(Ekos::GuideState status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Guide State" << Ekos::getGuideStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_GUIDING) { qCDebug(KSTARS_EKOS_SCHEDULER) << "Calibration & Guide stage..."; // If calibration stage complete? if (status == Ekos::GUIDE_GUIDING) { appendLogText(i18n("Job '%1' guiding is in progress.", currentJob->getName())); guideFailureCount = 0; // if guiding recovered while we are waiting, abort the restart restartGuidingTimer.stop(); currentJob->setStage(SchedulerJob::STAGE_GUIDING_COMPLETE); getNextAction(); } else if (status == Ekos::GUIDE_CALIBRATION_ERROR || status == Ekos::GUIDE_ABORTED) { if (status == Ekos::GUIDE_ABORTED) appendLogText(i18n("Warning: job '%1' guiding failed.", currentJob->getName())); else appendLogText(i18n("Warning: job '%1' calibration failed.", currentJob->getName())); // if the timer for restarting the guiding is already running, we do nothing and // wait for the action triggered by the timer. This way we avoid that a small guiding problem // abort the scheduler job if (restartGuidingTimer.isActive()) return; if (guideFailureCount++ < MAX_FAILURE_ATTEMPTS) { if (status == Ekos::GUIDE_CALIBRATION_ERROR && Options::realignAfterCalibrationFailure()) { appendLogText(i18n("Restarting %1 alignment procedure...", currentJob->getName())); // JM: We have to go back to startSlew() since if we just call startAstrometry() // It would captureAndSolve at the _current_ coords which could be way off center if the calibration // process took a wild ride search for a suitable guide star and then failed. So startSlew() would ensure // we're back on our target and then it proceed to alignment (focus is skipped since it is done if it was checked anyway). startSlew(); } else { - appendLogText(i18n("Job '%1' is guiding, guiding procedure will be restarted in %2 seconds.", currentJob->getName(), (RESTART_GUIDING_DELAY_MS * guideFailureCount) / 1000)); + appendLogText(i18n("Job '%1' is guiding, guiding procedure will be restarted in %2 seconds.", currentJob->getName(), + (RESTART_GUIDING_DELAY_MS * guideFailureCount) / 1000)); restartGuidingTimer.start(RESTART_GUIDING_DELAY_MS * guideFailureCount); } } else { appendLogText(i18n("Warning: job '%1' guiding procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } } GuideState Scheduler::getGuidingStatus() { QVariant guideStatus = guideInterface->property("status"); Ekos::GuideState gStatus = static_cast(guideStatus.toInt()); return gStatus; } void Scheduler::setCaptureStatus(Ekos::CaptureState status) { if (currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Capture State" << Ekos::getCaptureStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_CAPTURING) { if (status == Ekos::CAPTURE_ABORTED) { appendLogText(i18n("Warning: job '%1' failed to capture target.", currentJob->getName())); if (captureFailureCount++ < MAX_FAILURE_ATTEMPTS) { // If capture failed due to guiding error, let's try to restart that if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE) { // Check if it is guiding related. Ekos::GuideState gStatus = getGuidingStatus(); if (gStatus == Ekos::GUIDE_ABORTED || gStatus == Ekos::GUIDE_CALIBRATION_ERROR || gStatus == GUIDE_DITHERING_ERROR) { - appendLogText(i18n("Job '%1' is capturing, is restarting its guiding procedure (attempt #%2 of %3).", currentJob->getName(), captureFailureCount, MAX_FAILURE_ATTEMPTS)); + appendLogText(i18n("Job '%1' is capturing, is restarting its guiding procedure (attempt #%2 of %3).", currentJob->getName(), + captureFailureCount, MAX_FAILURE_ATTEMPTS)); startGuiding(true); return; } } /* FIXME: it's not clear whether it is actually possible to continue capturing when capture fails this way */ appendLogText(i18n("Warning: job '%1' failed its capture procedure, restarting capture.", currentJob->getName())); startCapture(true); } else { /* FIXME: it's not clear whether this situation can be recovered at all */ appendLogText(i18n("Warning: job '%1' failed its capture procedure, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } else if (status == Ekos::CAPTURE_COMPLETE) { KNotification::event(QLatin1String("EkosScheduledImagingFinished"), i18n("Ekos job (%1) - Capture finished", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_COMPLETE); findNextJob(); } else if (status == Ekos::CAPTURE_IMAGE_RECEIVED) { // We received a new image, but we don't know precisely where so update the storage map and re-estimate job times. // FIXME: rework this once capture storage is reworked if (Options::rememberJobProgress()) { updateCompletedJobsCount(true); for (SchedulerJob * job : jobs) estimateJobTime(job); } // Else if we don't remember the progress on jobs, increase the completed count for the current job only - no cross-checks else currentJob->setCompletedCount(currentJob->getCompletedCount() + 1); captureFailureCount = 0; } } } void Scheduler::setFocusStatus(Ekos::FocusState status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Focus State" << Ekos::getFocusStatusString(status); /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) { QDateTime const now = KStarsData::Instance()->lt(); if (now < currentJob->getStartupTime()) return; } if (currentJob->getStage() == SchedulerJob::STAGE_FOCUSING) { // Is focus complete? if (status == Ekos::FOCUS_COMPLETE) { appendLogText(i18n("Job '%1' focusing is complete.", currentJob->getName())); autofocusCompleted = true; currentJob->setStage(SchedulerJob::STAGE_FOCUS_COMPLETE); getNextAction(); } else if (status == Ekos::FOCUS_FAILED || status == Ekos::FOCUS_ABORTED) { appendLogText(i18n("Warning: job '%1' focusing failed.", currentJob->getName())); if (focusFailureCount++ < MAX_FAILURE_ATTEMPTS) { appendLogText(i18n("Job '%1' is restarting its focusing procedure.", currentJob->getName())); // Reset frame to original size. focusInterface->call(QDBus::AutoDetect, "resetFrame"); // Restart focusing qCDebug(KSTARS_EKOS_SCHEDULER) << "startFocusing on 6883"; startFocusing(); } else { appendLogText(i18n("Warning: job '%1' focusing procedure failed, marking aborted.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ABORTED); findNextJob(); } } } } void Scheduler::setMountStatus(ISD::Telescope::Status status) { if (state == SCHEDULER_PAUSED || currentJob == nullptr) return; qCDebug(KSTARS_EKOS_SCHEDULER) << "Mount State changed to" << status; /* If current job is scheduled and has not started yet, wait */ if (SchedulerJob::JOB_SCHEDULED == currentJob->getState()) if (static_cast(KStarsData::Instance()->lt()) < currentJob->getStartupTime()) return; switch (currentJob->getStage()) { case SchedulerJob::STAGE_SLEWING: { qCDebug(KSTARS_EKOS_SCHEDULER) << "Slewing stage..."; if (status == ISD::Telescope::MOUNT_TRACKING) { appendLogText(i18n("Job '%1' slew is complete.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_SLEW_COMPLETE); /* getNextAction is deferred to checkJobStage for dome support */ } else if (status == ISD::Telescope::MOUNT_ERROR) { appendLogText(i18n("Warning: job '%1' slew failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } else if (status == ISD::Telescope::MOUNT_IDLE) { appendLogText(i18n("Warning: job '%1' found not slewing, restarting.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_IDLE); getNextAction(); } } break; case SchedulerJob::STAGE_RESLEWING: { qCDebug(KSTARS_EKOS_SCHEDULER) << "Re-slewing stage..."; if (status == ISD::Telescope::MOUNT_TRACKING) { appendLogText(i18n("Job '%1' repositioning is complete.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_RESLEWING_COMPLETE); /* getNextAction is deferred to checkJobStage for dome support */ } else if (status == ISD::Telescope::MOUNT_ERROR) { appendLogText(i18n("Warning: job '%1' repositioning failed, marking terminated due to errors.", currentJob->getName())); currentJob->setState(SchedulerJob::JOB_ERROR); findNextJob(); } else if (status == ISD::Telescope::MOUNT_IDLE) { appendLogText(i18n("Warning: job '%1' found not repositioning, restarting.", currentJob->getName())); currentJob->setStage(SchedulerJob::STAGE_IDLE); getNextAction(); } } break; default: break; } } void Scheduler::setWeatherStatus(ISD::Weather::Status status) { ISD::Weather::Status newStatus = status; QString statusString; switch (newStatus) { case ISD::Weather::WEATHER_OK: statusString = i18n("Weather conditions are OK."); break; case ISD::Weather::WEATHER_WARNING: statusString = i18n("Warning: weather conditions are in the WARNING zone."); break; case ISD::Weather::WEATHER_ALERT: statusString = i18n("Caution: weather conditions are in the DANGER zone!"); break; default: break; } if (newStatus != weatherStatus) { weatherStatus = newStatus; qCDebug(KSTARS_EKOS_SCHEDULER) << statusString; if (weatherStatus == ISD::Weather::WEATHER_OK) weatherLabel->setPixmap( QIcon::fromTheme("security-high") .pixmap(QSize(32, 32))); else if (weatherStatus == ISD::Weather::WEATHER_WARNING) { weatherLabel->setPixmap( QIcon::fromTheme("security-medium") .pixmap(QSize(32, 32))); KNotification::event(QLatin1String("WeatherWarning"), i18n("Weather conditions in warning zone")); } else if (weatherStatus == ISD::Weather::WEATHER_ALERT) { weatherLabel->setPixmap( QIcon::fromTheme("security-low") .pixmap(QSize(32, 32))); KNotification::event(QLatin1String("WeatherAlert"), i18n("Weather conditions are critical. Observatory shutdown is imminent")); } else weatherLabel->setPixmap(QIcon::fromTheme("chronometer") .pixmap(QSize(32, 32))); weatherLabel->show(); weatherLabel->setToolTip(statusString); appendLogText(statusString); emit weatherChanged(weatherStatus); } // Shutdown scheduler if it was started and not already in shutdown if (weatherStatus == ISD::Weather::WEATHER_ALERT && state != Ekos::SCHEDULER_IDLE && state != Ekos::SCHEDULER_SHUTDOWN) { appendLogText(i18n("Starting shutdown procedure due to severe weather.")); if (currentJob) { currentJob->setState(SchedulerJob::JOB_ABORTED); stopCurrentJobAction(); jobTimer.stop(); } checkShutdownState(); //connect(KStars::Instance()->data()->clock(), SIGNAL(timeAdvanced()), this, SLOT(checkStatus()), &Scheduler::Qt::UniqueConnection); } } bool Scheduler::shouldSchedulerSleep(SchedulerJob *currentJob) { - Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, "There must be a valid current job for Scheduler to test sleep requirement"); + Q_ASSERT_X(nullptr != currentJob, __FUNCTION__, + "There must be a valid current job for Scheduler to test sleep requirement"); if (currentJob->getLightFramesRequired() == false) return false; QDateTime const now = KStarsData::Instance()->lt(); int const nextObservationTime = now.secsTo(currentJob->getStartupTime()); // If start up procedure is complete and the user selected pre-emptive shutdown, let us check if the next observation time exceed // the pre-emptive shutdown time in hours (default 2). If it exceeds that, we perform complete shutdown until next job is ready if (startupState == STARTUP_COMPLETE && Options::preemptiveShutdown() && nextObservationTime > (Options::preemptiveShutdownTime() * 3600)) { appendLogText(i18n( "Job '%1' scheduled for execution at %2. " "Observatory scheduled for shutdown until next job is ready.", currentJob->getName(), currentJob->getStartupTime().toString(currentJob->getDateTimeDisplayFormat()))); preemptiveShutdown = true; weatherCheck->setEnabled(false); weatherLabel->hide(); checkShutdownState(); //schedulerTimer.stop(); // Wake up when job is due. // FIXME: Implement waking up periodically before job is due for weather check. // int const nextWakeup = nextObservationTime < 60 ? nextObservationTime : 60; sleepTimer.setInterval(std::lround(((nextObservationTime + 1) * 1000) / KStarsData::Instance()->clock()->scale())); sleepTimer.start(); return true; } // Otherwise, sleep until job is ready /* FIXME: if not parking, stop tracking maybe? this would prevent crashes or scheduler stops from leaving the mount to track and bump the pier */ // If start up procedure is already complete, and we didn't issue any parking commands before and parking is checked and enabled // Then we park the mount until next job is ready. But only if the job uses TRACK as its first step, otherwise we cannot get into position again. // This is also only performed if next job is due more than the default lead time (5 minutes). // If job is due sooner than that is not worth parking and we simply go into sleep or wait modes. else if (nextObservationTime > Options::leadTime() * 60 && startupState == STARTUP_COMPLETE && parkWaitState == PARKWAIT_IDLE && (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) && parkMountCheck->isEnabled() && parkMountCheck->isChecked()) { appendLogText(i18n( "Job '%1' scheduled for execution at %2. " "Parking the mount until the job is ready.", currentJob->getName(), currentJob->getStartupTime().toString())); parkWaitState = PARKWAIT_PARK; return false; } else if (nextObservationTime > Options::leadTime() * 60) { appendLogText(i18n("Sleeping until observation job %1 is ready at %2...", currentJob->getName(), now.addSecs(nextObservationTime + 1).toString())); sleepLabel->setToolTip(i18n("Scheduler is in sleep mode")); sleepLabel->show(); // Warn the user if the next job is really far away - 60/5 = 12 times the lead time if (nextObservationTime > Options::leadTime() * 60 * 12 && !Options::preemptiveShutdown()) { dms delay(static_cast(nextObservationTime * 15.0 / 3600.0)); appendLogText(i18n( "Warning: Job '%1' is %2 away from now, you may want to enable Preemptive Shutdown.", currentJob->getName(), delay.toHMSString())); } /* FIXME: stop tracking now */ schedulerTimer.stop(); // Wake up when job is due. // FIXME: Implement waking up periodically before job is due for weather check. // int const nextWakeup = nextObservationTime < 60 ? nextObservationTime : 60; sleepTimer.setInterval(std::lround(((nextObservationTime + 1) * 1000) / KStarsData::Instance()->clock()->scale())); sleepTimer.start(); return true; } return false; } } diff --git a/kstars/ekos/scheduler/scheduler.h b/kstars/ekos/scheduler/scheduler.h index 85dc651b2..6bd34646e 100644 --- a/kstars/ekos/scheduler/scheduler.h +++ b/kstars/ekos/scheduler/scheduler.h @@ -1,800 +1,800 @@ /* Ekos Scheduler Module Copyright (C) 2015 Jasem Mutlaq DBus calls from GSoC 2015 Ekos Scheduler project by Daniel Leu This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "ui_scheduler.h" #include "ekos/align/align.h" #include "indi/indiweather.h" #include #include #include #include #include #include #include class QProgressIndicator; class GeoLocation; class SchedulerJob; class SkyObject; class KConfigDialog; namespace Ekos { class SequenceJob; /** * @brief The Ekos scheduler is a simple scheduler class to orchestrate automated multi object observation jobs. * @author Jasem Mutlaq * @version 1.2 */ class Scheduler : public QWidget, public Ui::Scheduler { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kstars.Ekos.Scheduler") Q_PROPERTY(Ekos::SchedulerState status READ status NOTIFY newStatus) Q_PROPERTY(QStringList logText READ logText NOTIFY newLog) Q_PROPERTY(QString profile READ profile WRITE setProfile) public: typedef enum { EKOS_IDLE, EKOS_STARTING, EKOS_STOPPING, EKOS_READY } EkosState; typedef enum { INDI_IDLE, INDI_CONNECTING, INDI_DISCONNECTING, INDI_PROPERTY_CHECK, INDI_READY } INDIState; typedef enum { STARTUP_IDLE, STARTUP_SCRIPT, STARTUP_UNPARK_DOME, STARTUP_UNPARKING_DOME, STARTUP_UNPARK_MOUNT, STARTUP_UNPARKING_MOUNT, STARTUP_UNPARK_CAP, STARTUP_UNPARKING_CAP, STARTUP_ERROR, STARTUP_COMPLETE } StartupState; typedef enum { SHUTDOWN_IDLE, SHUTDOWN_PARK_CAP, SHUTDOWN_PARKING_CAP, SHUTDOWN_PARK_MOUNT, SHUTDOWN_PARKING_MOUNT, SHUTDOWN_PARK_DOME, SHUTDOWN_PARKING_DOME, SHUTDOWN_SCRIPT, SHUTDOWN_SCRIPT_RUNNING, SHUTDOWN_ERROR, SHUTDOWN_COMPLETE } ShutdownState; typedef enum { PARKWAIT_IDLE, PARKWAIT_PARK, PARKWAIT_PARKING, PARKWAIT_PARKED, PARKWAIT_UNPARK, PARKWAIT_UNPARKING, PARKWAIT_UNPARKED, PARKWAIT_ERROR } ParkWaitStatus; /** @brief options what should happen if an error or abort occurs */ typedef enum { ERROR_DONT_RESTART, ERROR_RESTART_AFTER_TERMINATION, ERROR_RESTART_IMMEDIATELY } ErrorHandlingStrategy; /** @brief Columns, in the same order as UI. */ typedef enum { SCHEDCOL_NAME = 0, SCHEDCOL_STATUS, SCHEDCOL_CAPTURES, SCHEDCOL_ALTITUDE, SCHEDCOL_SCORE, SCHEDCOL_STARTTIME, SCHEDCOL_ENDTIME, SCHEDCOL_DURATION, SCHEDCOL_LEADTIME, SCHEDCOL_COUNT } SchedulerColumns; Scheduler(); ~Scheduler() = default; QString getCurrentJobName(); void appendLogText(const QString &); QStringList logText() { return m_LogText; } QString getLogText() { return m_LogText.join("\n"); } void clearLog(); void applyConfig(); void addObject(SkyObject *object); /** * @brief startSlew DBus call for initiating slew */ void startSlew(); /** * @brief startFocusing DBus call for feeding ekos the specified settings and initiating focus operation */ void startFocusing(); /** * @brief startAstrometry initiation of the capture and solve operation. We change the job state * after solver is started */ void startAstrometry(); /** * @brief startGuiding After ekos is fed the calibration options, we start the guiding process * @param resetCalibration By default calibration is not reset until it is explicitly requested */ void startGuiding(bool resetCalibration = false); /** * @brief startCapture The current job file name is solved to an url which is fed to ekos. We then start the capture process * @param restart Set to true if the goal to restart an existing sequence. The only difference is that when a sequence is restarted, sequence file * is not loaded from disk again since that results in erasing all the history of the capture process. */ void startCapture(bool restart = false); /** * @brief getNextAction Checking for the next appropriate action regarding the current state of the scheduler and execute it */ void getNextAction(); /** * @brief disconnectINDI disconnect all INDI devices from server. */ void disconnectINDI(); /** * @brief stopEkos shutdown Ekos completely */ void stopEkos(); /** * @brief stopGuiding After guiding is done we need to stop the process */ void stopGuiding(); /** * @brief setSolverAction set the GOTO mode for the solver * @param mode 0 For Sync, 1 for SlewToTarget, 2 for Nothing */ void setSolverAction(Align::GotoMode mode); /** @defgroup SchedulerDBusInterface Ekos DBus Interface - Scheduler Module * Ekos::Align interface provides primary functions to run and stop the scheduler. */ /*@{*/ /** DBUS interface function. * @brief Start the scheduler main loop and evaluate jobs and execute them accordingly. */ Q_SCRIPTABLE Q_NOREPLY void start(); /** DBUS interface function. * @brief Stop the scheduler. */ Q_SCRIPTABLE Q_NOREPLY void stop(); /** DBUS interface function. * @brief Loads the Ekos Scheduler List (.esl) file. * @param fileURL path to a file * @return true if loading file is successful, false otherwise. */ Q_SCRIPTABLE bool loadScheduler(const QString &fileURL); /** DBUS interface function. * @brief Resets all jobs to IDLE */ Q_SCRIPTABLE void resetAllJobs(); /** DBUS interface function. * @brief Resets all jobs to IDLE */ Q_SCRIPTABLE void sortJobsPerAltitude(); Ekos::SchedulerState status() { return state; } void setProfile(const QString &profile) { schedulerProfileCombo->setCurrentText(profile); } QString profile() { return schedulerProfileCombo->currentText(); } /** * @brief retrieve the error handling strategy from the UI */ ErrorHandlingStrategy getErrorHandlingStrategy(); /** * @brief select the error handling strategy (no restart, restart after all terminated, restart immediately) */ void setErrorHandlingStrategy (ErrorHandlingStrategy strategy); /** @}*/ /** @{ */ private: /** @internal Safeguard flag to avoid registering signals from widgets multiple times. */ bool jobChangesAreWatched { false }; protected: /** @internal Enables signal watch on SchedulerJob form values in order to apply changes to current job. * @param enable is the toggle flag, true to watch for changes, false to ignore them. */ void watchJobChanges(bool enable); /** @internal Marks the currently selected SchedulerJob as modified change. * * This triggers job re-evaluation. * Next time save button is invoked, the complete content is written to disk. */ void setDirty(); /** @} */ protected: /** @internal Associate job table cells on a row to the corresponding SchedulerJob. * @param row is an integer indexing the row to associate cells from, and also the index of the job in the job list.. */ void setJobStatusCells(int row); protected slots: /** * @brief registerNewModule Register an Ekos module as it arrives via DBus * and create the appropriate DBus interface to communicate with it. * @param name of module */ void registerNewModule(const QString &name); /** * @brief syncProperties Sync startup properties from the various device to enable/disable features in the scheduler * like the ability to park/unpark..etc */ void syncProperties(); void setAlignStatus(Ekos::AlignState status); void setGuideStatus(Ekos::GuideState status); void setCaptureStatus(Ekos::CaptureState status); void setFocusStatus(Ekos::FocusState status); void setMountStatus(ISD::Telescope::Status status); void setWeatherStatus(ISD::Weather::Status status); /** * @brief select object from KStars's find dialog. */ void selectObject(); /** * @brief Selects FITS file for solving. */ void selectFITS(); /** * @brief Selects sequence queue. */ void selectSequence(); /** * @brief Selects sequence queue. */ void selectStartupScript(); /** * @brief Selects sequence queue. */ void selectShutdownScript(); /** * @brief addToQueue Construct a SchedulerJob and add it to the queue or save job settings from current form values. * jobUnderEdit determines whether to add or edit */ void saveJob(); /** * @brief addJob Add a new job from form values */ void addJob(); /** * @brief editJob Edit an observation job * @param i index model in queue table */ void loadJob(QModelIndex i); /** * @brief removeJob Remove a job from the currently selected row. If no row is selected, it remove the last job in the queue. */ void removeJob(); /** * @brief setJobAddApply Set first button state to add new job or apply changes. */ void setJobAddApply(bool add_mode); /** * @brief setJobManipulation Enable or disable job manipulation buttons. */ void setJobManipulation(bool can_reorder, bool can_delete); /** * @brief set all GUI fields to the values of the given scheduler job */ void syncGUIToJob(SchedulerJob *job); /** * @brief jobSelectionChanged Update UI state when the job list is clicked once. */ void clickQueueTable(QModelIndex index); /** * @brief Update scheduler parameters to the currently selected scheduler job * @param current table position * @param previous table position */ void queueTableSelectionChanged(QModelIndex current, QModelIndex previous); /** * @brief reorderJobs Change the order of jobs in the UI based on a subset of its jobs. */ bool reorderJobs(QList reordered_sublist); /** * @brief moveJobUp Move the selected job up in the job list. */ void moveJobUp(); /** * @brief moveJobDown Move the selected job down in the list. */ void moveJobDown(); /** * @brief shouldSchedulerSleep Check if the scheduler needs to sleep until the job is ready * @param currentJob Job to check * @return True if we set the scheduler to sleep mode. False, if not required and we need to execute now */ bool shouldSchedulerSleep(SchedulerJob *currentJob); void toggleScheduler(); void pause(); void setPaused(); void save(); void saveAs(); void load(); void resetJobEdit(); /** * @brief checkJobStatus Check the overall state of the scheduler, Ekos, and INDI. When all is OK, it calls evaluateJobs() when no job is current or executeJob() if a job is selected. * @return False if this function needs to be called again later, true if situation is stable and operations may continue. */ bool checkStatus(); /** * @brief checkJobStage Check the progress of the job states and make DBUS call to start the next stage until the job is complete. */ void checkJobStage(); /** * @brief findNextJob Check if the job met the completion criteria, and if it did, then it search for next job candidate. If no jobs are found, it starts the shutdown stage. */ void findNextJob(); /** * @brief stopCurrentJobAction Stop whatever action taking place in the current job (eg. capture, guiding...etc). */ void stopCurrentJobAction(); /** * @brief manageConnectionLoss Mitigate loss of connection with the INDI server. * @return true if connection to Ekos/INDI should be attempted again, false if not mitigation is available or needed. */ bool manageConnectionLoss(); /** * @brief readProcessOutput read running script process output and display it in Ekos */ void readProcessOutput(); /** * @brief checkProcessExit Check script process exist status. This is called when the process exists either normally or abnormally. * @param exitCode exit code from the script process. Depending on the exist code, the status of startup/shutdown procedure is set accordingly. */ void checkProcessExit(int exitCode); /** * @brief resumeCheckStatus If the scheduler primary loop was suspended due to weather or sleep event, resume it again. */ void resumeCheckStatus(); /** * @brief checkWeather Check weather status and act accordingly depending on the current status of the scheduler and running jobs. */ //void checkWeather(); /** * @brief wakeUpScheduler Wake up scheduler from sleep state */ void wakeUpScheduler(); /** * @brief startJobEvaluation Start job evaluation only without starting the scheduler process itself. Display the result to the user. */ void startJobEvaluation(); /** * @brief startMosaicTool Start Mosaic tool and create jobs if necessary. */ void startMosaicTool(); /** * @brief displayTwilightWarning Display twilight warning to user if it is unchecked. */ void checkTwilightWarning(bool enabled); void runStartupProcedure(); void checkStartupProcedure(); void runShutdownProcedure(); void checkShutdownProcedure(); void setINDICommunicationStatus(Ekos::CommunicationStatus status); void setEkosCommunicationStatus(Ekos::CommunicationStatus status); void simClockScaleChanged(float); signals: void newLog(const QString &text); void newStatus(Ekos::SchedulerState state); void weatherChanged(ISD::Weather::Status state); void newTarget(const QString &); private: /** * @brief evaluateJobs evaluates the current state of each objects and gives each one a score based on the constraints. * Given that score, the scheduler will decide which is the best job that needs to be executed. */ void evaluateJobs(); /** * @brief executeJob After the best job is selected, we call this in order to start the process that will execute the job. * checkJobStatus slot will be connected in order to figure the exact state of the current job each second * @param value */ void executeJob(SchedulerJob *job); void executeScript(const QString &filename); /** * @brief getDarkSkyScore Get the dark sky score of a date and time. The further from dawn the better. * @param when date and time to check the dark sky score, now if omitted * @return Dark sky score. Daylight get bad score, as well as pre-dawn to dawn. */ int16_t getDarkSkyScore(QDateTime const &when = QDateTime()) const; /** * @brief calculateJobScore Calculate job dark sky score, altitude score, and moon separation scores and returns the sum. * @param job Target * @param when date and time to evaluate constraints, now if omitted. * @return Total score */ int16_t calculateJobScore(SchedulerJob const *job, QDateTime const &when = QDateTime()) const; /** * @brief getWeatherScore Get current weather condition score. * @return If weather condition OK, return score 0, else bad score. */ int16_t getWeatherScore() const; /** * @brief calculateDawnDusk Get dawn and dusk times for today */ void calculateDawnDusk(); /** * @brief checkEkosState Check ekos startup stages and take whatever action necessary to get Ekos up and running * @return True if Ekos is running, false if Ekos start up is in progress. */ bool checkEkosState(); /** * @brief isINDIConnected Determines the status of the INDI connection. * @return True if INDI connection is up and usable, else false. */ bool isINDIConnected(); /** * @brief checkINDIState Check INDI startup stages and take whatever action necessary to get INDI devices connected. * @return True if INDI devices are connected, false if it is under progress. */ bool checkINDIState(); /** * @brief checkStartupState Check startup procedure stages and make sure all stages are complete. * @return True if startup is complete, false otherwise. */ bool checkStartupState(); /** * @brief checkShutdownState Check shutdown procedure stages and make sure all stages are complete. * @return */ bool checkShutdownState(); /** * @brief checkParkWaitState Check park wait state. * @return If parking/unparking in progress, return false. If parking/unparking complete, return true. */ bool checkParkWaitState(); /** * @brief parkMount Park mount */ void parkMount(); /** * @brief unParkMount Unpark mount */ void unParkMount(); /** * @return True if mount is parked */ bool isMountParked(); /** * @brief parkDome Park dome */ void parkDome(); /** * @brief unParkDome Unpark dome */ void unParkDome(); /** * @return True if dome is parked */ bool isDomeParked(); /** * @brief parkCap Close dust cover */ void parkCap(); /** * @brief unCap Open dust cover */ void unParkCap(); /** * @brief checkMountParkingStatus check mount parking status and updating corresponding states accordingly. */ void checkMountParkingStatus(); /** * @brief checkDomeParkingStatus check dome parking status and updating corresponding states accordingly. */ void checkDomeParkingStatus(); /** * @brief checkDomeParkingStatus check dome parking status and updating corresponding states accordingly. */ void checkCapParkingStatus(); /** * @brief saveScheduler Save scheduler jobs to a file * @param path path of a file * @return true on success, false on failure. */ bool saveScheduler(const QUrl &fileURL); /** * @brief processJobInfo Process the job information from a scheduler file and populate jobs accordingly * @param root XML root element of JOB * @return true on success, false on failure. */ bool processJobInfo(XMLEle *root); /** * @brief updatePreDawn Update predawn time depending on current time and user offset */ void updatePreDawn(); /** * @brief estimateJobTime Estimates the time the job takes to complete based on the sequence file and what modules to utilize during the observation run. * @param job target job * @return Estimated time in seconds. */ bool estimateJobTime(SchedulerJob *schedJob); /** * @brief createJobSequence Creates a job sequence for the mosaic tool given the prefix and output dir. The currently selected sequence file is modified * and a new version given the supplied parameters are saved to the output directory * @param prefix Prefix to set for the job sequence * @param outputDir Output dir to set for the job sequence * @return True if new file is saved, false otherwise */ bool createJobSequence(XMLEle *root, const QString &prefix, const QString &outputDir); /** @internal Change the current job, updating associated widgets. * @param job is an existing SchedulerJob to set as current, or nullptr. */ void setCurrentJob(SchedulerJob *job); /** * @brief processFITSSelection When a FITS file is selected, open it and try to guess * the object name, and its J2000 RA/DE to fill the UI with such info automatically. */ void processFITSSelection(); void loadProfiles(); XMLEle *getSequenceJobRoot(); bool isWeatherOK(SchedulerJob *job); /** * @brief updateCompletedJobsCount For each scheduler job, examine sequence job storage and count captures. * @param forced forces recounting captures unconditionally if true, else only IDLE, EVALUATION or new jobs are examined. */ void updateCompletedJobsCount(bool forced = false); SequenceJob *processJobInfo(XMLEle *root, SchedulerJob *schedJob); bool loadSequenceQueue(const QString &fileURL, SchedulerJob *schedJob, QList &jobs, bool &hasAutoFocus); int getCompletedFiles(const QString &path, const QString &seqPrefix); // retrieve the guiding status GuideState getGuidingStatus(); Ekos::Scheduler *ui { nullptr }; //DBus interfaces QPointer focusInterface { nullptr }; QPointer ekosInterface { nullptr }; QPointer captureInterface { nullptr }; QPointer mountInterface { nullptr }; QPointer alignInterface { nullptr }; QPointer guideInterface { nullptr }; QPointer domeInterface { nullptr }; QPointer weatherInterface { nullptr }; QPointer capInterface { nullptr }; // Scheduler and job state and stages SchedulerState state { SCHEDULER_IDLE }; EkosState ekosState { EKOS_IDLE }; INDIState indiState { INDI_IDLE }; StartupState startupState { STARTUP_IDLE }; ShutdownState shutdownState { SHUTDOWN_IDLE }; ParkWaitStatus parkWaitState { PARKWAIT_IDLE }; Ekos::CommunicationStatus m_EkosCommunicationStatus { Ekos::Idle }; Ekos::CommunicationStatus m_INDICommunicationStatus { Ekos::Idle }; /// List of all jobs as entered by the user or file QList jobs; /// Active job SchedulerJob *currentJob { nullptr }; /// URL to store the scheduler file QUrl schedulerURL; /// URL for Ekos Sequence QUrl sequenceURL; /// FITS URL to solve QUrl fitsURL; /// Startup script URL QUrl startupScriptURL; /// Shutdown script URL QUrl shutdownScriptURL; /// Store all log strings QStringList m_LogText; /// Busy indicator widget QProgressIndicator *pi { nullptr }; /// Are we editing a job right now? Job row index int jobUnderEdit { -1 }; /// Pointer to Geographic location GeoLocation *geo { nullptr }; /// How many repeated job batches did we complete thus far? uint16_t captureBatch { 0 }; /// Startup and Shutdown scripts process QProcess scriptProcess; /// Store day fraction of dawn to calculate dark skies range double Dawn { -1 }; /// Store day fraction of dusk to calculate dark skies range double Dusk { -1 }; /// Pre-dawn is where we stop all jobs, it is a user-configurable value before Dawn. QDateTime preDawnDateTime; /// Dusk date time QDateTime duskDateTime; /// Was job modified and needs saving? bool mDirty { false }; /// Keep watch of weather status ISD::Weather::Status weatherStatus { ISD::Weather::WEATHER_IDLE }; /// Keep track of how many times we didn't receive weather updates uint8_t noWeatherCounter { 0 }; /// Are we shutting down until later? bool preemptiveShutdown { false }; /// Only run job evaluation bool jobEvaluationOnly { false }; /// Keep track of Load & Slew operation bool loadAndSlewProgress { false }; /// Check if initial autofocus is completed and do not run autofocus until there is a change is telescope position/alignment. bool autofocusCompleted { false }; /// Keep track of INDI connection failures uint8_t indiConnectFailureCount { 0 }; /// Keep track of Ekos connection failures uint8_t ekosConnectFailureCount { 0 }; /// Keep track of Ekos focus module failures uint8_t focusFailureCount { 0 }; /// Keep track of Ekos guide module failures uint8_t guideFailureCount { 0 }; /// Keep track of Ekos align module failures uint8_t alignFailureCount { 0 }; /// Keep track of Ekos capture module failures uint8_t captureFailureCount { 0 }; /// Counter to keep debug logging in check uint8_t checkJobStageCounter { 0 }; /// Call checkWeather when weatherTimer time expires. It is equal to the UpdatePeriod time in INDI::Weather device. //QTimer weatherTimer; /// Timer to put the scheduler into sleep mode until a job is ready QTimer sleepTimer; /// To call checkStatus QTimer schedulerTimer; /// To call checkJobStage QTimer jobTimer; /// Delay for restarting the guider QTimer restartGuidingTimer; /// Generic time to track timeout of current operation in progress - QTime currentOperationTime; + QElapsedTimer currentOperationTime; QUrl dirPath; QMap capturedFramesCount; bool m_MountReady { false }; bool m_CaptureReady { false }; bool m_DomeReady { false }; bool m_CapReady { false }; // When a module is commanded to perform an action, wait this many milliseconds // before check its state again. If State is still IDLE, then it either didn't received the command // or there is another problem. static const uint32_t ALIGN_INACTIVITY_TIMEOUT = 120000; static const uint32_t FOCUS_INACTIVITY_TIMEOUT = 120000; static const uint32_t CAPTURE_INACTIVITY_TIMEOUT = 120000; static const uint16_t GUIDE_INACTIVITY_TIMEOUT = 60000; }; } diff --git a/kstars/indi/indidome.cpp b/kstars/indi/indidome.cpp index f4e96a2b8..518ffe90a 100644 --- a/kstars/indi/indidome.cpp +++ b/kstars/indi/indidome.cpp @@ -1,578 +1,579 @@ /* INDI Dome Copyright (C) 2015 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include #include #include #include #include #include "indidome.h" #include "kstars.h" #include "clientmanager.h" namespace ISD { Dome::Dome(GDInterface *iPtr) : DeviceDecorator(iPtr) { dType = KSTARS_DOME; qRegisterMetaType("ISD::Dome::Status"); qDBusRegisterMetaType(); readyTimer.reset(new QTimer()); readyTimer.get()->setInterval(250); readyTimer.get()->setSingleShot(true); connect(readyTimer.get(), &QTimer::timeout, this, &Dome::ready); } void Dome::registerProperty(INDI::Property *prop) { if (!prop->getRegistered()) return; if (isConnected()) readyTimer.get()->start(); if (!strcmp(prop->getName(), "DOME_PARK")) { ISwitchVectorProperty *svp = prop->getSwitch(); m_CanPark = true; if (svp) { ISwitch *sp = IUFindSwitch(svp, "PARK"); if (sp) { if ((sp->s == ISS_ON) && svp->s == IPS_OK) { m_ParkStatus = PARK_PARKED; m_Status = DOME_PARKED; emit newParkStatus(m_ParkStatus); QAction *parkAction = KStars::Instance()->actionCollection()->action("dome_park"); if (parkAction) parkAction->setEnabled(false); QAction *unParkAction = KStars::Instance()->actionCollection()->action("dome_unpark"); if (unParkAction) unParkAction->setEnabled(true); } else if ((sp->s == ISS_OFF) && svp->s == IPS_OK) { m_ParkStatus = PARK_UNPARKED; m_Status = DOME_IDLE; emit newParkStatus(m_ParkStatus); QAction *parkAction = KStars::Instance()->actionCollection()->action("dome_park"); if (parkAction) parkAction->setEnabled(true); QAction *unParkAction = KStars::Instance()->actionCollection()->action("dome_unpark"); if (unParkAction) unParkAction->setEnabled(false); } } } } else if (!strcmp(prop->getName(), "ABS_DOME_POSITION")) { m_CanAbsMove = true; } else if (!strcmp(prop->getName(), "REL_DOME_POSITION")) { m_CanRelMove = true; } else if (!strcmp(prop->getName(), "DOME_ABORT_MOTION")) { m_CanAbort = true; } else if (!strcmp(prop->getName(), "DOME_SHUTTER")) { m_HasShutter = true; } DeviceDecorator::registerProperty(prop); } void Dome::processLight(ILightVectorProperty *lvp) { DeviceDecorator::processLight(lvp); } void Dome::processNumber(INumberVectorProperty *nvp) { if (!strcmp(nvp->name, "ABS_DOME_POSITION")) { emit azimuthPositionChanged(nvp->np[0].value); } DeviceDecorator::processNumber(nvp); } void Dome::processSwitch(ISwitchVectorProperty *svp) { if (!strcmp(svp->name, "CONNECTION")) { ISwitch *conSP = IUFindSwitch(svp, "CONNECT"); if (conSP) { if (isConnected() == false && conSP->s == ISS_ON) KStars::Instance()->slotSetDomeEnabled(true); else if (isConnected() && conSP->s == ISS_OFF) { KStars::Instance()->slotSetDomeEnabled(false); m_CanAbsMove = false; m_CanPark = false; } } } else if (!strcmp(svp->name, "DOME_PARK")) { m_CanPark = true; ISwitch *sp = IUFindSwitch(svp, "PARK"); if (sp) { if (svp->s == IPS_ALERT) { + m_ParkStatus = PARK_ERROR; emit newParkStatus(PARK_ERROR); // If alert, set park status to whatever it was opposite to. That is, if it was parking and failed // then we set status to unparked since it did not successfully complete parking. - if (m_ParkStatus == PARK_PARKING) - m_ParkStatus = PARK_UNPARKED; - else if (m_ParkStatus == PARK_UNPARKING) - m_ParkStatus = PARK_PARKED; + // if (m_ParkStatus == PARK_PARKING) + // m_ParkStatus = PARK_UNPARKED; + // else if (m_ParkStatus == PARK_UNPARKING) + // m_ParkStatus = PARK_PARKED; - emit newParkStatus(m_ParkStatus); + // emit newParkStatus(m_ParkStatus); } else if (svp->s == IPS_BUSY && sp->s == ISS_ON && m_ParkStatus != PARK_PARKING) { m_ParkStatus = PARK_PARKING; KNotification::event(QLatin1String("DomeParking"), i18n("Dome parking is in progress")); emit newParkStatus(m_ParkStatus); if (m_Status != DOME_PARKING) { m_Status = DOME_PARKING; emit newStatus(m_Status); } } else if (svp->s == IPS_BUSY && sp->s == ISS_OFF && m_ParkStatus != PARK_UNPARKING) { m_ParkStatus = PARK_UNPARKING; KNotification::event(QLatin1String("DomeUnparking"), i18n("Dome unparking is in progress")); emit newParkStatus(m_ParkStatus); if (m_Status != DOME_UNPARKING) { m_Status = DOME_UNPARKING; emit newStatus(m_Status); } } else if (svp->s == IPS_OK && sp->s == ISS_ON && m_ParkStatus != PARK_PARKED) { m_ParkStatus = PARK_PARKED; KNotification::event(QLatin1String("DomeParked"), i18n("Dome parked")); emit newParkStatus(m_ParkStatus); QAction *parkAction = KStars::Instance()->actionCollection()->action("dome_park"); if (parkAction) parkAction->setEnabled(false); QAction *unParkAction = KStars::Instance()->actionCollection()->action("dome_unpark"); if (unParkAction) unParkAction->setEnabled(true); if (m_Status != DOME_PARKED) { m_Status = DOME_PARKED; emit newStatus(m_Status); } } else if ( (svp->s == IPS_OK || svp->s == IPS_IDLE) && sp->s == ISS_OFF && m_ParkStatus != PARK_UNPARKED) { m_ParkStatus = PARK_UNPARKED; KNotification::event(QLatin1String("DomeUnparked"), i18n("Dome unparked")); QAction *parkAction = KStars::Instance()->actionCollection()->action("dome_park"); if (parkAction) parkAction->setEnabled(true); QAction *unParkAction = KStars::Instance()->actionCollection()->action("dome_unpark"); if (unParkAction) unParkAction->setEnabled(false); emit newParkStatus(m_ParkStatus); if (m_Status != DOME_IDLE) { m_Status = DOME_IDLE; emit newStatus(m_Status); } } } } else if (!strcmp(svp->name, "DOME_MOTION")) { Status lastStatus = m_Status; if (svp->s == IPS_BUSY && lastStatus != DOME_MOVING_CW && lastStatus != DOME_MOVING_CCW && lastStatus != DOME_PARKING && lastStatus != DOME_UNPARKING) { m_Status = svp->sp->s == ISS_ON ? DOME_MOVING_CW : DOME_MOVING_CCW; emit newStatus(m_Status); // rolloff roofs: cw = opening = unparking, ccw = closing = parking if (!canAbsMove() && !canRelMove()) { m_ParkStatus = (m_Status == DOME_MOVING_CW) ? PARK_UNPARKING : PARK_PARKING; emit newParkStatus(m_ParkStatus); } } else if (svp->s == IPS_OK && (lastStatus == DOME_MOVING_CW || lastStatus == DOME_MOVING_CCW)) { m_Status = DOME_TRACKING; emit newStatus(m_Status); } else if (svp->s == IPS_IDLE && lastStatus != DOME_IDLE) { m_Status = DOME_IDLE; emit newStatus(m_Status); } else if (svp->s == IPS_ALERT) { m_Status = DOME_ERROR; emit newStatus(m_Status); } } else if (!strcmp(svp->name, "DOME_SHUTTER")) { if (svp->s == IPS_ALERT) { emit newShutterStatus(SHUTTER_ERROR); // If alert, set shutter status to whatever it was opposite to. That is, if it was opening and failed // then we set status to closed since it did not successfully complete opening. if (m_ShutterStatus == SHUTTER_CLOSING) m_ShutterStatus = SHUTTER_OPEN; else if (m_ShutterStatus == SHUTTER_CLOSING) m_ShutterStatus = SHUTTER_CLOSED; emit newShutterStatus(m_ShutterStatus); } ShutterStatus status = shutterStatus(svp); switch (status) { case SHUTTER_CLOSING: if (m_ShutterStatus != SHUTTER_CLOSING) { m_ShutterStatus = SHUTTER_CLOSING; KNotification::event(QLatin1String("ShutterClosing"), i18n("Shutter closing is in progress")); emit newShutterStatus(m_ShutterStatus); } break; case SHUTTER_OPENING: if (m_ShutterStatus != SHUTTER_OPENING) { m_ShutterStatus = SHUTTER_OPENING; KNotification::event(QLatin1String("ShutterOpening"), i18n("Shutter opening is in progress")); emit newShutterStatus(m_ShutterStatus); } break; case SHUTTER_CLOSED: if (m_ShutterStatus != SHUTTER_CLOSED) { m_ShutterStatus = SHUTTER_CLOSED; KNotification::event(QLatin1String("ShutterClosed"), i18n("Shutter closed")); emit newShutterStatus(m_ShutterStatus); } break; case SHUTTER_OPEN: if (m_ShutterStatus != SHUTTER_OPEN) { m_ShutterStatus = SHUTTER_OPEN; KNotification::event(QLatin1String("ShutterOpened"), i18n("Shutter opened")); emit newShutterStatus(m_ShutterStatus); } break; default: break; } return; } else if (!strcmp(svp->name, "DOME_AUTOSYNC")) { ISwitch *sp = IUFindSwitch(svp, "DOME_AUTOSYNC_ENABLE"); if (sp != nullptr) emit newAutoSyncStatus(sp->s == ISS_ON); } DeviceDecorator::processSwitch(svp); } void Dome::processText(ITextVectorProperty *tvp) { DeviceDecorator::processText(tvp); } bool Dome::Abort() { if (m_CanAbort == false) return false; ISwitchVectorProperty *motionSP = baseDevice->getSwitch("DOME_ABORT_MOTION"); if (motionSP == nullptr) return false; ISwitch *abortSW = IUFindSwitch(motionSP, "ABORT"); if (abortSW == nullptr) return false; abortSW->s = ISS_ON; clientManager->sendNewSwitch(motionSP); return true; } bool Dome::Park() { ISwitchVectorProperty *parkSP = baseDevice->getSwitch("DOME_PARK"); if (parkSP == nullptr) return false; ISwitch *parkSW = IUFindSwitch(parkSP, "PARK"); if (parkSW == nullptr) return false; IUResetSwitch(parkSP); parkSW->s = ISS_ON; clientManager->sendNewSwitch(parkSP); return true; } bool Dome::UnPark() { ISwitchVectorProperty *parkSP = baseDevice->getSwitch("DOME_PARK"); if (parkSP == nullptr) return false; ISwitch *parkSW = IUFindSwitch(parkSP, "UNPARK"); if (parkSW == nullptr) return false; IUResetSwitch(parkSP); parkSW->s = ISS_ON; clientManager->sendNewSwitch(parkSP); return true; } bool Dome::isMoving() const { ISwitchVectorProperty *motionSP = baseDevice->getSwitch("DOME_MOTION"); if (motionSP && motionSP->s == IPS_BUSY) return true; return false; } double Dome::azimuthPosition() const { INumberVectorProperty *az = baseDevice->getNumber("ABS_DOME_POSITION"); if (az == nullptr) return -1; else return az->np[0].value; } bool Dome::setAzimuthPosition(double position) { INumberVectorProperty *az = baseDevice->getNumber("ABS_DOME_POSITION"); if (az == nullptr) return false; az->np[0].value = position; clientManager->sendNewNumber(az); return true; } bool Dome::setRelativePosition(double position) { INumberVectorProperty *azDiff = baseDevice->getNumber("REL_DOME_POSITION"); if (azDiff == nullptr) return false; azDiff->np[0].value = position; clientManager->sendNewNumber(azDiff); return true; } bool Dome::isAutoSync() { ISwitchVectorProperty *autosync = baseDevice->getSwitch("DOME_AUTOSYNC"); if (autosync == nullptr) return false; ISwitch *autosyncSW = IUFindSwitch(autosync, "DOME_AUTOSYNC_ENABLE"); if (autosync == nullptr) return false; else return (autosyncSW->s == ISS_ON); } bool Dome::setAutoSync(bool activate) { ISwitchVectorProperty *autosync = baseDevice->getSwitch("DOME_AUTOSYNC"); if (autosync == nullptr) return false; ISwitch *autosyncSW = IUFindSwitch(autosync, activate ? "DOME_AUTOSYNC_ENABLE" : "DOME_AUTOSYNC_DISABLE"); if (autosyncSW == nullptr) return false; IUResetSwitch(autosync); autosyncSW->s = ISS_ON; clientManager->sendNewSwitch(autosync); return true; } bool Dome::moveDome(DomeDirection dir, DomeMotionCommand operation) { ISwitchVectorProperty *domeMotion = baseDevice->getSwitch("DOME_MOTION"); if (domeMotion == nullptr) return false; ISwitch *opSwitch = IUFindSwitch(domeMotion, dir == DomeDirection::DOME_CW ? "DOME_CW" : "DOME_CCW"); IUResetSwitch(domeMotion); opSwitch->s = (operation == DomeMotionCommand::MOTION_START ? ISS_ON : ISS_OFF); clientManager->sendNewSwitch(domeMotion); return true; } bool Dome::ControlShutter(bool open) { ISwitchVectorProperty *shutterSP = baseDevice->getSwitch("DOME_SHUTTER"); if (shutterSP == nullptr) return false; ISwitch *shutterSW = IUFindSwitch(shutterSP, open ? "SHUTTER_OPEN" : "SHUTTER_CLOSE"); if (shutterSW == nullptr) return false; IUResetSwitch(shutterSP); shutterSW->s = ISS_ON; clientManager->sendNewSwitch(shutterSP); return true; } Dome::ShutterStatus Dome::shutterStatus() { ISwitchVectorProperty *shutterSP = baseDevice->getSwitch("DOME_SHUTTER"); return shutterStatus(shutterSP); } Dome::ShutterStatus Dome::shutterStatus(ISwitchVectorProperty *svp) { if (svp == nullptr) return SHUTTER_UNKNOWN; ISwitch *sp = IUFindSwitch(svp, "SHUTTER_OPEN"); if (sp == nullptr) return SHUTTER_UNKNOWN; if (svp->s == IPS_ALERT) return SHUTTER_ERROR; else if (svp->s == IPS_BUSY) return (sp->s == ISS_ON) ? SHUTTER_OPENING : SHUTTER_CLOSING; else if (svp->s == IPS_OK) return (sp->s == ISS_ON) ? SHUTTER_OPEN : SHUTTER_CLOSED; // this should not happen return SHUTTER_UNKNOWN; } const QString Dome::getStatusString(Dome::Status status) { switch (status) { case ISD::Dome::DOME_IDLE: return i18n("Idle"); case ISD::Dome::DOME_PARKED: return i18n("Parked"); case ISD::Dome::DOME_PARKING: return i18n("Parking"); case ISD::Dome::DOME_UNPARKING: return i18n("UnParking"); case ISD::Dome::DOME_MOVING_CW: return i18n("Moving clockwise"); case ISD::Dome::DOME_MOVING_CCW: return i18n("Moving counter clockwise"); case ISD::Dome::DOME_TRACKING: return i18n("Tracking"); case ISD::Dome::DOME_ERROR: return i18n("Error"); } return i18n("Error"); } } QDBusArgument &operator<<(QDBusArgument &argument, const ISD::Dome::Status &source) { argument.beginStructure(); argument << static_cast(source); argument.endStructure(); return argument; } const QDBusArgument &operator>>(const QDBusArgument &argument, ISD::Dome::Status &dest) { int a; argument.beginStructure(); argument >> a; argument.endStructure(); dest = static_cast(a); return argument; } diff --git a/kstars/indi/inditelescope.cpp b/kstars/indi/inditelescope.cpp index 4a120ea79..a47ec847a 100644 --- a/kstars/indi/inditelescope.cpp +++ b/kstars/indi/inditelescope.cpp @@ -1,1451 +1,1453 @@ /* INDI CCD Copyright (C) 2012 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "inditelescope.h" #include "clientmanager.h" #include "driverinfo.h" #include "indidevice.h" #include "kstars.h" #include "Options.h" #include "skymap.h" #include "skymapcomposite.h" #include "ksnotification.h" #include #include #include namespace ISD { Telescope::Telescope(GDInterface *iPtr) : DeviceDecorator(iPtr) { dType = KSTARS_TELESCOPE; minAlt = -1; maxAlt = -1; EqCoordPreviousState = IPS_IDLE; // Set it for 5 seconds for now as not to spam the display update centerLockTimer.setInterval(5000); centerLockTimer.setSingleShot(true); connect(¢erLockTimer, &QTimer::timeout, this, [this]() { runCommand(INDI_CENTER_LOCK); }); // If after 250ms no new properties are registered then emit ready readyTimer.setInterval(250); readyTimer.setSingleShot(true); connect(&readyTimer, &QTimer::timeout, this, &Telescope::ready); qRegisterMetaType("ISD::Telescope::Status"); qDBusRegisterMetaType(); qRegisterMetaType("ISD::Telescope::PierSide"); qDBusRegisterMetaType(); } void Telescope::registerProperty(INDI::Property *prop) { if (isConnected()) readyTimer.start(); if (!strcmp(prop->getName(), "TELESCOPE_INFO")) { INumberVectorProperty *ti = prop->getNumber(); if (ti == nullptr) return; bool aperture_ok = false, focal_ok = false; double temp = 0; INumber *aperture = IUFindNumber(ti, "TELESCOPE_APERTURE"); if (aperture && aperture->value == 0) { if (getDriverInfo()->getAuxInfo().contains("TELESCOPE_APERTURE")) { temp = getDriverInfo()->getAuxInfo().value("TELESCOPE_APERTURE").toDouble(&aperture_ok); if (aperture_ok) { aperture->value = temp; INumber *g_aperture = IUFindNumber(ti, "GUIDER_APERTURE"); if (g_aperture && g_aperture->value == 0) g_aperture->value = aperture->value; } } } INumber *focal_length = IUFindNumber(ti, "TELESCOPE_FOCAL_LENGTH"); if (focal_length && focal_length->value == 0) { if (getDriverInfo()->getAuxInfo().contains("TELESCOPE_FOCAL_LENGTH")) { temp = getDriverInfo()->getAuxInfo().value("TELESCOPE_FOCAL_LENGTH").toDouble(&focal_ok); if (focal_ok) { focal_length->value = temp; INumber *g_focal = IUFindNumber(ti, "GUIDER_FOCAL_LENGTH"); if (g_focal && g_focal->value == 0) g_focal->value = focal_length->value; } } } if (aperture_ok && focal_ok) clientManager->sendNewNumber(ti); } else if (!strcmp(prop->getName(), "ON_COORD_SET")) { m_canGoto = IUFindSwitch(prop->getSwitch(), "TRACK") != nullptr; m_canSync = IUFindSwitch(prop->getSwitch(), "SYNC") != nullptr; } // Telescope Park else if (!strcmp(prop->getName(), "TELESCOPE_PARK")) { ISwitchVectorProperty *svp = prop->getSwitch(); if (svp) { ISwitch *sp = IUFindSwitch(svp, "PARK"); if (sp) { if ((sp->s == ISS_ON) && svp->s == IPS_OK) { m_ParkStatus = PARK_PARKED; emit newParkStatus(m_ParkStatus); QAction *parkAction = KStars::Instance()->actionCollection()->action("telescope_park"); if (parkAction) parkAction->setEnabled(false); QAction *unParkAction = KStars::Instance()->actionCollection()->action("telescope_unpark"); if (unParkAction) unParkAction->setEnabled(true); } else if ((sp->s == ISS_OFF) && svp->s == IPS_OK) { m_ParkStatus = PARK_UNPARKED; emit newParkStatus(m_ParkStatus); QAction *parkAction = KStars::Instance()->actionCollection()->action("telescope_park"); if (parkAction) parkAction->setEnabled(true); QAction *unParkAction = KStars::Instance()->actionCollection()->action("telescope_unpark"); if (unParkAction) unParkAction->setEnabled(false); } } } } else if (!strcmp(prop->getName(), "TELESCOPE_PIER_SIDE")) { ISwitchVectorProperty *svp = prop->getSwitch(); int currentSide = IUFindOnSwitchIndex(svp); if (currentSide != m_PierSide) { m_PierSide = static_cast(currentSide); emit pierSideChanged(m_PierSide); } } else if (!strcmp(prop->getName(), "ALIGNMENT_POINTSET_ACTION") || !strcmp(prop->getName(), "ALIGNLIST")) m_hasAlignmentModel = true; else if (!strcmp(prop->getName(), "TELESCOPE_TRACK_STATE")) m_canControlTrack = true; else if (!strcmp(prop->getName(), "TELESCOPE_TRACK_MODE")) { m_hasTrackModes = true; ISwitchVectorProperty *svp = prop->getSwitch(); for (int i = 0; i < svp->nsp; i++) { if (!strcmp(svp->sp[i].name, "TRACK_SIDEREAL")) TrackMap[TRACK_SIDEREAL] = i; else if (!strcmp(svp->sp[i].name, "TRACK_SOLAR")) TrackMap[TRACK_SOLAR] = i; else if (!strcmp(svp->sp[i].name, "TRACK_LUNAR")) TrackMap[TRACK_LUNAR] = i; else if (!strcmp(svp->sp[i].name, "TRACK_CUSTOM")) TrackMap[TRACK_CUSTOM] = i; } } else if (!strcmp(prop->getName(), "TELESCOPE_TRACK_RATE")) m_hasCustomTrackRate = true; else if (!strcmp(prop->getName(), "TELESCOPE_ABORT_MOTION")) m_canAbort = true; else if (!strcmp(prop->getName(), "TELESCOPE_PARK_OPTION")) m_hasCustomParking = true; else if (!strcmp(prop->getName(), "TELESCOPE_SLEW_RATE")) { m_hasSlewRates = true; ISwitchVectorProperty *svp = prop->getSwitch(); if (svp) { m_slewRates.clear(); for (int i = 0; i < svp->nsp; i++) m_slewRates << svp->sp[i].label; } } else if (!strcmp(prop->getName(), "EQUATORIAL_EOD_COORD")) { m_isJ2000 = false; } else if (!strcmp(prop->getName(), "EQUATORIAL_COORD")) { m_isJ2000 = true; } DeviceDecorator::registerProperty(prop); } void Telescope::processNumber(INumberVectorProperty *nvp) { if (!strcmp(nvp->name, "EQUATORIAL_EOD_COORD") || !strcmp(nvp->name, "EQUATORIAL_COORD")) { INumber *RA = IUFindNumber(nvp, "RA"); INumber *DEC = IUFindNumber(nvp, "DEC"); if (RA == nullptr || DEC == nullptr) return; currentCoord.setRA(RA->value); currentCoord.setDec(DEC->value); // If J2000, convert it to JNow if (!strcmp(nvp->name, "EQUATORIAL_COORD")) { currentCoord.setRA0(RA->value); currentCoord.setDec0(DEC->value); currentCoord.apparentCoord(static_cast(J2000), KStars::Instance()->data()->ut().djd()); } currentCoord.EquatorialToHorizontal(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); if (nvp->s == IPS_BUSY && EqCoordPreviousState != IPS_BUSY) { if (status() != MOUNT_PARKING) KSNotification::event(QLatin1String("SlewStarted"), i18n("Mount is slewing to target location")); } else if (EqCoordPreviousState == IPS_BUSY && nvp->s == IPS_OK) { KSNotification::event(QLatin1String("SlewCompleted"), i18n("Mount arrived at target location")); double maxrad = 1000.0 / Options::zoomFactor(); currentObject = KStarsData::Instance()->skyComposite()->objectNearest(¤tCoord, maxrad); if (currentObject != nullptr) emit newTarget(currentObject->name()); } EqCoordPreviousState = nvp->s; KStars::Instance()->map()->update(); } else if (!strcmp(nvp->name, "HORIZONTAL_COORD")) { INumber *Az = IUFindNumber(nvp, "AZ"); INumber *Alt = IUFindNumber(nvp, "ALT"); if (Az == nullptr || Alt == nullptr) return; currentCoord.setAz(Az->value); currentCoord.setAlt(Alt->value); currentCoord.HorizontalToEquatorial(KStars::Instance()->data()->lst(), KStars::Instance()->data()->geo()->lat()); KStars::Instance()->map()->update(); } DeviceDecorator::processNumber(nvp); } void Telescope::processSwitch(ISwitchVectorProperty *svp) { bool manualMotionChanged = false; if (!strcmp(svp->name, "CONNECTION")) { ISwitch *conSP = IUFindSwitch(svp, "CONNECT"); if (conSP) { // TODO We must allow for multiple mount drivers to be online, not just one // For the actions taken, the user should be able to specify which mounts shall receive the commands. It could be one // or more. For now, we enable/disable telescope group on the assumption there is only one mount present. if (isConnected() == false && conSP->s == ISS_ON) KStars::Instance()->slotSetTelescopeEnabled(true); else if (isConnected() && conSP->s == ISS_OFF) { KStars::Instance()->slotSetTelescopeEnabled(false); centerLockTimer.stop(); } } } else if (!strcmp(svp->name, "TELESCOPE_PARK")) { ISwitch *sp = IUFindSwitch(svp, "PARK"); if (sp) { if (svp->s == IPS_ALERT) { // First, inform everyone watch this that an error occurred. + m_ParkStatus = PARK_ERROR; emit newParkStatus(PARK_ERROR); + // JM 2020-05-27: Retain status and let whatever process process decide what to do after the error. // If alert, set park status to whatever it was opposite to. That is, if it was parking and failed // then we set status to unparked since it did not successfully complete parking. - if (m_ParkStatus == PARK_PARKING) - m_ParkStatus = PARK_UNPARKED; - else if (m_ParkStatus == PARK_UNPARKING) - m_ParkStatus = PARK_PARKED; + // if (m_ParkStatus == PARK_PARKING) + // m_ParkStatus = PARK_UNPARKED; + // else if (m_ParkStatus == PARK_UNPARKING) + // m_ParkStatus = PARK_PARKED; - emit newParkStatus(m_ParkStatus); + // emit newParkStatus(m_ParkStatus); KSNotification::event(QLatin1String("MountParkingFailed"), i18n("Mount parking failed"), KSNotification::EVENT_ALERT); } else if (svp->s == IPS_BUSY && sp->s == ISS_ON && m_ParkStatus != PARK_PARKING) { m_ParkStatus = PARK_PARKING; KSNotification::event(QLatin1String("MountParking"), i18n("Mount parking is in progress")); currentObject = nullptr; emit newParkStatus(m_ParkStatus); } else if (svp->s == IPS_BUSY && sp->s == ISS_OFF && m_ParkStatus != PARK_UNPARKING) { m_ParkStatus = PARK_UNPARKING; KSNotification::event(QLatin1String("MountUnParking"), i18n("Mount unparking is in progress")); emit newParkStatus(m_ParkStatus); } else if (svp->s == IPS_OK && sp->s == ISS_ON && m_ParkStatus != PARK_PARKED) { m_ParkStatus = PARK_PARKED; KSNotification::event(QLatin1String("MountParked"), i18n("Mount parked")); currentObject = nullptr; emit newParkStatus(m_ParkStatus); QAction *parkAction = KStars::Instance()->actionCollection()->action("telescope_park"); if (parkAction) parkAction->setEnabled(false); QAction *unParkAction = KStars::Instance()->actionCollection()->action("telescope_unpark"); if (unParkAction) unParkAction->setEnabled(true); emit newTarget(QString()); } else if ( (svp->s == IPS_OK || svp->s == IPS_IDLE) && sp->s == ISS_OFF && m_ParkStatus != PARK_UNPARKED) { m_ParkStatus = PARK_UNPARKED; KSNotification::event(QLatin1String("MountUnparked"), i18n("Mount unparked")); currentObject = nullptr; emit newParkStatus(m_ParkStatus); QAction *parkAction = KStars::Instance()->actionCollection()->action("telescope_park"); if (parkAction) parkAction->setEnabled(true); QAction *unParkAction = KStars::Instance()->actionCollection()->action("telescope_unpark"); if (unParkAction) unParkAction->setEnabled(false); } } } else if (!strcmp(svp->name, "TELESCOPE_ABORT_MOTION")) { if (svp->s == IPS_OK) { inCustomParking = false; KSNotification::event(QLatin1String("MountAborted"), i18n("Mount motion was aborted"), KSNotification::EVENT_WARN); } } else if (!strcmp(svp->name, "TELESCOPE_PIER_SIDE")) { int currentSide = IUFindOnSwitchIndex(svp); if (currentSide != m_PierSide) { m_PierSide = static_cast(currentSide); emit pierSideChanged(m_PierSide); } } else if (!strcmp(svp->name, "TELESCOPE_TRACK_MODE")) { ISwitch *sp = IUFindOnSwitch(svp); if (sp) { if (!strcmp(sp->name, "TRACK_SIDEREAL")) currentTrackMode = TRACK_SIDEREAL; else if (!strcmp(sp->name, "TRACK_SOLAR")) currentTrackMode = TRACK_SOLAR; else if (!strcmp(sp->name, "TRACK_LUNAR")) currentTrackMode = TRACK_LUNAR; else currentTrackMode = TRACK_CUSTOM; } } else if (!strcmp(svp->name, "TELESCOPE_MOTION_NS")) manualMotionChanged = true; else if (!strcmp(svp->name, "TELESCOPE_MOTION_WE")) manualMotionChanged = true; if (manualMotionChanged) { IPState NSCurrentMotion, WECurrentMotion; NSCurrentMotion = baseDevice->getSwitch("TELESCOPE_MOTION_NS")->s; WECurrentMotion = baseDevice->getSwitch("TELESCOPE_MOTION_WE")->s; inCustomParking = false; if (NSCurrentMotion == IPS_BUSY || WECurrentMotion == IPS_BUSY || NSPreviousState == IPS_BUSY || WEPreviousState == IPS_BUSY) { if (inManualMotion == false && ((NSCurrentMotion == IPS_BUSY && NSPreviousState != IPS_BUSY) || (WECurrentMotion == IPS_BUSY && WEPreviousState != IPS_BUSY))) { inManualMotion = true; KSNotification::event(QLatin1String("MotionStarted"), i18n("Mount is manually moving")); } else if (inManualMotion && ((NSCurrentMotion != IPS_BUSY && NSPreviousState == IPS_BUSY) || (WECurrentMotion != IPS_BUSY && WEPreviousState == IPS_BUSY))) { inManualMotion = false; KSNotification::event(QLatin1String("MotionStopped"), i18n("Mount motion stopped")); } NSPreviousState = NSCurrentMotion; WEPreviousState = WECurrentMotion; } } DeviceDecorator::processSwitch(svp); } void Telescope::processText(ITextVectorProperty *tvp) { DeviceDecorator::processText(tvp); } bool Telescope::canGuide() { INumberVectorProperty *raPulse = baseDevice->getNumber("TELESCOPE_TIMED_GUIDE_WE"); INumberVectorProperty *decPulse = baseDevice->getNumber("TELESCOPE_TIMED_GUIDE_NS"); if (raPulse && decPulse) return true; else return false; } bool Telescope::canPark() { ISwitchVectorProperty *parkSP = baseDevice->getSwitch("TELESCOPE_PARK"); if (parkSP == nullptr) return false; ISwitch *parkSW = IUFindSwitch(parkSP, "PARK"); return (parkSW != nullptr); } bool Telescope::isSlewing() { INumberVectorProperty *EqProp = baseDevice->getNumber("EQUATORIAL_EOD_COORD"); if (EqProp == nullptr) return false; return (EqProp->s == IPS_BUSY); } bool Telescope::isInMotion() { return (isSlewing() || inManualMotion); } bool Telescope::doPulse(GuideDirection ra_dir, int ra_msecs, GuideDirection dec_dir, int dec_msecs) { if (canGuide() == false) return false; bool raOK = doPulse(ra_dir, ra_msecs); bool decOK = doPulse(dec_dir, dec_msecs); if (raOK && decOK) return true; else return false; } bool Telescope::doPulse(GuideDirection dir, int msecs) { INumberVectorProperty *raPulse = baseDevice->getNumber("TELESCOPE_TIMED_GUIDE_WE"); INumberVectorProperty *decPulse = baseDevice->getNumber("TELESCOPE_TIMED_GUIDE_NS"); INumberVectorProperty *npulse = nullptr; INumber *dirPulse = nullptr; if (raPulse == nullptr || decPulse == nullptr) return false; switch (dir) { case RA_INC_DIR: dirPulse = IUFindNumber(raPulse, "TIMED_GUIDE_W"); if (dirPulse == nullptr) return false; npulse = raPulse; break; case RA_DEC_DIR: dirPulse = IUFindNumber(raPulse, "TIMED_GUIDE_E"); if (dirPulse == nullptr) return false; npulse = raPulse; break; case DEC_INC_DIR: dirPulse = IUFindNumber(decPulse, "TIMED_GUIDE_N"); if (dirPulse == nullptr) return false; npulse = decPulse; break; case DEC_DEC_DIR: dirPulse = IUFindNumber(decPulse, "TIMED_GUIDE_S"); if (dirPulse == nullptr) return false; npulse = decPulse; break; default: return false; } dirPulse->value = msecs; clientManager->sendNewNumber(npulse); //qDebug() << "Sending pulse for " << npulse->name << " in direction " << dirPulse->name << " for " << msecs << " ms " << endl; return true; } bool Telescope::runCommand(int command, void *ptr) { //qDebug() << "Telescope run command is called!!!" << endl; switch (command) { // set pending based on the outcome of send coords case INDI_CUSTOM_PARKING: { bool rc = false; if (ptr == nullptr) rc = sendCoords(KStars::Instance()->map()->clickedPoint()); else rc = sendCoords(static_cast(ptr)); inCustomParking = rc; } break; case INDI_SEND_COORDS: if (ptr == nullptr) sendCoords(KStars::Instance()->map()->clickedPoint()); else sendCoords(static_cast(ptr)); break; case INDI_FIND_TELESCOPE: { SkyPoint J2000Coord(currentCoord.ra(), currentCoord.dec()); J2000Coord.catalogueCoord(KStars::Instance()->data()->ut().djd()); currentCoord.setRA0(J2000Coord.ra()); currentCoord.setDec0(J2000Coord.dec()); double maxrad = 1000.0 / Options::zoomFactor(); SkyObject *currentObject = KStarsData::Instance()->skyComposite()->objectNearest(¤tCoord, maxrad); KStars::Instance()->map()->setFocusObject(currentObject); KStars::Instance()->map()->setDestination(currentCoord); } break; case INDI_CENTER_LOCK: { //if (currentObject == nullptr || KStars::Instance()->map()->focusObject() != currentObject) if (Options::isTracking() == false || currentCoord.angularDistanceTo(KStars::Instance()->map()->focus()).Degrees() > 0.5) { SkyPoint J2000Coord(currentCoord.ra(), currentCoord.dec()); J2000Coord.catalogueCoord(KStars::Instance()->data()->ut().djd()); currentCoord.setRA0(J2000Coord.ra()); currentCoord.setDec0(J2000Coord.dec()); //KStars::Instance()->map()->setClickedPoint(¤tCoord); //KStars::Instance()->map()->slotCenter(); KStars::Instance()->map()->setDestination(currentCoord); KStars::Instance()->map()->setFocusPoint(¤tCoord); //KStars::Instance()->map()->setFocusObject(currentObject); KStars::Instance()->map()->setFocusObject(nullptr); Options::setIsTracking(true); } centerLockTimer.start(); } break; case INDI_CENTER_UNLOCK: KStars::Instance()->map()->stopTracking(); centerLockTimer.stop(); break; default: return DeviceDecorator::runCommand(command, ptr); } return true; } bool Telescope::sendCoords(SkyPoint *ScopeTarget) { INumber *RAEle = nullptr; INumber *DecEle = nullptr; INumber *AzEle = nullptr; INumber *AltEle = nullptr; INumberVectorProperty *EqProp = nullptr; INumberVectorProperty *HorProp = nullptr; double currentRA = 0, currentDEC = 0, currentAlt = 0, currentAz = 0, targetAlt = 0; bool useJ2000(false); EqProp = baseDevice->getNumber("EQUATORIAL_EOD_COORD"); if (EqProp == nullptr) { // J2000 Property EqProp = baseDevice->getNumber("EQUATORIAL_COORD"); if (EqProp) useJ2000 = true; } HorProp = baseDevice->getNumber("HORIZONTAL_COORD"); if (EqProp && EqProp->p == IP_RO) EqProp = nullptr; if (HorProp && HorProp->p == IP_RO) HorProp = nullptr; //qDebug() << "Skymap click - RA: " << scope_target->ra().toHMSString() << " DEC: " << scope_target->dec().toDMSString(); if (EqProp) { RAEle = IUFindNumber(EqProp, "RA"); if (!RAEle) return false; DecEle = IUFindNumber(EqProp, "DEC"); if (!DecEle) return false; //if (useJ2000) //ScopeTarget->apparentCoord( KStars::Instance()->data()->ut().djd(), static_cast(J2000)); currentRA = RAEle->value; currentDEC = DecEle->value; ScopeTarget->EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat()); } if (HorProp) { AzEle = IUFindNumber(HorProp, "AZ"); if (!AzEle) return false; AltEle = IUFindNumber(HorProp, "ALT"); if (!AltEle) return false; currentAz = AzEle->value; currentAlt = AltEle->value; } /* Could not find either properties! */ if (EqProp == nullptr && HorProp == nullptr) return false; //targetAz = ScopeTarget->az().Degrees(); targetAlt = ScopeTarget->altRefracted().Degrees(); if (minAlt != -1 && maxAlt != -1) { if (targetAlt < minAlt || targetAlt > maxAlt) { KSNotification::error(i18n("Requested altitude %1 is outside the specified altitude limit boundary (%2,%3).", QString::number(targetAlt, 'g', 3), QString::number(minAlt, 'g', 3), QString::number(maxAlt, 'g', 3)), i18n("Telescope Motion")); return false; } } if (targetAlt < 0) { if (KMessageBox::warningContinueCancel( nullptr, i18n("Requested altitude is below the horizon. Are you sure you want to proceed?"), i18n("Telescope Motion"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString("telescope_coordinates_below_horizon_warning")) == KMessageBox::Cancel) { if (EqProp) { RAEle->value = currentRA; DecEle->value = currentDEC; } if (HorProp) { AzEle->value = currentAz; AltEle->value = currentAlt; } return false; } } double maxrad = 1000.0 / Options::zoomFactor(); currentObject = KStarsData::Instance()->skyComposite()->objectNearest(ScopeTarget, maxrad); if (currentObject) { // Sun Warning if (currentObject->name() == i18n("Sun")) { if (KMessageBox::warningContinueCancel( nullptr, i18n("Warning! Looking at the Sun without proper protection can lead to irreversible eye damage!"), i18n("Sun Warning"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString("telescope_ignore_sun_warning")) == KMessageBox::Cancel) return false; } if (m_hasTrackModes) { // Tracking Moon if (currentObject->type() == SkyObject::MOON) { if (currentTrackMode != TRACK_LUNAR && TrackMap.contains(TRACK_LUNAR)) setTrackMode(TrackMap.value(TRACK_LUNAR)); } // Tracking Sun else if (currentObject->name() == i18n("Sun")) { if (currentTrackMode != TRACK_SOLAR && TrackMap.contains(TRACK_SOLAR)) setTrackMode(TrackMap.value(TRACK_SOLAR)); } // If Last track mode was either set to SOLAR or LUNAR but now we are slewing to a different object // then we automatically fallback to sidereal. If the current track mode is CUSTOM or something else, nothing // changes. else if (currentTrackMode == TRACK_SOLAR || currentTrackMode == TRACK_LUNAR) setTrackMode(TRACK_SIDEREAL); } emit newTarget(currentObject->name()); } if (EqProp) { dms ra, de; if (useJ2000) { // If we have invalid DEC, then convert coords to J2000 if (ScopeTarget->dec0().Degrees() == 180.0) { ScopeTarget->setRA0(ScopeTarget->ra()); ScopeTarget->setDec0(ScopeTarget->dec()); ScopeTarget->catalogueCoord( KStars::Instance()->data()->ut().djd()); ra = ScopeTarget->ra(); de = ScopeTarget->dec(); } else { ra = ScopeTarget->ra0(); de = ScopeTarget->dec0(); } } else { ra = ScopeTarget->ra(); de = ScopeTarget->dec(); } RAEle->value = ra.Hours(); DecEle->value = de.Degrees(); clientManager->sendNewNumber(EqProp); qCDebug(KSTARS_INDI) << "ISD:Telescope sending coords RA:" << ra.toHMSString() << "(" << RAEle->value << ") DE:" << de.toDMSString() << "(" << DecEle->value << ")"; RAEle->value = currentRA; DecEle->value = currentDEC; } // Only send Horizontal Coord property if Equatorial is not available. else if (HorProp) { AzEle->value = ScopeTarget->az().Degrees(); AltEle->value = ScopeTarget->alt().Degrees(); clientManager->sendNewNumber(HorProp); AzEle->value = currentAz; AltEle->value = currentAlt; } return true; } bool Telescope::Slew(double ra, double dec) { SkyPoint target; if (m_isJ2000) { target.setRA0(ra); target.setDec0(dec); } else { target.setRA(ra); target.setDec(dec); } return Slew(&target); } bool Telescope::Slew(SkyPoint *ScopeTarget) { ISwitchVectorProperty *motionSP = baseDevice->getSwitch("ON_COORD_SET"); if (motionSP == nullptr) return false; ISwitch *slewSW = IUFindSwitch(motionSP, "TRACK"); if (slewSW == nullptr) slewSW = IUFindSwitch(motionSP, "SLEW"); if (slewSW == nullptr) return false; if (slewSW->s != ISS_ON) { IUResetSwitch(motionSP); slewSW->s = ISS_ON; clientManager->sendNewSwitch(motionSP); qCDebug(KSTARS_INDI) << "ISD:Telescope: " << slewSW->name; } return sendCoords(ScopeTarget); } bool Telescope::Sync(double ra, double dec) { SkyPoint target; target.setRA(ra); target.setDec(dec); return Sync(&target); } bool Telescope::Sync(SkyPoint *ScopeTarget) { ISwitchVectorProperty *motionSP = baseDevice->getSwitch("ON_COORD_SET"); if (motionSP == nullptr) return false; ISwitch *syncSW = IUFindSwitch(motionSP, "SYNC"); if (syncSW == nullptr) return false; if (syncSW->s != ISS_ON) { IUResetSwitch(motionSP); syncSW->s = ISS_ON; clientManager->sendNewSwitch(motionSP); qCDebug(KSTARS_INDI) << "ISD:Telescope: Syncing..."; } return sendCoords(ScopeTarget); } bool Telescope::Abort() { ISwitchVectorProperty *motionSP = baseDevice->getSwitch("TELESCOPE_ABORT_MOTION"); if (motionSP == nullptr) return false; ISwitch *abortSW = IUFindSwitch(motionSP, "ABORT"); if (abortSW == nullptr) return false; qCDebug(KSTARS_INDI) << "ISD:Telescope: Aborted." << endl; abortSW->s = ISS_ON; clientManager->sendNewSwitch(motionSP); inCustomParking = false; return true; } bool Telescope::Park() { ISwitchVectorProperty *parkSP = baseDevice->getSwitch("TELESCOPE_PARK"); if (parkSP == nullptr) return false; ISwitch *parkSW = IUFindSwitch(parkSP, "PARK"); if (parkSW == nullptr) return false; qCDebug(KSTARS_INDI) << "ISD:Telescope: Parking..." << endl; IUResetSwitch(parkSP); parkSW->s = ISS_ON; clientManager->sendNewSwitch(parkSP); return true; } bool Telescope::UnPark() { ISwitchVectorProperty *parkSP = baseDevice->getSwitch("TELESCOPE_PARK"); if (parkSP == nullptr) return false; ISwitch *parkSW = IUFindSwitch(parkSP, "UNPARK"); if (parkSW == nullptr) return false; qCDebug(KSTARS_INDI) << "ISD:Telescope: UnParking..." << endl; IUResetSwitch(parkSP); parkSW->s = ISS_ON; clientManager->sendNewSwitch(parkSP); return true; } bool Telescope::getEqCoords(double *ra, double *dec) { INumberVectorProperty *EqProp = nullptr; INumber *RAEle = nullptr; INumber *DecEle = nullptr; EqProp = baseDevice->getNumber("EQUATORIAL_EOD_COORD"); if (EqProp == nullptr) { EqProp = baseDevice->getNumber("EQUATORIAL_COORD"); if (EqProp == nullptr) return false; } RAEle = IUFindNumber(EqProp, "RA"); if (!RAEle) return false; DecEle = IUFindNumber(EqProp, "DEC"); if (!DecEle) return false; *ra = RAEle->value; *dec = DecEle->value; return true; } bool Telescope::MoveNS(TelescopeMotionNS dir, TelescopeMotionCommand cmd) { ISwitchVectorProperty *motionSP = baseDevice->getSwitch("TELESCOPE_MOTION_NS"); if (motionSP == nullptr) return false; ISwitch *motionNorth = IUFindSwitch(motionSP, "MOTION_NORTH"); ISwitch *motionSouth = IUFindSwitch(motionSP, "MOTION_SOUTH"); if (motionNorth == nullptr || motionSouth == nullptr) return false; // If same direction, return if (dir == MOTION_NORTH && motionNorth->s == ((cmd == MOTION_START) ? ISS_ON : ISS_OFF)) return true; if (dir == MOTION_SOUTH && motionSouth->s == ((cmd == MOTION_START) ? ISS_ON : ISS_OFF)) return true; IUResetSwitch(motionSP); if (cmd == MOTION_START) { if (dir == MOTION_NORTH) motionNorth->s = ISS_ON; else motionSouth->s = ISS_ON; } clientManager->sendNewSwitch(motionSP); return true; } bool Telescope::StopWE() { ISwitchVectorProperty *motionSP = baseDevice->getSwitch("TELESCOPE_MOTION_WE"); if (motionSP == nullptr) return false; IUResetSwitch(motionSP); clientManager->sendNewSwitch(motionSP); return true; } bool Telescope::StopNS() { ISwitchVectorProperty *motionSP = baseDevice->getSwitch("TELESCOPE_MOTION_NS"); if (motionSP == nullptr) return false; IUResetSwitch(motionSP); clientManager->sendNewSwitch(motionSP); return true; } bool Telescope::MoveWE(TelescopeMotionWE dir, TelescopeMotionCommand cmd) { ISwitchVectorProperty *motionSP = baseDevice->getSwitch("TELESCOPE_MOTION_WE"); if (motionSP == nullptr) return false; ISwitch *motionWest = IUFindSwitch(motionSP, "MOTION_WEST"); ISwitch *motionEast = IUFindSwitch(motionSP, "MOTION_EAST"); if (motionWest == nullptr || motionEast == nullptr) return false; // If same direction, return if (dir == MOTION_WEST && motionWest->s == ((cmd == MOTION_START) ? ISS_ON : ISS_OFF)) return true; if (dir == MOTION_EAST && motionEast->s == ((cmd == MOTION_START) ? ISS_ON : ISS_OFF)) return true; IUResetSwitch(motionSP); if (cmd == MOTION_START) { if (dir == MOTION_WEST) motionWest->s = ISS_ON; else motionEast->s = ISS_ON; } clientManager->sendNewSwitch(motionSP); return true; } bool Telescope::setSlewRate(int index) { ISwitchVectorProperty *slewRateSP = baseDevice->getSwitch("TELESCOPE_SLEW_RATE"); if (slewRateSP == nullptr) return false; if (index < 0 || index > slewRateSP->nsp) return false; else if (IUFindOnSwitchIndex(slewRateSP) == index) return true; IUResetSwitch(slewRateSP); slewRateSP->sp[index].s = ISS_ON; clientManager->sendNewSwitch(slewRateSP); emit slewRateChanged(index); return true; } int Telescope::getSlewRate() const { ISwitchVectorProperty *slewRateSP = baseDevice->getSwitch("TELESCOPE_SLEW_RATE"); if (slewRateSP == nullptr) return -1; return IUFindOnSwitchIndex(slewRateSP); } void Telescope::setAltLimits(double minAltitude, double maxAltitude) { minAlt = minAltitude; maxAlt = maxAltitude; } bool Telescope::setAlignmentModelEnabled(bool enable) { bool wasExecuted = false; ISwitchVectorProperty *alignSwitch = nullptr; // For INDI Alignment Subsystem alignSwitch = baseDevice->getSwitch("ALIGNMENT_SUBSYSTEM_ACTIVE"); if (alignSwitch) { alignSwitch->sp[0].s = enable ? ISS_ON : ISS_OFF; clientManager->sendNewSwitch(alignSwitch); wasExecuted = true; } // For EQMod Alignment --- Temporary until all drivers switch fully to INDI Alignment Subsystem alignSwitch = baseDevice->getSwitch("ALIGNMODE"); if (alignSwitch) { IUResetSwitch(alignSwitch); // For now, always set alignment mode to NSTAR on enable. if (enable) alignSwitch->sp[2].s = ISS_ON; // Otherwise, set to NO ALIGN else alignSwitch->sp[0].s = ISS_ON; clientManager->sendNewSwitch(alignSwitch); wasExecuted = true; } return wasExecuted; } bool Telescope::clearParking() { ISwitchVectorProperty *parkSwitch = baseDevice->getSwitch("TELESCOPE_PARK_OPTION"); if (!parkSwitch) return false; ISwitch *clearParkSW = IUFindSwitch(parkSwitch, "PARK_PURGE_DATA"); if (!clearParkSW) return false; IUResetSwitch(parkSwitch); clearParkSW->s = ISS_ON; clientManager->sendNewSwitch(parkSwitch); return true; } bool Telescope::clearAlignmentModel() { bool wasExecuted = false; // Note: Should probably use INDI Alignment Subsystem Client API in the future? ISwitchVectorProperty *clearSwitch = baseDevice->getSwitch("ALIGNMENT_POINTSET_ACTION"); ISwitchVectorProperty *commitSwitch = baseDevice->getSwitch("ALIGNMENT_POINTSET_COMMIT"); if (clearSwitch && commitSwitch) { IUResetSwitch(clearSwitch); // ALIGNMENT_POINTSET_ACTION.CLEAR clearSwitch->sp[4].s = ISS_ON; clientManager->sendNewSwitch(clearSwitch); commitSwitch->sp[0].s = ISS_ON; clientManager->sendNewSwitch(commitSwitch); wasExecuted = true; } // For EQMod Alignment --- Temporary until all drivers switch fully to INDI Alignment Subsystem clearSwitch = baseDevice->getSwitch("ALIGNLIST"); if (clearSwitch) { // ALIGNLISTCLEAR IUResetSwitch(clearSwitch); clearSwitch->sp[1].s = ISS_ON; clientManager->sendNewSwitch(clearSwitch); wasExecuted = true; } return wasExecuted; } Telescope::Status Telescope::status() { INumberVectorProperty *EqProp = nullptr; EqProp = baseDevice->getNumber("EQUATORIAL_EOD_COORD"); if (EqProp == nullptr) { EqProp = baseDevice->getNumber("EQUATORIAL_COORD"); if (EqProp == nullptr) return MOUNT_ERROR; } switch (EqProp->s) { case IPS_IDLE: if (inManualMotion) return MOUNT_MOVING; else if (isParked()) return MOUNT_PARKED; else return MOUNT_IDLE; case IPS_OK: if (inManualMotion) return MOUNT_MOVING; else if (inCustomParking) { inCustomParking = false; // set CURRENT position as the desired parking position sendParkingOptionCommand(PARK_OPTION_CURRENT); // Write data to disk sendParkingOptionCommand(PARK_OPTION_WRITE_DATA); return MOUNT_TRACKING; } else return MOUNT_TRACKING; case IPS_BUSY: { ISwitchVectorProperty *parkSP = baseDevice->getSwitch("TELESCOPE_PARK"); if (parkSP && parkSP->s == IPS_BUSY) return MOUNT_PARKING; else return MOUNT_SLEWING; } case IPS_ALERT: inCustomParking = false; return MOUNT_ERROR; } return MOUNT_ERROR; } const QString Telescope::getStatusString(Telescope::Status status) { switch (status) { case ISD::Telescope::MOUNT_IDLE: return i18n("Idle"); case ISD::Telescope::MOUNT_PARKED: return i18n("Parked"); case ISD::Telescope::MOUNT_PARKING: return i18n("Parking"); case ISD::Telescope::MOUNT_SLEWING: return i18n("Slewing"); case ISD::Telescope::MOUNT_MOVING: return i18n("Moving %1", getManualMotionString()); case ISD::Telescope::MOUNT_TRACKING: return i18n("Tracking"); case ISD::Telescope::MOUNT_ERROR: return i18n("Error"); } return i18n("Error"); } QString Telescope::getManualMotionString() const { ISwitchVectorProperty *movementSP = nullptr; QString NSMotion, WEMotion; movementSP = baseDevice->getSwitch("TELESCOPE_MOTION_NS"); if (movementSP) { if (movementSP->sp[MOTION_NORTH].s == ISS_ON) NSMotion = 'N'; else if (movementSP->sp[MOTION_SOUTH].s == ISS_ON) NSMotion = 'S'; } movementSP = baseDevice->getSwitch("TELESCOPE_MOTION_WE"); if (movementSP) { if (movementSP->sp[MOTION_WEST].s == ISS_ON) WEMotion = 'W'; else if (movementSP->sp[MOTION_EAST].s == ISS_ON) WEMotion = 'E'; } return QString("%1%2").arg(NSMotion, WEMotion); } bool Telescope::setTrackEnabled(bool enable) { ISwitchVectorProperty *trackSP = baseDevice->getSwitch("TELESCOPE_TRACK_STATE"); if (trackSP == nullptr) return false; ISwitch *trackON = IUFindSwitch(trackSP, "TRACK_ON"); ISwitch *trackOFF = IUFindSwitch(trackSP, "TRACK_OFF"); if (trackON == nullptr || trackOFF == nullptr) return false; trackON->s = enable ? ISS_ON : ISS_OFF; trackOFF->s = enable ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(trackSP); return true; } bool Telescope::isTracking() { return (status() == MOUNT_TRACKING); } bool Telescope::setTrackMode(uint8_t index) { ISwitchVectorProperty *trackModeSP = baseDevice->getSwitch("TELESCOPE_TRACK_MODE"); if (trackModeSP == nullptr) return false; if (index >= trackModeSP->nsp) return false; IUResetSwitch(trackModeSP); trackModeSP->sp[index].s = ISS_ON; clientManager->sendNewSwitch(trackModeSP); return true; } bool Telescope::getTrackMode(uint8_t &index) { ISwitchVectorProperty *trackModeSP = baseDevice->getSwitch("TELESCOPE_TRACK_MODE"); if (trackModeSP == nullptr) return false; index = IUFindOnSwitchIndex(trackModeSP); return true; } bool Telescope::setCustomTrackRate(double raRate, double deRate) { INumberVectorProperty *trackRateNP = baseDevice->getNumber("TELESCOPE_TRACK_RATE"); if (trackRateNP == nullptr) return false; INumber *raRateN = IUFindNumber(trackRateNP, "TRACK_RATE_RA"); INumber *deRateN = IUFindNumber(trackRateNP, "TRACK_RATE_DE"); if (raRateN == nullptr || deRateN == nullptr) return false; raRateN->value = raRate; deRateN->value = deRate; clientManager->sendNewNumber(trackRateNP); return true; } bool Telescope::getCustomTrackRate(double &raRate, double &deRate) { INumberVectorProperty *trackRateNP = baseDevice->getNumber("TELESCOPE_TRACK_RATE"); if (trackRateNP == nullptr) return false; INumber *raRateN = IUFindNumber(trackRateNP, "TRACK_RATE_RA"); INumber *deRateN = IUFindNumber(trackRateNP, "TRACK_RATE_DE"); if (raRateN == nullptr || deRateN == nullptr) return false; raRate = raRateN->value; deRate = deRateN->value; return true; } bool Telescope::sendParkingOptionCommand(ParkOptionCommand command) { ISwitchVectorProperty *parkOptionsSP = baseDevice->getSwitch("TELESCOPE_PARK_OPTION"); if (parkOptionsSP == nullptr) return false; IUResetSwitch(parkOptionsSP); parkOptionsSP->sp[command].s = ISS_ON; clientManager->sendNewSwitch(parkOptionsSP); return true; } } QDBusArgument &operator<<(QDBusArgument &argument, const ISD::Telescope::Status &source) { argument.beginStructure(); argument << static_cast(source); argument.endStructure(); return argument; } const QDBusArgument &operator>>(const QDBusArgument &argument, ISD::Telescope::Status &dest) { int a; argument.beginStructure(); argument >> a; argument.endStructure(); dest = static_cast(a); return argument; } QDBusArgument &operator<<(QDBusArgument &argument, const ISD::Telescope::PierSide &source) { argument.beginStructure(); argument << static_cast(source); argument.endStructure(); return argument; } const QDBusArgument &operator>>(const QDBusArgument &argument, ISD::Telescope::PierSide &dest) { int a; argument.beginStructure(); argument >> a; argument.endStructure(); dest = static_cast(a); return argument; }