diff --git a/kstars/ekos/ekoslive/cloud.cpp b/kstars/ekos/ekoslive/cloud.cpp index 75a8a5364..4ec889a61 100644 --- a/kstars/ekos/ekoslive/cloud.cpp +++ b/kstars/ekos/ekoslive/cloud.cpp @@ -1,228 +1,251 @@ /* Ekos Live Cloud Copyright (C) 2018 Jasem Mutlaq Cloud Channel This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "cloud.h" #include "commands.h" #include "profileinfo.h" #include "fitsviewer/fitsview.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fpack.h" #include "ekos_debug.h" #include #include #include namespace EkosLive { Cloud::Cloud(Ekos::Manager * manager): m_Manager(manager) { connect(&m_WebSocket, &QWebSocket::connected, this, &Cloud::onConnected); connect(&m_WebSocket, &QWebSocket::disconnected, this, &Cloud::onDisconnected); connect(&m_WebSocket, static_cast(&QWebSocket::error), this, &Cloud::onError); connect(&watcher, &QFutureWatcher::finished, this, &Cloud::sendImage, Qt::UniqueConnection); connect(this, &Cloud::newMetadata, this, &Cloud::uploadMetadata); connect(this, &Cloud::newImage, this, &Cloud::uploadImage); } void Cloud::connectServer() { QUrl requestURL(m_URL); - QString token = m_AuthResponse.contains("remoteToken") ? m_AuthResponse["remoteToken"].toString() - : m_AuthResponse["token"].toString(); QUrlQuery query; query.addQueryItem("username", m_AuthResponse["username"].toString()); - query.addQueryItem("token", token); + query.addQueryItem("token", m_AuthResponse["token"].toString()); + if (m_AuthResponse.contains("remoteToken")) + query.addQueryItem("remoteToken", m_AuthResponse["remoteToken"].toString()); + if (m_Options[OPTION_SET_CLOUD_STORAGE]) + query.addQueryItem("cloudEnabled", "true"); query.addQueryItem("email", m_AuthResponse["email"].toString()); query.addQueryItem("from_date", m_AuthResponse["from_date"].toString()); query.addQueryItem("to_date", m_AuthResponse["to_date"].toString()); query.addQueryItem("plan_id", m_AuthResponse["plan_id"].toString()); query.addQueryItem("type", m_AuthResponse["type"].toString()); requestURL.setPath("/cloud/ekos"); requestURL.setQuery(query); m_WebSocket.open(requestURL); qCInfo(KSTARS_EKOS) << "Connecting to cloud websocket server at" << requestURL.toDisplayString(); } void Cloud::disconnectServer() { m_WebSocket.close(); } void Cloud::onConnected() { qCInfo(KSTARS_EKOS) << "Connected to Cloud Websocket server at" << m_URL.toDisplayString(); connect(&m_WebSocket, &QWebSocket::textMessageReceived, this, &Cloud::onTextReceived); m_isConnected = true; m_ReconnectTries = 0; emit connected(); } void Cloud::onDisconnected() { qCInfo(KSTARS_EKOS) << "Disconnected from Cloud Websocket server."; m_isConnected = false; disconnect(&m_WebSocket, &QWebSocket::textMessageReceived, this, &Cloud::onTextReceived); m_sendBlobs = true; for (const QString &oneFile : temporaryFiles) QFile::remove(oneFile); temporaryFiles.clear(); emit disconnected(); } void Cloud::onError(QAbstractSocket::SocketError error) { qCritical(KSTARS_EKOS) << "Cloud Websocket connection error" << m_WebSocket.errorString(); if (error == QAbstractSocket::RemoteHostClosedError || error == QAbstractSocket::ConnectionRefusedError) { if (m_ReconnectTries++ < RECONNECT_MAX_TRIES) QTimer::singleShot(RECONNECT_INTERVAL, this, SLOT(connectServer())); } } void Cloud::onTextReceived(const QString &message) { qCInfo(KSTARS_EKOS) << "Cloud Text Websocket Message" << message; QJsonParseError error; auto serverMessage = QJsonDocument::fromJson(message.toLatin1(), &error); if (error.error != QJsonParseError::NoError) { qCWarning(KSTARS_EKOS) << "Ekos Live Parsing Error" << error.errorString(); return; } const QJsonObject msgObj = serverMessage.object(); const QString command = msgObj["type"].toString(); // const QJsonObject payload = msgObj["payload"].toObject(); // if (command == commands[ALIGN_SET_FILE_EXTENSION]) // extension = payload["ext"].toString(); if (command == commands[SET_BLOBS]) m_sendBlobs = msgObj["payload"].toBool(); else if (command == commands[LOGOUT]) disconnectServer(); } void Cloud::sendPreviewImage(const QString &filename, const QString &uuid) { if (m_isConnected == false || m_Options[OPTION_SET_CLOUD_STORAGE] == false || m_sendBlobs == false) return; watcher.waitForFinished(); m_UUID = uuid; imageData.reset(new FITSData()); imageData->setAutoRemoveTemporaryFITS(false); QFuture result = imageData->loadFITS(filename); watcher.setFuture(result); } void Cloud::sendImage() { QtConcurrent::run(this, &Cloud::asyncUpload); } void Cloud::asyncUpload() { // Send complete metadata // Add file name and size QJsonObject metadata; // Skip empty or useless metadata for (FITSData::Record * oneRecord : imageData->getRecords()) { if (oneRecord->key == "EXTEND" || oneRecord->key == "SIMPLE" || oneRecord->key == "COMMENT" || oneRecord->key.isEmpty() || oneRecord->value.toString().isEmpty()) continue; metadata.insert(oneRecord->key.toLower(), QJsonValue::fromVariant(oneRecord->value)); } // Filename only without path QString filepath = imageData->isCompressed() ? imageData->compressedFilename() : imageData->filename(); QString filenameOnly = QFileInfo(filepath).fileName(); // Add filename and size as wells metadata.insert("uuid", m_UUID); metadata.insert("filename", filenameOnly); metadata.insert("filesize", static_cast(imageData->size())); // Must set Content-Disposition so if (imageData->isCompressed()) metadata.insert("Content-Disposition", QString("attachment;filename=%1").arg(filenameOnly)); else metadata.insert("Content-Disposition", QString("attachment;filename=%1.fz").arg(filenameOnly)); emit newMetadata(QJsonDocument(metadata).toJson(QJsonDocument::Compact)); //m_WebSocket.sendTextMessage(QJsonDocument(metadata).toJson(QJsonDocument::Compact)); qCInfo(KSTARS_EKOS) << "Uploading file to the cloud with metadata" << metadata; QString compressedFile = filepath; // Use cfitsio pack to compress the file first if (imageData->isCompressed() == false) { compressedFile = QDir::tempPath() + QString("/ekoslivecloud%1").arg(m_UUID); int isLossLess = 0; fpstate fpvar; fp_init (&fpvar); if (fp_pack(filepath.toLatin1().data(), compressedFile.toLatin1().data(), fpvar, &isLossLess) < 0) { if (filepath.startsWith(QDir::tempPath())) QFile::remove(filepath); qCCritical(KSTARS_EKOS) << "Cloud upload failed. Failed to compress" << filepath; return; } } // Upload the compressed image QFile image(compressedFile); if (image.open(QIODevice::ReadOnly)) { //m_WebSocket.sendBinaryMessage(image.readAll()); emit newImage(image.readAll()); qCInfo(KSTARS_EKOS) << "Uploaded" << compressedFile << " to the cloud"; } image.close(); // Remove from disk if temporary if (compressedFile != filepath && compressedFile.startsWith(QDir::tempPath())) QFile::remove(compressedFile); imageData.reset(); } void Cloud::uploadMetadata(const QByteArray &metadata) { m_WebSocket.sendTextMessage(metadata); } void Cloud::uploadImage(const QByteArray &image) { m_WebSocket.sendBinaryMessage(image); } +void Cloud::setOptions(QMap options) +{ + bool cloudEnabled = m_Options[OPTION_SET_CLOUD_STORAGE]; + m_Options = options; + + // In case cloud storage is toggled, inform cloud + // websocket channel of this change. + if (cloudEnabled != m_Options[OPTION_SET_CLOUD_STORAGE]) + { + bool enabled = m_Options[OPTION_SET_CLOUD_STORAGE]; + QJsonObject payload = {{"value", enabled}}; + QJsonObject message = + { + {"type", commands[OPTION_SET_CLOUD_STORAGE]}, + {"payload", payload} + }; + + m_WebSocket.sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact)); + } +} + } diff --git a/kstars/ekos/ekoslive/cloud.h b/kstars/ekos/ekoslive/cloud.h index 8a3ff3bb8..cdf937dc7 100644 --- a/kstars/ekos/ekoslive/cloud.h +++ b/kstars/ekos/ekoslive/cloud.h @@ -1,114 +1,111 @@ /* Ekos Live Client Copyright (C) 2018 Jasem Mutlaq Cloud Channel This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include #include #include "ekos/ekos.h" #include "ekos/manager.h" class FITSView; namespace EkosLive { class Cloud : public QObject { Q_OBJECT public: explicit Cloud(Ekos::Manager * manager); virtual ~Cloud() = default; void sendResponse(const QString &command, const QJsonObject &payload); void sendResponse(const QString &command, const QJsonArray &payload); void setAuthResponse(const QJsonObject &response) { m_AuthResponse = response; } void setURL(const QUrl &url) { m_URL = url; } void registerCameras(); // Ekos Cloud Message to User void sendPreviewImage(const QString &filename, const QString &uuid); signals: void connected(); void disconnected(); void newMetadata(const QByteArray &metadata); void newImage(const QByteArray &image); public slots: void connectServer(); void disconnectServer(); - void setOptions(QMap options) - { - m_Options = options; - } + void setOptions(QMap options); private slots: // Connection void onConnected(); void onDisconnected(); void onError(QAbstractSocket::SocketError error); // Communication void onTextReceived(const QString &message); // Send image void sendImage(); // Metadata and Image upload void uploadMetadata(const QByteArray &metadata); void uploadImage(const QByteArray &image); private: void asyncUpload(); QWebSocket m_WebSocket; QJsonObject m_AuthResponse; uint16_t m_ReconnectTries {0}; Ekos::Manager * m_Manager { nullptr }; QUrl m_URL; QString m_UUID; std::unique_ptr imageData; QFutureWatcher watcher; QString extension; QStringList temporaryFiles; bool m_isConnected {false}; bool m_sendBlobs {true}; QMap m_Options; // Image width for high-bandwidth setting static const uint16_t HB_WIDTH = 640; // Image high bandwidth image quality (jpg) static const uint8_t HB_IMAGE_QUALITY = 76; // Video high bandwidth video quality (jpg) static const uint8_t HB_VIDEO_QUALITY = 64; // Retry every 5 seconds in case remote server is down static const uint16_t RECONNECT_INTERVAL = 5000; // Retry for 1 hour before giving up static const uint16_t RECONNECT_MAX_TRIES = 720; }; } diff --git a/kstars/ekos/ekoslive/ekosliveclient.cpp b/kstars/ekos/ekoslive/ekosliveclient.cpp index 36182ec82..243d478ba 100644 --- a/kstars/ekos/ekoslive/ekosliveclient.cpp +++ b/kstars/ekos/ekoslive/ekosliveclient.cpp @@ -1,360 +1,358 @@ /* Ekos Live Client Copyright (C) 2018 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "Options.h" #include "ekosliveclient.h" #include "ekos_debug.h" #include "ekos/manager.h" #include "ekos/capture/capture.h" #include "ekos/mount/mount.h" #include "ekos/focus/focus.h" #include "kspaths.h" #include "kstarsdata.h" #include "filedownloader.h" #include "QProgressIndicator.h" #include "indi/indilistener.h" #include "indi/indiccd.h" #include "indi/indifilter.h" #include #ifdef HAVE_KEYCHAIN #include #endif #include #include #include #include namespace EkosLive { Client::Client(Ekos::Manager *manager) : QDialog(manager), m_Manager(manager) { setupUi(this); connect(closeB, SIGNAL(clicked()), this, SLOT(close())); networkManager = new QNetworkAccessManager(this); connect(networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(onResult(QNetworkReply*))); QPixmap im; if (im.load(KSPaths::locate(QStandardPaths::GenericDataLocation, "ekoslive.png"))) leftBanner->setPixmap(im); pi = new QProgressIndicator(this); bottomLayout->insertWidget(1, pi); connectionState->setPixmap(QIcon::fromTheme("state-offline").pixmap(QSize(64, 64))); username->setText(Options::ekosLiveUsername()); connect(username, &QLineEdit::editingFinished, [ = ]() { Options::setEkosLiveUsername(username->text()); }); connect(connectB, &QPushButton::clicked, [ = ]() { if (m_isConnected) disconnectAuthServer(); else connectAuthServer(); }); connect(password, &QLineEdit::returnPressed, [ = ]() { if (!m_isConnected) connectAuthServer(); }); rememberCredentialsCheck->setChecked(Options::rememberCredentials()); connect(rememberCredentialsCheck, &QCheckBox::toggled, [ = ](bool toggled) { Options::setRememberCredentials(toggled); }); autoStartCheck->setChecked(Options::autoStartEkosLive()); connect(autoStartCheck, &QCheckBox::toggled, [ = ](bool toggled) { Options::setAutoStartEkosLive(toggled); }); m_serviceURL.setUrl("https://live.stellarmate.com"); m_wsURL.setUrl("wss://live.stellarmate.com"); if (Options::ekosLiveOnline()) ekosLiveOnlineR->setChecked(true); else ekosLiveOfflineR->setChecked(true); connect(ekosLiveOnlineR, &QRadioButton::toggled, [&](bool toggled) { Options::setEkosLiveOnline(toggled); if (toggled) { m_serviceURL.setUrl("https://live.stellarmate.com"); m_wsURL.setUrl("wss://live.stellarmate.com"); m_Message->setURL(m_wsURL); m_Media->setURL(m_wsURL); m_Cloud->setURL(m_wsURL); } else { m_serviceURL.setUrl("http://localhost:3000"); m_wsURL.setUrl("ws://localhost:3000"); m_Message->setURL(m_wsURL); m_Media->setURL(m_wsURL); - // Offline does not support cloud - //m_Cloud->setURL(m_wsURL); - m_Cloud->setURL(QUrl("wss://live.stellarmate.com")); + m_Cloud->setURL(m_wsURL); } } ); if (Options::ekosLiveOnline() == false) { m_serviceURL.setUrl("http://localhost:3000"); m_wsURL.setUrl("ws://localhost:3000"); } #ifdef HAVE_KEYCHAIN QKeychain::ReadPasswordJob *job = new QKeychain::ReadPasswordJob(QLatin1String("kstars")); job->setAutoDelete(false); job->setKey(QLatin1String("ekoslive")); connect(job, &QKeychain::Job::finished, [&](QKeychain::Job * job) { if (job->error() == false) { //QJsonObject data = QJsonDocument::fromJson(dynamic_cast(job)->textData().toLatin1()).object(); //const QString usernameText = data["username"].toString(); //const QString passwordText = data["password"].toString(); const auto passwordText = dynamic_cast(job)->textData().toLatin1(); // Only set and attempt connection if the data is not empty //if (usernameText.isEmpty() == false && passwordText.isEmpty() == false) if (passwordText.isEmpty() == false && username->text().isEmpty() == false) { //username->setText(usernameText); password->setText(passwordText); if (autoStartCheck->isChecked()) connectAuthServer(); } } job->deleteLater(); }); job->start(); #endif m_Message = new Message(m_Manager); m_Message->setURL(m_wsURL); connect(m_Message, &Message::connected, this, &Client::onConnected); connect(m_Message, &Message::disconnected, this, &Client::onDisconnected); connect(m_Message, &Message::expired, [&]() { // If token expired, disconnect and reconnect again. disconnectAuthServer(); connectAuthServer(); }); m_Media = new Media(m_Manager); connect(m_Message, &Message::optionsChanged, m_Media, &Media::setOptions); m_Media->setURL(m_wsURL); m_Cloud = new Cloud(m_Manager); connect(m_Message, &Message::optionsChanged, m_Cloud, &Cloud::setOptions); - m_Cloud->setURL(QUrl("wss://live.stellarmate.com")); + m_Cloud->setURL(m_wsURL); } Client::~Client() { m_Message->disconnectServer(); m_Media->disconnectServer(); m_Cloud->disconnectServer(); } void Client::onConnected() { pi->stopAnimation(); m_isConnected = true; connectB->setText(i18n("Disconnect")); connectionState->setPixmap(QIcon::fromTheme("state-ok").pixmap(QSize(64, 64))); if (rememberCredentialsCheck->isChecked()) { #ifdef HAVE_KEYCHAIN // QJsonObject credentials = // { // {"username", username->text()}, // {"password", password->text()} // }; QKeychain::WritePasswordJob *job = new QKeychain::WritePasswordJob(QLatin1String("kstars")); job->setAutoDelete(true); job->setKey(QLatin1String("ekoslive")); //job->setTextData(QJsonDocument(credentials).toJson()); job->setTextData(password->text()); job->start(); #endif } } void Client::onDisconnected() { connectionState->setPixmap(QIcon::fromTheme("state-offline").pixmap(QSize(64, 64))); m_isConnected = false; connectB->setText(i18n("Connect")); } void Client::connectAuthServer() { if (username->text().isEmpty() || password->text().isEmpty()) { KSNotification::error(i18n("Username or password is missing.")); return; } pi->startAnimation(); authenticate(); } void Client::disconnectAuthServer() { token.clear(); m_Message->disconnectServer(); m_Media->disconnectServer(); m_Cloud->disconnectServer(); modeLabel->setEnabled(true); ekosLiveOnlineR->setEnabled(true); ekosLiveOfflineR->setEnabled(true); } void Client::authenticate() { QNetworkRequest request; request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QUrl authURL(m_serviceURL); authURL.setPath("/api/authenticate"); request.setUrl(authURL); QJsonObject json = { {"username", username->text()}, {"password", password->text()} }; auto postData = QJsonDocument(json).toJson(QJsonDocument::Compact); networkManager->post(request, postData); } void Client::onResult(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { // If connection refused, retry up to 3 times if (reply->error() == QNetworkReply::ConnectionRefusedError && m_AuthReconnectTries++ < RECONNECT_MAX_TRIES) { reply->deleteLater(); QTimer::singleShot(RECONNECT_INTERVAL, this, &Client::connectAuthServer); return; } m_AuthReconnectTries = 0; pi->stopAnimation(); connectionState->setPixmap(QIcon::fromTheme("state-error").pixmap(QSize(64, 64))); KSNotification::error(i18n("Error authentication with Ekos Live server: %1", reply->errorString())); reply->deleteLater(); return; } m_AuthReconnectTries = 0; QJsonParseError error; auto response = QJsonDocument::fromJson(reply->readAll(), &error); if (error.error != QJsonParseError::NoError) { pi->stopAnimation(); connectionState->setPixmap(QIcon::fromTheme("state-error").pixmap(QSize(64, 64))); KSNotification::error(i18n("Error parsing server response: %1", error.errorString())); reply->deleteLater(); return; } authResponse = response.object(); if (authResponse["success"].toBool() == false) { pi->stopAnimation(); connectionState->setPixmap(QIcon::fromTheme("state-error").pixmap(QSize(64, 64))); KSNotification::error(authResponse["message"].toString()); reply->deleteLater(); return; } token = authResponse["token"].toString(); m_Message->setAuthResponse(authResponse); m_Message->connectServer(); m_Media->setAuthResponse(authResponse); m_Media->connectServer(); // If we are using EkosLive Offline // We need to check for internet connection before we connect to the online web server if (ekosLiveOnlineR->isChecked() || (ekosLiveOfflineR->isChecked() && networkManager->networkAccessible() == QNetworkAccessManager::Accessible)) { m_Cloud->setAuthResponse(authResponse); m_Cloud->connectServer(); } modeLabel->setEnabled(false); ekosLiveOnlineR->setEnabled(false); ekosLiveOfflineR->setEnabled(false); reply->deleteLater(); } void Client::setConnected(bool enabled) { // Return if there is no change. if (enabled == m_isConnected) return; connectB->click(); } void Client::setConfig(bool onlineService, bool rememberCredentials, bool autoConnect) { ekosLiveOnlineR->setChecked(onlineService); ekosLiveOfflineR->setChecked(!onlineService); rememberCredentialsCheck->setChecked(rememberCredentials); autoStartCheck->setChecked(autoConnect); } void Client::setUser(const QString &user, const QString &pass) { username->setText(user); Options::setEkosLiveUsername(user); password->setText(pass); } } diff --git a/kstars/ekos/ekoslive/media.cpp b/kstars/ekos/ekoslive/media.cpp index ef254583a..728713599 100644 --- a/kstars/ekos/ekoslive/media.cpp +++ b/kstars/ekos/ekoslive/media.cpp @@ -1,317 +1,319 @@ /* Ekos Live Media Copyright (C) 2018 Jasem Mutlaq Media Channel This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "media.h" #include "commands.h" #include "profileinfo.h" #include "fitsviewer/fitsview.h" #include "fitsviewer/fitsdata.h" #include "ekos_debug.h" #include #include namespace EkosLive { Media::Media(Ekos::Manager * manager): m_Manager(manager) { connect(&m_WebSocket, &QWebSocket::connected, this, &Media::onConnected); connect(&m_WebSocket, &QWebSocket::disconnected, this, &Media::onDisconnected); connect(&m_WebSocket, static_cast(&QWebSocket::error), this, &Media::onError); connect(this, &Media::newMetadata, this, &Media::uploadMetadata); connect(this, &Media::newImage, this, &Media::uploadImage); } void Media::connectServer() { QUrl requestURL(m_URL); QUrlQuery query; query.addQueryItem("username", m_AuthResponse["username"].toString()); query.addQueryItem("token", m_AuthResponse["token"].toString()); if (m_AuthResponse.contains("remoteToken")) query.addQueryItem("remoteToken", m_AuthResponse["remoteToken"].toString()); + if (m_Options[OPTION_SET_CLOUD_STORAGE]) + query.addQueryItem("cloudEnabled", "true"); query.addQueryItem("email", m_AuthResponse["email"].toString()); query.addQueryItem("from_date", m_AuthResponse["from_date"].toString()); query.addQueryItem("to_date", m_AuthResponse["to_date"].toString()); query.addQueryItem("plan_id", m_AuthResponse["plan_id"].toString()); query.addQueryItem("type", m_AuthResponse["type"].toString()); requestURL.setPath("/media/ekos"); requestURL.setQuery(query); m_WebSocket.open(requestURL); qCInfo(KSTARS_EKOS) << "Connecting to Websocket server at" << requestURL.toDisplayString(); } void Media::disconnectServer() { m_WebSocket.close(); } void Media::onConnected() { qCInfo(KSTARS_EKOS) << "Connected to media Websocket server at" << m_URL.toDisplayString(); connect(&m_WebSocket, &QWebSocket::textMessageReceived, this, &Media::onTextReceived); connect(&m_WebSocket, &QWebSocket::binaryMessageReceived, this, &Media::onBinaryReceived); m_isConnected = true; m_ReconnectTries = 0; emit connected(); } void Media::onDisconnected() { qCInfo(KSTARS_EKOS) << "Disconnected from media Websocket server."; m_isConnected = false; disconnect(&m_WebSocket, &QWebSocket::textMessageReceived, this, &Media::onTextReceived); disconnect(&m_WebSocket, &QWebSocket::binaryMessageReceived, this, &Media::onBinaryReceived); m_sendBlobs = true; for (const QString &oneFile : temporaryFiles) QFile::remove(oneFile); temporaryFiles.clear(); emit disconnected(); } void Media::onError(QAbstractSocket::SocketError error) { qCritical(KSTARS_EKOS) << "Media Websocket connection error" << m_WebSocket.errorString(); if (error == QAbstractSocket::RemoteHostClosedError || error == QAbstractSocket::ConnectionRefusedError) { if (m_ReconnectTries++ < RECONNECT_MAX_TRIES) QTimer::singleShot(RECONNECT_INTERVAL, this, SLOT(connectServer())); } } void Media::onTextReceived(const QString &message) { qCInfo(KSTARS_EKOS) << "Media Text Websocket Message" << message; QJsonParseError error; auto serverMessage = QJsonDocument::fromJson(message.toLatin1(), &error); if (error.error != QJsonParseError::NoError) { qCWarning(KSTARS_EKOS) << "Ekos Live Parsing Error" << error.errorString(); return; } const QJsonObject msgObj = serverMessage.object(); const QString command = msgObj["type"].toString(); const QJsonObject payload = msgObj["payload"].toObject(); if (command == commands[ALIGN_SET_FILE_EXTENSION]) extension = payload["ext"].toString(); else if (command == commands[SET_BLOBS]) m_sendBlobs = msgObj["payload"].toBool(); } void Media::onBinaryReceived(const QByteArray &message) { // For now, we are only receiving binary image (jpg or FITS) for load and slew QTemporaryFile file(QString("/tmp/XXXXXX.%1").arg(extension)); file.setAutoRemove(false); file.open(); file.write(message); file.close(); Ekos::Align * align = m_Manager->alignModule(); const QString filename = file.fileName(); temporaryFiles << filename; align->loadAndSlew(filename); } void Media::sendPreviewJPEG(const QString &filename, QJsonObject metadata) { QString uuid = QUuid::createUuid().toString(); uuid = uuid.remove(QRegularExpression("[-{}]")); metadata.insert("uuid", uuid); QFile jpegFile(filename); if (!jpegFile.open(QFile::ReadOnly)) return; QByteArray jpegData = jpegFile.readAll(); emit newMetadata(QJsonDocument(metadata).toJson(QJsonDocument::Compact)); emit newImage(jpegData); } void Media::sendPreviewImage(const QString &filename, const QString &uuid) { if (m_isConnected == false || m_Options[OPTION_SET_IMAGE_TRANSFER] == false || m_sendBlobs == false) return; m_UUID = uuid; previewImage.reset(new FITSView()); connect(previewImage.get(), &FITSView::loaded, this, &Media::sendImage); previewImage->loadFITS(filename); } void Media::sendPreviewImage(FITSView * view, const QString &uuid) { if (m_isConnected == false || m_Options[OPTION_SET_IMAGE_TRANSFER] == false || m_sendBlobs == false) return; m_UUID = uuid; upload(view); } void Media::sendImage() { QtConcurrent::run(this, &Media::upload, previewImage.get()); } void Media::upload(FITSView * view) { QByteArray jpegData; QBuffer buffer(&jpegData); buffer.open(QIODevice::WriteOnly); QImage scaledImage = view->getDisplayImage().scaledToWidth(m_Options[OPTION_SET_HIGH_BANDWIDTH] ? HB_WIDTH : HB_WIDTH / 2); scaledImage.save(&buffer, "jpg", m_Options[OPTION_SET_HIGH_BANDWIDTH] ? HB_IMAGE_QUALITY : HB_IMAGE_QUALITY / 2); buffer.close(); const FITSData * imageData = view->getImageData(); QString resolution = QString("%1x%2").arg(imageData->width()).arg(imageData->height()); QString sizeBytes = KFormat().formatByteSize(imageData->size()); QVariant xbin(1), ybin(1); imageData->getRecordValue("XBINNING", xbin); imageData->getRecordValue("YBINNING", ybin); QString binning = QString("%1x%2").arg(xbin.toString()).arg(ybin.toString()); QString bitDepth = QString::number(imageData->bpp()); QString uuid; // Only send UUID for non-temporary compressed file or non-tempeorary files if ( (imageData->isCompressed() && imageData->compressedFilename().startsWith(QDir::tempPath()) == false) || (imageData->isTempFile() == false)) uuid = m_UUID; QJsonObject metadata = { {"resolution", resolution}, {"size", sizeBytes}, {"bin", binning}, {"bpp", bitDepth}, {"uuid", uuid}, }; emit newMetadata(QJsonDocument(metadata).toJson(QJsonDocument::Compact)); emit newImage(jpegData); //m_WebSocket.sendTextMessage(QJsonDocument(metadata).toJson(QJsonDocument::Compact)); //m_WebSocket.sendBinaryMessage(jpegData); if (view == previewImage.get()) previewImage.reset(); } void Media::sendUpdatedFrame(FITSView * view) { if (m_isConnected == false || m_Options[OPTION_SET_HIGH_BANDWIDTH] == false || m_sendBlobs == false) return; QByteArray jpegData; QBuffer buffer(&jpegData); buffer.open(QIODevice::WriteOnly); QPixmap displayPixmap = view->getDisplayPixmap(); if (correctionVector.isNull() == false) { QPointF center = 0.5 * correctionVector.p1() + 0.5 * correctionVector.p2(); double length = correctionVector.length(); if (length < 100) length = 100; QRect boundingRectable; boundingRectable.setSize(QSize(static_cast(length * 2), static_cast(length * 2))); QPoint topLeft = (center - QPointF(length, length)).toPoint(); boundingRectable.moveTo(topLeft); boundingRectable = boundingRectable.intersected(displayPixmap.rect()); emit newBoundingRect(boundingRectable, displayPixmap.size()); displayPixmap = displayPixmap.copy(boundingRectable); } else emit newBoundingRect(QRect(), QSize()); displayPixmap.save(&buffer, "jpg", m_Options[OPTION_SET_HIGH_BANDWIDTH] ? HB_PAH_IMAGE_QUALITY : HB_PAH_IMAGE_QUALITY / 2); buffer.close(); m_WebSocket.sendBinaryMessage(jpegData); } void Media::sendVideoFrame(std::unique_ptr &frame) { if (m_isConnected == false || m_Options[OPTION_SET_IMAGE_TRANSFER] == false || m_sendBlobs == false || !frame) return; // TODO Scale should be configurable QImage scaledImage = frame.get()->scaledToWidth(m_Options[OPTION_SET_HIGH_BANDWIDTH] ? HB_WIDTH : HB_WIDTH / 2); QTemporaryFile jpegFile; jpegFile.open(); jpegFile.close(); // TODO Quality should be configurable scaledImage.save(jpegFile.fileName(), "jpg", m_Options[OPTION_SET_HIGH_BANDWIDTH] ? HB_VIDEO_QUALITY : HB_VIDEO_QUALITY / 2); jpegFile.open(); m_WebSocket.sendBinaryMessage(jpegFile.readAll()); } void Media::registerCameras() { if (m_isConnected == false) return; for(ISD::GDInterface * gd : m_Manager->findDevices(KSTARS_CCD)) { ISD::CCD * oneCCD = dynamic_cast(gd); connect(oneCCD, &ISD::CCD::newVideoFrame, this, &Media::sendVideoFrame, Qt::UniqueConnection); } } void Media::resetPolarView() { this->correctionVector = QLineF(); m_Manager->alignModule()->zoomAlignView(); } void Media::uploadMetadata(const QByteArray &metadata) { m_WebSocket.sendTextMessage(metadata); } void Media::uploadImage(const QByteArray &image) { m_WebSocket.sendBinaryMessage(image); } }