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

0001 /*
0002     SPDX-FileCopyrightText: 2018 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     Media Channel
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "media.h"
0010 #include "commands.h"
0011 #include "skymapcomposite.h"
0012 #include "fitsviewer/fitsview.h"
0013 #include "fitsviewer/fitsdata.h"
0014 #include "indi/indilistener.h"
0015 #include "hips/hipsfinder.h"
0016 #include "kstarsdata.h"
0017 #include "ekos/auxiliary/darklibrary.h"
0018 #include "ekos/guide/guide.h"
0019 #include "ekos/align/align.h"
0020 #include "kspaths.h"
0021 #include "Options.h"
0022 
0023 #include "ekos_debug.h"
0024 #include "kstars.h"
0025 #include "version.h"
0026 
0027 #include <QtConcurrent>
0028 #include <KFormat>
0029 
0030 namespace EkosLive
0031 {
0032 
0033 ///////////////////////////////////////////////////////////////////////////////////////////
0034 ///
0035 ///////////////////////////////////////////////////////////////////////////////////////////
0036 Media::Media(Ekos::Manager * manager, QVector<QSharedPointer<NodeManager>> &nodeManagers):
0037     m_Manager(manager), m_NodeManagers(nodeManagers)
0038 {
0039     for (auto &nodeManager : m_NodeManagers)
0040     {
0041         connect(nodeManager->media(), &Node::connected, this, &Media::onConnected);
0042         connect(nodeManager->media(), &Node::disconnected, this, &Media::onDisconnected);
0043         connect(nodeManager->media(), &Node::onTextReceived, this, &Media::onTextReceived);
0044         connect(nodeManager->media(), &Node::onBinaryReceived, this, &Media::onBinaryReceived);
0045     }
0046 
0047     connect(this, &Media::newMetadata, this, &Media::uploadMetadata);
0048     connect(this, &Media::newImage, this, [this](const QByteArray & image)
0049     {
0050         uploadImage(image);
0051         m_TemporaryView.clear();
0052     });
0053 }
0054 
0055 ///////////////////////////////////////////////////////////////////////////////////////////
0056 ///
0057 ///////////////////////////////////////////////////////////////////////////////////////////
0058 bool Media::isConnected() const
0059 {
0060     return std::any_of(m_NodeManagers.begin(), m_NodeManagers.end(), [](auto & nodeManager)
0061     {
0062         return nodeManager->media()->isConnected();
0063     });
0064 }
0065 
0066 ///////////////////////////////////////////////////////////////////////////////////////////
0067 ///
0068 ///////////////////////////////////////////////////////////////////////////////////////////
0069 void Media::onConnected()
0070 {
0071     auto node = qobject_cast<Node*>(sender());
0072     if (!node)
0073         return;
0074 
0075     qCInfo(KSTARS_EKOS) << "Connected to Media Websocket server at" << node->url().toDisplayString();
0076 
0077     emit connected();
0078 }
0079 
0080 ///////////////////////////////////////////////////////////////////////////////////////////
0081 ///
0082 ///////////////////////////////////////////////////////////////////////////////////////////
0083 void Media::onDisconnected()
0084 {
0085     auto node = qobject_cast<Node*>(sender());
0086     if (!node)
0087         return;
0088 
0089     qCInfo(KSTARS_EKOS) << "Disconnected from Message Websocket server at" << node->url().toDisplayString();
0090 
0091     if (isConnected() == false)
0092     {
0093         m_sendBlobs = true;
0094 
0095         for (const QString &oneFile : temporaryFiles)
0096             QFile::remove(oneFile);
0097         temporaryFiles.clear();
0098 
0099         emit disconnected();
0100     }
0101 }
0102 
0103 ///////////////////////////////////////////////////////////////////////////////////////////
0104 ///
0105 ///////////////////////////////////////////////////////////////////////////////////////////
0106 void Media::onTextReceived(const QString &message)
0107 {
0108     qCInfo(KSTARS_EKOS) << "Media Text Websocket Message" << message;
0109     QJsonParseError error;
0110     auto serverMessage = QJsonDocument::fromJson(message.toLatin1(), &error);
0111     if (error.error != QJsonParseError::NoError)
0112     {
0113         qCWarning(KSTARS_EKOS) << "Ekos Live Parsing Error" << error.errorString();
0114         return;
0115     }
0116 
0117     const QJsonObject msgObj = serverMessage.object();
0118     const QString command = msgObj["type"].toString();
0119     const QJsonObject payload = msgObj["payload"].toObject();
0120 
0121     if (command == commands[ALIGN_SET_FILE_EXTENSION])
0122         extension = payload["ext"].toString();
0123     else if (command == commands[SET_BLOBS])
0124         m_sendBlobs = msgObj["payload"].toBool();
0125     // Get a list of object based on criteria
0126     else if (command == commands[ASTRO_GET_OBJECTS_IMAGE])
0127     {
0128         int level = payload["level"].toInt(5);
0129         double zoom = payload["zoom"].toInt(20000);
0130 
0131         // Object Names
0132         QVariantList objectNames = payload["names"].toArray().toVariantList();
0133 
0134         for (auto &oneName : objectNames)
0135         {
0136             const QString name = oneName.toString();
0137             SkyObject *oneObject = KStarsData::Instance()->skyComposite()->findByName(name, false);
0138             if (oneObject)
0139             {
0140                 QImage centerImage(HIPS_TILE_WIDTH, HIPS_TILE_HEIGHT, QImage::Format_ARGB32_Premultiplied);
0141                 double fov_w = 0, fov_h = 0;
0142 
0143                 if (oneObject->type() == SkyObject::MOON || oneObject->type() == SkyObject::PLANET)
0144                 {
0145                     QProcess xplanetProcess;
0146                     const QString output = KSPaths::writableLocation(QStandardPaths::TempLocation) + QDir::separator() + "xplanet.jpg";
0147                     xplanetProcess.start(Options::xplanetPath(), QStringList()
0148                                          << "--num_times" << "1"
0149                                          << "--geometry" << QString("%1x%2").arg(HIPS_TILE_WIDTH).arg(HIPS_TILE_HEIGHT)
0150                                          << "--body" << name.toLower()
0151                                          << "--output" << output);
0152                     xplanetProcess.waitForFinished(5000);
0153                     centerImage.load(output);
0154                 }
0155                 else
0156                     HIPSFinder::Instance()->render(oneObject, level, zoom, &centerImage, fov_w, fov_h);
0157 
0158                 if (!centerImage.isNull())
0159                 {
0160                     // Use seed from name, level, and zoom so that it is unique
0161                     // even if regenerated again.
0162                     auto seed = QString("%1%2%3").arg(QString::number(level), QString::number(zoom), name);
0163                     QString uuid = "hips_" + QCryptographicHash::hash(seed.toLatin1(), QCryptographicHash::Md5).toHex();
0164                     // Send everything as strings
0165                     QJsonObject metadata =
0166                     {
0167                         {"uuid", uuid},
0168                         {"name", name},
0169                         {"zoom", zoom},
0170                         {"resolution", QString("%1x%2").arg(HIPS_TILE_WIDTH).arg(HIPS_TILE_HEIGHT)},
0171                         {"bin", "1x1"},
0172                         {"fov_w", QString::number(fov_w)},
0173                         {"fov_h", QString::number(fov_h)},
0174                         {"ext", "jpg"}
0175                     };
0176 
0177                     QByteArray jpegData;
0178                     QBuffer buffer(&jpegData);
0179                     buffer.open(QIODevice::WriteOnly);
0180 
0181                     // First METADATA_PACKET bytes of the binary data is always allocated
0182                     // to the metadata, the rest to the image data.
0183                     QByteArray meta = QJsonDocument(metadata).toJson(QJsonDocument::Compact);
0184                     meta = meta.leftJustified(METADATA_PACKET, 0);
0185                     buffer.write(meta);
0186                     centerImage.save(&buffer, "jpg", 90);
0187                     buffer.close();
0188 
0189                     emit newImage(jpegData);
0190                 }
0191             }
0192         }
0193     }
0194     else if (command == commands[ASTRO_GET_SKYPOINT_IMAGE])
0195     {
0196         int level = payload["level"].toInt(5);
0197         double zoom = payload["zoom"].toInt(20000);
0198         double ra = payload["ra"].toDouble(0);
0199         double de = payload["de"].toDouble(0);
0200         double width = payload["width"].toDouble(512);
0201         double height = payload["height"].toDouble(512);
0202 
0203         QImage centerImage(width, height, QImage::Format_ARGB32_Premultiplied);
0204         SkyPoint coords(ra, de);
0205         SkyPoint J2000Coord(coords.ra(), coords.dec());
0206         J2000Coord.catalogueCoord(KStars::Instance()->data()->ut().djd());
0207         coords.setRA0(J2000Coord.ra());
0208         coords.setDec0(J2000Coord.dec());
0209         coords.EquatorialToHorizontal(KStarsData::Instance()->lst(), KStarsData::Instance()->geo()->lat());
0210 
0211         volatile auto jnowRAString = coords.ra().toHMSString();
0212         volatile auto jnowDEString = coords.dec().toDMSString();
0213         volatile auto j2000RAString = coords.ra0().toHMSString();
0214         volatile auto j2000DEString = coords.dec0().toDMSString();
0215 
0216 
0217         double fov_w = 0, fov_h = 0;
0218         HIPSFinder::Instance()->render(&coords, level, zoom, &centerImage, fov_w, fov_h);
0219 
0220         if (!centerImage.isNull())
0221         {
0222             // Use seed from name, level, and zoom so that it is unique
0223             // even if regenerated again.
0224             // Send everything as strings
0225             QJsonObject metadata =
0226             {
0227                 {"uuid", "skypoint_hips"},
0228                 {"name", "skypoint_hips"},
0229                 {"zoom", zoom},
0230                 {"resolution", QString("%1x%2").arg(width).arg(height)},
0231                 {"bin", "1x1"},
0232                 {"fov_w", QString::number(fov_w)},
0233                 {"fov_h", QString::number(fov_h)},
0234                 {"ext", "jpg"}
0235             };
0236 
0237             QByteArray jpegData;
0238             QBuffer buffer(&jpegData);
0239             buffer.open(QIODevice::WriteOnly);
0240 
0241             // First METADATA_PACKET bytes of the binary data is always allocated
0242             // to the metadata, the rest to the image data.
0243             QByteArray meta = QJsonDocument(metadata).toJson(QJsonDocument::Compact);
0244             meta = meta.leftJustified(METADATA_PACKET, 0);
0245             buffer.write(meta);
0246             centerImage.save(&buffer, "jpg", 95);
0247             buffer.close();
0248 
0249             emit newImage(jpegData);
0250         }
0251     }
0252 }
0253 
0254 ///////////////////////////////////////////////////////////////////////////////////////////
0255 ///
0256 ///////////////////////////////////////////////////////////////////////////////////////////
0257 void Media::onBinaryReceived(const QByteArray &message)
0258 {
0259     // Sometimes this is triggered even though it's a text message
0260     Ekos::Align * align = m_Manager->alignModule();
0261     if (align)
0262     {
0263         QString metadataString = message.left(METADATA_PACKET);
0264         QJsonDocument metadataDocument = QJsonDocument::fromJson(metadataString.toLatin1());
0265         QJsonObject metadataJSON = metadataDocument.object();
0266         QString extension = metadataJSON.value("ext").toString();
0267         align->loadAndSlew(message.mid(METADATA_PACKET), extension);
0268     }
0269 }
0270 
0271 ///////////////////////////////////////////////////////////////////////////////////////////
0272 ///
0273 ///////////////////////////////////////////////////////////////////////////////////////////
0274 void Media::sendDarkLibraryData(const QSharedPointer<FITSData> &data)
0275 {
0276     sendData(data, "+D");
0277 };
0278 
0279 ///////////////////////////////////////////////////////////////////////////////////////////
0280 ///
0281 ///////////////////////////////////////////////////////////////////////////////////////////
0282 void Media::sendData(const QSharedPointer<FITSData> &data, const QString &uuid)
0283 {
0284     if (Options::ekosLiveImageTransfer() == false || m_sendBlobs == false)
0285         return;
0286 
0287     m_UUID = uuid;
0288 
0289     m_TemporaryView.reset(new FITSView());
0290     m_TemporaryView->loadData(data);
0291     QtConcurrent::run(this, &Media::upload, m_TemporaryView);
0292 }
0293 
0294 ///////////////////////////////////////////////////////////////////////////////////////////
0295 ///
0296 ///////////////////////////////////////////////////////////////////////////////////////////
0297 void Media::sendFile(const QString &filename, const QString &uuid)
0298 {
0299     if (Options::ekosLiveImageTransfer() == false || m_sendBlobs == false)
0300         return;
0301 
0302     m_UUID = uuid;
0303 
0304     QSharedPointer<FITSView> previewImage(new FITSView());
0305     connect(previewImage.get(), &FITSView::loaded, this, [this, previewImage]()
0306     {
0307         QtConcurrent::run(this, &Media::upload, previewImage);
0308     });
0309     previewImage->loadFile(filename);
0310 }
0311 
0312 ///////////////////////////////////////////////////////////////////////////////////////////
0313 ///
0314 ///////////////////////////////////////////////////////////////////////////////////////////
0315 void Media::sendView(const QSharedPointer<FITSView> &view, const QString &uuid)
0316 {
0317     if (Options::ekosLiveImageTransfer() == false || m_sendBlobs == false)
0318         return;
0319 
0320     m_UUID = uuid;
0321 
0322     upload(view);
0323 }
0324 
0325 ///////////////////////////////////////////////////////////////////////////////////////////
0326 ///
0327 ///////////////////////////////////////////////////////////////////////////////////////////
0328 void Media::upload(const QSharedPointer<FITSView> &view)
0329 {
0330     const QString ext = "jpg";
0331     QByteArray jpegData;
0332     QBuffer buffer(&jpegData);
0333     buffer.open(QIODevice::WriteOnly);
0334 
0335     const QSharedPointer<FITSData> imageData = view->imageData();
0336     QString resolution = QString("%1x%2").arg(imageData->width()).arg(imageData->height());
0337     QString sizeBytes = KFormat().formatByteSize(imageData->size());
0338     QVariant xbin(1), ybin(1), exposure(0), focal_length(0), gain(0), pixel_size(0), aperture(0);
0339     imageData->getRecordValue("XBINNING", xbin);
0340     imageData->getRecordValue("YBINNING", ybin);
0341     imageData->getRecordValue("EXPTIME", exposure);
0342     imageData->getRecordValue("GAIN", gain);
0343     imageData->getRecordValue("PIXSIZE1", pixel_size);
0344     imageData->getRecordValue("FOCALLEN", focal_length);
0345     imageData->getRecordValue("APTDIA", aperture);
0346 
0347     auto stretchParameters = view->getStretchParams();
0348 
0349     // Account for binning
0350     const double binned_pixel = pixel_size.toDouble() * xbin.toInt();
0351     // Send everything as strings
0352     QJsonObject metadata =
0353     {
0354         {"resolution", resolution},
0355         {"size", sizeBytes},
0356         {"channels", imageData->channels()},
0357         {"mean", imageData->getAverageMean()},
0358         {"median", imageData->getAverageMedian()},
0359         {"stddev", imageData->getAverageStdDev()},
0360         {"min", imageData->getMin()},
0361         {"max", imageData->getMax()},
0362         {"bin", QString("%1x%2").arg(xbin.toString(), ybin.toString())},
0363         {"bpp", QString::number(imageData->bpp())},
0364         {"uuid", m_UUID},
0365         {"exposure", exposure.toString()},
0366         {"focal_length", focal_length.toString()},
0367         {"aperture", aperture.toString()},
0368         {"gain", gain.toString()},
0369         {"pixel_size", QString::number(binned_pixel, 'f', 4)},
0370         {"shadows", stretchParameters.grey_red.shadows},
0371         {"midtones", stretchParameters.grey_red.midtones},
0372         {"highlights", stretchParameters.grey_red.highlights},
0373         {"hasWCS", imageData->hasWCS()},
0374         {"hfr", imageData->getHFR()},
0375         {"ext", ext}
0376     };
0377 
0378     // First METADATA_PACKET bytes of the binary data is always allocated
0379     // to the metadata
0380     // the rest to the image data.
0381     QByteArray meta = QJsonDocument(metadata).toJson(QJsonDocument::Compact);
0382     meta = meta.leftJustified(METADATA_PACKET, 0);
0383     buffer.write(meta);
0384 
0385     auto fastImage = (!Options::ekosLiveHighBandwidth() || m_UUID[0] == "+");
0386     auto scaleWidth = fastImage ? HB_IMAGE_WIDTH / 2 : HB_IMAGE_WIDTH;
0387 
0388     // For low bandwidth images
0389     // Except for dark frames +D
0390     QPixmap scaledImage = view->getDisplayPixmap().width() > scaleWidth ?
0391                           view->getDisplayPixmap().scaledToWidth(scaleWidth, fastImage ? Qt::FastTransformation : Qt::SmoothTransformation) :
0392                           view->getDisplayPixmap();
0393     scaledImage.save(&buffer, ext.toLatin1().constData(), HB_IMAGE_QUALITY);
0394 
0395     buffer.close();
0396 
0397     emit newImage(jpegData);
0398 }
0399 
0400 ///////////////////////////////////////////////////////////////////////////////////////////
0401 ///
0402 ///////////////////////////////////////////////////////////////////////////////////////////
0403 void Media::sendUpdatedFrame(const QSharedPointer<FITSView> &view)
0404 {
0405     QString ext = "jpg";
0406     QByteArray jpegData;
0407     QBuffer buffer(&jpegData);
0408     buffer.open(QIODevice::WriteOnly);
0409 
0410     const QSharedPointer<FITSData> imageData = view->imageData();
0411 
0412     if (!imageData)
0413         return;
0414 
0415     const int32_t width = imageData->width();
0416     const int32_t height = imageData->height();
0417     QString resolution = QString("%1x%2").arg(width).arg(height);
0418     QString sizeBytes = KFormat().formatByteSize(imageData->size());
0419     QVariant xbin(1), ybin(1), exposure(0), focal_length(0), gain(0), pixel_size(0), aperture(0);
0420     imageData->getRecordValue("XBINNING", xbin);
0421     imageData->getRecordValue("YBINNING", ybin);
0422     imageData->getRecordValue("EXPTIME", exposure);
0423     imageData->getRecordValue("GAIN", gain);
0424     imageData->getRecordValue("PIXSIZE1", pixel_size);
0425     imageData->getRecordValue("FOCALLEN", focal_length);
0426     imageData->getRecordValue("APTDIA", aperture);
0427 
0428     // Account for binning
0429     const double binned_pixel = pixel_size.toDouble() * xbin.toInt();
0430     // Send everything as strings
0431     QJsonObject metadata =
0432     {
0433         {"resolution", resolution},
0434         {"size", sizeBytes},
0435         {"channels", imageData->channels()},
0436         {"mean", imageData->getAverageMean()},
0437         {"median", imageData->getAverageMedian()},
0438         {"stddev", imageData->getAverageStdDev()},
0439         {"bin", QString("%1x%2").arg(xbin.toString()).arg(ybin.toString())},
0440         {"bpp", QString::number(imageData->bpp())},
0441         {"uuid", "+A"},
0442         {"exposure", exposure.toString()},
0443         {"focal_length", focal_length.toString()},
0444         {"aperture", aperture.toString()},
0445         {"gain", gain.toString()},
0446         {"pixel_size", QString::number(binned_pixel, 'f', 4)},
0447         {"ext", ext}
0448     };
0449 
0450     // First METADATA_PACKET bytes of the binary data is always allocated
0451     // to the metadata
0452     // the rest to the image data.
0453     QByteArray meta = QJsonDocument(metadata).toJson(QJsonDocument::Compact);
0454     meta = meta.leftJustified(METADATA_PACKET, 0);
0455     buffer.write(meta);
0456 
0457     // For low bandwidth images
0458     QPixmap scaledImage;
0459     // Align images
0460     if (correctionVector.isNull() == false)
0461     {
0462         scaledImage = view->getDisplayPixmap();
0463         const double currentZoom = view->getCurrentZoom();
0464         const double normalizedZoom = currentZoom / 100;
0465         // If zoom level is not 100%, then scale.
0466         if (fabs(normalizedZoom - 1) > 0.001)
0467             scaledImage = view->getDisplayPixmap().scaledToWidth(view->zoomedWidth());
0468         else
0469             scaledImage = view->getDisplayPixmap();
0470         // as we factor in the zoom level, we adjust center and length accordingly
0471         QPointF center = 0.5 * correctionVector.p1() * normalizedZoom + 0.5 * correctionVector.p2() * normalizedZoom;
0472         uint32_t length = qMax(correctionVector.length() / normalizedZoom, 100 / normalizedZoom);
0473 
0474         QRect boundingRectable;
0475         boundingRectable.setSize(QSize(length * 2, length * 2));
0476         QPoint topLeft = (center - QPointF(length, length)).toPoint();
0477         boundingRectable.moveTo(topLeft);
0478         boundingRectable = boundingRectable.intersected(scaledImage.rect());
0479 
0480         emit newBoundingRect(boundingRectable, scaledImage.size(), currentZoom);
0481 
0482         scaledImage = scaledImage.copy(boundingRectable);
0483     }
0484     else
0485     {
0486         scaledImage = view->getDisplayPixmap().width() > HB_IMAGE_WIDTH / 2 ?
0487                       view->getDisplayPixmap().scaledToWidth(HB_IMAGE_WIDTH / 2, Qt::FastTransformation) :
0488                       view->getDisplayPixmap();
0489         emit newBoundingRect(QRect(), QSize(), 100);
0490     }
0491 
0492     scaledImage.save(&buffer, ext.toLatin1().constData(), HB_IMAGE_QUALITY);
0493     buffer.close();
0494     emit newImage(jpegData);
0495 }
0496 
0497 ///////////////////////////////////////////////////////////////////////////////////////////
0498 ///
0499 ///////////////////////////////////////////////////////////////////////////////////////////
0500 void Media::sendVideoFrame(const QSharedPointer<QImage> &frame)
0501 {
0502     if (Options::ekosLiveImageTransfer() == false || m_sendBlobs == false || !frame)
0503         return;
0504 
0505     int32_t width = Options::ekosLiveHighBandwidth() ? HB_VIDEO_WIDTH : HB_VIDEO_WIDTH / 2;
0506     QByteArray image;
0507     QBuffer buffer(&image);
0508     buffer.open(QIODevice::WriteOnly);
0509 
0510     QImage videoImage = (frame->width() > width) ? frame->scaledToWidth(width) : *frame;
0511 
0512     QString resolution = QString("%1x%2").arg(videoImage.width()).arg(videoImage.height());
0513 
0514     // First METADATA_PACKET bytes of the binary data is always allocated
0515     // to the metadata
0516     // the rest to the image data.
0517     QJsonObject metadata =
0518     {
0519         {"resolution", resolution},
0520         {"ext", "jpg"}
0521     };
0522     QByteArray meta = QJsonDocument(metadata).toJson(QJsonDocument::Compact);
0523     meta = meta.leftJustified(METADATA_PACKET, 0);
0524     buffer.write(meta);
0525 
0526     QImageWriter writer;
0527     writer.setDevice(&buffer);
0528     writer.setFormat("JPG");
0529     writer.setCompression(6);
0530     writer.write(videoImage);
0531     buffer.close();
0532 
0533     for (auto &nodeManager : m_NodeManagers)
0534     {
0535         nodeManager->media()->sendBinaryMessage(image);
0536     }
0537 }
0538 
0539 ///////////////////////////////////////////////////////////////////////////////////////////
0540 ///
0541 ///////////////////////////////////////////////////////////////////////////////////////////
0542 void Media::registerCameras()
0543 {
0544     static const QRegularExpression re("[-{}]");
0545     for(auto &oneDevice : INDIListener::devices())
0546     {
0547         auto camera = oneDevice->getCamera();
0548         if (camera)
0549         {
0550             camera->disconnect(this);
0551             connect(camera, &ISD::Camera::newVideoFrame, this, &Media::sendVideoFrame);
0552             connect(camera, &ISD::Camera::newView, this, [this](const QSharedPointer<FITSView> &view)
0553             {
0554                 QString uuid = QUuid::createUuid().toString();
0555                 uuid = uuid.remove(re);
0556                 sendView(view, uuid);
0557             });
0558         }
0559     }
0560 }
0561 
0562 void Media::resetPolarView()
0563 {
0564     this->correctionVector = QLineF();
0565     m_Manager->alignModule()->zoomAlignView();
0566 }
0567 
0568 void Media::uploadMetadata(const QByteArray &metadata)
0569 {
0570     for (auto &nodeManager : m_NodeManagers)
0571     {
0572         nodeManager->media()->sendTextMessage(metadata);
0573     }
0574 }
0575 
0576 void Media::uploadImage(const QByteArray &image)
0577 {
0578     for (auto &nodeManager : m_NodeManagers)
0579     {
0580         nodeManager->media()->sendBinaryMessage(image);
0581     }
0582 }
0583 
0584 void Media::processNewBLOB(IBLOB * bp)
0585 {
0586     Q_UNUSED(bp)
0587 }
0588 
0589 void Media::sendModuleFrame(const QSharedPointer<FITSView> &view)
0590 {
0591     if (Options::ekosLiveImageTransfer() == false || m_sendBlobs == false)
0592         return;
0593 
0594     if (qobject_cast<Ekos::Align*>(sender()) == m_Manager->alignModule())
0595         sendView(view, "+A");
0596     else if (qobject_cast<Ekos::Focus*>(sender()) == m_Manager->focusModule())
0597         sendView(view, "+F");
0598     else if (qobject_cast<Ekos::Guide*>(sender()) == m_Manager->guideModule())
0599         sendView(view, "+G");
0600     else if (qobject_cast<Ekos::DarkLibrary*>(sender()) == Ekos::DarkLibrary::Instance())
0601         sendView(view, "+D");
0602 }
0603 }