1 /*******************************************************************************
2   Copyright(c) 2015 Jasem Mutlaq. All rights reserved.
3 
4   INDI Watchdog driver.
5 
6   The driver expects a heartbeat from the client every X minutes. If no heartbeat
7   is received, the driver executes the shutdown procedures.
8 
9   This program is free software; you can redistribute it and/or modify it
10   under the terms of the GNU General Public License as published by the Free
11   Software Foundation; either version 2 of the License, or (at your option)
12   any later version.
13 
14   This program is distributed in the hope that it will be useful, but WITHOUT
15   ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
17   more details.
18 
19   You should have received a copy of the GNU Library General Public License
20   along with this library; see the file COPYING.LIB.  If not, write to
21   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
22   Boston, MA 02110-1301, USA.
23 
24   The full GNU General Public License is included in this distribution in the
25   file called LICENSE.
26 *******************************************************************************/
27 
28 #include "watchdog.h"
29 #include "watchdogclient.h"
30 
31 #include <memory>
32 #include <cstring>
33 #include <unistd.h>
34 #include <sys/wait.h>
35 
36 // Naming the object after my love Juli which I lost in 2018. May she rest in peace.
37 // http://indilib.org/images/juli_tommy.jpg
38 std::unique_ptr<WatchDog> juli(new WatchDog());
39 
40 ////////////////////////////////////////////////////////////////////////////////////
41 ///
42 ////////////////////////////////////////////////////////////////////////////////////
WatchDog()43 WatchDog::WatchDog()
44 {
45     setVersion(0, 3);
46     setDriverInterface(AUX_INTERFACE);
47 
48     watchdogClient = new WatchDogClient();
49 
50     m_ShutdownStage = WATCHDOG_IDLE;
51 }
52 
53 ////////////////////////////////////////////////////////////////////////////////////
54 ///
55 ////////////////////////////////////////////////////////////////////////////////////
~WatchDog()56 WatchDog::~WatchDog()
57 {
58     delete (watchdogClient);
59 }
60 
61 ////////////////////////////////////////////////////////////////////////////////////
62 ///
63 ////////////////////////////////////////////////////////////////////////////////////
getDefaultName()64 const char *WatchDog::getDefaultName()
65 {
66     return "WatchDog";
67 }
68 
69 ////////////////////////////////////////////////////////////////////////////////////
70 ///
71 ////////////////////////////////////////////////////////////////////////////////////
Connect()72 bool WatchDog::Connect()
73 {
74     if (ShutdownTriggerS[TRIGGER_CLIENT].s == ISS_ON && HeartBeatN[0].value > 0)
75     {
76         LOGF_INFO("Client Watchdog is enabled. Shutdown is triggered after %.f seconds of communication loss with the client.",
77                   HeartBeatN[0].value);
78         if (m_WatchDogTimer > 0)
79             RemoveTimer(m_WatchDogTimer);
80         m_WatchDogTimer = SetTimer(HeartBeatN[0].value * 1000);
81     }
82 
83     if (ShutdownTriggerS[TRIGGER_WEATHER].s == ISS_ON)
84     {
85         if (WeatherThresholdN[0].value > 0)
86             LOGF_INFO("Weather Watchdog is enabled. Shutdown is triggered %.f seconds after Weather status enters DANGER zone.",
87                       WeatherThresholdN[0].value);
88         else
89             LOG_INFO("Weather Watchdog is enabled. Shutdown is triggered when Weather status in DANGER zone.");
90         // Trigger Snoop
91         IDSnoopDevice(ActiveDeviceT[ACTIVE_WEATHER].text, "WEATHER_STATUS");
92     }
93 
94     IDSnoopDevice(ActiveDeviceT[ACTIVE_TELESCOPE].text, "TELESCOPE_PARK");
95     IDSnoopDevice(ActiveDeviceT[ACTIVE_DOME].text, "DOME_PARK");
96 
97     return true;
98 }
99 
100 ////////////////////////////////////////////////////////////////////////////////////
101 ///
102 ////////////////////////////////////////////////////////////////////////////////////
Disconnect()103 bool WatchDog::Disconnect()
104 {
105     if (m_WatchDogTimer > 0)
106     {
107         RemoveTimer(m_WatchDogTimer);
108         m_WatchDogTimer = -1;
109     }
110     if (m_WeatherAlertTimer > 0)
111     {
112         RemoveTimer(m_WeatherAlertTimer);
113         m_WeatherAlertTimer = -1;
114     }
115 
116     LOG_INFO("Watchdog is disabled.");
117     m_ShutdownStage = WATCHDOG_IDLE;
118 
119     return true;
120 }
121 
122 ////////////////////////////////////////////////////////////////////////////////////
123 ///
124 ////////////////////////////////////////////////////////////////////////////////////
initProperties()125 bool WatchDog::initProperties()
126 {
127     INDI::DefaultDevice::initProperties();
128 
129     // Heart Beat to client
130     IUFillNumber(&HeartBeatN[0], "WATCHDOG_HEARTBEAT_VALUE", "Threshold (s)", "%.f", 0, 3600, 60, 0);
131     IUFillNumberVector(&HeartBeatNP, HeartBeatN, 1, getDeviceName(), "WATCHDOG_HEARTBEAT", "Heart beat",
132                        MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
133 
134     // Weather Threshold
135     IUFillNumber(&WeatherThresholdN[0], "WATCHDOG_WEATHER_VALUE", "Threshold (s)", "%.f", 0, 3600, 60, 0);
136     IUFillNumberVector(&WeatherThresholdNP, WeatherThresholdN, 1, getDeviceName(), "WATCHDOG_WEATHER", "Weather",
137                        MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
138 
139     // INDI Server Settings
140     IUFillText(&SettingsT[INDISERVER_HOST], "INDISERVER_HOST", "indiserver host", "localhost");
141     IUFillText(&SettingsT[INDISERVER_PORT], "INDISERVER_PORT", "indiserver port", "7624");
142     IUFillText(&SettingsT[SHUTDOWN_SCRIPT], "SHUTDOWN_SCRIPT", "shutdown script", nullptr);
143     IUFillTextVector(&SettingsTP, SettingsT, 3, getDeviceName(), "WATCHDOG_SETTINGS", "Settings", MAIN_CONTROL_TAB,
144                      IP_RW, 60, IPS_IDLE);
145 
146     // Shutdown procedure
147     IUFillSwitch(&ShutdownProcedureS[PARK_MOUNT], "PARK_MOUNT", "Park Mount", ISS_OFF);
148     IUFillSwitch(&ShutdownProcedureS[PARK_DOME], "PARK_DOME", "Park Dome", ISS_OFF);
149     IUFillSwitch(&ShutdownProcedureS[EXECUTE_SCRIPT], "EXECUTE_SCRIPT", "Execute Script", ISS_OFF);
150     IUFillSwitchVector(&ShutdownProcedureSP, ShutdownProcedureS, 3, getDeviceName(), "WATCHDOG_SHUTDOWN", "Shutdown",
151                        MAIN_CONTROL_TAB, IP_RW, ISR_NOFMANY, 60, IPS_IDLE);
152 
153     // Shutdown Trigger
154     IUFillSwitch(&ShutdownTriggerS[TRIGGER_CLIENT], "TRIGGER_CLIENT", "Client", ISS_OFF);
155     IUFillSwitch(&ShutdownTriggerS[TRIGGER_WEATHER], "TRIGGER_WEATHER", "Weather", ISS_OFF);
156     IUFillSwitchVector(&ShutdownTriggerSP, ShutdownTriggerS, 2, getDeviceName(), "WATCHDOG_Trigger", "Trigger",
157                        MAIN_CONTROL_TAB, IP_RW, ISR_NOFMANY, 60, IPS_IDLE);
158 
159     // Mount Policy
160     IUFillSwitch(&MountPolicyS[MOUNT_IGNORED], "MOUNT_IGNORED", "Mount ignored", ISS_ON);
161     IUFillSwitch(&MountPolicyS[MOUNT_LOCKS], "MOUNT_LOCKS", "Mount locks", ISS_OFF);
162     IUFillSwitchVector(&MountPolicySP, MountPolicyS, 2, getDeviceName(), "WATCHDOG_MOUNT_POLICY", "Mount Policy",
163                        OPTIONS_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
164 
165     // Active Devices
166     IUFillText(&ActiveDeviceT[ACTIVE_TELESCOPE], "ACTIVE_TELESCOPE", "Telescope", "Telescope Simulator");
167     IUFillText(&ActiveDeviceT[ACTIVE_DOME], "ACTIVE_DOME", "Dome", "Dome Simulator");
168     IUFillText(&ActiveDeviceT[ACTIVE_WEATHER], "ACTIVE_WEATHER", "Weather", "Weather Simulator");
169     IUFillTextVector(&ActiveDeviceTP, ActiveDeviceT, 3, getDeviceName(), "ACTIVE_DEVICES", "Active devices",
170                      OPTIONS_TAB, IP_RW, 60, IPS_IDLE);
171 
172     addDebugControl();
173 
174     return true;
175 }
176 
177 ////////////////////////////////////////////////////////////////////////////////////
178 ///
179 ////////////////////////////////////////////////////////////////////////////////////
ISGetProperties(const char * dev)180 void WatchDog::ISGetProperties(const char *dev)
181 {
182     DefaultDevice::ISGetProperties(dev);
183 
184     defineProperty(&HeartBeatNP);
185     defineProperty(&WeatherThresholdNP);
186     defineProperty(&SettingsTP);
187     defineProperty(&ShutdownTriggerSP);
188     defineProperty(&ShutdownProcedureSP);
189     defineProperty(&MountPolicySP);
190     defineProperty(&ActiveDeviceTP);
191 }
192 
193 ////////////////////////////////////////////////////////////////////////////////////
194 ///
195 ////////////////////////////////////////////////////////////////////////////////////
ISNewText(const char * dev,const char * name,char * texts[],char * names[],int n)196 bool WatchDog::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
197 {
198     if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
199     {
200         // Update Settings
201         if (!strcmp(SettingsTP.name, name))
202         {
203             IUUpdateText(&SettingsTP, texts, names, n);
204             SettingsTP.s = IPS_OK;
205             IDSetText(&SettingsTP, nullptr);
206 
207             try
208             {
209                 m_INDIServerPort = std::stoi(SettingsT[INDISERVER_PORT].text);
210             }
211             catch(...)
212             {
213                 SettingsTP.s = IPS_ALERT;
214                 LOG_ERROR("Failed to parse numbers.");
215             }
216 
217             IDSetText(&SettingsTP, nullptr);
218             return true;
219         }
220 
221         // Snoop Active Devices.
222         if (!strcmp(ActiveDeviceTP.name, name))
223         {
224             if (watchdogClient->isBusy())
225             {
226                 ActiveDeviceTP.s = IPS_ALERT;
227                 IDSetText(&ActiveDeviceTP, nullptr);
228                 LOG_ERROR("Cannot change devices names while shutdown is in progress...");
229                 return true;
230             }
231 
232             IDSnoopDevice(ActiveDeviceT[ACTIVE_WEATHER].text, "WEATHER_STATUS");
233             IDSnoopDevice(ActiveDeviceT[ACTIVE_TELESCOPE].text, "TELESCOPE_PARK");
234             IDSnoopDevice(ActiveDeviceT[ACTIVE_DOME].text, "DOME_PARK");
235 
236             IUUpdateText(&ActiveDeviceTP, texts, names, n);
237             ActiveDeviceTP.s = IPS_OK;
238             IDSetText(&ActiveDeviceTP, nullptr);
239             return true;
240         }
241     }
242 
243     return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n);
244 }
245 
246 ////////////////////////////////////////////////////////////////////////////////////
247 ///
248 ////////////////////////////////////////////////////////////////////////////////////
ISNewNumber(const char * dev,const char * name,double values[],char * names[],int n)249 bool WatchDog::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
250 {
251     if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
252     {
253         // Weather threshold
254         if (!strcmp(WeatherThresholdNP.name, name))
255         {
256             IUUpdateNumber(&WeatherThresholdNP, values, names, n);
257             WeatherThresholdNP.s = IPS_OK;
258             IDSetNumber(&WeatherThresholdNP, nullptr);
259             return true;
260         }
261         // Heart Beat
262         // Client must set this property to indicate it is alive.
263         // If heat beat not received from client then shutdown procedure begins if
264         // the client trigger is selected.
265         else if (!strcmp(HeartBeatNP.name, name))
266         {
267             double prevHeartBeat = HeartBeatN[0].value;
268 
269             if (watchdogClient->isBusy())
270             {
271                 HeartBeatNP.s = IPS_ALERT;
272                 IDSetNumber(&HeartBeatNP, nullptr);
273                 LOG_ERROR("Cannot change heart beat while shutdown is in progress...");
274                 return true;
275             }
276 
277             IUUpdateNumber(&HeartBeatNP, values, names, n);
278             HeartBeatNP.s = IPS_OK;
279 
280             // If trigger is not set, don't do anything else
281             if (ShutdownTriggerS[TRIGGER_CLIENT].s == ISS_OFF)
282             {
283                 if (m_WatchDogTimer > 0)
284                 {
285                     RemoveTimer(m_WatchDogTimer);
286                     m_WatchDogTimer = -1;
287                 }
288                 IDSetNumber(&HeartBeatNP, nullptr);
289                 return true;
290             }
291 
292             if (HeartBeatN[0].value == 0)
293                 LOG_INFO("Client Watchdog is disabled.");
294             else
295             {
296                 if (prevHeartBeat != HeartBeatN[0].value)
297                     LOGF_INFO("Client Watchdog is enabled. Shutdown is triggered after %.f seconds of communication loss with the client.",
298                               HeartBeatN[0].value);
299 
300                 LOG_DEBUG("Received heart beat from client.");
301 
302                 if (m_WatchDogTimer > 0)
303                 {
304                     RemoveTimer(m_WatchDogTimer);
305                     m_WatchDogTimer = -1;
306                 }
307 
308                 m_WatchDogTimer = SetTimer(HeartBeatN[0].value * 1000);
309             }
310             IDSetNumber(&HeartBeatNP, nullptr);
311 
312             return true;
313         }
314     }
315 
316     return DefaultDevice::ISNewNumber(dev, name, values, names, n);
317 }
318 
319 ////////////////////////////////////////////////////////////////////////////////////
320 ///
321 ////////////////////////////////////////////////////////////////////////////////////
ISNewSwitch(const char * dev,const char * name,ISState * states,char * names[],int n)322 bool WatchDog::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
323 {
324     if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
325     {
326         // Shutdown procedure setting
327         if (!strcmp(ShutdownProcedureSP.name, name))
328         {
329             IUUpdateSwitch(&ShutdownProcedureSP, states, names, n);
330 
331             if (ShutdownProcedureS[EXECUTE_SCRIPT].s == ISS_ON &&
332                     (SettingsT[EXECUTE_SCRIPT].text == nullptr || SettingsT[EXECUTE_SCRIPT].text[0] == '\0'))
333             {
334                 LOG_ERROR("Error: shutdown script file is not set.");
335                 ShutdownProcedureSP.s                = IPS_ALERT;
336                 ShutdownProcedureS[EXECUTE_SCRIPT].s = ISS_OFF;
337             }
338             else
339                 ShutdownProcedureSP.s = IPS_OK;
340             IDSetSwitch(&ShutdownProcedureSP, nullptr);
341             return true;
342         }
343         // Mount Lock Policy
344         else if (!strcmp(MountPolicySP.name, name))
345         {
346             IUUpdateSwitch(&MountPolicySP, states, names, n);
347             MountPolicySP.s = IPS_OK;
348 
349             if (MountPolicyS[MOUNT_IGNORED].s == ISS_ON)
350                 LOG_INFO("Mount is ignored. Dome can start parking without waiting for mount to complete parking.");
351             else
352                 LOG_INFO("Mount locks. Dome must wait for mount to park before it can start the parking procedure.");
353             IDSetSwitch(&MountPolicySP, nullptr);
354             return true;
355         }
356         // Shutdown Trigger handling
357         else if (!strcmp(ShutdownTriggerSP.name, name))
358         {
359             std::vector<ISState> oldStates(ShutdownTriggerSP.nsp, ISS_OFF);
360             std::vector<ISState> newStates(ShutdownTriggerSP.nsp, ISS_OFF);
361             for (int i = 0; i < ShutdownTriggerSP.nsp; i++)
362                 oldStates[i] = ShutdownTriggerS[i].s;
363             IUUpdateSwitch(&ShutdownTriggerSP, states, names, n);
364             for (int i = 0; i < ShutdownTriggerSP.nsp; i++)
365                 newStates[i] = ShutdownTriggerS[i].s;
366 
367 
368             // Check for client changes
369             if (oldStates[TRIGGER_CLIENT] != newStates[TRIGGER_CLIENT])
370             {
371                 // User disabled client trigger
372                 if (newStates[TRIGGER_CLIENT] == ISS_OFF)
373                 {
374                     LOG_INFO("Disabling client watchdog. Lost communication with client shall not trigger the shutdown procedure.");
375                     if (m_WatchDogTimer > 0)
376                     {
377                         RemoveTimer(m_WatchDogTimer);
378                         m_WatchDogTimer = -1;
379                     }
380                 }
381                 // User enabled client trigger
382                 else
383                 {
384                     // Check first that we have a valid heart beat
385                     if (HeartBeatN[0].value == 0)
386                     {
387                         LOG_ERROR("Heart beat timeout should be set first.");
388                         ShutdownTriggerSP.s = IPS_ALERT;
389                         for (int i = 0; i < ShutdownTriggerSP.nsp; i++)
390                             ShutdownTriggerS[i].s = oldStates[i];
391                         IDSetSwitch(&ShutdownTriggerSP, nullptr);
392                         return true;
393                     }
394 
395                     LOGF_INFO("Client Watchdog is enabled. Shutdown is triggered after %.f seconds of communication loss with the client.",
396                               HeartBeatN[0].value);
397                     if (m_WatchDogTimer > 0)
398                         RemoveTimer(m_WatchDogTimer);
399                     m_WatchDogTimer = SetTimer(HeartBeatN[0].value * 1000);
400                 }
401             }
402 
403             // Check for weather changes
404             if (oldStates[TRIGGER_WEATHER] != newStates[TRIGGER_WEATHER])
405             {
406                 // User disabled weather trigger
407                 if (newStates[TRIGGER_WEATHER] == ISS_OFF)
408                 {
409                     // If we have an active timer, remove it.
410                     if (m_WeatherAlertTimer > 0)
411                     {
412                         RemoveTimer(m_WeatherAlertTimer);
413                         m_WeatherAlertTimer = -1;
414                     }
415 
416                     LOG_INFO("Weather Watchdog is disabled.");
417                 }
418                 else
419                     LOG_INFO("Weather Watchdog is enabled.");
420 
421             }
422 
423             ShutdownTriggerSP.s = IPS_OK;
424             IDSetSwitch(&ShutdownTriggerSP, nullptr);
425             return true;
426         }
427     }
428 
429     return DefaultDevice::ISNewSwitch(dev, name, states, names, n);
430 }
431 
432 ////////////////////////////////////////////////////////////////////////////////////
433 ///
434 ////////////////////////////////////////////////////////////////////////////////////
ISSnoopDevice(XMLEle * root)435 bool WatchDog::ISSnoopDevice(XMLEle * root)
436 {
437     const char * propName = findXMLAttValu(root, "name");
438 
439     // Weather Status
440     if (!strcmp("WEATHER_STATUS", propName))
441     {
442         IPState newWeatherState;
443         crackIPState(findXMLAttValu(root, "state"), &newWeatherState);
444 
445         // In case timer is active, and weather status is now OK
446         // Let's disable the timer
447         if (m_WeatherState == IPS_ALERT && newWeatherState != IPS_ALERT)
448         {
449             LOG_INFO("Weather status is no longer in DANGER zone.");
450             if (m_WeatherAlertTimer > 0)
451             {
452                 LOG_INFO("Shutdown procedure cancelled.");
453                 RemoveTimer(m_WeatherAlertTimer);
454                 m_WeatherAlertTimer = -1;
455             }
456         }
457 
458         // In case weather shutdown is active and;
459         // weather timer is off and;
460         // previous weather status is not alert and;
461         // current weather status is alert, then
462         // we start the weather timer which on timeout would case the shutdown procedure to commence.
463         if (m_WeatherState != IPS_ALERT && newWeatherState == IPS_ALERT)
464         {
465             LOG_WARN("Weather is in DANGER zone.");
466             if (ShutdownTriggerS[TRIGGER_WEATHER].s == ISS_ON && m_WeatherAlertTimer == -1)
467             {
468                 if (WeatherThresholdN[0].value > 0)
469                     LOGF_INFO("Shutdown procedure shall commence in %.f seconds unless weather status improves.", WeatherThresholdN[0].value);
470                 m_WeatherAlertTimer = SetTimer(WeatherThresholdN[0].value * 1000);
471             }
472         }
473 
474         m_WeatherState = newWeatherState;
475     }
476     // Check Telescope Park status
477     else if (!strcmp("TELESCOPE_PARK", propName))
478     {
479         if (!strcmp(findXMLAttValu(root, "state"), "Ok"))
480         {
481             XMLEle * ep = nullptr;
482             bool parked = false;
483             for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
484             {
485                 const char * elemName = findXMLAttValu(ep, "name");
486 
487                 if ((!strcmp(elemName, "PARK") && !strcmp(pcdataXMLEle(ep), "On")))
488                     parked = true;
489                 else if ((!strcmp(elemName, "UNPARK") && !strcmp(pcdataXMLEle(ep), "On")))
490                     parked = false;
491             }
492             if (parked != m_IsMountParked)
493             {
494                 LOGF_INFO("Mount is %s", parked ? "Parked" : "Unparked");
495                 m_IsMountParked = parked;
496                 // In case mount was UNPARKED while weather status is still ALERT
497                 // And weather shutdown trigger was active and mount parking was selected
498                 // then we force the mount to park again.
499                 if (parked == false &&
500                         ShutdownTriggerS[TRIGGER_WEATHER].s == ISS_ON &&
501                         m_WeatherState == IPS_ALERT &&
502                         ShutdownProcedureS[PARK_MOUNT].s == ISS_ON)
503                 {
504                     LOG_WARN("Mount unparked while weather alert is active! Parking mount...");
505                     watchdogClient->parkMount();
506                 }
507             }
508             return true;
509         }
510     }
511     // Check Telescope Park status
512     else if (!strcmp("DOME_PARK", propName))
513     {
514         if (!strcmp(findXMLAttValu(root, "state"), "Ok") || !strcmp(findXMLAttValu(root, "state"), "Busy"))
515         {
516             XMLEle * ep = nullptr;
517             bool parked = false;
518             for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
519             {
520                 const char * elemName = findXMLAttValu(ep, "name");
521 
522                 if ((!strcmp(elemName, "PARK") && !strcmp(pcdataXMLEle(ep), "On")))
523                     parked = true;
524                 else if ((!strcmp(elemName, "UNPARK") && !strcmp(pcdataXMLEle(ep), "On")))
525                     parked = false;
526             }
527             if (parked != m_IsDomeParked)
528             {
529                 LOGF_INFO("Dome is %s", parked ? "Parked" : "Unparked");
530                 m_IsDomeParked = parked;
531                 // In case mount was UNPARKED while weather status is still ALERT
532                 // And weather shutdown trigger was active and mount parking was selected
533                 // then we force the mount to park again.
534                 if (parked == false &&
535                         ShutdownTriggerS[TRIGGER_WEATHER].s == ISS_ON &&
536                         m_WeatherState == IPS_ALERT &&
537                         ShutdownProcedureS[PARK_DOME].s == ISS_ON)
538                 {
539                     LOG_WARN("Dome unparked while weather alert is active! Parking dome...");
540                     watchdogClient->parkDome();
541                 }
542             }
543             return true;
544         }
545     }
546 
547     return DefaultDevice::ISSnoopDevice(root);
548 }
549 
550 ////////////////////////////////////////////////////////////////////////////////////
551 ///
552 ////////////////////////////////////////////////////////////////////////////////////
saveConfigItems(FILE * fp)553 bool WatchDog::saveConfigItems(FILE * fp)
554 {
555     INDI::DefaultDevice::saveConfigItems(fp);
556 
557     IUSaveConfigNumber(fp, &HeartBeatNP);
558     IUSaveConfigNumber(fp, &WeatherThresholdNP);
559     IUSaveConfigText(fp, &SettingsTP);
560     IUSaveConfigText(fp, &ActiveDeviceTP);
561     IUSaveConfigSwitch(fp, &MountPolicySP);
562     IUSaveConfigSwitch(fp, &ShutdownTriggerSP);
563     IUSaveConfigSwitch(fp, &ShutdownProcedureSP);
564 
565     return true;
566 }
567 
568 ////////////////////////////////////////////////////////////////////////////////////
569 ///
570 ////////////////////////////////////////////////////////////////////////////////////
TimerHit()571 void WatchDog::TimerHit()
572 {
573     // Timer is up, we need to start shutdown procedure
574 
575     // If nothing to do, then return
576     if (ShutdownProcedureS[PARK_DOME].s == ISS_OFF && ShutdownProcedureS[PARK_MOUNT].s == ISS_OFF &&
577             ShutdownProcedureS[EXECUTE_SCRIPT].s == ISS_OFF)
578         return;
579 
580     switch (m_ShutdownStage)
581     {
582         // Connect to server
583         case WATCHDOG_IDLE:
584 
585             ShutdownProcedureSP.s = IPS_BUSY;
586             IDSetSwitch(&ShutdownProcedureSP, nullptr);
587 
588             if (m_WeatherState == IPS_ALERT)
589                 LOG_WARN("Warning! Weather status in DANGER zone, executing shutdown procedure...");
590             else
591                 LOG_WARN("Warning! Heartbeat threshold timed out, executing shutdown procedure...");
592 
593             // No need to start client if only we need to execute the script
594             if (ShutdownProcedureS[PARK_MOUNT].s == ISS_OFF && ShutdownProcedureS[PARK_DOME].s == ISS_OFF &&
595                     ShutdownProcedureS[EXECUTE_SCRIPT].s == ISS_ON)
596             {
597                 executeScript();
598                 break;
599             }
600 
601             // Watch mount if requied
602             if (ShutdownProcedureS[PARK_MOUNT].s == ISS_ON)
603                 watchdogClient->setMount(ActiveDeviceT[ACTIVE_TELESCOPE].text);
604             // Watch dome
605             if (ShutdownProcedureS[PARK_DOME].s == ISS_ON)
606                 watchdogClient->setDome(ActiveDeviceT[ACTIVE_DOME].text);
607 
608             // Set indiserver host and port
609             watchdogClient->setServer(SettingsT[INDISERVER_HOST].text, m_INDIServerPort);
610 
611             LOG_DEBUG("Connecting to INDI server...");
612 
613             watchdogClient->connectServer();
614 
615             m_ShutdownStage = WATCHDOG_CLIENT_STARTED;
616 
617             break;
618 
619         case WATCHDOG_CLIENT_STARTED:
620             // Check if client is ready
621             if (watchdogClient->isConnected())
622             {
623                 LOGF_DEBUG("Connected to INDI server %s @ %s", SettingsT[0].text,
624                            SettingsT[1].text);
625 
626                 if (ShutdownProcedureS[PARK_MOUNT].s == ISS_ON)
627                     parkMount();
628                 else if (ShutdownProcedureS[PARK_DOME].s == ISS_ON)
629                     parkDome();
630                 else if (ShutdownProcedureS[EXECUTE_SCRIPT].s == ISS_ON)
631                     executeScript();
632             }
633             else
634                 LOG_DEBUG("Waiting for INDI server connection...");
635             break;
636 
637         case WATCHDOG_MOUNT_PARKED:
638         {
639             // check if mount is parked
640             IPState mountState = watchdogClient->getMountParkState();
641 
642             if (mountState == IPS_OK || mountState == IPS_IDLE)
643             {
644                 LOG_INFO("Mount parked.");
645 
646                 if (ShutdownProcedureS[PARK_DOME].s == ISS_ON)
647                     parkDome();
648                 else if (ShutdownProcedureS[EXECUTE_SCRIPT].s == ISS_ON)
649                     executeScript();
650                 else
651                     m_ShutdownStage = WATCHDOG_COMPLETE;
652             }
653         }
654         break;
655 
656         case WATCHDOG_DOME_PARKED:
657         {
658             // check if dome is parked
659             IPState domeState = watchdogClient->getDomeParkState();
660 
661             if (domeState == IPS_OK || domeState == IPS_IDLE)
662             {
663                 LOG_INFO("Dome parked.");
664 
665                 if (ShutdownProcedureS[EXECUTE_SCRIPT].s == ISS_ON)
666                     executeScript();
667                 else
668                     m_ShutdownStage = WATCHDOG_COMPLETE;
669             }
670         }
671         break;
672 
673         case WATCHDOG_COMPLETE:
674             LOG_INFO("Shutdown procedure complete.");
675             ShutdownProcedureSP.s = IPS_OK;
676             IDSetSwitch(&ShutdownProcedureSP, nullptr);
677             // If watch dog client still connected, keep it as such
678             if (watchdogClient->isConnected())
679                 m_ShutdownStage = WATCHDOG_CLIENT_STARTED;
680             // If server is shutdown, then we reset to IDLE
681             else
682                 m_ShutdownStage = WATCHDOG_IDLE;
683             return;
684 
685         case WATCHDOG_ERROR:
686             ShutdownProcedureSP.s = IPS_ALERT;
687             IDSetSwitch(&ShutdownProcedureSP, nullptr);
688             return;
689     }
690 
691     SetTimer(getCurrentPollingPeriod());
692 }
693 
parkDome()694 void WatchDog::parkDome()
695 {
696     if (watchdogClient->parkDome() == false)
697     {
698         LOG_ERROR("Error: Unable to park dome! Shutdown procedure terminated.");
699         m_ShutdownStage = WATCHDOG_ERROR;
700         return;
701     }
702 
703     LOG_INFO("Parking dome...");
704     m_ShutdownStage = WATCHDOG_DOME_PARKED;
705 }
706 
parkMount()707 void WatchDog::parkMount()
708 {
709     if (watchdogClient->parkMount() == false)
710     {
711         LOG_ERROR("Error: Unable to park mount! Shutdown procedure terminated.");
712         m_ShutdownStage = WATCHDOG_ERROR;
713         return;
714     }
715 
716     LOG_INFO("Parking mount...");
717 
718     // If mount is set to ignored, and we have active dome shutdown then we start
719     // parking the dome immediately.
720     if (MountPolicyS[MOUNT_IGNORED].s == ISS_ON && ShutdownProcedureS[PARK_DOME].s == ISS_ON)
721         parkDome();
722     else
723         m_ShutdownStage = WATCHDOG_MOUNT_PARKED;
724 }
725 
executeScript()726 void WatchDog::executeScript()
727 {
728     // child
729     if (fork() == 0)
730     {
731         int rc = execlp(SettingsT[EXECUTE_SCRIPT].text, SettingsT[EXECUTE_SCRIPT].text, nullptr);
732 
733         if (rc)
734             exit(rc);
735     }
736     // parent
737     else
738     {
739         int statval;
740         LOGF_INFO("Executing script %s...", SettingsT[EXECUTE_SCRIPT].text);
741         LOGF_INFO("Waiting for script with PID %d to complete...", getpid());
742         wait(&statval);
743         if (WIFEXITED(statval))
744         {
745             int exit_code = WEXITSTATUS(statval);
746             LOGF_INFO("Script complete with exit code %d", exit_code);
747 
748             if (exit_code == 0)
749                 m_ShutdownStage = WATCHDOG_COMPLETE;
750             else
751             {
752                 LOGF_ERROR("Error: script %s failed. Shutdown procedure terminated.",
753                            SettingsT[EXECUTE_SCRIPT].text);
754                 m_ShutdownStage = WATCHDOG_ERROR;
755                 return;
756             }
757         }
758         else
759         {
760             LOGF_ERROR(
761                 "Error: script %s did not terminate with exit. Shutdown procedure terminated.",
762                 SettingsT[EXECUTE_SCRIPT].text);
763             m_ShutdownStage = WATCHDOG_ERROR;
764             return;
765         }
766     }
767 }
768