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