File indexing completed on 2024-04-21 14:45:37

0001 /*
0002     SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikartech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "manager.h"
0008 
0009 #include "analyze/analyze.h"
0010 #include "capture/capture.h"
0011 #include "scheduler/scheduler.h"
0012 #include "focus/focus.h"
0013 #include "align/align.h"
0014 #include "guide/guide.h"
0015 #include "mount/mount.h"
0016 #include "observatory/observatory.h"
0017 
0018 #include "opsekos.h"
0019 #include "ekosadaptor.h"
0020 #include "kstars.h"
0021 #include "kstarsdata.h"
0022 #include "Options.h"
0023 #include "ekos/capture/rotatorsettings.h"
0024 #include "profileeditor.h"
0025 #include "profilewizard.h"
0026 #include "indihub.h"
0027 #include "auxiliary/darklibrary.h"
0028 #include "auxiliary/ksmessagebox.h"
0029 #include "auxiliary/profilesettings.h"
0030 #include "capture/sequencejob.h"
0031 #include "capture/captureprocess.h"
0032 #include "fitsviewer/fitsview.h"
0033 #include "fitsviewer/fitsdata.h"
0034 #include "indi/clientmanager.h"
0035 #include "indi/driverinfo.h"
0036 #include "indi/drivermanager.h"
0037 #include "indi/guimanager.h"
0038 #include "indi/indilistener.h"
0039 #include "auxiliary/opticaltrainmanager.h"
0040 #include "auxiliary/opticaltrainsettings.h"
0041 #include "indi/indiwebmanager.h"
0042 #include "indi/indigps.h"
0043 #include "indi/indiguider.h"
0044 #include "indi/indirotator.h"
0045 #include "mount/meridianflipstatuswidget.h"
0046 #include "ekos/auxiliary/rotatorutils.h"
0047 
0048 #include "ekoslive/ekosliveclient.h"
0049 #include "ekoslive/message.h"
0050 #include "ekoslive/media.h"
0051 
0052 #include <basedevice.h>
0053 
0054 #include <KConfigDialog>
0055 #include <KMessageBox>
0056 #include <KActionCollection>
0057 #include <KNotifications/KNotification>
0058 
0059 #include <QFutureWatcher>
0060 #include <QComboBox>
0061 
0062 #include <ekos_debug.h>
0063 
0064 #define MAX_REMOTE_INDI_TIMEOUT 15000
0065 #define MAX_LOCAL_INDI_TIMEOUT  10000
0066 
0067 namespace Ekos
0068 {
0069 
0070 Manager *Manager::_Manager = nullptr;
0071 
0072 Manager *Manager::Instance()
0073 {
0074     if (_Manager == nullptr)
0075         _Manager = new Manager(Options::independentWindowEkos() ? nullptr : KStars::Instance());
0076 
0077     return _Manager;
0078 }
0079 
0080 void Manager::release()
0081 {
0082     ProfileSettings::release();
0083     OpticalTrainManager::release();
0084     OpticalTrainSettings::release();
0085     RotatorUtils::release();
0086     delete _Manager;
0087 }
0088 
0089 Manager::Manager(QWidget * parent) : QDialog(parent)
0090 {
0091 #ifdef Q_OS_OSX
0092 
0093     if (Options::independentWindowEkos())
0094         setWindowFlags(Qt::Window);
0095     else
0096     {
0097         setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint);
0098         connect(QApplication::instance(), SIGNAL(applicationStateChanged(Qt::ApplicationState)), this,
0099                 SLOT(changeAlwaysOnTop(Qt::ApplicationState)));
0100     }
0101 #else
0102     if (Options::independentWindowEkos())
0103         //setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint);
0104         setWindowFlags(Qt::Window);
0105 #endif
0106     setupUi(this);
0107     // do not show empty targets
0108     capturePreview->targetLabel->setVisible(false);
0109     capturePreview->mountTarget->setVisible(false);
0110 
0111     // position the vertical splitter by 2/3
0112     deviceSplitter->setSizes(QList<int>({20000, 10000}));
0113 
0114     qRegisterMetaType<Ekos::CommunicationStatus>("Ekos::CommunicationStatus");
0115     qDBusRegisterMetaType<Ekos::CommunicationStatus>();
0116 
0117     new EkosAdaptor(this);
0118     QDBusConnection::sessionBus().registerObject("/KStars/Ekos", this);
0119 
0120     setWindowIcon(QIcon::fromTheme("kstars_ekos"));
0121 
0122     profileModel.reset(new QStandardItemModel(0, 4));
0123     profileModel->setHorizontalHeaderLabels(QStringList() << "id"
0124                                             << "name"
0125                                             << "host"
0126                                             << "port");
0127 
0128     m_CountdownTimer.setInterval(1000);
0129     connect(&m_CountdownTimer, &QTimer::timeout, this, &Ekos::Manager::updateCaptureCountDown);
0130 
0131     toolsWidget->setIconSize(QSize(48, 48));
0132     connect(toolsWidget, &QTabWidget::currentChanged, this, &Ekos::Manager::processTabChange, Qt::UniqueConnection);
0133 
0134     // Enable scheduler Tab
0135     toolsWidget->setTabEnabled(1, false);
0136 
0137     // Enable analyze Tab
0138     toolsWidget->setTabEnabled(2, false);
0139 
0140     // Start/Stop INDI Server
0141     connect(processINDIB, &QPushButton::clicked, this, &Ekos::Manager::processINDI);
0142     processINDIB->setIcon(QIcon::fromTheme("media-playback-start"));
0143     processINDIB->setToolTip(i18n("Start"));
0144 
0145     // Connect/Disconnect INDI devices
0146     connect(connectB, &QPushButton::clicked, this, &Ekos::Manager::connectDevices);
0147     connect(disconnectB, &QPushButton::clicked, this, &Ekos::Manager::disconnectDevices);
0148 
0149     // Init EkosLive client
0150     ekosLiveB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0151     ekosLiveClient.reset(new EkosLive::Client(this));
0152     connect(ekosLiveClient.get(), &EkosLive::Client::connected, this, [this]()
0153     {
0154         emit ekosLiveStatusChanged(true);
0155     });
0156     connect(ekosLiveClient.get(), &EkosLive::Client::disconnected, this, [this]()
0157     {
0158         emit ekosLiveStatusChanged(false);
0159     });
0160 
0161     // INDI Control Panel
0162     //connect(controlPanelB, &QPushButton::clicked, GUIManager::Instance(), SLOT(show()));
0163     connect(ekosLiveB, &QPushButton::clicked, this, [&]()
0164     {
0165         ekosLiveClient.get()->show();
0166         ekosLiveClient.get()->raise();
0167     });
0168 
0169     connect(this, &Manager::ekosStatusChanged, ekosLiveClient.get()->message(), &EkosLive::Message::setEkosStatingStatus);
0170     connect(this, &Manager::indiStatusChanged, ekosLiveClient.get()->message(), &EkosLive::Message::setINDIStatus);
0171     connect(ekosLiveClient.get()->message(), &EkosLive::Message::connected, this, [&]()
0172     {
0173         ekosLiveB->setIcon(QIcon(":/icons/cloud-online.svg"));
0174     });
0175     connect(ekosLiveClient.get()->message(), &EkosLive::Message::disconnected, this, [&]()
0176     {
0177         ekosLiveB->setIcon(QIcon::fromTheme("folder-cloud"));
0178     });
0179     connect(ekosLiveClient.get()->media(), &EkosLive::Media::newBoundingRect, ekosLiveClient.get()->message(),
0180             &EkosLive::Message::setBoundingRect);
0181     connect(ekosLiveClient.get()->message(), &EkosLive::Message::resetPolarView, ekosLiveClient.get()->media(),
0182             &EkosLive::Media::resetPolarView);
0183     connect(KSMessageBox::Instance(), &KSMessageBox::newMessage, ekosLiveClient.get()->message(),
0184             &EkosLive::Message::sendDialog);
0185 
0186     // Port Selector
0187     m_PortSelectorTimer.setInterval(500);
0188     m_PortSelectorTimer.setSingleShot(true);
0189     connect(&m_PortSelectorTimer, &QTimer::timeout, this, [this]()
0190     {
0191         if (m_PortSelector && m_CurrentProfile->portSelector)
0192         {
0193             if (m_PortSelector->shouldShow())
0194             {
0195                 m_PortSelector->show();
0196                 m_PortSelector->raise();
0197 
0198                 ekosLiveClient.get()->message()->requestPortSelection(true);
0199             }
0200             // If port selector is enabled, but we have zero ports to work with, let's proceed to connecting if it is enabled.
0201             else if (m_CurrentProfile->autoConnect)
0202                 setPortSelectionComplete();
0203         }
0204         else if (m_CurrentProfile->autoConnect)
0205             setPortSelectionComplete();
0206     });
0207     connect(portSelectorB, &QPushButton::clicked, this, [&]()
0208     {
0209         if (m_PortSelector)
0210         {
0211             m_PortSelector->show();
0212             m_PortSelector->raise();
0213         }
0214     });
0215 
0216     connect(this, &Ekos::Manager::ekosStatusChanged, this, [&](Ekos::CommunicationStatus status)
0217     {
0218         indiControlPanelB->setEnabled(status == Ekos::Success);
0219         connectB->setEnabled(false);
0220         disconnectB->setEnabled(false);
0221         profileGroup->setEnabled(status == Ekos::Idle || status == Ekos::Error);
0222         m_isStarted = (status == Ekos::Success || status == Ekos::Pending);
0223         if (status == Ekos::Success)
0224         {
0225             processINDIB->setIcon(QIcon::fromTheme("media-playback-stop"));
0226             processINDIB->setToolTip(i18n("Stop"));
0227             setWindowTitle(i18nc("@title:window", "Ekos - %1 Profile", m_CurrentProfile->name));
0228         }
0229         else if (status == Ekos::Error || status == Ekos::Idle)
0230         {
0231             processINDIB->setIcon(QIcon::fromTheme("media-playback-start"));
0232             processINDIB->setToolTip(i18n("Start"));
0233         }
0234         else
0235         {
0236             processINDIB->setIcon(QIcon::fromTheme("call-stop"));
0237             processINDIB->setToolTip(i18n("Connection in progress. Click to abort."));
0238         }
0239     });
0240     connect(indiControlPanelB, &QPushButton::clicked, this, [&]()
0241     {
0242         KStars::Instance()->actionCollection()->action("show_control_panel")->trigger();
0243     });
0244     connect(optionsB, &QPushButton::clicked, this, [&]()
0245     {
0246         KStars::Instance()->actionCollection()->action("configure")->trigger();
0247     });
0248     // Save as above, but it appears in all modules
0249     connect(ekosOptionsB, &QPushButton::clicked, this, &Ekos::Manager::showEkosOptions);
0250 
0251     // Clear Ekos Log
0252     connect(clearB, &QPushButton::clicked, this, &Ekos::Manager::clearLog);
0253 
0254     // Logs
0255     KConfigDialog * dialog = new KConfigDialog(this, "logssettings", Options::self());
0256     opsLogs = new Ekos::OpsLogs();
0257     KPageWidgetItem * page = dialog->addPage(opsLogs, i18n("Logging"));
0258     page->setIcon(QIcon::fromTheme("configure"));
0259     connect(logsB, &QPushButton::clicked, dialog, &KConfigDialog::show);
0260     connect(dialog->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &Ekos::Manager::updateDebugInterfaces);
0261     connect(dialog->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &Ekos::Manager::updateDebugInterfaces);
0262 
0263     // Profiles
0264     connect(addProfileB, &QPushButton::clicked, this, &Ekos::Manager::addProfile);
0265     connect(editProfileB, &QPushButton::clicked, this, &Ekos::Manager::editProfile);
0266     connect(deleteProfileB, &QPushButton::clicked, this, &Ekos::Manager::deleteProfile);
0267     connect(profileCombo, static_cast<void(QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged), this,
0268             [ = ](const QString & text)
0269     {
0270         Options::setProfile(text);
0271         if (text == "Simulators")
0272         {
0273             editProfileB->setEnabled(false);
0274             deleteProfileB->setEnabled(false);
0275         }
0276         else
0277         {
0278             editProfileB->setEnabled(true);
0279             deleteProfileB->setEnabled(true);
0280         }
0281     });
0282 
0283     // Settle timer
0284     // Debounce until property stream settles down for a second.
0285     settleTimer.setInterval(1000);
0286     connect(&settleTimer, &QTimer::timeout, this, [&]()
0287     {
0288         if (m_settleStatus != Ekos::Success)
0289         {
0290             m_settleStatus = Ekos::Success;
0291             emit settleStatusChanged(m_settleStatus);
0292         }
0293     });
0294 
0295     // Ekos Wizard
0296     connect(wizardProfileB, &QPushButton::clicked, this, &Ekos::Manager::wizardProfile);
0297 
0298     addProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0299     editProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0300     deleteProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0301 
0302     // Set Profile icons
0303     addProfileB->setIcon(QIcon::fromTheme("list-add"));
0304     addProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0305     editProfileB->setIcon(QIcon::fromTheme("document-edit"));
0306     editProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0307     deleteProfileB->setIcon(QIcon::fromTheme("list-remove"));
0308     deleteProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0309     wizardProfileB->setIcon(QIcon::fromTheme("tools-wizard"));
0310     wizardProfileB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0311     customDriversB->setIcon(QIcon::fromTheme("roll"));
0312     customDriversB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0313 
0314     connect(customDriversB, &QPushButton::clicked, DriverManager::Instance(), &DriverManager::showCustomDrivers);
0315 
0316     // Load all drivers
0317     loadDrivers();
0318 
0319     // Load add driver profiles
0320     loadProfiles();
0321 
0322     // INDI Control Panel and Ekos Options
0323     optionsB->setIcon(QIcon::fromTheme("configure", QIcon(":/icons/ekos_setup.png")));
0324     optionsB->setAttribute(Qt::WA_LayoutUsesWidgetRect);
0325 
0326     // Setup Tab
0327     toolsWidget->tabBar()->setTabIcon(0, QIcon(":/icons/ekos_setup.png"));
0328     toolsWidget->tabBar()->setTabToolTip(0, i18n("Setup"));
0329 
0330     // Initialize Ekos Scheduler Module
0331     schedulerProcess.reset(new Scheduler());
0332     int index = addModuleTab(EkosModule::Scheduler, schedulerModule(), QIcon(":/icons/ekos_scheduler.png"));
0333     toolsWidget->tabBar()->setTabToolTip(index, i18n("Scheduler"));
0334     capturePreview->shareSchedulerModule(schedulerModule());
0335     connect(schedulerModule(), &Scheduler::newLog, this, &Ekos::Manager::updateLog);
0336     connect(schedulerModule(), &Ekos::Scheduler::newTarget, this, &Manager::setTarget);
0337     // Scheduler <---> EkosLive connections
0338     connect(schedulerModule(), &Ekos::Scheduler::jobsUpdated, ekosLiveClient.get()->message(),
0339             &EkosLive::Message::sendSchedulerJobs, Qt::UniqueConnection);
0340     connect(schedulerModule(), &Ekos::Scheduler::settingsUpdated, ekosLiveClient.get()->message(),
0341             &EkosLive::Message::sendSchedulerSettings, Qt::UniqueConnection);
0342     connect(schedulerModule(), &Ekos::Scheduler::newLog, ekosLiveClient.get()->message(),
0343             [this]()
0344     {
0345         QJsonObject cStatus =
0346         {
0347             {"log", schedulerModule()->getLogText()}
0348         };
0349 
0350         ekosLiveClient.get()->message()->sendSchedulerStatus(cStatus);
0351     });
0352     connect(schedulerModule(), &Ekos::Scheduler::newStatus, ekosLiveClient.get()->message(),
0353             [this](Ekos::SchedulerState state)
0354     {
0355         QJsonObject cStatus =
0356         {
0357             {"status", state}
0358         };
0359 
0360         ekosLiveClient.get()->message()->sendSchedulerStatus(cStatus);
0361     });
0362 
0363     // Initialize Ekos Analyze Module
0364     analyzeProcess.reset(new Ekos::Analyze());
0365     connect(analyzeProcess.get(), &Ekos::Analyze::newLog, this, &Ekos::Manager::updateLog);
0366 
0367     index = addModuleTab(EkosModule::Analyze, analyzeProcess.get(), QIcon(":/icons/ekos_analyze.png"));
0368     toolsWidget->tabBar()->setTabToolTip(index, i18n("Analyze"));
0369 
0370     numPermanentTabs = index + 1;
0371 
0372     // Temporary fix. Not sure how to resize Ekos Dialog to fit contents of the various tabs in the QScrollArea which are added
0373     // dynamically. I used setMinimumSize() but it doesn't appear to make any difference.
0374     // Also set Layout policy to SetMinAndMaxSize as well. Any idea how to fix this?
0375     // FIXME
0376     //resize(1000,750);
0377 
0378     m_SummaryView.reset(new SummaryFITSView(capturePreview->previewWidget));
0379     m_SummaryView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
0380     // sterne-jaeger 2021-08-08: Do not set base size here, otherwise the zoom will be incorrect
0381     // summaryPreview->setBaseSize(capturePreview->previewWidget->size());
0382     m_SummaryView->createFloatingToolBar();
0383     m_SummaryView->setCursorMode(FITSView::dragCursor);
0384     m_SummaryView->showProcessInfo(false);
0385     capturePreview->setSummaryFITSView(m_SummaryView.get());
0386     mountStatusLayout->setAlignment(Qt::AlignVCenter);
0387 
0388     if (Options::ekosLeftIcons())
0389     {
0390         toolsWidget->setTabPosition(QTabWidget::West);
0391         QTransform trans;
0392         trans.rotate(90);
0393 
0394         for (int i = 0; i < numPermanentTabs; ++i)
0395         {
0396             QIcon icon  = toolsWidget->tabIcon(i);
0397             QPixmap pix = icon.pixmap(QSize(48, 48));
0398             icon        = QIcon(pix.transformed(trans));
0399             toolsWidget->setTabIcon(i, icon);
0400         }
0401     }
0402 
0403     //Note:  This is to prevent a button from being called the default button
0404     //and then executing when the user hits the enter key such as when on a Text Box
0405 
0406     QList<QPushButton *> qButtons = findChildren<QPushButton *>();
0407     for (auto &button : qButtons)
0408         button->setAutoDefault(false);
0409 
0410 
0411     resize(Options::ekosWindowWidth(), Options::ekosWindowHeight());
0412 }
0413 
0414 void Manager::changeAlwaysOnTop(Qt::ApplicationState state)
0415 {
0416     if (isVisible())
0417     {
0418         if (state == Qt::ApplicationActive)
0419             setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint);
0420         else
0421             setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint);
0422         show();
0423     }
0424 }
0425 
0426 Manager::~Manager()
0427 {
0428     toolsWidget->disconnect(this);
0429 }
0430 
0431 void Manager::closeEvent(QCloseEvent * event)
0432 {
0433     //    QAction * a = KStars::Instance()->actionCollection()->action("show_ekos");
0434     //    a->setChecked(false);
0435 
0436     // 2019-02-14 JM: Close event, for some reason, make all the children disappear
0437     // when the widget is shown again. Applying a workaround here
0438 
0439     event->ignore();
0440     hide();
0441 }
0442 
0443 void Manager::hideEvent(QHideEvent * /*event*/)
0444 {
0445     Options::setEkosWindowWidth(width());
0446     Options::setEkosWindowHeight(height());
0447 
0448     QAction * a = KStars::Instance()->actionCollection()->action("show_ekos");
0449     a->setChecked(false);
0450 }
0451 
0452 void Manager::showEvent(QShowEvent * /*event*/)
0453 {
0454     QAction * a = KStars::Instance()->actionCollection()->action("show_ekos");
0455     a->setChecked(true);
0456 
0457     // Just show the profile wizard ONCE per session
0458     if (profileWizardLaunched == false && profiles.count() == 1)
0459     {
0460         profileWizardLaunched = true;
0461         wizardProfile();
0462     }
0463 }
0464 
0465 void Manager::resizeEvent(QResizeEvent *)
0466 {
0467     focusManager->updateFocusDetailView();
0468     guideManager->updateGuideDetailView();
0469 }
0470 
0471 void Manager::loadProfiles()
0472 {
0473     profiles.clear();
0474     KStarsData::Instance()->userdb()->GetAllProfiles(profiles);
0475 
0476     profileModel->clear();
0477 
0478     for (auto &pi : profiles)
0479     {
0480         QList<QStandardItem *> info;
0481 
0482         info << new QStandardItem(pi->id) << new QStandardItem(pi->name) << new QStandardItem(pi->host)
0483              << new QStandardItem(pi->port);
0484         profileModel->appendRow(info);
0485     }
0486 
0487     profileModel->sort(0);
0488     profileCombo->blockSignals(true);
0489     profileCombo->setModel(profileModel.get());
0490     profileCombo->setModelColumn(1);
0491     profileCombo->blockSignals(false);
0492 
0493     // Load last used profile from options
0494     int index = profileCombo->findText(Options::profile());
0495     // If not found, set it to first item
0496     if (index == -1)
0497         index = 0;
0498     profileCombo->setCurrentIndex(index);
0499 }
0500 
0501 int Manager::addModuleTab(Manager::EkosModule module, QWidget *tab, const QIcon &icon)
0502 {
0503     int index = 0;
0504     switch(module)
0505     {
0506         case EkosModule::Observatory:
0507             index += guideProcess ? 1 : 0; /* FALLTHRU */
0508         case EkosModule::Guide:
0509             index += alignProcess ? 1 : 0; /* FALLTHRU */
0510         case EkosModule::Align:
0511             index += mountProcess ? 1 : 0; /* FALLTHRU */
0512         case EkosModule::Mount:
0513             index += focusProcess ? 1 : 0; /* FALLTHRU */
0514         case EkosModule::Focus:
0515             index += captureProcess ? 1 : 0; /* FALLTHRU */
0516         case EkosModule::Capture:
0517             index += analyzeProcess ? 1 : 0; /* FALLTHRU */
0518         case EkosModule::Analyze:
0519             index += schedulerProcess ? 1 : 0; /* FALLTHRU */
0520         case EkosModule::Scheduler:
0521             index += 1; /* FALLTHRU */
0522         case EkosModule::Setup:
0523             // do nothing
0524             break;
0525         default:
0526             index = toolsWidget->count();
0527             break;
0528     }
0529 
0530     toolsWidget->insertTab(index, tab, icon, "");
0531     return index;
0532 }
0533 
0534 void Manager::loadDrivers()
0535 {
0536     for (auto &dv : DriverManager::Instance()->getDrivers())
0537     {
0538         if (dv->getDriverSource() != HOST_SOURCE)
0539             driversList[dv->getLabel()] = dv;
0540     }
0541 }
0542 
0543 void Manager::reset()
0544 {
0545     qCDebug(KSTARS_EKOS) << "Resetting Ekos Manager...";
0546 
0547     ProfileSettings::release();
0548     OpticalTrainManager::release();
0549     OpticalTrainSettings::release();
0550     RotatorUtils::release();
0551 
0552     m_DriverDevicesCount = 0;
0553 
0554     removeTabs();
0555 
0556     captureProcess.reset();
0557     focusProcess.reset();
0558     guideProcess.reset();
0559     alignProcess.reset();
0560     mountProcess.reset();
0561     observatoryProcess.reset();
0562 
0563     for (auto &oneManger : m_FilterManagers)
0564         oneManger.reset();
0565     m_FilterManagers.clear();
0566 
0567     for (auto &oneController : m_RotatorControllers)
0568         oneController.reset();
0569     m_RotatorControllers.clear();
0570 
0571     DarkLibrary::Release();
0572     m_PortSelector.reset();
0573     m_PortSelectorTimer.stop();
0574 
0575     Ekos::CommunicationStatus previousStatus;
0576 
0577     previousStatus = m_settleStatus;
0578     m_settleStatus = Ekos::Idle;
0579     if (previousStatus != m_settleStatus)
0580         emit settleStatusChanged(m_settleStatus);
0581 
0582     previousStatus = m_ekosStatus;
0583     m_ekosStatus   = Ekos::Idle;
0584     if (previousStatus != m_ekosStatus)
0585         emit ekosStatusChanged(m_ekosStatus);
0586 
0587     previousStatus = m_indiStatus;
0588     m_indiStatus = Ekos::Idle;
0589     if (previousStatus != m_indiStatus)
0590         emit indiStatusChanged(m_indiStatus);
0591 
0592     connectB->setEnabled(false);
0593     disconnectB->setEnabled(false);
0594     //controlPanelB->setEnabled(false);
0595     processINDIB->setEnabled(true);
0596 
0597     mountGroup->setEnabled(false);
0598     capturePreview->setEnabled(false);
0599     capturePreview->reset();
0600     mountStatus->setStatus(i18n("Idle"), Qt::gray);
0601     mountStatus->setStyleSheet(QString());
0602     focusManager->reset();
0603     guideManager->reset();
0604 
0605     m_isStarted = false;
0606 
0607     processINDIB->setIcon(QIcon::fromTheme("media-playback-start"));
0608     processINDIB->setToolTip(i18n("Start"));
0609 }
0610 
0611 void Manager::processINDI()
0612 {
0613     if (m_isStarted == false)
0614         start();
0615     else
0616         stop();
0617 }
0618 
0619 void Manager::stop()
0620 {
0621     cleanDevices();
0622     m_PortSelector.reset();
0623     m_PortSelectorTimer.stop();
0624     m_CountdownTimer.stop();
0625     portSelectorB->setEnabled(false);
0626 
0627     if (indiHubAgent)
0628         indiHubAgent->terminate();
0629 
0630     profileGroup->setEnabled(true);
0631 
0632     setWindowTitle(i18nc("@title:window", "Ekos"));
0633 }
0634 
0635 void Manager::start()
0636 {
0637     // Don't start if it is already started before
0638     if (m_ekosStatus == Ekos::Pending || m_ekosStatus == Ekos::Success)
0639     {
0640         qCWarning(KSTARS_EKOS) << "Ekos Manager start called but current Ekos Status is" << m_ekosStatus << "Ignoring request.";
0641         return;
0642     }
0643 
0644     managedDrivers.clear();
0645 
0646     // If clock was paused, unpaused it and sync time
0647     if (KStarsData::Instance()->clock()->isActive() == false)
0648     {
0649         KStarsData::Instance()->changeDateTime(KStarsDateTime::currentDateTimeUtc());
0650         KStarsData::Instance()->clock()->start();
0651     }
0652 
0653     // Reset Ekos Manager
0654     reset();
0655 
0656     // Get Current Profile
0657     getCurrentProfile(m_CurrentProfile);
0658     m_LocalMode      = m_CurrentProfile->isLocal();
0659 
0660     ProfileSettings::Instance()->setProfile(m_CurrentProfile);
0661 
0662     // Load profile location if one exists
0663     updateProfileLocation(m_CurrentProfile);
0664 
0665     bool haveCCD = false, haveGuider = false;
0666 
0667     // If external guide is specified in the profile, set the
0668     // corresponding options
0669     if (m_CurrentProfile->guidertype == Ekos::Guide::GUIDE_PHD2)
0670     {
0671         Options::setPHD2Host(m_CurrentProfile->guiderhost);
0672         Options::setPHD2Port(m_CurrentProfile->guiderport);
0673     }
0674     else if (m_CurrentProfile->guidertype == Ekos::Guide::GUIDE_LINGUIDER)
0675     {
0676         Options::setLinGuiderHost(m_CurrentProfile->guiderhost);
0677         Options::setLinGuiderPort(m_CurrentProfile->guiderport);
0678     }
0679 
0680     // Parse script, if any
0681     QJsonParseError jsonError;
0682     QJsonArray profileScripts;
0683     QJsonDocument doc = QJsonDocument::fromJson(m_CurrentProfile->scripts, &jsonError);
0684 
0685     if (jsonError.error == QJsonParseError::NoError)
0686         profileScripts = doc.array();
0687 
0688     // For locally running INDI server
0689     if (m_LocalMode)
0690     {
0691         auto drv = driversList.value(m_CurrentProfile->mount());
0692 
0693         if (!drv.isNull())
0694             managedDrivers.append(drv->clone());
0695 
0696         drv = driversList.value(m_CurrentProfile->ccd());
0697         if (!drv.isNull())
0698         {
0699             managedDrivers.append(drv->clone());
0700             haveCCD = true;
0701         }
0702 
0703         Options::setGuiderType(m_CurrentProfile->guidertype);
0704 
0705         drv = driversList.value(m_CurrentProfile->guider());
0706         if (!drv.isNull())
0707         {
0708             haveGuider = true;
0709 
0710             // If the guider and ccd are the same driver, we have two cases:
0711             // #1 Drivers that only support ONE device per driver (such as sbig)
0712             // #2 Drivers that supports multiples devices per driver (such as sx)
0713             // For #1, we modify guider_di to make a unique label for the other device with postfix "Guide"
0714             // For #2, we set guider_di to nullptr and we prompt the user to select which device is primary ccd and which is guider
0715             // since this is the only way to find out in real time.
0716             if (haveCCD && m_CurrentProfile->guider() == m_CurrentProfile->ccd())
0717             {
0718                 if (checkUniqueBinaryDriver( driversList.value(m_CurrentProfile->ccd()), drv))
0719                 {
0720                     drv.clear();
0721                 }
0722                 else
0723                 {
0724                     drv->setUniqueLabel(drv->getLabel() + " Guide");
0725                 }
0726             }
0727 
0728             if (!drv.isNull())
0729                 managedDrivers.append(drv->clone());
0730         }
0731 
0732         drv = driversList.value(m_CurrentProfile->ao());
0733         if (!drv.isNull())
0734             managedDrivers.append(drv->clone());
0735 
0736         drv = driversList.value(m_CurrentProfile->filter());
0737         if (!drv.isNull())
0738             managedDrivers.append(drv->clone());
0739 
0740         drv = driversList.value(m_CurrentProfile->focuser());
0741         if (!drv.isNull())
0742             managedDrivers.append(drv->clone());
0743 
0744         drv = driversList.value(m_CurrentProfile->dome());
0745         if (!drv.isNull())
0746             managedDrivers.append(drv->clone());
0747 
0748         drv = driversList.value(m_CurrentProfile->weather());
0749         if (!drv.isNull())
0750             managedDrivers.append(drv->clone());
0751 
0752         drv = driversList.value(m_CurrentProfile->aux1());
0753         if (!drv.isNull())
0754         {
0755             if (!checkUniqueBinaryDriver(driversList.value(m_CurrentProfile->ccd()), drv) &&
0756                     !checkUniqueBinaryDriver(driversList.value(m_CurrentProfile->guider()), drv))
0757                 managedDrivers.append(drv->clone());
0758         }
0759         drv = driversList.value(m_CurrentProfile->aux2());
0760         if (!drv.isNull())
0761         {
0762             if (!checkUniqueBinaryDriver(driversList.value(m_CurrentProfile->ccd()), drv) &&
0763                     !checkUniqueBinaryDriver(driversList.value(m_CurrentProfile->guider()), drv))
0764                 managedDrivers.append(drv->clone());
0765         }
0766 
0767         drv = driversList.value(m_CurrentProfile->aux3());
0768         if (!drv.isNull())
0769         {
0770             if (!checkUniqueBinaryDriver(driversList.value(m_CurrentProfile->ccd()), drv) &&
0771                     !checkUniqueBinaryDriver(driversList.value(m_CurrentProfile->guider()), drv))
0772                 managedDrivers.append(drv->clone());
0773         }
0774 
0775         drv = driversList.value(m_CurrentProfile->aux4());
0776         if (!drv.isNull())
0777         {
0778             if (!checkUniqueBinaryDriver(driversList.value(m_CurrentProfile->ccd()), drv) &&
0779                     !checkUniqueBinaryDriver(driversList.value(m_CurrentProfile->guider()), drv))
0780                 managedDrivers.append(drv->clone());
0781         }
0782 
0783         // Add remote drivers if we have any
0784         if (m_CurrentProfile->remotedrivers.isEmpty() == false && m_CurrentProfile->remotedrivers.contains("@"))
0785         {
0786             for (auto remoteDriver : m_CurrentProfile->remotedrivers.split(","))
0787             {
0788                 QString name, label, host("localhost"), port("7624"), hostport(host + ':' + port);
0789 
0790                 // Possible configurations:
0791                 // - device
0792                 // - device@host
0793                 // - device@host:port
0794                 // - @host
0795                 // - @host:port
0796 
0797                 {
0798                     QStringList device_location = remoteDriver.split('@');
0799 
0800                     // device or device@host:port
0801                     if (device_location.length() > 0)
0802                         name = device_location[0];
0803 
0804                     // device@host:port or @host:port
0805                     if (device_location.length() > 1)
0806                         hostport = device_location[1];
0807                 }
0808 
0809                 {
0810                     QStringList location = hostport.split(':');
0811 
0812                     // host or host:port
0813                     if (location.length() > 0)
0814                         host = location[0];
0815 
0816                     // host:port
0817                     if (location.length() > 1)
0818                         port = location[1];
0819                 }
0820 
0821                 QSharedPointer<DriverInfo> dv(new DriverInfo(name));
0822                 dv->setRemoteHost(host);
0823                 dv->setRemotePort(port);
0824 
0825                 label = name;
0826                 // Remove extra quotes
0827                 label.remove("\"");
0828                 dv->setLabel(label);
0829                 dv->setUniqueLabel(label);
0830                 managedDrivers.append(dv);
0831             }
0832         }
0833 
0834 
0835         if (haveCCD == false && haveGuider == false && m_CurrentProfile->remotedrivers.isEmpty())
0836         {
0837             KSNotification::error(i18n("Ekos requires at least one CCD or Guider to operate."));
0838             managedDrivers.clear();
0839             m_ekosStatus = Ekos::Error;
0840             emit ekosStatusChanged(m_ekosStatus);
0841             return;
0842         }
0843 
0844         m_DriverDevicesCount = managedDrivers.count();
0845     }
0846     else
0847     {
0848         QSharedPointer<DriverInfo> remote_indi(new DriverInfo(QString("Ekos Remote Host")));
0849 
0850         remote_indi->setHostParameters(m_CurrentProfile->host, m_CurrentProfile->port);
0851 
0852         remote_indi->setDriverSource(GENERATED_SOURCE);
0853 
0854         managedDrivers.append(remote_indi);
0855 
0856         haveCCD    = m_CurrentProfile->drivers.contains("CCD");
0857         haveGuider = m_CurrentProfile->drivers.contains("Guider");
0858 
0859         Options::setGuiderType(m_CurrentProfile->guidertype);
0860 
0861         if (haveCCD == false && haveGuider == false && m_CurrentProfile->remotedrivers.isEmpty())
0862         {
0863             KSNotification::error(i18n("Ekos requires at least one CCD or Guider to operate."));
0864             m_DriverDevicesCount = 0;
0865             m_ekosStatus = Ekos::Error;
0866             emit ekosStatusChanged(m_ekosStatus);
0867             return;
0868         }
0869 
0870         m_DriverDevicesCount = m_CurrentProfile->drivers.count();
0871     }
0872 
0873 
0874     // Prioritize profile script drivers over other drivers
0875     QList<QSharedPointer<DriverInfo>> sortedList;
0876     for (const auto &oneRule : qAsConst(profileScripts))
0877     {
0878         auto matchingDriver = std::find_if(managedDrivers.begin(), managedDrivers.end(), [oneRule](const auto & oneDriver)
0879         {
0880             return oneDriver->getLabel() == oneRule.toObject()["Driver"].toString();
0881         });
0882 
0883         if (matchingDriver != managedDrivers.end())
0884         {
0885             (*matchingDriver)->setStartupRule(oneRule.toObject());
0886             sortedList.append(*matchingDriver);
0887         }
0888     }
0889 
0890     // If we have any profile scripts drivers, let's re-sort managed drivers
0891     // so that profile script drivers
0892     if (!sortedList.isEmpty())
0893     {
0894         for (auto &oneDriver : managedDrivers)
0895         {
0896             if (sortedList.contains(oneDriver) == false)
0897                 sortedList.append(oneDriver);
0898         }
0899 
0900         managedDrivers = sortedList;
0901     }
0902 
0903     connect(DriverManager::Instance(), &DriverManager::serverStarted, this,
0904             &Manager::setServerStarted, Qt::UniqueConnection);
0905     connect(DriverManager::Instance(), &DriverManager::serverFailed, this,
0906             &Manager::setServerFailed, Qt::UniqueConnection);
0907     connect(DriverManager::Instance(), &DriverManager::clientStarted, this,
0908             &Manager::setClientStarted, Qt::UniqueConnection);
0909     connect(DriverManager::Instance(), &DriverManager::clientFailed, this,
0910             &Manager::setClientFailed, Qt::UniqueConnection);
0911     connect(DriverManager::Instance(), &DriverManager::clientTerminated, this,
0912             &Manager::setClientTerminated, Qt::UniqueConnection);
0913 
0914     connect(INDIListener::Instance(), &INDIListener::newDevice, this, &Ekos::Manager::processNewDevice);
0915     connect(INDIListener::Instance(), &INDIListener::deviceRemoved, this, &Ekos::Manager::removeDevice, Qt::DirectConnection);
0916 
0917 
0918 #ifdef Q_OS_OSX
0919     if (m_LocalMode || m_CurrentProfile->host == "localhost")
0920     {
0921         if (isRunning("PTPCamera"))
0922         {
0923             if (KMessageBox::Yes ==
0924                     (KMessageBox::questionYesNo(nullptr,
0925                                                 i18n("Ekos detected that PTP Camera is running and may prevent a Canon or Nikon camera from connecting to Ekos. Do you want to quit PTP Camera now?"),
0926                                                 i18n("PTP Camera"), KStandardGuiItem::yes(), KStandardGuiItem::no(),
0927                                                 "ekos_shutdown_PTPCamera")))
0928             {
0929                 //TODO is there a better way to do this.
0930                 QProcess p;
0931                 p.start("killall PTPCamera");
0932                 p.waitForFinished();
0933             }
0934         }
0935     }
0936 #endif
0937     if (m_LocalMode)
0938     {
0939         auto executeStartINDIServices = [this]()
0940         {
0941             appendLogText(i18n("Starting INDI services..."));
0942 
0943             m_ekosStatus = Ekos::Pending;
0944             emit ekosStatusChanged(m_ekosStatus);
0945 
0946             DriverManager::Instance()->startDevices(managedDrivers);
0947         };
0948 
0949         // If INDI server is already running, let's see if we need to shut it down first
0950         if (isRunning("indiserver"))
0951         {
0952             connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executeStartINDIServices]()
0953             {
0954                 KSMessageBox::Instance()->disconnect(this);
0955                 DriverManager::Instance()->stopAllDevices();
0956                 //TODO is there a better way to do this.
0957                 QProcess p;
0958                 const QString program = "pkill";
0959                 QStringList arguments;
0960                 arguments << "indiserver";
0961                 p.start(program, arguments);
0962                 p.waitForFinished();
0963 
0964                 QTimer::singleShot(1000, this, executeStartINDIServices);
0965             });
0966             connect(KSMessageBox::Instance(), &KSMessageBox::rejected, this, [this, executeStartINDIServices]()
0967             {
0968                 KSMessageBox::Instance()->disconnect(this);
0969                 executeStartINDIServices();
0970             });
0971 
0972             KSMessageBox::Instance()->questionYesNo(i18n("Ekos detected an instance of INDI server running. Do you wish to "
0973                                                     "shut down the existing instance before starting a new one?"),
0974                                                     i18n("INDI Server"), 5);
0975         }
0976         else
0977             executeStartINDIServices();
0978 
0979     }
0980     else
0981     {
0982         auto runConnection = [this]()
0983         {
0984             // If it got cancelled by the user, return immediately.
0985             if (m_ekosStatus != Ekos::Pending)
0986                 return;
0987 
0988             appendLogText(
0989                 i18n("Connecting to remote INDI server at %1 on port %2 ...", m_CurrentProfile->host, m_CurrentProfile->port));
0990 
0991             DriverManager::Instance()->connectRemoteHost(managedDrivers.first());
0992         };
0993 
0994         auto runProfile = [this, runConnection]()
0995         {
0996             // If it got cancelled by the user, return immediately.
0997             if (m_ekosStatus != Ekos::Pending)
0998                 return;
0999 
1000             INDI::WebManager::syncCustomDrivers(m_CurrentProfile);
1001             INDI::WebManager::checkVersion(m_CurrentProfile);
1002 
1003             if (INDI::WebManager::areDriversRunning(m_CurrentProfile) == false)
1004             {
1005                 INDI::WebManager::stopProfile(m_CurrentProfile);
1006 
1007                 if (INDI::WebManager::startProfile(m_CurrentProfile) == false)
1008                 {
1009                     appendLogText(i18n("Failed to start profile on remote INDI Web Manager."));
1010                     return;
1011                 }
1012 
1013                 appendLogText(i18n("Starting profile on remote INDI Web Manager..."));
1014                 m_RemoteManagerStart = true;
1015             }
1016 
1017             runConnection();
1018         };
1019 
1020         m_ekosStatus = Ekos::Pending;
1021         emit ekosStatusChanged(m_ekosStatus);
1022 
1023         // If we need to use INDI Web Manager
1024         if (m_CurrentProfile->INDIWebManagerPort > 0)
1025         {
1026             appendLogText(i18n("Establishing communication with remote INDI Web Manager..."));
1027             m_RemoteManagerStart = false;
1028             QFutureWatcher<bool> *watcher = new QFutureWatcher<bool>();
1029             connect(watcher, &QFutureWatcher<bool>::finished, this, [this, runConnection, runProfile, watcher]()
1030             {
1031                 watcher->deleteLater();
1032 
1033                 // If it got cancelled by the user, return immediately.
1034                 if (m_ekosStatus != Ekos::Pending)
1035                     return;
1036 
1037                 // If web manager is online, try to run the profile in it
1038                 if (watcher->result())
1039                 {
1040                     runProfile();
1041                 }
1042                 // Else, try to connect directly to INDI server as there could be a chance
1043                 // that it is already running.
1044                 else
1045                 {
1046                     appendLogText(i18n("Warning: INDI Web Manager is not online."));
1047                     runConnection();
1048                 }
1049 
1050             });
1051 
1052             QFuture<bool> result = INDI::AsyncWebManager::isOnline(m_CurrentProfile);
1053             watcher->setFuture(result);
1054         }
1055         else
1056         {
1057             runConnection();
1058         }
1059     }
1060 }
1061 
1062 void Manager::setClientStarted(const QString &host, int port)
1063 {
1064     if (managedDrivers.size() > 0)
1065     {
1066         if (m_LocalMode)
1067         {
1068             if (m_CurrentProfile->autoConnect)
1069                 appendLogText(i18n("INDI services started on port %1.", port));
1070             else
1071                 appendLogText(
1072                     i18n("INDI services started on port %1. Please connect devices.", port));
1073         }
1074         else
1075         {
1076             appendLogText(
1077                 i18n("INDI services started. Connection to remote INDI server %1:%2 is successful. Waiting for devices...", host, port));
1078         }
1079     }
1080 
1081     QTimer::singleShot(MAX_LOCAL_INDI_TIMEOUT, this, &Ekos::Manager::checkINDITimeout);
1082 }
1083 
1084 void Manager::setClientFailed(const QString &host, int port, const QString &errorMessage)
1085 {
1086     if (m_LocalMode)
1087         appendLogText(i18n("Failed to connect to local INDI server %1:%2", host, port));
1088     else
1089         appendLogText(i18n("Failed to connect to remote INDI server %1:%2", host, port));
1090 
1091     //INDIListener::Instance()->disconnect(this);
1092     //    qDeleteAll(managedDrivers);
1093     //    managedDrivers.clear();
1094     m_ekosStatus = Ekos::Error;
1095     emit ekosStatusChanged(m_ekosStatus);
1096     KSNotification::error(errorMessage, i18n("Error"), 15);
1097 }
1098 
1099 void Manager::setClientTerminated(const QString &host, int port, const QString &errorMessage)
1100 {
1101     if (m_LocalMode)
1102         appendLogText(i18n("Lost connection to local INDI server %1:%2", host, port));
1103     else
1104         appendLogText(i18n("Lost connection to remote INDI server %1:%2", host, port));
1105 
1106     //INDIListener::Instance()->disconnect(this);
1107     //    qDeleteAll(managedDrivers);
1108     //    managedDrivers.clear();
1109     m_ekosStatus = Ekos::Error;
1110     emit ekosStatusChanged(m_ekosStatus);
1111     KSNotification::error(errorMessage, i18n("Error"), 15);
1112 }
1113 
1114 void Manager::setServerStarted(const QString &host, int port)
1115 {
1116     if (m_LocalMode && m_CurrentProfile->indihub != INDIHub::None)
1117     {
1118         if (QFile(Options::iNDIHubAgent()).exists())
1119         {
1120             indiHubAgent = new QProcess();
1121             QStringList args;
1122 
1123             args << "--indi-server" << QString("%1:%2").arg(host).arg(port);
1124             if (m_CurrentProfile->guidertype == Ekos::Guide::GUIDE_PHD2)
1125                 args << "--phd2-server" << QString("%1:%2").arg(m_CurrentProfile->guiderhost).arg(m_CurrentProfile->guiderport);
1126             args << "--mode" << INDIHub::toString(m_CurrentProfile->indihub);
1127             indiHubAgent->start(Options::iNDIHubAgent(), args);
1128 
1129             qCDebug(KSTARS_EKOS) << "Started INDIHub agent.";
1130         }
1131     }
1132 }
1133 
1134 void Manager::setServerFailed(const QString &host, int port, const QString &message)
1135 {
1136     Q_UNUSED(host)
1137     Q_UNUSED(port)
1138     managedDrivers.clear();
1139     m_ekosStatus = Ekos::Error;
1140     emit ekosStatusChanged(m_ekosStatus);
1141     KSNotification::error(message, i18n("Error"), 15);
1142 }
1143 
1144 //void Manager::setServerTerminated(const QString &host, int port, const QString &message)
1145 //{
1146 //    if ((m_LocalMode && managedDrivers.first()->getPort() == port) ||
1147 //            (currentProfile->host == host && currentProfile->port == port))
1148 //    {
1149 //        cleanDevices(false);
1150 //        if (indiHubAgent)
1151 //            indiHubAgent->terminate();
1152 //    }
1153 
1154 //    INDIListener::Instance()->disconnect(this);
1155 //    qDeleteAll(managedDrivers);
1156 //    managedDrivers.clear();
1157 //    m_ekosStatus = Ekos::Error;
1158 //    emit ekosStatusChanged(m_ekosStatus);
1159 //    KSNotification::error(message, i18n("Error"), 15);
1160 //}
1161 
1162 void Manager::checkINDITimeout()
1163 {
1164     // Don't check anything unless we're still pending
1165     if (m_ekosStatus != Ekos::Pending)
1166     {
1167         // All devices are connected already, nothing to do.
1168         if (m_indiStatus != Ekos::Pending || m_CurrentProfile->portSelector || m_CurrentProfile->autoConnect == false)
1169             return;
1170 
1171         QStringList disconnectedDevices;
1172         for (auto &oneDevice : INDIListener::devices())
1173         {
1174             if (oneDevice->isConnected() == false)
1175                 disconnectedDevices << oneDevice->getDeviceName();
1176         }
1177 
1178         QString message;
1179 
1180         if (disconnectedDevices.count() == 1)
1181             message = i18n("Failed to connect to %1. Please ensure device is connected and powered on.", disconnectedDevices.first());
1182         else
1183             message = i18n("Failed to connect to \n%1\nPlease ensure each device is connected and powered on.",
1184                            disconnectedDevices.join("\n"));
1185 
1186         appendLogText(message);
1187         KSNotification::event(QLatin1String("IndiServerMessage"), message, KSNotification::General, KSNotification::Warn);
1188         return;
1189     }
1190 
1191 
1192     if (m_DriverDevicesCount <= 0)
1193     {
1194         m_ekosStatus = Ekos::Success;
1195         emit ekosStatusChanged(m_ekosStatus);
1196         return;
1197     }
1198 
1199     if (m_LocalMode)
1200     {
1201         QStringList remainingDevices;
1202         for (auto &drv : managedDrivers)
1203         {
1204             if (drv->getDevices().count() == 0)
1205                 remainingDevices << QString("+ %1").arg(
1206                                      drv->getUniqueLabel().isEmpty() == false ? drv->getUniqueLabel() : drv->getName());
1207         }
1208 
1209         if (remainingDevices.count() == 1)
1210         {
1211             QString message = i18n("Unable to establish:\n%1\nPlease ensure the device is connected and powered on.",
1212                                    remainingDevices.at(0));
1213             appendLogText(message);
1214             KSNotification::event(QLatin1String("IndiServerMessage"), message, KSNotification::General, KSNotification::Warn);
1215             KNotification::beep(i18n("Ekos startup error"));
1216         }
1217         else
1218         {
1219             QString message = i18n("Unable to establish the following devices:\n%1\nPlease ensure each device is connected "
1220                                    "and powered on.", remainingDevices.join("\n"));
1221             appendLogText(message);
1222             KSNotification::event(QLatin1String("IndiServerMessage"), message, KSNotification::General, KSNotification::Warn);
1223             KNotification::beep(i18n("Ekos startup error"));
1224         }
1225     }
1226     else
1227     {
1228         QStringList remainingDevices;
1229 
1230         for (auto &driver : m_CurrentProfile->drivers.values())
1231         {
1232             bool driverFound = false;
1233 
1234             for (auto &device : INDIListener::devices())
1235             {
1236                 if (device->getBaseDevice().getDriverName() == driver)
1237                 {
1238                     driverFound = true;
1239                     break;
1240                 }
1241             }
1242 
1243             if (driverFound == false)
1244                 remainingDevices << QString("+ %1").arg(driver);
1245         }
1246 
1247         if (remainingDevices.count() == 1)
1248         {
1249             QString message = i18n("Unable to remotely establish:\n%1\nPlease ensure the device is connected and powered on.",
1250                                    remainingDevices.at(0));
1251             appendLogText(message);
1252             KSNotification::event(QLatin1String("IndiServerMessage"), message, KSNotification::General, KSNotification::Warn);
1253             KNotification::beep(i18n("Ekos startup error"));
1254         }
1255         else
1256         {
1257             QString message = i18n("Unable to remotely establish the following devices:\n%1\nPlease ensure each device is connected "
1258                                    "and powered on.", remainingDevices.join("\n"));
1259             appendLogText(message);
1260             KSNotification::event(QLatin1String("IndiServerMessage"), message, KSNotification::General, KSNotification::Warn);
1261             KNotification::beep(i18n("Ekos startup error"));
1262         }
1263     }
1264 
1265     m_ekosStatus = Ekos::Error;
1266 }
1267 
1268 bool Manager::isINDIReady()
1269 {
1270     // Check if already connected
1271     int nConnected = 0;
1272 
1273     Ekos::CommunicationStatus previousStatus = m_indiStatus;
1274 
1275     auto devices = INDIListener::devices();
1276     for (auto &device : devices)
1277     {
1278         // Make sure we're not only connected, but also ready (i.e. all properties have already been defined).
1279         if (device->isConnected() && device->isReady())
1280             nConnected++;
1281     }
1282     if (devices.count() == nConnected)
1283     {
1284         m_indiStatus = Ekos::Success;
1285         emit indiStatusChanged(m_indiStatus);
1286         return true;
1287     }
1288 
1289     m_indiStatus = Ekos::Pending;
1290     if (previousStatus != m_indiStatus)
1291         emit indiStatusChanged(m_indiStatus);
1292 
1293     return false;
1294 }
1295 
1296 void Manager::connectDevices()
1297 {
1298     if (isINDIReady())
1299         return;
1300 
1301     auto devices = INDIListener::devices();
1302 
1303     for (auto &device : devices)
1304     {
1305         qCDebug(KSTARS_EKOS) << "Connecting " << device->getDeviceName();
1306         device->Connect();
1307     }
1308 
1309     connectB->setEnabled(false);
1310     disconnectB->setEnabled(true);
1311 
1312     appendLogText(i18n("Connecting INDI devices..."));
1313 }
1314 
1315 void Manager::disconnectDevices()
1316 {
1317     for (auto &device : INDIListener::devices())
1318     {
1319         qCDebug(KSTARS_EKOS) << "Disconnecting " << device->getDeviceName();
1320         device->Disconnect();
1321     }
1322 
1323     appendLogText(i18n("Disconnecting INDI devices..."));
1324 }
1325 
1326 void Manager::cleanDevices(bool stopDrivers)
1327 {
1328     if (m_ekosStatus == Ekos::Idle)
1329         return;
1330 
1331     if (mountModule())
1332         mountModule()->stopTimers();
1333 
1334     INDIListener::Instance()->disconnect(this);
1335     DriverManager::Instance()->disconnect(this);
1336 
1337     if (managedDrivers.isEmpty() == false)
1338     {
1339         if (m_LocalMode)
1340         {
1341             if (stopDrivers)
1342                 DriverManager::Instance()->stopDevices(managedDrivers);
1343         }
1344         else
1345         {
1346             if (stopDrivers)
1347             {
1348                 DriverManager::Instance()->disconnectRemoteHost(managedDrivers.first());
1349 
1350                 if (m_RemoteManagerStart && m_CurrentProfile->INDIWebManagerPort != -1)
1351                     INDI::WebManager::stopProfile(m_CurrentProfile);
1352             }
1353             m_RemoteManagerStart = false;
1354         }
1355     }
1356 
1357     reset();
1358 
1359     profileGroup->setEnabled(true);
1360 
1361     appendLogText(i18n("INDI services stopped."));
1362 }
1363 
1364 void Manager::processNewDevice(const QSharedPointer<ISD::GenericDevice> &device)
1365 {
1366     qCInfo(KSTARS_EKOS) << "Ekos received a new device: " << device->getDeviceName();
1367 
1368     Ekos::CommunicationStatus previousStatus = m_indiStatus;
1369 
1370     //    for(auto &oneDevice : INDIListener::devices())
1371     //    {
1372     //        if (oneDevice->getDeviceName() == device->getDeviceName())
1373     //        {
1374     //            qCWarning(KSTARS_EKOS) << "Found duplicate device, ignoring...";
1375     //            return;
1376     //        }
1377     //    }
1378 
1379     // Always reset INDI Connection status if we receive a new device
1380     m_indiStatus = Ekos::Idle;
1381     if (previousStatus != m_indiStatus)
1382         emit indiStatusChanged(m_indiStatus);
1383 
1384     m_DriverDevicesCount--;
1385 
1386     connect(device.get(), &ISD::GenericDevice::ready, this, &Ekos::Manager::setDeviceReady, Qt::UniqueConnection);
1387     connect(device.get(), &ISD::GenericDevice::newMount, this, &Ekos::Manager::addMount, Qt::UniqueConnection);
1388     connect(device.get(), &ISD::GenericDevice::newCamera, this, &Ekos::Manager::addCamera, Qt::UniqueConnection);
1389     connect(device.get(), &ISD::GenericDevice::newGuider, this, &Ekos::Manager::addGuider, Qt::UniqueConnection);
1390     connect(device.get(), &ISD::GenericDevice::newFilterWheel, this, &Ekos::Manager::addFilterWheel, Qt::UniqueConnection);
1391     connect(device.get(), &ISD::GenericDevice::newFocuser, this, &Ekos::Manager::addFocuser, Qt::UniqueConnection);
1392     connect(device.get(), &ISD::GenericDevice::newDome, this, &Ekos::Manager::addDome, Qt::UniqueConnection);
1393     connect(device.get(), &ISD::GenericDevice::newRotator, this, &Ekos::Manager::addRotator, Qt::UniqueConnection);
1394     connect(device.get(), &ISD::GenericDevice::newWeather, this, &Ekos::Manager::addWeather, Qt::UniqueConnection);
1395     connect(device.get(), &ISD::GenericDevice::newDustCap, this, &Ekos::Manager::addDustCap, Qt::UniqueConnection);
1396     connect(device.get(), &ISD::GenericDevice::newLightBox, this, &Ekos::Manager::addLightBox, Qt::UniqueConnection);
1397     connect(device.get(), &ISD::GenericDevice::newGPS, this, &Ekos::Manager::addGPS, Qt::UniqueConnection);
1398 
1399     connect(device.get(), &ISD::GenericDevice::Connected, this, &Ekos::Manager::deviceConnected, Qt::UniqueConnection);
1400     connect(device.get(), &ISD::GenericDevice::Disconnected, this, &Ekos::Manager::deviceDisconnected, Qt::UniqueConnection);
1401     connect(device.get(), &ISD::GenericDevice::propertyDefined, this, &Ekos::Manager::processNewProperty, Qt::UniqueConnection);
1402     connect(device.get(), &ISD::GenericDevice::propertyDeleted, this, &Ekos::Manager::processDeleteProperty,
1403             Qt::UniqueConnection);
1404     connect(device.get(), &ISD::GenericDevice::propertyUpdated, this, &Ekos::Manager::processUpdateProperty,
1405             Qt::UniqueConnection);
1406     connect(device.get(), &ISD::GenericDevice::interfaceDefined, this, &Ekos::Manager::syncActiveDevices, Qt::UniqueConnection);
1407     connect(device.get(), &ISD::GenericDevice::messageUpdated, this, &Ekos::Manager::processMessage, Qt::UniqueConnection);
1408 
1409 
1410 
1411     // Only look for primary & guider CCDs if we can tell a difference between them
1412     // otherwise rely on saved options
1413     if (m_CurrentProfile->ccd() != m_CurrentProfile->guider())
1414     {
1415         for (auto &oneCamera : INDIListener::devices())
1416         {
1417             if (oneCamera->getDeviceName().startsWith(m_CurrentProfile->ccd(), Qt::CaseInsensitive))
1418                 m_PrimaryCamera = QString(oneCamera->getDeviceName());
1419             else if (oneCamera->getDeviceName().startsWith(m_CurrentProfile->guider(), Qt::CaseInsensitive))
1420                 m_GuideCamera = QString(oneCamera->getDeviceName());
1421         }
1422     }
1423 
1424     if (m_DriverDevicesCount <= 0)
1425     {
1426         m_ekosStatus = Ekos::Success;
1427         emit ekosStatusChanged(m_ekosStatus);
1428 
1429         connectB->setEnabled(true);
1430         disconnectB->setEnabled(false);
1431 
1432         if (m_LocalMode == false && m_DriverDevicesCount == 0)
1433         {
1434             if (m_CurrentProfile->autoConnect)
1435                 appendLogText(i18n("Remote devices established."));
1436             else
1437                 appendLogText(i18n("Remote devices established. Please connect devices."));
1438         }
1439     }
1440 }
1441 
1442 void Manager::deviceConnected()
1443 {
1444     connectB->setEnabled(false);
1445     disconnectB->setEnabled(true);
1446     processINDIB->setEnabled(false);
1447 
1448     auto device = qobject_cast<ISD::GenericDevice *>(sender());
1449 
1450     if (Options::verboseLogging())
1451     {
1452         qCInfo(KSTARS_EKOS) << device->getDeviceName()
1453                             << "Version:" << device->getDriverVersion()
1454                             << "Interface:" << device->getDriverInterface()
1455                             << "is connected.";
1456     }
1457 
1458     if (Options::neverLoadConfig() == false)
1459     {
1460         INDIConfig tConfig = Options::loadConfigOnConnection() ? LOAD_LAST_CONFIG : LOAD_DEFAULT_CONFIG;
1461 
1462         for (auto &oneDevice : INDIListener::devices())
1463         {
1464             if (oneDevice == device)
1465             {
1466                 connect(device, &ISD::GenericDevice::propertyUpdated, this, &Ekos::Manager::watchDebugProperty, Qt::UniqueConnection);
1467 
1468                 auto configProp = device->getBaseDevice().getSwitch("CONFIG_PROCESS");
1469                 if (configProp && configProp.getState() == IPS_IDLE)
1470                     device->setConfig(tConfig);
1471                 break;
1472             }
1473         }
1474     }
1475 }
1476 
1477 void Manager::deviceDisconnected()
1478 {
1479     ISD::GenericDevice * dev = static_cast<ISD::GenericDevice *>(sender());
1480 
1481     Ekos::CommunicationStatus previousStatus = m_indiStatus;
1482 
1483     if (dev != nullptr)
1484     {
1485         if (dev->getState("CONNECTION") == IPS_ALERT)
1486             m_indiStatus = Ekos::Error;
1487         else if (dev->getState("CONNECTION") == IPS_BUSY)
1488             m_indiStatus = Ekos::Pending;
1489         else
1490             m_indiStatus = Ekos::Idle;
1491 
1492         if (Options::verboseLogging())
1493             qCDebug(KSTARS_EKOS) << dev->getDeviceName() << " is disconnected.";
1494 
1495         // In case a device fails to connect, display and log a useful message for the user.
1496         if (m_indiStatus == Ekos::Error)
1497         {
1498             QString message = i18n("%1 failed to connect.\nPlease ensure the device is connected and powered on.",
1499                                    dev->getDeviceName());
1500             appendLogText(message);
1501             KSNotification::event(QLatin1String("IndiServerMessage"), message, KSNotification::General, KSNotification::Warn);
1502         }
1503         else if (m_indiStatus == Ekos::Idle)
1504         {
1505             QString message = i18n("%1 is disconnected.", dev->getDeviceName());
1506             appendLogText(message);
1507         }
1508     }
1509     else
1510         m_indiStatus = Ekos::Idle;
1511 
1512     if (previousStatus != m_indiStatus)
1513         emit indiStatusChanged(m_indiStatus);
1514 
1515     connectB->setEnabled(true);
1516     disconnectB->setEnabled(false);
1517     processINDIB->setEnabled(true);
1518 }
1519 
1520 void Manager::addMount(ISD::Mount *device)
1521 {
1522     ekosLiveClient->message()->sendScopes();
1523 
1524     appendLogText(i18n("%1 is online.", device->getDeviceName()));
1525 
1526     emit newDevice(device->getDeviceName(), device->getDriverInterface());
1527 }
1528 
1529 void Manager::addCamera(ISD::Camera * device)
1530 {
1531     ekosLiveClient.get()->media()->registerCameras();
1532 
1533     appendLogText(i18n("%1 is online.", device->getDeviceName()));
1534 
1535     emit newDevice(device->getDeviceName(), device->getDriverInterface());
1536 }
1537 
1538 void Manager::addFilterWheel(ISD::FilterWheel * device)
1539 {
1540     QString name = device->getDeviceName();
1541     appendLogText(i18n("%1 filter is online.", name));
1542 
1543     createFilterManager(device);
1544 
1545     emit newDevice(name, device->getDriverInterface());
1546 }
1547 
1548 void Manager::addFocuser(ISD::Focuser *device)
1549 {
1550     appendLogText(i18n("%1 focuser is online.", device->getDeviceName()));
1551 
1552     emit newDevice(device->getDeviceName(), device->getDriverInterface());
1553 }
1554 
1555 void Manager::addRotator(ISD::Rotator *device)
1556 {
1557     appendLogText(i18n("Rotator %1 is online.", device->getDeviceName()));
1558 
1559     // createRotatorControl(device);
1560 
1561     emit newDevice(device->getDeviceName(), device->getDriverInterface());
1562 }
1563 
1564 void Manager::addDome(ISD::Dome * device)
1565 {
1566     appendLogText(i18n("%1 is online.", device->getDeviceName()));
1567 
1568     emit newDevice(device->getDeviceName(), device->getDriverInterface());
1569 }
1570 
1571 void Manager::addWeather(ISD::Weather * device)
1572 {
1573     appendLogText(i18n("%1 Weather is online.", device->getDeviceName()));
1574 
1575     emit newDevice(device->getDeviceName(), device->getDriverInterface());
1576 }
1577 
1578 void Manager::addGPS(ISD::GPS * device)
1579 {
1580     appendLogText(i18n("%1 GPS is online.", device->getDeviceName()));
1581 
1582     emit newDevice(device->getDeviceName(), device->getDriverInterface());
1583 }
1584 
1585 void Manager::addDustCap(ISD::DustCap * device)
1586 {
1587     OpticalTrainManager::Instance()->syncDevices();
1588 
1589     appendLogText(i18n("%1 Dust cap is online.", device->getDeviceName()));
1590 
1591     emit newDevice(device->getDeviceName(), device->getDriverInterface());
1592 }
1593 
1594 void Manager::addLightBox(ISD::LightBox * device)
1595 {
1596     appendLogText(i18n("%1 Light box is online.", device->getDeviceName()));
1597 
1598     emit newDevice(device->getDeviceName(), device->getDriverInterface());
1599 }
1600 
1601 void Manager::syncGenericDevice(const QSharedPointer<ISD::GenericDevice> &device)
1602 {
1603     createModules(device);
1604 
1605     ////////////////////////////////////////////////////////////////////////////////////////////////////
1606     /// Cameras
1607     ////////////////////////////////////////////////////////////////////////////////////////////////////
1608     auto camera = device->getCamera();
1609     if (camera)
1610     {
1611         // Focus Module
1612         if (focusProcess)
1613         {
1614             if (camera->hasCooler())
1615             {
1616                 QSharedPointer<ISD::GenericDevice> generic;
1617                 if (INDIListener::findDevice(camera->getDeviceName(), generic))
1618                     focusModule()->addTemperatureSource(generic);
1619             }
1620         }
1621 
1622     }
1623 
1624     ////////////////////////////////////////////////////////////////////////////////////////////////////
1625     /// Mount
1626     ////////////////////////////////////////////////////////////////////////////////////////////////////
1627 
1628     ////////////////////////////////////////////////////////////////////////////////////////////////////
1629     /// Focuser
1630     ////////////////////////////////////////////////////////////////////////////////////////////////////
1631     auto focuser = device->getFocuser();
1632     if (focuser)
1633     {
1634         if (focusProcess)
1635         {
1636             // Temperature sources.
1637             QSharedPointer<ISD::GenericDevice> generic;
1638             if (INDIListener::findDevice(focuser->getDeviceName(), generic))
1639                 focusModule()->addTemperatureSource(generic);
1640         }
1641     }
1642 
1643     ////////////////////////////////////////////////////////////////////////////////////////////////////
1644     /// Filter Wheel
1645     ////////////////////////////////////////////////////////////////////////////////////////////////////
1646 
1647     ////////////////////////////////////////////////////////////////////////////////////////////////////
1648     /// Rotators
1649     ////////////////////////////////////////////////////////////////////////////////////////////////////
1650 
1651     ////////////////////////////////////////////////////////////////////////////////////////////////////
1652     /// Domes
1653     ////////////////////////////////////////////////////////////////////////////////////////////////////
1654     auto dome = device->getDome();
1655     if (dome)
1656     {
1657         if (captureProcess)
1658             captureProcess->setDome(dome);
1659         if (alignProcess)
1660             alignProcess->setDome(dome);
1661         if (observatoryProcess)
1662             observatoryProcess->setDome(dome);
1663     }
1664 
1665     ////////////////////////////////////////////////////////////////////////////////////////////////////
1666     /// Weather
1667     ////////////////////////////////////////////////////////////////////////////////////////////////////
1668     auto weather = device->getWeather();
1669     if (weather)
1670     {
1671         if (observatoryProcess)
1672             observatoryProcess->addWeatherSource(weather);
1673 
1674         if (focusProcess)
1675         {
1676             QSharedPointer<ISD::GenericDevice> generic;
1677             if (INDIListener::findDevice(weather->getDeviceName(), generic))
1678                 focusModule()->addTemperatureSource(generic);
1679         }
1680     }
1681 
1682     ////////////////////////////////////////////////////////////////////////////////////////////////////
1683     /// GPS
1684     ////////////////////////////////////////////////////////////////////////////////////////////////////
1685     auto gps = device->getGPS();
1686     if (gps)
1687     {
1688         if (mountProcess)
1689             mountModule()->addGPS(gps);
1690     }
1691 
1692     ////////////////////////////////////////////////////////////////////////////////////////////////////
1693     /// Dust Cap
1694     ////////////////////////////////////////////////////////////////////////////////////////////////////
1695 
1696     ////////////////////////////////////////////////////////////////////////////////////////////////////
1697     /// Light Box
1698     ////////////////////////////////////////////////////////////////////////////////////////////////////
1699 }
1700 
1701 void Manager::removeDevice(const QSharedPointer<ISD::GenericDevice> &device)
1702 {
1703     if (alignProcess)
1704         alignModule()->removeDevice(device);
1705     if (captureProcess)
1706         captureProcess->removeDevice(device);
1707     if (focusProcess)
1708         focusModule()->removeDevice(device);
1709     if (mountProcess)
1710         mountModule()->removeDevice(device);
1711     if (guideProcess)
1712         guideProcess->removeDevice(device);
1713     if (observatoryProcess)
1714         observatoryProcess->removeDevice(device);
1715     if (m_PortSelector)
1716         m_PortSelector->removeDevice(device->getDeviceName());
1717 
1718     DarkLibrary::Instance()->removeDevice(device);
1719 
1720     // Remove from filter managers
1721     for (auto &oneManager : m_FilterManagers)
1722     {
1723         oneManager->removeDevice(device);
1724     }
1725 
1726     ekosLiveClient->message()->clearPendingProperties();
1727 
1728     // Remove from rotator controllers
1729     for (auto &oneController : m_RotatorControllers)
1730     {
1731         oneController->close();
1732     }
1733 
1734     appendLogText(i18n("%1 is offline.", device->getDeviceName()));
1735 
1736 
1737     if (INDIListener::devices().isEmpty())
1738     {
1739         cleanDevices();
1740         removeTabs();
1741     }
1742 }
1743 
1744 void Manager::processDeleteProperty(INDI::Property prop)
1745 {
1746     ekosLiveClient.get()->message()->processDeleteProperty(prop);
1747 }
1748 
1749 void Manager::processMessage(int id)
1750 {
1751     auto origin = static_cast<ISD::GenericDevice *>(sender());
1752     // Shouldn't happen
1753     if (!origin)
1754         return;
1755     QSharedPointer<ISD::GenericDevice> device;
1756     if (!INDIListener::findDevice(origin->getDeviceName(), device))
1757         return;
1758 
1759     ekosLiveClient.get()->message()->processMessage(device, id);
1760 }
1761 
1762 void Manager::processUpdateProperty(INDI::Property prop)
1763 {
1764     ekosLiveClient.get()->message()->processUpdateProperty(prop);
1765 
1766     if (prop.isNameMatch("CCD_INFO") ||
1767             prop.isNameMatch("GUIDER_INFO") ||
1768             prop.isNameMatch("CCD_FRAME") ||
1769             prop.isNameMatch("GUIDER_FRAME"))
1770     {
1771         if (focusModule() != nullptr)
1772             focusModule()->syncCameraInfo();
1773 
1774         if (guideModule() != nullptr)
1775             guideModule()->syncCameraInfo();
1776 
1777         if (alignModule() != nullptr)
1778             alignModule()->syncCameraInfo();
1779 
1780         return;
1781     }
1782     else if (prop.isNameMatch("ACTIVE_DEVICES"))
1783     {
1784         syncActiveDevices();
1785     }
1786 }
1787 
1788 void Manager::processNewProperty(INDI::Property prop)
1789 {
1790     QSharedPointer<ISD::GenericDevice> device;
1791     if (!INDIListener::findDevice(prop.getDeviceName(), device))
1792         return;
1793 
1794     settleTimer.start();
1795 
1796     ekosLiveClient.get()->message()->processNewProperty(prop);
1797 
1798     if (prop.isNameMatch("DEVICE_PORT_SCAN") || prop.isNameMatch("CONNECTION_TYPE"))
1799     {
1800         if (!m_PortSelector)
1801         {
1802             m_PortSelector.reset(new Selector::Dialog(KStars::Instance()));
1803             connect(m_PortSelector.get(), &Selector::Dialog::accepted, this, &Manager::setPortSelectionComplete);
1804         }
1805         m_PortSelectorTimer.start();
1806         portSelectorB->setEnabled(true);
1807         m_PortSelector->addDevice(device);
1808         return;
1809     }
1810 
1811     // Check if we need to turn on DEBUG for logging purposes
1812     if (prop.isNameMatch("DEBUG"))
1813     {
1814         uint16_t interface = device->getDriverInterface();
1815         if ( opsLogs->getINDIDebugInterface() & interface )
1816         {
1817             // Check if we need to enable debug logging for the INDI drivers.
1818             auto debugSP = prop.getSwitch();
1819             debugSP->at(0)->setState(ISS_ON);
1820             debugSP->at(1)->setState(ISS_OFF);
1821             device->sendNewProperty(debugSP);
1822         }
1823         return;
1824     }
1825 
1826     // Handle debug levels for logging purposes
1827     if (prop.isNameMatch("DEBUG_LEVEL"))
1828     {
1829         uint16_t interface = device->getDriverInterface();
1830         // Check if the logging option for the specific device class is on and if the device interface matches it.
1831         if ( opsLogs->getINDIDebugInterface() & interface )
1832         {
1833             // Turn on everything
1834             auto debugLevel = prop.getSwitch();
1835             for (auto &it : *debugLevel)
1836                 it.setState(ISS_ON);
1837 
1838             device->sendNewProperty(debugLevel);
1839         }
1840         return;
1841     }
1842 
1843     if (prop.isNameMatch("ACTIVE_DEVICES"))
1844     {
1845         if (device->getDriverInterface() > 0)
1846             syncActiveDevices();
1847 
1848         return;
1849     }
1850 
1851     if (prop.isNameMatch("ASTROMETRY_SOLVER"))
1852     {
1853         for (auto &oneDevice : INDIListener::devices())
1854         {
1855             if (oneDevice->getDeviceName() == prop.getDeviceName())
1856             {
1857                 initAlign();
1858                 alignModule()->setAstrometryDevice(oneDevice);
1859                 break;
1860             }
1861         }
1862 
1863         return;
1864     }
1865 
1866     if (focusModule() != nullptr && strstr(prop.getName(), "FOCUS_"))
1867     {
1868         focusModule()->checkFocuser();
1869         return;
1870     }
1871 }
1872 
1873 void Manager::processTabChange()
1874 {
1875     auto currentWidget = toolsWidget->currentWidget();
1876 
1877     if (alignProcess && alignModule() == currentWidget)
1878     {
1879         auto alignReady = alignModule()->isEnabled() == false && alignModule()->isParserOK();
1880         auto captureReady = captureProcess && captureModule()->isEnabled();
1881         auto mountReady = mountProcess && mountModule()->isEnabled();
1882         if (alignReady && captureReady && mountReady)
1883             alignModule()->setEnabled(true);
1884 
1885         alignModule()->checkCamera();
1886     }
1887     else if (captureProcess && currentWidget == captureModule())
1888     {
1889         captureModule()->process()->checkCamera();
1890     }
1891     else if (focusProcess && currentWidget == focusModule())
1892     {
1893         focusModule()->checkCamera();
1894     }
1895     else if (guideProcess && currentWidget == guideModule())
1896     {
1897         guideModule()->checkCamera();
1898     }
1899 
1900     updateLog();
1901 }
1902 
1903 void Manager::updateLog()
1904 {
1905     QWidget * currentWidget = toolsWidget->currentWidget();
1906 
1907     if (currentWidget == setupTab)
1908         ekosLogOut->setPlainText(m_LogText.join("\n"));
1909     else if (currentWidget == alignModule())
1910         ekosLogOut->setPlainText(alignModule()->getLogText());
1911     else if (currentWidget == captureModule())
1912         ekosLogOut->setPlainText(captureModule()->getLogText());
1913     else if (currentWidget == focusModule())
1914         ekosLogOut->setPlainText(focusModule()->getLogText());
1915     else if (currentWidget == guideModule())
1916         ekosLogOut->setPlainText(guideModule()->getLogText());
1917     else if (currentWidget == mountModule())
1918         ekosLogOut->setPlainText(mountModule()->getLogText());
1919     else if (currentWidget == schedulerModule())
1920         ekosLogOut->setPlainText(schedulerModule()->getLogText());
1921     else if (currentWidget == observatoryProcess.get())
1922         ekosLogOut->setPlainText(observatoryProcess->getLogText());
1923     else if (currentWidget == analyzeProcess.get())
1924         ekosLogOut->setPlainText(analyzeProcess->getLogText());
1925 
1926 #ifdef Q_OS_OSX
1927     repaint(); //This is a band-aid for a bug in QT 5.10.0
1928 #endif
1929 }
1930 
1931 void Manager::appendLogText(const QString &text)
1932 {
1933     m_LogText.insert(0, i18nc("log entry; %1 is the date, %2 is the text", "%1 %2",
1934                               KStarsData::Instance()->lt().toString("yyyy-MM-ddThh:mm:ss"), text));
1935 
1936     qCInfo(KSTARS_EKOS) << text;
1937 
1938     emit newLog(text);
1939 
1940     updateLog();
1941 }
1942 
1943 void Manager::clearLog()
1944 {
1945     QWidget * currentWidget = toolsWidget->currentWidget();
1946 
1947     if (currentWidget == setupTab)
1948     {
1949         m_LogText.clear();
1950         updateLog();
1951     }
1952     else if (currentWidget == alignModule())
1953         alignModule()->clearLog();
1954     else if (currentWidget == captureModule())
1955         captureModule()->clearLog();
1956     else if (currentWidget == focusModule())
1957         focusModule()->clearLog();
1958     else if (currentWidget == guideModule())
1959         guideModule()->clearLog();
1960     else if (currentWidget == mountModule())
1961         mountModule()->clearLog();
1962     else if (currentWidget == schedulerModule())
1963         schedulerModule()->clearLog();
1964     else if (currentWidget == observatoryProcess.get())
1965         observatoryProcess->clearLog();
1966     else if (currentWidget == analyzeProcess.get())
1967         analyzeProcess->clearLog();
1968 }
1969 
1970 void Manager::initCapture()
1971 {
1972     if (captureModule() != nullptr)
1973         return;
1974 
1975     captureProcess.reset(new Capture());
1976 
1977     emit newModule("Capture");
1978 
1979     // retrieve the meridian flip state machine from the mount module if the module is already present
1980     if (mountModule() != nullptr)
1981         captureModule()->setMeridianFlipState(mountModule()->getMeridianFlipState());
1982 
1983     capturePreview->shareCaptureModule(captureModule());
1984     int index = addModuleTab(EkosModule::Capture, captureModule(), QIcon(":/icons/ekos_ccd.png"));
1985     toolsWidget->tabBar()->setTabToolTip(index, i18nc("Charge-Coupled Device", "CCD"));
1986     if (Options::ekosLeftIcons())
1987     {
1988         QTransform trans;
1989         trans.rotate(90);
1990         QIcon icon  = toolsWidget->tabIcon(index);
1991         QPixmap pix = icon.pixmap(QSize(48, 48));
1992         icon        = QIcon(pix.transformed(trans));
1993         toolsWidget->setTabIcon(index, icon);
1994     }
1995     connect(captureModule(), &Ekos::Capture::newLog, this, &Ekos::Manager::updateLog);
1996     connect(captureModule(), &Ekos::Capture::newLog, ekosLiveClient.get()->message(),
1997             [this]()
1998     {
1999         QJsonObject cStatus =
2000         {
2001             {"log", captureModule()->getLogText()}
2002         };
2003 
2004         ekosLiveClient.get()->message()->updateCaptureStatus(cStatus);
2005     });
2006     connect(captureModule(), &Ekos::Capture::newStatus, this, &Ekos::Manager::updateCaptureStatus);
2007     connect(captureModule(), &Ekos::Capture::newImage, this, &Ekos::Manager::updateCaptureProgress);
2008     connect(captureModule(), &Ekos::Capture::driverTimedout, this, &Ekos::Manager::restartDriver);
2009     connect(captureModule(), &Ekos::Capture::newExposureProgress, this, &Ekos::Manager::updateExposureProgress);
2010     capturePreview->setEnabled(true);
2011 
2012     // display capture status changes
2013     connect(captureModule(), &Ekos::Capture::newFilterStatus, capturePreview->captureStatusWidget,
2014             &LedStatusWidget::setFilterState);
2015 
2016     // display target drift
2017     connect(schedulerModule(), &Ekos::Scheduler::targetDistance,
2018             captureModule(), &Ekos::Capture::updateTargetDistance,  Qt::UniqueConnection);
2019     connect(schedulerModule(), &Ekos::Scheduler::targetDistance, this, [this](double distance)
2020     {
2021         capturePreview->updateTargetDistance(distance);
2022     });
2023 
2024 
2025     connectModules();
2026 }
2027 
2028 void Manager::initAlign()
2029 {
2030     if (alignModule() != nullptr)
2031         return;
2032 
2033     alignProcess.reset(new Ekos::Align(m_CurrentProfile));
2034 
2035     emit newModule("Align");
2036 
2037     int index = addModuleTab(EkosModule::Align, alignModule(), QIcon(":/icons/ekos_align.png"));
2038     toolsWidget->tabBar()->setTabToolTip(index, i18n("Align"));
2039     connect(alignModule(), &Ekos::Align::newLog, this, &Ekos::Manager::updateLog);
2040     if (Options::ekosLeftIcons())
2041     {
2042         QTransform trans;
2043         trans.rotate(90);
2044         QIcon icon  = toolsWidget->tabIcon(index);
2045         QPixmap pix = icon.pixmap(QSize(48, 48));
2046         icon        = QIcon(pix.transformed(trans));
2047         toolsWidget->setTabIcon(index, icon);
2048     }
2049 
2050     connectModules();
2051 }
2052 
2053 void Manager::initFocus()
2054 {
2055     if (focusModule() != nullptr)
2056         return;
2057 
2058     focusProcess.reset(new Ekos::Focus());
2059 
2060     emit newModule("Focus");
2061 
2062     int index    = addModuleTab(EkosModule::Focus, focusModule(), QIcon(":/icons/ekos_focus.png"));
2063 
2064     toolsWidget->tabBar()->setTabToolTip(index, i18n("Focus"));
2065 
2066     // Focus <---> Manager connections
2067     connect(focusModule(), &Ekos::Focus::newLog, this, &Ekos::Manager::updateLog);
2068     connect(focusModule(), &Ekos::Focus::newStatus, this, &Ekos::Manager::updateFocusStatus);
2069     connect(focusModule(), &Ekos::Focus::newStarPixmap, focusManager, &Ekos::FocusManager::updateFocusStarPixmap);
2070     connect(focusModule(), &Ekos::Focus::newHFR, this, &Ekos::Manager::updateCurrentHFR);
2071     connect(focusModule(), &Ekos::Focus::focuserTimedout, this, &Ekos::Manager::restartDriver);
2072 
2073     // connect HFR plot widget
2074     connect(focusModule(), &Ekos::Focus::initHFRPlot, focusManager->hfrVPlot, &FocusHFRVPlot::init);
2075     connect(focusModule(), &Ekos::Focus::redrawHFRPlot, focusManager->hfrVPlot, &FocusHFRVPlot::redraw);
2076     connect(focusModule(), &Ekos::Focus::newHFRPlotPosition, focusManager->hfrVPlot, &FocusHFRVPlot::addPosition);
2077     connect(focusModule(), &Ekos::Focus::drawPolynomial, focusManager->hfrVPlot, &FocusHFRVPlot::drawPolynomial);
2078     connect(focusModule(), &Ekos::Focus::setTitle, focusManager->hfrVPlot, &FocusHFRVPlot::setTitle);
2079     connect(focusModule(), &Ekos::Focus::finalUpdates, focusManager->hfrVPlot, &FocusHFRVPlot::finalUpdates);
2080     connect(focusModule(), &Ekos::Focus::minimumFound, focusManager->hfrVPlot, &FocusHFRVPlot::drawMinimum);
2081     // setup signal/slots for Linear 1 Pass focus algo
2082     connect(focusModule(), &Ekos::Focus::drawCurve, focusManager->hfrVPlot, &FocusHFRVPlot::drawCurve);
2083     connect(focusModule(), &Ekos::Focus::drawCFZ, focusManager->hfrVPlot, &FocusHFRVPlot::drawCFZ);
2084 
2085     if (Options::ekosLeftIcons())
2086     {
2087         QTransform trans;
2088         trans.rotate(90);
2089         QIcon icon  = toolsWidget->tabIcon(index);
2090         QPixmap pix = icon.pixmap(QSize(48, 48));
2091         icon        = QIcon(pix.transformed(trans));
2092         toolsWidget->setTabIcon(index, icon);
2093     }
2094 
2095     focusManager->init();
2096     focusManager->setEnabled(true);
2097 
2098     for (auto &oneDevice : INDIListener::devices())
2099     {
2100         auto prop1  = oneDevice->getProperty("CCD_TEMPERATURE");
2101         auto prop2  = oneDevice->getProperty("FOCUSER_TEMPERATURE");
2102         auto prop3  = oneDevice->getProperty("WEATHER_PARAMETERS");
2103         if (prop1 || prop2 || prop3)
2104             focusModule()->addTemperatureSource(oneDevice);
2105     }
2106 
2107     connectModules();
2108 }
2109 
2110 void Manager::updateCurrentHFR(double newHFR, int position, bool inAutofocus)
2111 {
2112     Q_UNUSED(inAutofocus);
2113     focusManager->updateCurrentHFR(newHFR);
2114 
2115     QJsonObject cStatus =
2116     {
2117         {"hfr", newHFR},
2118         {"pos", position}
2119     };
2120 
2121     ekosLiveClient.get()->message()->updateFocusStatus(cStatus);
2122 }
2123 
2124 void Manager::updateSigmas(double ra, double de)
2125 {
2126     guideManager->updateSigmas(ra, de);
2127 
2128     QJsonObject cStatus = { {"rarms", ra}, {"derms", de} };
2129 
2130     ekosLiveClient.get()->message()->updateGuideStatus(cStatus);
2131 }
2132 
2133 void Manager::initMount()
2134 {
2135     if (mountModule() != nullptr)
2136         return;
2137 
2138     mountProcess.reset(new Ekos::Mount());
2139 
2140     // share the meridian flip state with capture if the module is already present
2141     if (captureModule() != nullptr)
2142         captureModule()->setMeridianFlipState(mountModule()->getMeridianFlipState());
2143 
2144     emit newModule("Mount");
2145 
2146     int index    = addModuleTab(EkosModule::Mount, mountModule(), QIcon(":/icons/ekos_mount.png"));
2147 
2148     toolsWidget->tabBar()->setTabToolTip(index, i18n("Mount"));
2149     connect(mountModule(), &Ekos::Mount::newLog, this, &Ekos::Manager::updateLog);
2150     connect(mountModule(), &Ekos::Mount::newCoords, this, &Ekos::Manager::updateMountCoords);
2151     connect(mountModule(), &Ekos::Mount::newStatus, this, &Ekos::Manager::updateMountStatus);
2152     connect(mountModule(), &Ekos::Mount::newTargetName, this, [this](const QString & name)
2153     {
2154         setTarget(name);
2155     });
2156     connect(mountModule(), &Ekos::Mount::pierSideChanged, this, [&](ISD::Mount::PierSide side)
2157     {
2158         ekosLiveClient.get()->message()->updateMountStatus(QJsonObject({{"pierSide", side}}));
2159     });
2160     connect(mountModule()->getMeridianFlipState().get(),
2161             &Ekos::MeridianFlipState::newMountMFStatus, [&](MeridianFlipState::MeridianFlipMountState status)
2162     {
2163         ekosLiveClient.get()->message()->updateMountStatus(QJsonObject(
2164         {
2165             {"meridianFlipStatus", status},
2166         }));
2167     });
2168     connect(mountModule()->getMeridianFlipState().get(),
2169             &Ekos::MeridianFlipState::newMeridianFlipMountStatusText, [&](const QString & text)
2170     {
2171         // Throttle this down
2172         ekosLiveClient.get()->message()->updateMountStatus(QJsonObject(
2173         {
2174             {"meridianFlipText", text},
2175         }), mountModule()->getMeridianFlipState()->getMeridianFlipMountState() == MeridianFlipState::MOUNT_FLIP_NONE);
2176         meridianFlipStatusWidget->setStatus(text);
2177     });
2178     connect(mountModule(), &Ekos::Mount::autoParkCountdownUpdated, this, [&](const QString & text)
2179     {
2180         ekosLiveClient.get()->message()->updateMountStatus(QJsonObject({{"autoParkCountdown", text}}), true);
2181     });
2182 
2183     connect(mountModule(), &Ekos::Mount::trainChanged, ekosLiveClient.get()->message(),
2184             &EkosLive::Message::sendTrainProfiles, Qt::UniqueConnection);
2185 
2186     connect(mountModule(), &Ekos::Mount::slewRateChanged, this, [&](int slewRate)
2187     {
2188         QJsonObject status = { { "slewRate", slewRate} };
2189         ekosLiveClient.get()->message()->updateMountStatus(status);
2190     });
2191 
2192     if (Options::ekosLeftIcons())
2193     {
2194         QTransform trans;
2195         trans.rotate(90);
2196         QIcon icon  = toolsWidget->tabIcon(index);
2197         QPixmap pix = icon.pixmap(QSize(48, 48));
2198         icon        = QIcon(pix.transformed(trans));
2199         toolsWidget->setTabIcon(index, icon);
2200     }
2201 
2202     mountGroup->setEnabled(true);
2203     capturePreview->shareMountModule(mountModule());
2204 
2205     connectModules();
2206 }
2207 
2208 void Manager::initGuide()
2209 {
2210     if (guideModule() == nullptr)
2211     {
2212         guideProcess.reset(new Ekos::Guide());
2213 
2214         emit newModule("Guide");
2215     }
2216 
2217     if (toolsWidget->indexOf(guideModule()) == -1)
2218     {
2219         // if (managedDevices.contains(KSTARS_TELESCOPE) && managedDevices.value(KSTARS_TELESCOPE)->isConnected())
2220         // guideProcess->addMount(managedDevices.value(KSTARS_TELESCOPE));
2221 
2222         int index = addModuleTab(EkosModule::Guide, guideModule(), QIcon(":/icons/ekos_guide.png"));
2223         toolsWidget->tabBar()->setTabToolTip(index, i18n("Guide"));
2224         connect(guideModule(), &Ekos::Guide::newLog, this, &Ekos::Manager::updateLog);
2225         connect(guideModule(), &Ekos::Guide::driverTimedout, this, &Ekos::Manager::restartDriver);
2226 
2227         guideManager->setEnabled(true);
2228 
2229         connect(guideModule(), &Ekos::Guide::newStatus, this, &Ekos::Manager::updateGuideStatus);
2230         connect(guideModule(), &Ekos::Guide::newStarPixmap, guideManager, &Ekos::GuideManager::updateGuideStarPixmap);
2231         connect(guideModule(), &Ekos::Guide::newAxisSigma, this, &Ekos::Manager::updateSigmas);
2232         connect(guideModule(), &Ekos::Guide::newAxisDelta, [&](double ra, double de)
2233         {
2234             QJsonObject status = { { "drift_ra", ra}, {"drift_de", de} };
2235             ekosLiveClient.get()->message()->updateGuideStatus(status);
2236         });
2237 
2238         if (Options::ekosLeftIcons())
2239         {
2240             QTransform trans;
2241             trans.rotate(90);
2242             QIcon icon  = toolsWidget->tabIcon(index);
2243             QPixmap pix = icon.pixmap(QSize(48, 48));
2244             icon        = QIcon(pix.transformed(trans));
2245             toolsWidget->setTabIcon(index, icon);
2246         }
2247         guideManager->init(guideModule());
2248     }
2249 
2250     connectModules();
2251 }
2252 
2253 void Manager::initObservatory()
2254 {
2255     if (observatoryProcess.get() == nullptr)
2256     {
2257         // Initialize the Observatory Module
2258         observatoryProcess.reset(new Ekos::Observatory());
2259 
2260         emit newModule("Observatory");
2261 
2262         int index = addModuleTab(EkosModule::Observatory, observatoryProcess.get(), QIcon(":/icons/ekos_observatory.png"));
2263         toolsWidget->tabBar()->setTabToolTip(index, i18n("Observatory"));
2264         connect(observatoryProcess.get(), &Ekos::Observatory::newLog, this, &Ekos::Manager::updateLog);
2265 
2266         if (Options::ekosLeftIcons())
2267         {
2268             QTransform trans;
2269             trans.rotate(90);
2270             QIcon icon  = toolsWidget->tabIcon(index);
2271             QPixmap pix = icon.pixmap(QSize(48, 48));
2272             icon        = QIcon(pix.transformed(trans));
2273             toolsWidget->setTabIcon(index, icon);
2274         }
2275     }
2276 }
2277 
2278 void Manager::addGuider(ISD::Guider * device)
2279 {
2280     appendLogText(i18n("Guider port from %1 is ready.", device->getDeviceName()));
2281 }
2282 
2283 void Manager::removeTabs()
2284 {
2285     disconnect(toolsWidget, &QTabWidget::currentChanged, this, &Ekos::Manager::processTabChange);
2286 
2287     for (int i = numPermanentTabs; i < toolsWidget->count(); i++)
2288         toolsWidget->removeTab(i);
2289 
2290     alignProcess.reset();
2291     captureProcess.reset();
2292     focusProcess.reset();
2293     guideProcess.reset();
2294     mountProcess.reset();
2295     observatoryProcess.reset();
2296 
2297     connect(toolsWidget, &QTabWidget::currentChanged, this, &Ekos::Manager::processTabChange, Qt::UniqueConnection);
2298 }
2299 
2300 bool Manager::isRunning(const QString &process)
2301 {
2302     QProcess ps;
2303 #ifdef Q_OS_OSX
2304     ps.start("pgrep", QStringList() << process);
2305     ps.waitForFinished();
2306     QString output = ps.readAllStandardOutput();
2307     return output.length() > 0;
2308 #else
2309     ps.start("ps", QStringList() << "-o"
2310              << "comm"
2311              << "--no-headers"
2312              << "-C" << process);
2313     ps.waitForFinished();
2314     QString output = ps.readAllStandardOutput();
2315     return output.contains(process);
2316 #endif
2317 }
2318 
2319 void Manager::addObjectToScheduler(SkyObject * object)
2320 {
2321     if (schedulerModule() != nullptr)
2322         schedulerModule()->addObject(object);
2323 }
2324 
2325 QString Manager::getCurrentJobName()
2326 {
2327     return schedulerModule()->getCurrentJobName();
2328 }
2329 
2330 bool Manager::setProfile(const QString &profileName)
2331 {
2332     int index = profileCombo->findText(profileName);
2333 
2334     if (index < 0)
2335         return false;
2336 
2337     profileCombo->setCurrentIndex(index);
2338 
2339     return true;
2340 }
2341 
2342 void Manager::editNamedProfile(const QJsonObject &profileInfo)
2343 {
2344     ProfileEditor editor(this);
2345     setProfile(profileInfo["name"].toString());
2346     if (getCurrentProfile(m_CurrentProfile))
2347     {
2348         editor.setPi(m_CurrentProfile);
2349         editor.setSettings(profileInfo);
2350         editor.saveProfile();
2351     }
2352 }
2353 
2354 void Manager::addNamedProfile(const QJsonObject &profileInfo)
2355 {
2356     ProfileEditor editor(this);
2357 
2358     editor.setSettings(profileInfo);
2359     editor.saveProfile();
2360     profiles.clear();
2361     loadProfiles();
2362     profileCombo->setCurrentIndex(profileCombo->count() - 1);
2363     getCurrentProfile(m_CurrentProfile);
2364 }
2365 
2366 void Manager::deleteNamedProfile(const QString &name)
2367 {
2368     if (!getCurrentProfile(m_CurrentProfile))
2369         return;
2370 
2371     for (auto &pi : profiles)
2372     {
2373         // Do not delete an actively running profile
2374         // Do not delete simulator profile
2375         if (pi->name == "Simulators" || pi->name != name || (pi.get() == m_CurrentProfile && ekosStatus() != Idle))
2376             continue;
2377 
2378         KStarsData::Instance()->userdb()->PurgeProfile(pi);
2379         profiles.clear();
2380         loadProfiles();
2381         getCurrentProfile(m_CurrentProfile);
2382         return;
2383     }
2384 }
2385 
2386 QJsonObject Manager::getNamedProfile(const QString &name)
2387 {
2388     QJsonObject profileInfo;
2389 
2390     // Get current profile
2391     for (auto &pi : profiles)
2392     {
2393         if (name == pi->name)
2394             return pi->toJson();
2395     }
2396 
2397     return QJsonObject();
2398 }
2399 
2400 QStringList Manager::getProfiles()
2401 {
2402     QStringList profiles;
2403 
2404     for (int i = 0; i < profileCombo->count(); i++)
2405         profiles << profileCombo->itemText(i);
2406 
2407     return profiles;
2408 }
2409 
2410 void Manager::addProfile()
2411 {
2412     ProfileEditor editor(this);
2413 
2414     if (editor.exec() == QDialog::Accepted)
2415     {
2416         profiles.clear();
2417         loadProfiles();
2418         profileCombo->setCurrentIndex(profileCombo->count() - 1);
2419     }
2420 
2421     getCurrentProfile(m_CurrentProfile);
2422 }
2423 
2424 void Manager::editProfile()
2425 {
2426     ProfileEditor editor(this);
2427 
2428     if (getCurrentProfile(m_CurrentProfile))
2429     {
2430 
2431         editor.setPi(m_CurrentProfile);
2432 
2433         if (editor.exec() == QDialog::Accepted)
2434         {
2435             int currentIndex = profileCombo->currentIndex();
2436 
2437             profiles.clear();
2438             loadProfiles();
2439             profileCombo->setCurrentIndex(currentIndex);
2440         }
2441 
2442         getCurrentProfile(m_CurrentProfile);
2443     }
2444 }
2445 
2446 void Manager::deleteProfile()
2447 {
2448     if (!getCurrentProfile(m_CurrentProfile))
2449         return;
2450 
2451     if (m_CurrentProfile->name == "Simulators")
2452         return;
2453 
2454     auto executeDeleteProfile = [&]()
2455     {
2456         KStarsData::Instance()->userdb()->PurgeProfile(m_CurrentProfile);
2457         profiles.clear();
2458         loadProfiles();
2459         getCurrentProfile(m_CurrentProfile);
2460     };
2461 
2462     connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, executeDeleteProfile]()
2463     {
2464         //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
2465         KSMessageBox::Instance()->disconnect(this);
2466         executeDeleteProfile();
2467     });
2468 
2469     KSMessageBox::Instance()->questionYesNo(i18n("Are you sure you want to delete the profile?"),
2470                                             i18n("Confirm Delete"));
2471 
2472 }
2473 
2474 void Manager::wizardProfile()
2475 {
2476     ProfileWizard wz;
2477     if (wz.exec() != QDialog::Accepted)
2478         return;
2479 
2480     ProfileEditor editor(this);
2481 
2482     editor.setProfileName(wz.profileName);
2483     editor.setAuxDrivers(wz.selectedAuxDrivers());
2484     if (wz.useInternalServer == false)
2485         editor.setHostPort(wz.host, wz.port);
2486     editor.setWebManager(wz.useWebManager);
2487     editor.setGuiderType(wz.selectedExternalGuider());
2488     // Disable connection options
2489     editor.setConnectionOptionsEnabled(false);
2490 
2491     if (editor.exec() == QDialog::Accepted)
2492     {
2493         profiles.clear();
2494         loadProfiles();
2495         profileCombo->setCurrentIndex(profileCombo->count() - 1);
2496     }
2497 
2498     getCurrentProfile(m_CurrentProfile);
2499 }
2500 
2501 bool Manager::getCurrentProfile(QSharedPointer<ProfileInfo> &profile) const
2502 {
2503     // Get current profile
2504     for (auto &pi : profiles)
2505     {
2506         if (profileCombo->currentText() == pi->name)
2507         {
2508             profile = pi;
2509             return true;
2510         }
2511     }
2512 
2513     return false;
2514 }
2515 
2516 void Manager::updateProfileLocation(const QSharedPointer<ProfileInfo> &profile)
2517 {
2518     if (profile->city.isEmpty() == false)
2519     {
2520         bool cityFound = KStars::Instance()->setGeoLocation(profile->city, profile->province, profile->country);
2521         if (cityFound)
2522             appendLogText(i18n("Site location updated to %1.", KStarsData::Instance()->geo()->fullName()));
2523         else
2524             appendLogText(i18n("Failed to update site location to %1. City not found.",
2525                                KStarsData::Instance()->geo()->fullName()));
2526     }
2527 }
2528 
2529 void Manager::updateMountStatus(ISD::Mount::Status status)
2530 {
2531     static ISD::Mount::Status lastStatus = ISD::Mount::MOUNT_IDLE;
2532 
2533     if (status == lastStatus)
2534         return;
2535 
2536     lastStatus = status;
2537 
2538     mountStatus->setMountState(mountModule()->statusString(), status);
2539     mountStatus->setStyleSheet(QString());
2540 
2541     QJsonObject cStatus =
2542     {
2543         {"status", mountModule()->statusString(false)}
2544     };
2545 
2546     ekosLiveClient.get()->message()->updateMountStatus(cStatus);
2547 }
2548 
2549 void Manager::updateMountCoords(const SkyPoint position, ISD::Mount::PierSide pierSide, const dms &ha)
2550 {
2551     Q_UNUSED(pierSide)
2552     raOUT->setText(position.ra().toHMSString());
2553     decOUT->setText(position.dec().toDMSString());
2554     azOUT->setText(position.az().toDMSString());
2555     altOUT->setText(position.alt().toDMSString());
2556 
2557     QJsonObject cStatus =
2558     {
2559         {"ra", dms::fromString(raOUT->text(), false).Degrees()},
2560         {"de", dms::fromString(decOUT->text(), true).Degrees()},
2561         {"ra0", position.ra0().Degrees()},
2562         {"de0", position.dec0().Degrees()},
2563         {"az", dms::fromString(azOUT->text(), true).Degrees()},
2564         {"at", dms::fromString(altOUT->text(), true).Degrees()},
2565         {"ha", ha.Degrees()},
2566     };
2567 
2568     ekosLiveClient.get()->message()->updateMountStatus(cStatus, true);
2569 }
2570 
2571 void Manager::updateCaptureStatus(Ekos::CaptureState status)
2572 {
2573     capturePreview->updateCaptureStatus(status);
2574 
2575     switch (status)
2576     {
2577         case Ekos::CAPTURE_IDLE:
2578         /* Fall through */
2579         case Ekos::CAPTURE_ABORTED:
2580         /* Fall through */
2581         case Ekos::CAPTURE_COMPLETE:
2582             m_CountdownTimer.stop();
2583             break;
2584         case Ekos::CAPTURE_CAPTURING:
2585             m_CountdownTimer.start();
2586             break;
2587         default:
2588             break;
2589     }
2590 
2591     QJsonObject cStatus =
2592     {
2593         {"status", captureStates[status]},
2594         {"seqt", capturePreview->captureCountsWidget->sequenceRemainingTime->text()},
2595         {"ovt", capturePreview->captureCountsWidget->overallRemainingTime->text()}
2596     };
2597 
2598     ekosLiveClient.get()->message()->updateCaptureStatus(cStatus);
2599 }
2600 
2601 void Manager::updateCaptureProgress(Ekos::SequenceJob * job, const QSharedPointer<FITSData> &data)
2602 {
2603     capturePreview->updateJobProgress(job, data);
2604 
2605     QJsonObject status =
2606     {
2607         {"seqv", job->getCompleted()},
2608         {"seqr", job->getCoreProperty(SequenceJob::SJ_Count).toInt()},
2609         {"seql", capturePreview->captureCountsWidget->sequenceRemainingTime->text()}
2610     };
2611 
2612     ekosLiveClient.get()->message()->updateCaptureStatus(status);
2613 
2614     //const QString filename = ;
2615     //if (!filename.isEmpty() && job->getStatus() == SequenceJob::JOB_BUSY)
2616     if (data && job->getStatus() == JOB_BUSY)
2617     {
2618         QString uuid = QUuid::createUuid().toString();
2619         uuid = uuid.remove(QRegularExpression("[-{}]"));
2620 
2621         // Normally FITS Viewer would trigger an upload
2622         // If off, then rely on summary view or raw data
2623         if (Options::useFITSViewer() == false)
2624         {
2625             if (Options::useSummaryPreview())
2626                 ekosLiveClient.get()->media()->sendView(m_SummaryView, uuid);
2627 
2628             else
2629                 ekosLiveClient.get()->media()->sendData(data, uuid);
2630         }
2631 
2632         if (job->jobType() != SequenceJob::JOBTYPE_PREVIEW)
2633             ekosLiveClient.get()->cloud()->upload(data, uuid);
2634 
2635     }
2636 }
2637 
2638 void Manager::updateExposureProgress(Ekos::SequenceJob * job)
2639 {
2640     QJsonObject status
2641     {
2642         {"expv", job->getExposeLeft()},
2643         {"expr", job->getCoreProperty(SequenceJob::SJ_Exposure).toDouble()}
2644     };
2645 
2646     ekosLiveClient.get()->message()->updateCaptureStatus(status);
2647 }
2648 
2649 void Manager::updateCaptureCountDown()
2650 {
2651     capturePreview->updateCaptureCountDown(-1);
2652 
2653     QJsonObject status =
2654     {
2655         {"seqt", capturePreview->captureCountsWidget->sequenceRemainingTime->text()},
2656         {"ovt", capturePreview->captureCountsWidget->overallRemainingTime->text()},
2657         {"ovp", capturePreview->captureCountsWidget->gr_overallProgressBar->value()},
2658         {"ovl", capturePreview->captureCountsWidget->gr_overallLabel->text()}
2659     };
2660 
2661     ekosLiveClient.get()->message()->updateCaptureStatus(status);
2662 }
2663 
2664 
2665 void Manager::updateFocusStatus(Ekos::FocusState status)
2666 {
2667     focusManager->updateFocusStatus(status);
2668 
2669     QJsonObject cStatus =
2670     {
2671         {"status", getFocusStatusString(status, false)}
2672     };
2673 
2674     ekosLiveClient.get()->message()->updateFocusStatus(cStatus);
2675 }
2676 
2677 void Manager::updateGuideStatus(Ekos::GuideState status)
2678 {
2679     guideManager->updateGuideStatus(status);
2680     QJsonObject cStatus =
2681     {
2682         {"status", getGuideStatusString(status, false)}
2683     };
2684 
2685     ekosLiveClient.get()->message()->updateGuideStatus(cStatus);
2686 }
2687 
2688 void Manager::setTarget(const QString &name)
2689 {
2690     capturePreview->targetLabel->setVisible(!name.isEmpty());
2691     capturePreview->mountTarget->setVisible(!name.isEmpty());
2692     capturePreview->mountTarget->setText(name);
2693     ekosLiveClient.get()->message()->updateMountStatus(QJsonObject({{"target", name}}));
2694 }
2695 
2696 void Manager::showEkosOptions()
2697 {
2698     QWidget * currentWidget = toolsWidget->currentWidget();
2699 
2700     if (alignModule() && alignModule() == currentWidget)
2701     {
2702         KConfigDialog * alignSettings = KConfigDialog::exists("alignsettings");
2703         if (alignSettings)
2704         {
2705             alignSettings->setEnabled(true);
2706             alignSettings->show();
2707         }
2708         return;
2709     }
2710 
2711     if (guideModule() && guideModule() == currentWidget)
2712     {
2713         KConfigDialog::showDialog("guidesettings");
2714         return;
2715     }
2716 
2717     if (focusModule() && focusModule() == currentWidget)
2718     {
2719         KConfigDialog * focusSettings = KConfigDialog::exists("focussettings");
2720         if (focusSettings)
2721         {
2722             focusSettings->show();
2723             focusSettings->raise();
2724         }
2725         return;
2726     }
2727 
2728     if ((captureModule() && captureModule() == currentWidget) ||
2729             (schedulerModule() && schedulerModule() == currentWidget))
2730     {
2731         if (opsEkos)
2732         {
2733             // Scheduler is tab 1, Capture is tab 2.
2734             const int index = schedulerModule() == currentWidget ? 1 : 2;
2735             opsEkos->setCurrentIndex(index);
2736         }
2737         KConfigDialog * cDialog = KConfigDialog::exists("settings");
2738         if (cDialog)
2739         {
2740             cDialog->setCurrentPage(ekosOptionsWidget);
2741             cDialog->show();
2742             cDialog->raise();  // for MacOS
2743             cDialog->activateWindow(); // for Windows
2744         }
2745         return;
2746     }
2747 
2748     if (ekosOptionsWidget == nullptr)
2749     {
2750         optionsB->click();
2751     }
2752     else if (KConfigDialog::showDialog("settings"))
2753     {
2754         KConfigDialog * cDialog = KConfigDialog::exists("settings");
2755         if (cDialog)
2756         {
2757             cDialog->setCurrentPage(ekosOptionsWidget);
2758             cDialog->show();
2759             cDialog->raise();  // for MacOS
2760             cDialog->activateWindow(); // for Windows
2761         }
2762     }
2763 }
2764 
2765 void Manager::updateDebugInterfaces()
2766 {
2767     KSUtils::Logging::SyncFilterRules();
2768 
2769     for (auto &device : INDIListener::devices())
2770     {
2771         auto debugProp = device->getProperty("DEBUG");
2772         if (!debugProp)
2773             continue;
2774 
2775         auto debugSP = debugProp.getSwitch();
2776 
2777         // Check if the debug interface matches the driver device class
2778         if ( ( opsLogs->getINDIDebugInterface() & device->getDriverInterface() ) &&
2779                 debugSP->sp[0].s != ISS_ON)
2780         {
2781             debugSP->at(0)->setState(ISS_ON);
2782             debugSP->at(1)->setState(ISS_OFF);
2783             device->sendNewProperty(debugSP);
2784             appendLogText(i18n("Enabling debug logging for %1...", device->getDeviceName()));
2785         }
2786         else if ( !( opsLogs->getINDIDebugInterface() & device->getDriverInterface() ) &&
2787                   debugSP->sp[0].s != ISS_OFF)
2788         {
2789             debugSP->at(0)->setState(ISS_OFF);
2790             debugSP->at(1)->setState(ISS_ON);
2791             device->sendNewProperty(debugSP);
2792             appendLogText(i18n("Disabling debug logging for %1...", device->getDeviceName()));
2793         }
2794 
2795         if (opsLogs->isINDISettingsChanged())
2796             device->setConfig(SAVE_CONFIG);
2797     }
2798 }
2799 
2800 void Manager::watchDebugProperty(INDI::Property prop)
2801 {
2802     if (prop.isNameMatch("DEBUG"))
2803     {
2804         auto svp = prop.getSwitch();
2805 
2806         ISD::GenericDevice * deviceInterface = qobject_cast<ISD::GenericDevice *>(sender());
2807 
2808         // We don't process pure general interfaces
2809         if (deviceInterface->getDriverInterface() == INDI::BaseDevice::GENERAL_INTERFACE)
2810             return;
2811 
2812         // If debug was turned off, but our logging policy requires it then turn it back on.
2813         // We turn on debug logging if AT LEAST one driver interface is selected by the logging settings
2814         if (svp->s == IPS_OK && svp->sp[0].s == ISS_OFF &&
2815                 (opsLogs->getINDIDebugInterface() & deviceInterface->getDriverInterface()))
2816         {
2817             svp->sp[0].s = ISS_ON;
2818             svp->sp[1].s = ISS_OFF;
2819             deviceInterface->sendNewProperty(svp);
2820             appendLogText(i18n("Re-enabling debug logging for %1...", deviceInterface->getDeviceName()));
2821         }
2822         // To turn off debug logging, NONE of the driver interfaces should be enabled in logging settings.
2823         // For example, if we have CCD+FilterWheel device and CCD + Filter Wheel logging was turned on in
2824         // the log settings, then if the user turns off only CCD logging, the debug logging is NOT
2825         // turned off until he turns off Filter Wheel logging as well.
2826         else if (svp->s == IPS_OK && svp->sp[0].s == ISS_ON
2827                  && !(opsLogs->getINDIDebugInterface() & deviceInterface->getDriverInterface()))
2828         {
2829             svp->sp[0].s = ISS_OFF;
2830             svp->sp[1].s = ISS_ON;
2831             deviceInterface->sendNewProperty(svp);
2832             appendLogText(i18n("Re-disabling debug logging for %1...", deviceInterface->getDeviceName()));
2833         }
2834     }
2835 }
2836 
2837 void Manager::announceEvent(const QString &message, KSNotification::EventSource source, KSNotification::EventType event)
2838 {
2839     ekosLiveClient.get()->message()->sendEvent(message, source, event);
2840 }
2841 
2842 void Manager::connectModules()
2843 {
2844     // Dark Library
2845     connect(DarkLibrary::Instance(), &DarkLibrary::newImage, ekosLiveClient.get()->media(),
2846             &EkosLive::Media::sendDarkLibraryData, Qt::UniqueConnection);
2847     connect(DarkLibrary::Instance(), &DarkLibrary::trainChanged, ekosLiveClient.get()->message(),
2848             &EkosLive::Message::sendTrainProfiles, Qt::UniqueConnection);
2849     connect(DarkLibrary::Instance(), &DarkLibrary::newFrame, ekosLiveClient.get()->media(), &EkosLive::Media::sendModuleFrame,
2850             Qt::UniqueConnection);
2851     connect(DarkLibrary::Instance(), &DarkLibrary::settingsUpdated, ekosLiveClient.get()->message(),
2852             &EkosLive::Message::sendGuideSettings, Qt::UniqueConnection);
2853 
2854     // Guide <---> Capture connections
2855     if (captureProcess && guideProcess)
2856     {
2857         //        captureProcess.get()->disconnect(guideProcess.get());
2858         //        guideProcess.get()->disconnect(captureProcess.get());
2859 
2860         // Guide Limits
2861         connect(guideModule(), &Ekos::Guide::newStatus, captureModule(), &Ekos::Capture::setGuideStatus,
2862                 Qt::UniqueConnection);
2863         connect(guideModule(), &Ekos::Guide::newAxisDelta, captureModule(), &Ekos::Capture::setGuideDeviation,
2864                 Qt::UniqueConnection);
2865 
2866         // Dithering
2867         connect(captureModule(), &Ekos::Capture::newStatus, guideModule(), &Ekos::Guide::setCaptureStatus,
2868                 Qt::UniqueConnection);
2869 
2870         // Guide Head
2871         connect(captureModule(), &Ekos::Capture::suspendGuiding, guideModule(), &Ekos::Guide::suspend,
2872                 Qt::UniqueConnection);
2873         connect(captureModule(), &Ekos::Capture::resumeGuiding, guideModule(), &Ekos::Guide::resume,
2874                 Qt::UniqueConnection);
2875         connect(guideModule(), &Ekos::Guide::guideChipUpdated, captureModule(), &Ekos::Capture::setGuideChip,
2876                 Qt::UniqueConnection);
2877 
2878         // Meridian Flip
2879         connect(captureModule(), &Ekos::Capture::meridianFlipStarted, guideModule(), &Ekos::Guide::abort,
2880                 Qt::UniqueConnection);
2881         connect(captureModule(), &Ekos::Capture::guideAfterMeridianFlip, guideModule(),
2882                 &Ekos::Guide::guideAfterMeridianFlip, Qt::UniqueConnection);
2883     }
2884 
2885     // Guide <---> Mount connections
2886     if (guideProcess && mountProcess)
2887     {
2888         // Parking
2889         connect(mountModule(), &Ekos::Mount::newStatus, guideModule(), &Ekos::Guide::setMountStatus,
2890                 Qt::UniqueConnection);
2891         connect(mountModule(), &Ekos::Mount::newCoords, guideModule(), &Ekos::Guide::setMountCoords,
2892                 Qt::UniqueConnection);
2893 
2894     }
2895 
2896     // Focus <---> Guide connections
2897     if (guideProcess && focusProcess)
2898     {
2899         // Suspend
2900         connect(focusModule(), &Ekos::Focus::suspendGuiding, guideModule(), &Ekos::Guide::suspend, Qt::UniqueConnection);
2901         connect(focusModule(), &Ekos::Focus::resumeGuiding, guideModule(), &Ekos::Guide::resume, Qt::UniqueConnection);
2902     }
2903 
2904     // Capture <---> Focus connections
2905     if (captureProcess && focusProcess)
2906     {
2907         // Check focus HFR value and if above threshold parameter, run autoFocus
2908         connect(captureModule(), &Ekos::Capture::checkFocus, focusModule(), &Ekos::Focus::checkFocus,
2909                 Qt::UniqueConnection);
2910 
2911         // Run autoFocus
2912         connect(captureProcess.get(), &Ekos::Capture::runAutoFocus, focusProcess.get(), &Ekos::Focus::runAutoFocus,
2913                 Qt::UniqueConnection);
2914 
2915         // Reset Focus
2916         connect(captureModule(), &Ekos::Capture::resetFocus, focusModule(), &Ekos::Focus::resetFrame,
2917                 Qt::UniqueConnection);
2918 
2919         // Abort Focus
2920         connect(captureModule(), &Ekos::Capture::abortFocus, focusModule(), &Ekos::Focus::abort,
2921                 Qt::UniqueConnection);
2922 
2923         // New Focus Status
2924         connect(focusModule(), &Ekos::Focus::newStatus, captureModule(), &Ekos::Capture::setFocusStatus,
2925                 Qt::UniqueConnection);
2926 
2927         // Perform adaptive focus
2928         connect(captureModule(), &Ekos::Capture::adaptiveFocus, focusModule(), &Ekos::Focus::adaptiveFocus,
2929                 Qt::UniqueConnection);
2930 
2931         // New Adaptive Focus Status
2932         connect(focusModule(), &Ekos::Focus::focusAdaptiveComplete, captureModule(),
2933                 &Ekos::Capture::focusAdaptiveComplete,
2934                 Qt::UniqueConnection);
2935 
2936         // New Focus HFR
2937         connect(focusModule(), &Ekos::Focus::newHFR, captureModule(), &Ekos::Capture::setHFR, Qt::UniqueConnection);
2938 
2939         // New Focus temperature delta
2940         connect(focusModule(), &Ekos::Focus::newFocusTemperatureDelta, captureModule(),
2941                 &Ekos::Capture::setFocusTemperatureDelta, Qt::UniqueConnection);
2942 
2943         // Meridian Flip
2944         connect(captureModule(), &Ekos::Capture::meridianFlipStarted, focusModule(), &Ekos::Focus::meridianFlipStarted,
2945                 Qt::UniqueConnection);
2946     }
2947 
2948     // Capture <---> Align connections
2949     if (captureProcess && alignProcess)
2950     {
2951         // Alignment flag
2952         connect(alignModule(), &Ekos::Align::newStatus, captureModule(), &Ekos::Capture::setAlignStatus,
2953                 Qt::UniqueConnection);
2954         // Solver data
2955         connect(alignModule(), &Ekos::Align::newSolverResults, captureModule(),  &Ekos::Capture::setAlignResults,
2956                 Qt::UniqueConnection);
2957         // Capture Status
2958         connect(captureModule(), &Ekos::Capture::newStatus, alignModule(), &Ekos::Align::setCaptureStatus,
2959                 Qt::UniqueConnection);
2960     }
2961 
2962     // Capture <---> Mount connections
2963     if (captureProcess && mountProcess)
2964     {
2965         // Register both modules since both are now created and ready
2966         // In case one module misses the DBus signal, then it will be correctly initialized.
2967         captureModule()->registerNewModule("Mount");
2968         mountModule()->registerNewModule("Capture");
2969 
2970         // Meridian Flip states
2971         connect(captureModule(), &Ekos::Capture::meridianFlipStarted, mountModule(), &Ekos::Mount::suspendAltLimits,
2972                 Qt::UniqueConnection);
2973         connect(captureModule(), &Ekos::Capture::guideAfterMeridianFlip, mountModule(), &Ekos::Mount::resumeAltLimits,
2974                 Qt::UniqueConnection);
2975 
2976         // Mount Status
2977         connect(mountModule(), &Ekos::Mount::newStatus, captureModule(), &Ekos::Capture::setMountStatus,
2978                 Qt::UniqueConnection);
2979     }
2980 
2981     // Optical Train Manager ---> EkosLive connections
2982     if (ekosLiveClient)
2983     {
2984         connect(OpticalTrainManager::Instance(), &OpticalTrainManager::updated, ekosLiveClient->message(),
2985                 &EkosLive::Message::sendTrains, Qt::UniqueConnection);
2986         connect(OpticalTrainManager::Instance(), &OpticalTrainManager::configurationRequested, ekosLiveClient->message(),
2987                 &EkosLive::Message::requestOpticalTrains, Qt::UniqueConnection);
2988     }
2989 
2990     // Capture <---> EkosLive connections
2991     if (captureProcess && ekosLiveClient)
2992     {
2993         //captureProcess.get()->disconnect(ekosLiveClient.get()->message());
2994 
2995         connect(captureModule(), &Ekos::Capture::dslrInfoRequested, ekosLiveClient.get()->message(),
2996                 &EkosLive::Message::requestDSLRInfo, Qt::UniqueConnection);
2997         connect(captureModule(), &Ekos::Capture::sequenceChanged, ekosLiveClient.get()->message(),
2998                 &EkosLive::Message::sendCaptureSequence, Qt::UniqueConnection);
2999         connect(captureModule(), &Ekos::Capture::settingsUpdated, ekosLiveClient.get()->message(),
3000                 &EkosLive::Message::sendCaptureSettings, Qt::UniqueConnection);
3001         connect(captureModule(), &Ekos::Capture::newLocalPreview, ekosLiveClient.get()->message(),
3002                 &EkosLive::Message::sendPreviewLabel, Qt::UniqueConnection);
3003         connect(captureModule(), &Ekos::Capture::trainChanged, ekosLiveClient.get()->message(),
3004                 &EkosLive::Message::sendTrainProfiles, Qt::UniqueConnection);
3005     }
3006 
3007     // Focus <---> Align connections
3008     if (focusProcess && alignProcess)
3009     {
3010         connect(focusModule(), &Ekos::Focus::newStatus, alignModule(), &Ekos::Align::setFocusStatus,
3011                 Qt::UniqueConnection);
3012     }
3013 
3014     // Focus <---> Mount connections
3015     if (focusProcess && mountProcess)
3016     {
3017         connect(mountModule(), &Ekos::Mount::newStatus, focusModule(), &Ekos::Focus::setMountStatus,
3018                 Qt::UniqueConnection);
3019         connect(mountModule(), &Ekos::Mount::newCoords, focusModule(), &Ekos::Focus::setMountCoords,
3020                 Qt::UniqueConnection);
3021     }
3022 
3023     // Mount <---> Align connections
3024     if (mountProcess && alignProcess)
3025     {
3026         connect(mountModule(), &Ekos::Mount::newStatus, alignModule(), &Ekos::Align::setMountStatus,
3027                 Qt::UniqueConnection);
3028         connect(mountModule(), &Ekos::Mount::newTarget,  alignModule(), &Ekos::Align::setTarget,
3029                 Qt::UniqueConnection);
3030         connect(mountModule(), &Ekos::Mount::newCoords,  alignModule(), &Ekos::Align::setTelescopeCoordinates,
3031                 Qt::UniqueConnection);
3032         connect(alignModule(), &Ekos::Align::newPAAStage, mountModule(), &Ekos::Mount::paaStageChanged,
3033                 Qt::UniqueConnection);
3034     }
3035 
3036     // Mount <---> Guide connections
3037     if (mountProcess && guideProcess)
3038     {
3039         connect(mountModule(), &Ekos::Mount::pierSideChanged, guideModule(), &Ekos::Guide::setPierSide,
3040                 Qt::UniqueConnection);
3041     }
3042 
3043     // Align <--> EkosLive connections
3044     if (alignProcess && ekosLiveClient)
3045     {
3046         //        alignProcess.get()->disconnect(ekosLiveClient.get()->message());
3047         //        alignProcess.get()->disconnect(ekosLiveClient.get()->media());
3048 
3049         connect(alignModule(), &Ekos::Align::newStatus, ekosLiveClient.get()->message(), &EkosLive::Message::setAlignStatus,
3050                 Qt::UniqueConnection);
3051         connect(alignModule(), &Ekos::Align::newSolution, ekosLiveClient.get()->message(),
3052                 &EkosLive::Message::setAlignSolution, Qt::UniqueConnection);
3053         connect(alignModule()->polarAlignmentAssistant(), &Ekos::PolarAlignmentAssistant::newPAHStage,
3054                 ekosLiveClient.get()->message(), &EkosLive::Message::setPAHStage,
3055                 Qt::UniqueConnection);
3056         connect(alignModule()->polarAlignmentAssistant(), &Ekos::PolarAlignmentAssistant::newPAHMessage,
3057                 ekosLiveClient.get()->message(),
3058                 &EkosLive::Message::setPAHMessage, Qt::UniqueConnection);
3059         connect(alignModule()->polarAlignmentAssistant(), &Ekos::PolarAlignmentAssistant::PAHEnabled,
3060                 ekosLiveClient.get()->message(), &EkosLive::Message::setPAHEnabled,
3061                 Qt::UniqueConnection);
3062         connect(alignModule()->polarAlignmentAssistant(), &Ekos::PolarAlignmentAssistant::polarResultUpdated,
3063                 ekosLiveClient.get()->message(),
3064                 &EkosLive::Message::setPolarResults, Qt::UniqueConnection);
3065         connect(alignModule()->polarAlignmentAssistant(), &Ekos::PolarAlignmentAssistant::updatedErrorsChanged,
3066                 ekosLiveClient.get()->message(),
3067                 &EkosLive::Message::setUpdatedErrors, Qt::UniqueConnection);
3068         connect(alignModule()->polarAlignmentAssistant(), &Ekos::PolarAlignmentAssistant::newCorrectionVector,
3069                 ekosLiveClient.get()->media(),
3070                 &EkosLive::Media::setCorrectionVector, Qt::UniqueConnection);
3071 
3072         connect(alignModule(), &Ekos::Align::newImage, ekosLiveClient.get()->media(), &EkosLive::Media::sendModuleFrame,
3073                 Qt::UniqueConnection);
3074         connect(alignModule(), &Ekos::Align::newFrame, ekosLiveClient.get()->media(), &EkosLive::Media::sendUpdatedFrame,
3075                 Qt::UniqueConnection);
3076 
3077         connect(alignModule(), &Ekos::Align::settingsUpdated, ekosLiveClient.get()->message(),
3078                 &EkosLive::Message::sendAlignSettings, Qt::UniqueConnection);
3079 
3080         connect(alignModule(), &Ekos::Align::trainChanged, ekosLiveClient.get()->message(),
3081                 &EkosLive::Message::sendTrainProfiles, Qt::UniqueConnection);
3082 
3083         connect(alignModule(), &Ekos::Align::manualRotatorChanged, ekosLiveClient.get()->message(),
3084                 &EkosLive::Message::sendManualRotatorStatus, Qt::UniqueConnection);
3085     }
3086 
3087     // Focus <--> EkosLive Connections
3088     if (focusProcess && ekosLiveClient)
3089     {
3090         connect(focusModule(), &Ekos::Focus::settingsUpdated, ekosLiveClient.get()->message(),
3091                 &EkosLive::Message::sendFocusSettings, Qt::UniqueConnection);
3092 
3093         connect(focusModule(), &Ekos::Focus::newImage, ekosLiveClient.get()->media(), &EkosLive::Media::sendModuleFrame,
3094                 Qt::UniqueConnection);
3095 
3096         connect(focusModule(), &Ekos::Focus::trainChanged, ekosLiveClient.get()->message(),
3097                 &EkosLive::Message::sendTrainProfiles,
3098                 Qt::UniqueConnection);
3099 
3100         connect(focusModule(), &Ekos::Focus::autofocusAborted,
3101                 ekosLiveClient.get()->message(), &EkosLive::Message::autofocusAborted, Qt::UniqueConnection);
3102     }
3103 
3104     // Guide <--> EkosLive Connections
3105     if (guideProcess && ekosLiveClient)
3106     {
3107         connect(guideModule(), &Ekos::Guide::settingsUpdated, ekosLiveClient.get()->message(),
3108                 &EkosLive::Message::sendGuideSettings, Qt::UniqueConnection);
3109 
3110         connect(guideModule(), &Ekos::Guide::trainChanged, ekosLiveClient.get()->message(),
3111                 &EkosLive::Message::sendTrainProfiles, Qt::UniqueConnection);
3112 
3113         connect(guideModule(), &Ekos::Guide::newImage, ekosLiveClient.get()->media(), &EkosLive::Media::sendModuleFrame,
3114                 Qt::UniqueConnection);
3115     }
3116 
3117     // Analyze connections.
3118     if (analyzeProcess)
3119     {
3120         // Scheduler <---> Analyze
3121         connect(schedulerModule(), &Ekos::Scheduler::jobStarted,
3122                 analyzeProcess.get(), &Ekos::Analyze::schedulerJobStarted, Qt::UniqueConnection);
3123         connect(schedulerModule(), &Ekos::Scheduler::jobEnded,
3124                 analyzeProcess.get(), &Ekos::Analyze::schedulerJobEnded, Qt::UniqueConnection);
3125         connect(schedulerModule(), &Ekos::Scheduler::targetDistance,
3126                 analyzeProcess.get(), &Ekos::Analyze::newTargetDistance,  Qt::UniqueConnection);
3127 
3128         // Capture <---> Analyze
3129         if (captureProcess)
3130         {
3131             connect(captureModule(), &Ekos::Capture::captureComplete,
3132                     analyzeProcess.get(), &Ekos::Analyze::captureComplete, Qt::UniqueConnection);
3133             connect(captureModule(), &Ekos::Capture::captureStarting,
3134                     analyzeProcess.get(), &Ekos::Analyze::captureStarting, Qt::UniqueConnection);
3135             connect(captureModule(), &Ekos::Capture::captureAborted,
3136                     analyzeProcess.get(), &Ekos::Analyze::captureAborted, Qt::UniqueConnection);
3137 #if 0
3138             // Meridian Flip
3139             connect(captureModule(), &Ekos::Capture::meridianFlipStarted,
3140                     analyzeProcess.get(), &Ekos::Analyze::meridianFlipStarted, Qt::UniqueConnection);
3141             connect(captureModule(), &Ekos::Capture::meridianFlipCompleted,
3142                     analyzeProcess.get(), &Ekos::Analyze::meridianFlipComplete, Qt::UniqueConnection);
3143 #endif
3144         }
3145 
3146         // Guide <---> Analyze
3147         if (guideProcess)
3148         {
3149             connect(guideModule(), &Ekos::Guide::newStatus,
3150                     analyzeProcess.get(), &Ekos::Analyze::guideState, Qt::UniqueConnection);
3151 
3152             connect(guideModule(), &Ekos::Guide::guideStats,
3153                     analyzeProcess.get(), &Ekos::Analyze::guideStats, Qt::UniqueConnection);
3154         }
3155     }
3156 
3157 
3158     // Focus <---> Analyze connections
3159     if (focusProcess && analyzeProcess)
3160     {
3161         connect(focusModule(), &Ekos::Focus::autofocusComplete,
3162                 analyzeProcess.get(), &Ekos::Analyze::autofocusComplete, Qt::UniqueConnection);
3163         connect(focusModule(), &Ekos::Focus::adaptiveFocusComplete,
3164                 analyzeProcess.get(), &Ekos::Analyze::adaptiveFocusComplete, Qt::UniqueConnection);
3165         connect(focusModule(), &Ekos::Focus::autofocusStarting,
3166                 analyzeProcess.get(), &Ekos::Analyze::autofocusStarting, Qt::UniqueConnection);
3167         connect(focusModule(), &Ekos::Focus::autofocusAborted,
3168                 analyzeProcess.get(), &Ekos::Analyze::autofocusAborted, Qt::UniqueConnection);
3169         connect(focusModule(), &Ekos::Focus::newFocusTemperatureDelta,
3170                 analyzeProcess.get(), &Ekos::Analyze::newTemperature, Qt::UniqueConnection);
3171     }
3172 
3173     // Align <---> Analyze connections
3174     if (alignProcess && analyzeProcess)
3175     {
3176         connect(alignModule(), &Ekos::Align::newStatus,
3177                 analyzeProcess.get(), &Ekos::Analyze::alignState, Qt::UniqueConnection);
3178 
3179     }
3180 
3181     // Mount <---> Analyze connections
3182     if (mountProcess && analyzeProcess)
3183     {
3184         connect(mountModule(), &Ekos::Mount::newStatus,
3185                 analyzeProcess.get(), &Ekos::Analyze::mountState, Qt::UniqueConnection);
3186         connect(mountModule(), &Ekos::Mount::newCoords,
3187                 analyzeProcess.get(), &Ekos::Analyze::mountCoords, Qt::UniqueConnection);
3188         connect(mountModule()->getMeridianFlipState().get(), &Ekos::MeridianFlipState::newMountMFStatus,
3189                 analyzeProcess.get(), &Ekos::Analyze::mountFlipStatus, Qt::UniqueConnection);
3190     }
3191 }
3192 
3193 void Manager::setEkosLiveConnected(bool enabled)
3194 {
3195     ekosLiveClient.get()->setConnected(enabled);
3196 }
3197 
3198 void Manager::setEkosLiveConfig(bool rememberCredentials, bool autoConnect)
3199 {
3200     ekosLiveClient.get()->setConfig(rememberCredentials, autoConnect);
3201 }
3202 
3203 void Manager::setEkosLiveUser(const QString &username, const QString &password)
3204 {
3205     ekosLiveClient.get()->setUser(username, password);
3206 }
3207 
3208 bool Manager::ekosLiveStatus()
3209 {
3210     return ekosLiveClient.get()->isConnected();
3211 }
3212 
3213 void Manager::syncActiveDevices()
3214 {
3215     for (auto oneDevice : INDIListener::devices())
3216     {
3217         // Find out what ACTIVE_DEVICES properties this driver needs
3218         // and update it from the existing drivers.
3219         auto tvp = oneDevice->getProperty("ACTIVE_DEVICES");
3220         if (!tvp)
3221             continue;
3222 
3223         // Only applicable to non-train devices
3224         for (auto &it : *tvp.getText())
3225         {
3226             QList<QSharedPointer<ISD::GenericDevice>> devs;
3227             if (it.isNameMatch("ACTIVE_DOME"))
3228             {
3229                 devs = INDIListener::devicesByInterface(INDI::BaseDevice::DOME_INTERFACE);
3230             }
3231             else if (it.isNameMatch("ACTIVE_GPS"))
3232             {
3233                 devs = INDIListener::devicesByInterface(INDI::BaseDevice::GPS_INTERFACE);
3234             }
3235 
3236             if (!devs.empty())
3237             {
3238                 if (it.getText() != devs.first()->getDeviceName())
3239                 {
3240                     it.setText(devs.first()->getDeviceName().toLatin1().constData());
3241                     oneDevice->sendNewProperty(tvp.getText());
3242                 }
3243             }
3244         }
3245     }
3246 }
3247 
3248 bool Manager::checkUniqueBinaryDriver(const QSharedPointer<DriverInfo> &primaryDriver,
3249                                       const QSharedPointer<DriverInfo> &secondaryDriver)
3250 {
3251     if (!primaryDriver || !secondaryDriver)
3252         return false;
3253 
3254     return (primaryDriver->getExecutable() == secondaryDriver->getExecutable() &&
3255             primaryDriver->getAuxInfo().value("mdpd", false).toBool() == true);
3256 }
3257 
3258 void Manager::restartDriver(const QString &deviceName)
3259 {
3260     qCInfo(KSTARS_EKOS) << "Restarting driver" << deviceName;
3261     if (m_LocalMode)
3262     {
3263         for (auto &oneDevice : INDIListener::devices())
3264         {
3265             if (oneDevice->getDeviceName() == deviceName)
3266             {
3267                 DriverManager::Instance()->restartDriver(oneDevice->getDriverInfo());
3268                 break;
3269             }
3270         }
3271     }
3272     else
3273         INDI::WebManager::restartDriver(m_CurrentProfile, deviceName);
3274 }
3275 
3276 void Manager::setEkosLoggingEnabled(const QString &name, bool enabled)
3277 {
3278     // LOGGING, FILE, DEFAULT are exclusive, so one of them must be SET to TRUE
3279     if (name == "LOGGING")
3280     {
3281         Options::setDisableLogging(!enabled);
3282         if (!enabled)
3283             KSUtils::Logging::Disable();
3284     }
3285     else if (name == "FILE")
3286     {
3287         Options::setLogToFile(enabled);
3288         if (enabled)
3289             KSUtils::Logging::UseFile();
3290     }
3291     else if (name == "DEFAULT")
3292     {
3293         Options::setLogToDefault(enabled);
3294         if (enabled)
3295             KSUtils::Logging::UseDefault();
3296     }
3297     // VERBOSE should be set to TRUE if INDI or Ekos logging is selected.
3298     else if (name == "VERBOSE")
3299     {
3300         Options::setVerboseLogging(enabled);
3301         KSUtils::Logging::SyncFilterRules();
3302     }
3303     // Toggle INDI Logging
3304     else if (name == "INDI")
3305     {
3306         Options::setINDILogging(enabled);
3307         KSUtils::Logging::SyncFilterRules();
3308     }
3309     else if (name == "FITS")
3310     {
3311         Options::setFITSLogging(enabled);
3312         KSUtils::Logging::SyncFilterRules();
3313     }
3314     else if (name == "CAPTURE")
3315     {
3316         Options::setCaptureLogging(enabled);
3317         Options::setINDICCDLogging(enabled);
3318         Options::setINDIFilterWheelLogging(enabled);
3319         KSUtils::Logging::SyncFilterRules();
3320     }
3321     else if (name == "FOCUS")
3322     {
3323         Options::setFocusLogging(enabled);
3324         Options::setINDIFocuserLogging(enabled);
3325         KSUtils::Logging::SyncFilterRules();
3326     }
3327     else if (name == "GUIDE")
3328     {
3329         Options::setGuideLogging(enabled);
3330         Options::setINDICCDLogging(enabled);
3331         KSUtils::Logging::SyncFilterRules();
3332     }
3333     else if (name == "ALIGNMENT")
3334     {
3335         Options::setAlignmentLogging(enabled);
3336         KSUtils::Logging::SyncFilterRules();
3337     }
3338     else if (name == "MOUNT")
3339     {
3340         Options::setMountLogging(enabled);
3341         Options::setINDIMountLogging(enabled);
3342         KSUtils::Logging::SyncFilterRules();
3343     }
3344     else if (name == "SCHEDULER")
3345     {
3346         Options::setSchedulerLogging(enabled);
3347         KSUtils::Logging::SyncFilterRules();
3348     }
3349     else if (name == "OBSERVATORY")
3350     {
3351         Options::setObservatoryLogging(enabled);
3352         KSUtils::Logging::SyncFilterRules();
3353     }
3354 }
3355 
3356 void Manager::acceptPortSelection()
3357 {
3358     if (m_PortSelector)
3359         m_PortSelector->accept();
3360 }
3361 
3362 void Manager::setPortSelectionComplete()
3363 {
3364     if (m_CurrentProfile->portSelector)
3365     {
3366         // Turn off port selector
3367         m_CurrentProfile->portSelector = false;
3368         KStarsData::Instance()->userdb()->SaveProfile(m_CurrentProfile);
3369     }
3370 
3371     if (m_CurrentProfile->autoConnect)
3372         connectDevices();
3373 }
3374 
3375 void Manager::activateModule(const QString &name, bool popup)
3376 {
3377     auto child = toolsWidget->findChild<QWidget *>(name);
3378     if (child)
3379     {
3380         toolsWidget->setCurrentWidget(child);
3381         if (popup)
3382         {
3383             raise();
3384             activateWindow();
3385             showNormal();
3386         }
3387     }
3388 }
3389 
3390 void Manager::createModules(const QSharedPointer<ISD::GenericDevice> &device)
3391 {
3392     if (device->isConnected())
3393     {
3394         if (device->getDriverInterface() & INDI::BaseDevice::CCD_INTERFACE)
3395         {
3396             initCapture();
3397             initFocus();
3398             initAlign();
3399             initGuide();
3400         }
3401         if (device->getDriverInterface() & INDI::BaseDevice::FILTER_INTERFACE)
3402         {
3403             initCapture();
3404             initFocus();
3405             initAlign();
3406         }
3407         if (device->getDriverInterface() & INDI::BaseDevice::FOCUSER_INTERFACE)
3408             initFocus();
3409         if (device->getDriverInterface() & INDI::BaseDevice::TELESCOPE_INTERFACE)
3410         {
3411             initCapture();
3412             initAlign();
3413             initGuide();
3414             initMount();
3415         }
3416         if (device->getDriverInterface() & INDI::BaseDevice::ROTATOR_INTERFACE)
3417         {
3418             initCapture();
3419             initAlign();
3420         }
3421         if (device->getDriverInterface() & INDI::BaseDevice::DOME_INTERFACE)
3422         {
3423             initCapture();
3424             initAlign();
3425             initObservatory();
3426         }
3427         if (device->getDriverInterface() & INDI::BaseDevice::WEATHER_INTERFACE)
3428         {
3429             initFocus();
3430             initObservatory();
3431         }
3432         if (device->getDriverInterface() & INDI::BaseDevice::DUSTCAP_INTERFACE)
3433         {
3434             initCapture();
3435         }
3436         if (device->getDriverInterface() & INDI::BaseDevice::LIGHTBOX_INTERFACE)
3437         {
3438             initCapture();
3439         }
3440         if (device->getDriverInterface() & INDI::BaseDevice::GPS_INTERFACE)
3441         {
3442             initMount();
3443         }
3444     }
3445 }
3446 
3447 void Manager::setDeviceReady()
3448 {
3449     // Check if ALL our devices are ready.
3450     // Ready indicates that all properties have been defined.
3451     if (isINDIReady() == false)
3452     {
3453         auto device = static_cast<ISD::GenericDevice*>(sender());
3454         if (device)
3455         {
3456 
3457             if (device->isConnected() == false && m_CurrentProfile->autoConnect)
3458             {
3459                 // Do we have port selector checked?
3460                 if (m_CurrentProfile->portSelector)
3461                 {
3462                     // If port selector was not initialized, kick off the timer
3463                     // so we can check if all devices should be connected.
3464                     // Otherwise, if port selector is started, then let user
3465                     // select ports first and then manually connect time.
3466                     if (!m_PortSelector)
3467                         m_PortSelectorTimer.start();
3468                 }
3469                 else
3470                 {
3471                     qCInfo(KSTARS_EKOS) << "Connecting to" << device->getDeviceName();
3472                     device->Connect();
3473                 }
3474             }
3475             else
3476                 qCInfo(KSTARS_EKOS) << device->getDeviceName() << "is connected and ready.";
3477         }
3478 
3479         if (m_ekosStatus != Ekos::Success)
3480             return;
3481     }
3482 
3483     for (auto &device : INDIListener::devices())
3484         syncGenericDevice(device);
3485 
3486     // If port selector is active, then do not show optical train dialog unless it is dismissed first.
3487     if (m_DriverDevicesCount <= 0 && (m_CurrentProfile->portSelector == false || !m_PortSelector))
3488         OpticalTrainManager::Instance()->setProfile(m_CurrentProfile);
3489 }
3490 
3491 void Manager::createFilterManager(ISD::FilterWheel *device)
3492 {
3493     auto name = device->getDeviceName();
3494     if (m_FilterManagers.contains(name) == false)
3495     {
3496         QSharedPointer<FilterManager> newFM(new FilterManager(this));
3497         newFM->setFilterWheel(device);
3498         m_FilterManagers[name] = newFM;
3499     }
3500     else
3501         m_FilterManagers[name]->setFilterWheel(device);
3502 
3503 }
3504 
3505 bool Manager::getFilterManager(const QString &name, QSharedPointer<FilterManager> &fm)
3506 {
3507     if (m_FilterManagers.contains(name))
3508     {
3509         fm = m_FilterManagers[name];
3510         return true;
3511     }
3512     return false;
3513 }
3514 
3515 bool Manager::getFilterManager(QSharedPointer<FilterManager> &fm)
3516 {
3517     if (m_FilterManagers.size() > 0)
3518     {
3519         fm = m_FilterManagers.values()[0];
3520         return true;
3521     }
3522     return false;
3523 }
3524 
3525 void Manager::createRotatorController(ISD::Rotator *device)
3526 {
3527     auto Name = device->getDeviceName();
3528     if (m_RotatorControllers.contains(Name) == false)
3529     {
3530         QSharedPointer<RotatorSettings> newRC(new RotatorSettings(this));
3531         // Properties are fetched in RotatorSettings::initRotator!
3532         m_RotatorControllers[Name] = newRC;
3533     }
3534 }
3535 
3536 bool Manager::getRotatorController(const QString &Name, QSharedPointer<RotatorSettings> &rs)
3537 {
3538     if (m_RotatorControllers.contains(Name))
3539     {
3540         rs = m_RotatorControllers[Name];
3541         return true;
3542     }
3543     return false;
3544 }
3545 
3546 bool Manager::existRotatorController()
3547 {
3548     return (!m_RotatorControllers.empty());
3549 }
3550 
3551 }