Kstars

indiwebmanager.cpp
1/*
2 SPDX-FileCopyrightText: 2016 Jasem Mutlaq <mutlaqja@ikarustech.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "indiwebmanager.h"
8
9#include "auxiliary/ksnotification.h"
10#include "driverinfo.h"
11#include "drivermanager.h"
12#include "Options.h"
13#include "profileinfo.h"
14
15#include <QJsonArray>
16#include <QJsonDocument>
17#include <QJsonObject>
18#include <QNetworkReply>
19#include <QtConcurrent>
20
21#include "ekos_debug.h"
22
23namespace INDI
24{
25
26namespace WebManager
27{
28bool getWebManagerResponse(QNetworkAccessManager::Operation operation, const QUrl &url, QJsonDocument *reply,
29 QByteArray *data)
30{
31 QNetworkAccessManager manager;
32 QNetworkReply *response = nullptr;
33 QNetworkRequest request;
34
35 request.setUrl(url);
36
37 if (data)
38 {
39 request.setRawHeader("Content-Type", "application/json");
40 request.setRawHeader("Content-Length", QByteArray::number(data->size()));
41 }
42
43 switch (operation)
44 {
46 response = manager.get(request);
47 break;
48
50 if (data)
51 response = manager.post(request, *data);
52 else
53 response = manager.post(request, QByteArray());
54 break;
55
57 response = manager.deleteResource(request);
58 break;
59
61 response = manager.put(request, *data);
62 break;
63
64 default:
65 return false;
66 }
67
68 // Wait synchronously
69 QEventLoop event;
70 QObject::connect(response, SIGNAL(finished()), &event, SLOT(quit()));
71 event.exec();
72
73 if (response->error() == QNetworkReply::NoError)
74 {
75 if (reply)
76 {
77 QJsonParseError parseError;
78 *reply = QJsonDocument::fromJson(response->readAll(), &parseError);
79
80 if (parseError.error != QJsonParseError::NoError)
81 {
82 qDebug() << Q_FUNC_INFO << "INDI: JSon error during parsing " << parseError.errorString();
83 return false;
84 }
85 }
86
87 return true;
88 }
89 else
90 {
91 qDebug() << Q_FUNC_INFO << "INDI: Error communicating with INDI Web Manager: " << response->errorString();
92 return false;
93 }
94}
95
96bool isOnline(const QSharedPointer<ProfileInfo> &pi)
97{
98 QTimer timer;
99 timer.setSingleShot(true);
100 QNetworkAccessManager manager;
101 QUrl url(QString("http://%1:%2/api/server/status").arg(pi->host).arg(pi->INDIWebManagerPort));
102 QNetworkReply *response = manager.get(QNetworkRequest(url));
103
104 // Wait synchronously
105 QEventLoop event;
107 QObject::connect(response, SIGNAL(finished()), &event, SLOT(quit()));
108 timer.start(3000);
109 event.exec();
110
111 if (timer.isActive() && response->error() == QNetworkReply::NoError)
112 return true;
113 // Fallback to default if DNS lookup fails for .local
114 else if (pi->host.contains(".local"))
115 {
116 QUrl url(QString("http://10.250.250.1:8624/api/server/status"));
117 QNetworkReply *response = manager.get(QNetworkRequest(url));
118 // Wait synchronously
119 QEventLoop event;
121 QObject::connect(response, SIGNAL(finished()), &event, SLOT(quit()));
122 timer.start(3000);
123 event.exec();
124
125 if (timer.isActive() && response->error() == QNetworkReply::NoError)
126 {
127 pi->host = "10.250.250.1";
128 return true;
129 }
130 }
131
132 return false;
133}
134
135bool checkVersion(const QSharedPointer<ProfileInfo> &pi)
136{
137 QNetworkAccessManager manager;
138 QUrl url(QString("http://%1:%2/api/info/version").arg(pi->host).arg(pi->INDIWebManagerPort));
139
140 QJsonDocument json;
141 if (getWebManagerResponse(QNetworkAccessManager::GetOperation, url, &json))
142 {
143 QJsonObject version = json.object();
144 if (version.contains("version") == false)
145 return false;
146 qInfo(KSTARS_EKOS) << "Detected Web Manager version" << version["version"].toString();
147 return true;
148 }
149 return false;
150}
151
152bool syncCustomDrivers(const QSharedPointer<ProfileInfo> &pi)
153{
154 QNetworkAccessManager manager;
155 QUrl url(QString("http://%1:%2/api/profiles/custom").arg(pi->host).arg(pi->INDIWebManagerPort));
156
157 QStringList customDriversLabels;
158 QMapIterator<QString, QString> i(pi->drivers);
159 while (i.hasNext())
160 {
161 QString name = i.next().value();
162 auto driver = DriverManager::Instance()->findDriverByName(name);
163
164 if (driver.isNull())
165 driver = DriverManager::Instance()->findDriverByLabel(name);
166 if (driver && driver->getDriverSource() == CUSTOM_SOURCE)
167 customDriversLabels << driver->getLabel();
168 }
169
170 // Search for locked filter by filter color name
171 const QList<QVariantMap> &customDrivers = DriverManager::Instance()->getCustomDrivers();
172
173 for (auto &label : customDriversLabels)
174 {
175 auto pos = std::find_if(customDrivers.begin(), customDrivers.end(), [label](QVariantMap oneDriver)
176 {
177 return (oneDriver["Label"] == label);
178 });
179
180 if (pos == customDrivers.end())
181 continue;
182
183 QVariantMap driver = (*pos);
184 QJsonObject jsonDriver = QJsonObject::fromVariantMap(driver);
185
186 QByteArray data = QJsonDocument(jsonDriver).toJson();
187 getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr, &data);
188 }
189
190 return true;
191}
192
193bool areDriversRunning(const QSharedPointer<ProfileInfo> &pi)
194{
195 QUrl url(QString("http://%1:%2/api/server/drivers").arg(pi->host).arg(pi->INDIWebManagerPort));
196 QJsonDocument json;
197
198 if (getWebManagerResponse(QNetworkAccessManager::GetOperation, url, &json))
199 {
200 QJsonArray array = json.array();
201
202 if (array.isEmpty())
203 return false;
204
205 QStringList piExecDrivers;
206 QMapIterator<QString, QString> i(pi->drivers);
207 while (i.hasNext())
208 {
209 QString name = i.next().value();
210 auto driver = DriverManager::Instance()->findDriverByName(name);
211
212 if (driver.isNull())
213 driver = DriverManager::Instance()->findDriverByLabel(name);
214 if (driver)
215 piExecDrivers << driver->getExecutable();
216 }
217
218 if (array.count() < piExecDrivers.count())
219 return false;
220
221 // Get all the drivers running remotely
222 QStringList webManagerDrivers;
223 for (auto value : array)
224 {
225 QJsonObject driver = value.toObject();
226 // Old Web Manager API API
227 QString exec = driver["driver"].toString();
228 if (exec.isEmpty())
229 // New v0.1.5+ Web Manager API
230 exec = driver["binary"].toString();
231 webManagerDrivers << exec;
232 }
233
234 // Make sure all the profile drivers are running there
235 for (auto &oneDriverExec : piExecDrivers)
236 {
237 if (webManagerDrivers.contains(oneDriverExec) == false)
238 {
239 KSNotification::error(i18n("Driver %1 failed to start on the remote INDI server.", oneDriverExec));
240 qCritical(KSTARS_EKOS) << "Driver" << oneDriverExec << "failed to start on the remote INDI server!";
241 return false;
242 }
243 }
244
245 return true;
246 }
247
248 return false;
249}
250
251bool syncProfile(const QSharedPointer<ProfileInfo> &pi)
252{
253 QUrl url;
254 QJsonDocument jsonDoc;
255 QJsonParseError jsonError;
256 QByteArray data;
257 QJsonArray profileScripts;
258
259 //Add Profile
260 url = QUrl(QString("http://%1:%2/api/profiles/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
261 getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
262
263 // If we have scripts, let's try to parse and send them.
264 if (pi->scripts.isEmpty() == false)
265 {
266 auto doc = QJsonDocument::fromJson(pi->scripts, &jsonError);
267
268 if (jsonError.error == QJsonParseError::NoError)
269 {
270 profileScripts = doc.array();
271 // Update profile info
272 url = QUrl(QString("http://%1:%2/api/profiles/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
273 QJsonObject profileObject{ { "port", pi->port }, {"scripts", profileScripts} };
274 jsonDoc = QJsonDocument(profileObject);
275 data = jsonDoc.toJson();
276 getWebManagerResponse(QNetworkAccessManager::PutOperation, url, nullptr, &data);
277 }
278 }
279
280 // Add drivers
281 url = QUrl(QString("http://%1:%2/api/profiles/%3/drivers").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
282 QJsonArray driverArray;
283 QMapIterator<QString, QString> i(pi->drivers);
284
285 // In case both Guider + CCD are Multiple-Devices-Per-Driver type
286 // Then we should not define guider as a separate driver since that would start the driver executable twice
287 // when we only need it once
288 if (pi->drivers.contains("Guider"))
289 {
290 if (pi->drivers["Guider"] == pi->drivers["CCD"])
291 {
292 QSharedPointer<DriverInfo> guiderInfo;
293 if ((guiderInfo = DriverManager::Instance()->findDriverByName(pi->drivers["Guider"])).isNull())
294 {
295 if ((guiderInfo = DriverManager::Instance()->findDriverByLabel(pi->drivers["Guider"])).isNull())
296 {
297 guiderInfo = DriverManager::Instance()->findDriverByExec(pi->drivers["Guider"]);
298 }
299 }
300
301 if (guiderInfo && guiderInfo->getAuxInfo().value("mdpd", false).toBool())
302 {
303 pi->drivers.remove("Guider");
304 i = QMapIterator<QString, QString>(pi->drivers);
305 }
306 }
307 }
308
309 // Regular Drivers
310 while (i.hasNext())
311 driverArray.append(QJsonObject({{"label", i.next().value()}}));
312
313 // Remote Drivers
314 if (pi->remotedrivers.isEmpty() == false)
315 {
316 for (auto &remoteDriver : pi->remotedrivers.split(","))
317 {
318 driverArray.append(QJsonObject({{"remote", remoteDriver}}));
319 }
320 }
321
322 QJsonArray sortedList;
323 for (const auto &oneRule : qAsConst(profileScripts))
324 {
325 auto matchingDriver = std::find_if(driverArray.begin(), driverArray.end(), [oneRule](const auto & oneDriver)
326 {
327 return oneDriver.toObject()["label"].toString() == oneRule.toObject()["Driver"].toString();
328 });
329
330 if (matchingDriver != driverArray.end())
331 {
332 sortedList.append(*matchingDriver);
333 }
334 }
335
336 // If we have any profile scripts drivers, let's re-sort managed drivers
337 // so that profile script drivers
338 if (!sortedList.isEmpty())
339 {
340 for (const auto oneDriver : driverArray)
341 {
342 if (sortedList.contains(oneDriver) == false)
343 sortedList.append(oneDriver);
344 }
345
346 driverArray = sortedList;
347 }
348
349 data = QJsonDocument(driverArray).toJson();
350 getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr, &data);
351
352 return true;
353}
354
355bool startProfile(const QSharedPointer<ProfileInfo> &pi)
356{
357 // First make sure profile is created and synced on web manager
358 syncProfile(pi);
359
360 // Start profile
361 QUrl url(QString("http://%1:%2/api/server/start/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
362 getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
363
364 // Make sure drivers are running
365 // Try up to 3 times
366 for (int i = 0; i < 3; i++)
367 {
368 if (areDriversRunning(pi))
369 return true;
370 }
371
372 return false;
373}
374
375bool stopProfile(const QSharedPointer<ProfileInfo> &pi)
376{
377 // Stop profile
378 QUrl url(QString("http://%1:%2/api/server/stop").arg(pi->host).arg(pi->INDIWebManagerPort));
379 return getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
380}
381
382bool restartDriver(const QSharedPointer<ProfileInfo> &pi, const QString &label)
383{
384 QUrl url(QString("http://%1:%2/api/drivers/restart/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(label));
385 return getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
386}
387}
388
389// Async version of the Web Manager
390namespace AsyncWebManager
391{
392
393QFuture<bool> isOnline(const QSharedPointer<ProfileInfo> &pi)
394{
395 return QtConcurrent::run(WebManager::isOnline, pi);
396}
397
398QFuture<bool> isStellarMate(const QSharedPointer<ProfileInfo> &pi)
399{
400 return QtConcurrent::run(WebManager::checkVersion, pi);
401}
402
403QFuture<bool> syncCustomDrivers(const QSharedPointer<ProfileInfo> &pi)
404{
405 return QtConcurrent::run(WebManager::syncCustomDrivers, pi);
406}
407
408QFuture<bool> areDriversRunning(const QSharedPointer<ProfileInfo> &pi)
409{
410 return QtConcurrent::run(WebManager::areDriversRunning, pi);
411}
412
413QFuture<bool> syncProfile(const QSharedPointer<ProfileInfo> &pi)
414{
415 return QtConcurrent::run(WebManager::syncProfile, pi);
416}
417
418QFuture<bool> startProfile(const QSharedPointer<ProfileInfo> &pi)
419{
420 return QtConcurrent::run(WebManager::startProfile, pi);
421}
422
423QFuture<bool> stopProfile(const QSharedPointer<ProfileInfo> &pi)
424{
425 return QtConcurrent::run(WebManager::stopProfile, pi);
426}
427}
428
429}
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
KDB_EXPORT KDbVersionInfo version()
QString name(StandardAction id)
QAction * quit(const QObject *recvr, const char *slot, QObject *parent)
QByteArray number(double n, char format, int precision)
qsizetype size() const const
void quit()
QString errorString() const const
QByteArray readAll()
void append(const QJsonValue &value)
iterator begin()
bool contains(const QJsonValue &value) const const
qsizetype count() const const
iterator end()
bool isEmpty() const const
QJsonArray array() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
QByteArray toJson(JsonFormat format) const const
QJsonObject fromVariantMap(const QVariantMap &map)
QString errorString() const const
iterator begin()
qsizetype count() const const
iterator end()
QNetworkReply * deleteResource(const QNetworkRequest &request)
QNetworkReply * get(const QNetworkRequest &request)
QNetworkReply * post(const QNetworkRequest &request, QHttpMultiPart *multiPart)
QNetworkReply * put(const QNetworkRequest &request, QHttpMultiPart *multiPart)
NetworkError error() const const
void setRawHeader(const QByteArray &headerName, const QByteArray &headerValue)
void setUrl(const QUrl &url)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool isEmpty() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
QFuture< T > run(Function function,...)
bool isActive() const const
void setSingleShot(bool singleShot)
void start()
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 31 2025 11:53:49 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.