1 /*
2 SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikarustech.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5
6 Handle INDI Standard properties.
7 */
8
9 #include "indilistener.h"
10
11 #include "clientmanager.h"
12 #include "deviceinfo.h"
13 #include "indicap.h"
14 #include "indiccd.h"
15 #include "indidome.h"
16 #include "indifilter.h"
17 #include "indifocuser.h"
18 #include "indilightbox.h"
19 #include "inditelescope.h"
20 #include "indiweather.h"
21 #include "kstars.h"
22 #include "Options.h"
23
24 #include "auxiliary/ksnotification.h"
25
26 #include <knotification.h>
27
28 #include <basedevice.h>
29 #include <indi_debug.h>
30
31 #define NINDI_STD 35
32
33 /* INDI standard property used across all clients to enable interoperability. */
34 static const char *indi_std[NINDI_STD] = { "CONNECTION",
35 "DEVICE_PORT",
36 "TIME_UTC",
37 "TIME_LST",
38 "GEOGRAPHIC_COORD",
39 "EQUATORIAL_COORD",
40 "EQUATORIAL_EOD_COORD",
41 "EQUATORIAL_EOD_COORD_REQUEST",
42 "HORIZONTAL_COORD",
43 "TELESCOPE_ABORT_MOTION",
44 "ON_COORD_SET",
45 "SOLAR_SYSTEM",
46 "TELESCOPE_MOTION_NS",
47 "TELESCOPE_MOTION_WE",
48 "TELESCOPE_PARK",
49 "DOME_PARK",
50 "GPS_REFRESH",
51 "WEATHER_STATUS",
52 "CCD_EXPOSURE",
53 "CCD_TEMPERATURE",
54 "CCD_FRAME",
55 "CCD_FRAME_TYPE",
56 "CCD_BINNING",
57 "CCD_INFO",
58 "CCD_VIDEO_STREAM",
59 "RAW_STREAM",
60 "IMAGE_STREAM",
61 "FOCUS_SPEED",
62 "FOCUS_MOTION",
63 "FOCUS_TIMER",
64 "FILTER_SLOT",
65 "WATCHDOG_HEARTBEAT",
66 "CAP_PARK",
67 "FLAT_LIGHT_CONTROL",
68 "FLAT_LIGHT_INTENSITY"
69 };
70
71 INDIListener *INDIListener::_INDIListener = nullptr;
72
Instance()73 INDIListener *INDIListener::Instance()
74 {
75 if (_INDIListener == nullptr)
76 {
77 _INDIListener = new INDIListener(KStars::Instance());
78
79 connect(_INDIListener, &INDIListener::newTelescope,
80 [&]()
81 {
82 KStars::Instance()->slotSetTelescopeEnabled(true);
83 });
84
85 connect(_INDIListener, &INDIListener::newDome,
86 [&]()
87 {
88 KStars::Instance()->slotSetDomeEnabled(true);
89 });
90 }
91
92 return _INDIListener;
93 }
94
INDIListener(QObject * parent)95 INDIListener::INDIListener(QObject *parent) : QObject(parent) {}
96
~INDIListener()97 INDIListener::~INDIListener()
98 {
99 qDeleteAll(devices);
100 qDeleteAll(st4Devices);
101 }
102
isStandardProperty(const QString & name)103 bool INDIListener::isStandardProperty(const QString &name)
104 {
105 for (auto &item : indi_std)
106 {
107 if (!strcmp(name.toLatin1().constData(), item))
108 return true;
109 }
110 return false;
111 }
112
getDevice(const QString & name)113 ISD::GDInterface *INDIListener::getDevice(const QString &name)
114 {
115 for (auto &oneDevice : devices)
116 {
117 if (oneDevice->getDeviceName() == name)
118 return oneDevice;
119 }
120 return nullptr;
121 }
122
addClient(ClientManager * cm)123 void INDIListener::addClient(ClientManager *cm)
124 {
125 qCDebug(KSTARS_INDI)
126 << "INDIListener: Adding a new client manager to INDI listener..";
127
128 clients.append(cm);
129
130 connect(cm, &ClientManager::newINDIDevice, this, &INDIListener::processDevice);
131 connect(cm, &ClientManager::newINDIProperty, this, &INDIListener::registerProperty);
132
133 connect(cm, &ClientManager::removeINDIDevice, this, &INDIListener::removeDevice);
134 connect(cm, &ClientManager::removeINDIProperty, this, &INDIListener::removeProperty);
135
136 connect(cm, &ClientManager::newINDISwitch, this, &INDIListener::processSwitch);
137 connect(cm, &ClientManager::newINDIText, this, &INDIListener::processText);
138 connect(cm, &ClientManager::newINDINumber, this, &INDIListener::processNumber);
139 connect(cm, &ClientManager::newINDILight, this, &INDIListener::processLight);
140 connect(cm, &ClientManager::newINDIBLOB, this, &INDIListener::processBLOB);
141 connect(cm, &ClientManager::newINDIUniversalMessage, this,
142 &INDIListener::processUniversalMessage);
143 }
144
removeClient(ClientManager * cm)145 void INDIListener::removeClient(ClientManager *cm)
146 {
147 qCDebug(KSTARS_INDI) << "INDIListener: Removing client manager for server"
148 << cm->getHost() << "@" << cm->getPort();
149
150 QList<ISD::GDInterface *>::iterator it = devices.begin();
151 clients.removeOne(cm);
152
153 while (it != devices.end())
154 {
155 DriverInfo *dv = (*it)->getDriverInfo();
156 bool hostSource = (dv->getDriverSource() == HOST_SOURCE) ||
157 (dv->getDriverSource() == GENERATED_SOURCE);
158
159 if (cm->isDriverManaged(dv))
160 {
161 // // If we have multiple devices per driver, we need to remove them all
162 // if (dv->getAuxInfo().value("mdpd", false).toBool() == true)
163 // {
164 // while (it != devices.end())
165 // {
166 // if (dv->getDevice((*it)->getDeviceName()) != nullptr)
167 // {
168 // it = devices.erase(it);
169 // }
170 // else
171 // break;
172 // }
173 // }
174 // else
175 // it = devices.erase(it);
176
177 cm->removeManagedDriver(dv);
178 cm->disconnect(this);
179 if (hostSource)
180 return;
181 }
182 else
183 ++it;
184 }
185 }
186
processDevice(DeviceInfo * dv)187 void INDIListener::processDevice(DeviceInfo *dv)
188 {
189 ClientManager *cm = qobject_cast<ClientManager *>(sender());
190 Q_ASSERT_X(cm, __FUNCTION__, "Client manager is not valid.");
191
192 qCDebug(KSTARS_INDI) << "INDIListener: New device" << dv->getDeviceName();
193
194 ISD::GDInterface *gd = new ISD::GenericDevice(*dv, cm);
195 devices.append(gd);
196
197 // Register all existing properties
198 for (const auto &oneProperty : dv->getBaseDevice()->getProperties())
199 gd->registerProperty(oneProperty);
200
201 emit newDevice(gd);
202 }
203
204 //void INDIListener::removeDevice(DeviceInfo *dv)
205 //{
206 // qCDebug(KSTARS_INDI) << "INDIListener: Removing device" << dv->getBaseDevice()->getDeviceName() << "with unique label "
207 // << dv->getDriverInfo()->getUniqueLabel();
208
209 // foreach (ISD::GDInterface *gd, devices)
210 // {
211 // if (gd->getDeviceInfo() == dv)
212 // {
213 // emit deviceRemoved(gd);
214 // devices.removeOne(gd);
215 // delete (gd);
216 // }
217 // }
218 //}
219
removeDevice(const QString & deviceName)220 void INDIListener::removeDevice(const QString &deviceName)
221 {
222 qCDebug(KSTARS_INDI) << "INDIListener: Removing device" << deviceName;
223
224 for (ISD::GDInterface *oneDevice : devices)
225 {
226 if (oneDevice->getDeviceName() == deviceName)
227 {
228 emit deviceRemoved(oneDevice);
229 devices.removeOne(oneDevice);
230 oneDevice->deleteLater();
231 return;
232 }
233 }
234 }
235
registerProperty(INDI::Property prop)236 void INDIListener::registerProperty(INDI::Property prop)
237 {
238 if (!prop.getRegistered())
239 return;
240
241 qCDebug(KSTARS_INDI) << "<" << prop.getDeviceName() << ">: <" << prop.getName()
242 << ">";
243
244 for (auto oneDevice : devices)
245 {
246 if (oneDevice->getDeviceName() == prop.getDeviceName())
247 {
248 if (prop.isNameMatch("ON_COORD_SET") ||
249 prop.isNameMatch("EQUATORIAL_EOD_COORD") ||
250 prop.isNameMatch("EQUATORIAL_COORD") ||
251 prop.isNameMatch("HORIZONTAL_COORD"))
252 {
253 if (oneDevice->getType() == KSTARS_UNKNOWN)
254 {
255 devices.removeOne(oneDevice);
256 oneDevice = new ISD::Telescope(oneDevice);
257 devices.append(oneDevice);
258 }
259
260 emit newTelescope(oneDevice);
261 }
262 else if (prop.isNameMatch("CCD_EXPOSURE"))
263 {
264 // Only register a CCD device if the interface explicitly contains CCD_INTERFACE
265 // and only if the device type is not already known.
266 // If the device type is alredy KSTARS_CCD then no need to remove and re-parent
267 // this happens in the case of disconnect/reconnect
268 if (oneDevice->getType() == KSTARS_UNKNOWN &&
269 (oneDevice->getDriverInterface() & INDI::BaseDevice::CCD_INTERFACE))
270 {
271 devices.removeOne(oneDevice);
272 oneDevice = new ISD::CCD(oneDevice);
273 devices.append(oneDevice);
274 }
275
276 emit newCCD(oneDevice);
277 }
278 else if (prop.isNameMatch("FILTER_NAME"))
279 {
280 if (oneDevice->getType() == KSTARS_UNKNOWN &&
281 !(oneDevice->getDriverInterface() & INDI::BaseDevice::CCD_INTERFACE))
282 {
283 devices.removeOne(oneDevice);
284 oneDevice = new ISD::Filter(oneDevice);
285 devices.append(oneDevice);
286 }
287
288 emit newFilter(oneDevice);
289 }
290 else if (prop.isNameMatch("FOCUS_MOTION"))
291 {
292 if (oneDevice->getType() == KSTARS_UNKNOWN &&
293 !(oneDevice->getDriverInterface() & INDI::BaseDevice::CCD_INTERFACE))
294 {
295 devices.removeOne(oneDevice);
296 oneDevice = new ISD::Focuser(oneDevice);
297 devices.append(oneDevice);
298 }
299
300 emit newFocuser(oneDevice);
301 }
302
303 else if (prop.isNameMatch("DOME_SHUTTER") || prop.isNameMatch("DOME_MOTION"))
304 {
305 if (oneDevice->getType() == KSTARS_UNKNOWN)
306 {
307 devices.removeOne(oneDevice);
308 oneDevice = new ISD::Dome(oneDevice);
309 devices.append(oneDevice);
310 }
311
312 emit newDome(oneDevice);
313 }
314 else if (prop.isNameMatch("WEATHER_STATUS"))
315 {
316 if (oneDevice->getType() == KSTARS_UNKNOWN)
317 {
318 devices.removeOne(oneDevice);
319 oneDevice = new ISD::Weather(oneDevice);
320 devices.append(oneDevice);
321 }
322
323 emit newWeather(oneDevice);
324 }
325 else if (prop.isNameMatch("CAP_PARK"))
326 {
327 if (oneDevice->getType() == KSTARS_UNKNOWN)
328 {
329 devices.removeOne(oneDevice);
330 oneDevice = new ISD::DustCap(oneDevice);
331 devices.append(oneDevice);
332 }
333
334 emit newDustCap(oneDevice);
335 }
336 else if (prop.isNameMatch("FLAT_LIGHT_CONTROL"))
337 {
338 // If light box part of dust cap
339 if (oneDevice->getType() == KSTARS_UNKNOWN)
340 {
341 if (oneDevice->getBaseDevice()->getDriverInterface() &
342 INDI::BaseDevice::DUSTCAP_INTERFACE)
343 {
344 devices.removeOne(oneDevice);
345 oneDevice = new ISD::DustCap(oneDevice);
346 devices.append(oneDevice);
347
348 emit newDustCap(oneDevice);
349 }
350 // If stand-alone light box
351 else
352 {
353 devices.removeOne(oneDevice);
354 oneDevice = new ISD::LightBox(oneDevice);
355 devices.append(oneDevice);
356
357 emit newLightBox(oneDevice);
358 }
359 }
360 }
361
362 if (prop.isNameMatch("TELESCOPE_TIMED_GUIDE_WE"))
363 {
364 ISD::ST4 *st4Driver = new ISD::ST4(oneDevice->getBaseDevice(), oneDevice->getDriverInfo()->getClientManager());
365 if (st4Driver == nullptr)
366 {
367 qCCritical(KSTARS_INDI) << "Failed to allocate ST4 driver";
368 }
369 else
370 {
371 st4Devices.append(st4Driver);
372 emit newST4(st4Driver);
373 }
374 }
375
376 oneDevice->registerProperty(prop);
377 break;
378 }
379 }
380 }
381
removeProperty(const QString & device,const QString & name)382 void INDIListener::removeProperty(const QString &device, const QString &name)
383 {
384 for (auto &oneDevice : devices)
385 {
386 if (oneDevice->getDeviceName() == device)
387 {
388 oneDevice->removeProperty(name);
389 return;
390 }
391 }
392 }
393
processSwitch(ISwitchVectorProperty * svp)394 void INDIListener::processSwitch(ISwitchVectorProperty *svp)
395 {
396 for (auto &oneDevice : devices)
397 {
398 if (oneDevice->getDeviceName() == svp->device)
399 {
400 oneDevice->processSwitch(svp);
401 break;
402 }
403 }
404 }
405
processNumber(INumberVectorProperty * nvp)406 void INDIListener::processNumber(INumberVectorProperty *nvp)
407 {
408 for (auto &oneDevice : devices)
409 {
410 if (oneDevice->getDeviceName() == nvp->device)
411 {
412 oneDevice->processNumber(nvp);
413 break;
414 }
415 }
416 }
417
processText(ITextVectorProperty * tvp)418 void INDIListener::processText(ITextVectorProperty *tvp)
419 {
420 for (auto &oneDevice : devices)
421 {
422 if (oneDevice->getDeviceName() == tvp->device)
423 {
424 oneDevice->processText(tvp);
425 break;
426 }
427 }
428 }
429
processLight(ILightVectorProperty * lvp)430 void INDIListener::processLight(ILightVectorProperty *lvp)
431 {
432 for (auto &oneDevice : devices)
433 {
434 if (oneDevice->getDeviceName() == lvp->device)
435 {
436 oneDevice->processLight(lvp);
437 break;
438 }
439 }
440 }
441
processBLOB(IBLOB * bp)442 void INDIListener::processBLOB(IBLOB *bp)
443 {
444 for (auto &oneDevice : devices)
445 {
446 if (oneDevice->getDeviceName() == bp->bvp->device)
447 {
448 oneDevice->processBLOB(bp);
449 break;
450 }
451 }
452 }
453
processMessage(INDI::BaseDevice * dp,int messageID)454 void INDIListener::processMessage(INDI::BaseDevice *dp, int messageID)
455 {
456 for (auto &oneDevice : devices)
457 {
458 if (oneDevice->getDeviceName() == dp->getDeviceName())
459 {
460 oneDevice->processMessage(messageID);
461 break;
462 }
463 }
464 }
465
processUniversalMessage(const QString & message)466 void INDIListener::processUniversalMessage(const QString &message)
467 {
468 QString displayMessage = message;
469 // Remove timestamp info as it is not suitable for message box
470 int colonIndex = displayMessage.indexOf(": ");
471 if (colonIndex > 0)
472 displayMessage = displayMessage.mid(colonIndex + 2);
473
474 // Special case for Alignment since it is not tied to a device
475 if (displayMessage.startsWith("[ALIGNMENT]"))
476 {
477 qCDebug(KSTARS_INDI) << "AlignmentSubSystem:" << displayMessage;
478 return;
479 }
480
481 if (Options::messageNotificationINDI())
482 {
483 KSNotification::event(QLatin1String("IndiServerMessage"), displayMessage,
484 KSNotification::EVENT_WARN);
485 }
486 else
487 {
488 KSNotification::transient(displayMessage, i18n("INDI Server Message"));
489 }
490 }
491