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
23 namespace INDI
24 {
25
26 namespace WebManager
27 {
getWebManagerResponse(QNetworkAccessManager::Operation operation,const QUrl & url,QJsonDocument * reply,QByteArray * data)28 bool 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 {
45 case QNetworkAccessManager::GetOperation:
46 response = manager.get(request);
47 break;
48
49 case QNetworkAccessManager::PostOperation:
50 if (data)
51 response = manager.post(request, *data);
52 else
53 response = manager.post(request, QByteArray());
54 break;
55
56 case QNetworkAccessManager::DeleteOperation:
57 response = manager.deleteResource(request);
58 break;
59
60 case QNetworkAccessManager::PutOperation:
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() << "INDI: JSon error during parsing " << parseError.errorString();
83 return false;
84 }
85 }
86
87 return true;
88 }
89 else
90 {
91 qDebug() << "INDI: Error communicating with INDI Web Manager: " << response->errorString();
92 return false;
93 }
94 }
95
isOnline(ProfileInfo * pi)96 bool isOnline(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;
106 QObject::connect(&timer, &QTimer::timeout, &event, &QEventLoop::quit);
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;
120 QObject::connect(&timer, &QTimer::timeout, &event, &QEventLoop::quit);
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
isStellarMate(ProfileInfo * pi)135 bool isStellarMate(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) << "Detect StellarMate version" << version["version"].toString();
147 return true;
148 }
149 return false;
150 }
151
syncCustomDrivers(ProfileInfo * pi)152 bool syncCustomDrivers(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 DriverInfo *driver = DriverManager::Instance()->findDriverByName(name);
163
164 if (driver == nullptr)
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
areDriversRunning(ProfileInfo * pi)193 bool areDriversRunning(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 DriverInfo *driver = DriverManager::Instance()->findDriverByName(name);
211
212 if (driver == nullptr)
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
syncProfile(ProfileInfo * pi)251 bool syncProfile(ProfileInfo *pi)
252 {
253 QUrl url;
254 QJsonDocument jsonDoc;
255 QByteArray data;
256
257 //Add Profile
258 url = QUrl(QString("http://%1:%2/api/profiles/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
259 getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
260
261 // Update profile info
262 url = QUrl(QString("http://%1:%2/api/profiles/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
263 QJsonObject profileObject{ { "port", pi->port } };
264 jsonDoc = QJsonDocument(profileObject);
265 data = jsonDoc.toJson();
266 getWebManagerResponse(QNetworkAccessManager::PutOperation, url, nullptr, &data);
267
268 // Add drivers
269 url = QUrl(QString("http://%1:%2/api/profiles/%3/drivers").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
270 QJsonArray driverArray;
271 QMapIterator<QString, QString> i(pi->drivers);
272
273 // In case both Guider + CCD are Multiple-Devices-Per-Driver type
274 // Then we should not define guider as a separate driver since that would start the driver executable twice
275 // when we only need it once
276 if (pi->drivers.contains("Guider"))
277 {
278 if (pi->drivers["Guider"] == pi->drivers["CCD"])
279 {
280 DriverInfo *guiderInfo = nullptr;
281 if ((guiderInfo = DriverManager::Instance()->findDriverByName(pi->drivers["Guider"])) == nullptr)
282 {
283 if ((guiderInfo = DriverManager::Instance()->findDriverByLabel(pi->drivers["Guider"])) == nullptr)
284 {
285 guiderInfo = DriverManager::Instance()->findDriverByExec(pi->drivers["Guider"]);
286 }
287 }
288
289 if (guiderInfo && guiderInfo->getAuxInfo().value("mdpd", false).toBool())
290 {
291 pi->drivers.remove("Guider");
292 i = QMapIterator<QString, QString>(pi->drivers);
293 }
294 }
295 }
296
297 // Regular Drivers
298 while (i.hasNext())
299 driverArray.append(QJsonObject({{"label", i.next().value()}}));
300
301 // Remote Drivers
302 if (pi->remotedrivers.isEmpty() == false)
303 {
304 for (auto remoteDriver : pi->remotedrivers.split(","))
305 {
306 driverArray.append(QJsonObject({{"remote", remoteDriver}}));
307 }
308 }
309
310 data = QJsonDocument(driverArray).toJson();
311 getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr, &data);
312
313 return true;
314 }
315
startProfile(ProfileInfo * pi)316 bool startProfile(ProfileInfo *pi)
317 {
318 // First make sure profile is created and synced on web manager
319 syncProfile(pi);
320
321 // Start profile
322 QUrl url(QString("http://%1:%2/api/server/start/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
323 getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
324
325 // Make sure drivers are running
326 // Try up to 3 times
327 for (int i = 0; i < 3; i++)
328 {
329 if (areDriversRunning(pi))
330 return true;
331 }
332
333 return false;
334 }
335
stopProfile(ProfileInfo * pi)336 bool stopProfile(ProfileInfo *pi)
337 {
338 // Stop profile
339 QUrl url(QString("http://%1:%2/api/server/stop").arg(pi->host).arg(pi->INDIWebManagerPort));
340 return getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
341 }
342
restartDriver(ProfileInfo * pi,const QString & label)343 bool restartDriver(ProfileInfo *pi, const QString &label)
344 {
345 QUrl url(QString("http://%1:%2/api/drivers/restart/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(label));
346 return getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
347 }
348 }
349
350 // Async version of the Web Manager
351 namespace AsyncWebManager
352 {
353
isOnline(ProfileInfo * pi)354 QFuture<bool> isOnline(ProfileInfo *pi)
355 {
356 return QtConcurrent::run(WebManager::isOnline, pi);
357 }
358
isStellarMate(ProfileInfo * pi)359 QFuture<bool> isStellarMate(ProfileInfo *pi)
360 {
361 return QtConcurrent::run(WebManager::isStellarMate, pi);
362 }
363
syncCustomDrivers(ProfileInfo * pi)364 QFuture<bool> syncCustomDrivers(ProfileInfo *pi)
365 {
366 return QtConcurrent::run(WebManager::syncCustomDrivers, pi);
367 }
368
areDriversRunning(ProfileInfo * pi)369 QFuture<bool> areDriversRunning(ProfileInfo *pi)
370 {
371 return QtConcurrent::run(WebManager::areDriversRunning, pi);
372 }
373
syncProfile(ProfileInfo * pi)374 QFuture<bool> syncProfile(ProfileInfo *pi)
375 {
376 return QtConcurrent::run(WebManager::syncProfile, pi);
377 }
378
startProfile(ProfileInfo * pi)379 QFuture<bool> startProfile(ProfileInfo *pi)
380 {
381 return QtConcurrent::run(WebManager::startProfile, pi);
382 }
383
stopProfile(ProfileInfo * pi)384 QFuture<bool> stopProfile(ProfileInfo *pi)
385 {
386 return QtConcurrent::run(WebManager::stopProfile, pi);
387 }
388 }
389
390 }
391