1 /*******************************************************************************
2   Copyright(c) 2019 Jasem Mutlaq. All rights reserved.
3 
4   Pegasus Pocket Power Box Driver.
5 
6   This program is free software; you can redistribute it and/or modify it
7   under the terms of the GNU General Public License as published by the Free
8   Software Foundation; either version 2 of the License, or (at your option)
9   any later version.
10 
11   This program is distributed in the hope that it will be useful, but WITHOUT
12   ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
14   more details.
15 
16   You should have received a copy of the GNU Library General Public License
17   along with this library; see the file COPYING.LIB.  If not, write to
18   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19   Boston, MA 02110-1301, USA.
20 
21   The full GNU General Public License is included in this distribution in the
22   file called LICENSE.
23 *******************************************************************************/
24 
25 #include "pegasus_ppb.h"
26 #include "indicom.h"
27 #include "connectionplugins/connectionserial.h"
28 
29 #include <memory>
30 #include <regex>
31 #include <termios.h>
32 #include <cstring>
33 #include <sys/ioctl.h>
34 
35 // We declare an auto pointer to PegasusPPB.
36 static std::unique_ptr<PegasusPPB> pocket_power_box(new PegasusPPB());
37 
PegasusPPB()38 PegasusPPB::PegasusPPB() : WI(this)
39 {
40     setVersion(1, 1);
41     lastSensorData.reserve(PA_N);
42 }
43 
initProperties()44 bool PegasusPPB::initProperties()
45 {
46     INDI::DefaultDevice::initProperties();
47 
48     setDriverInterface(AUX_INTERFACE | WEATHER_INTERFACE);
49 
50     WI::initProperties(ENVIRONMENT_TAB, ENVIRONMENT_TAB);
51 
52     addAuxControls();
53 
54     ////////////////////////////////////////////////////////////////////////////
55     /// Main Control Panel
56     ////////////////////////////////////////////////////////////////////////////
57     // Cycle all power on/off
58     IUFillSwitch(&PowerCycleAllS[POWER_CYCLE_OFF], "POWER_CYCLE_OFF", "All Off", ISS_OFF);
59     IUFillSwitch(&PowerCycleAllS[POWER_CYCLE_ON], "POWER_CYCLE_ON", "All On", ISS_OFF);
60     IUFillSwitchVector(&PowerCycleAllSP, PowerCycleAllS, 2, getDeviceName(), "POWER_CYCLE", "Cycle Power", MAIN_CONTROL_TAB,
61                        IP_RW, ISR_ATMOST1, 60, IPS_IDLE);
62 
63     // DSLR on/off
64     IUFillSwitch(&DSLRPowerS[DSLR_OFF], "DSLR_OFF", "Off", ISS_OFF);
65     IUFillSwitch(&DSLRPowerS[DSLR_ON], "DSLR_ON", "On", ISS_OFF);
66     IUFillSwitchVector(&DSLRPowerSP, DSLRPowerS, 2, getDeviceName(), "DSLR_POWER", "DSLR Power", MAIN_CONTROL_TAB, IP_RW,
67                        ISR_ATMOST1, 60, IPS_IDLE);
68 
69     // Reboot
70     IUFillSwitch(&RebootS[0], "REBOOT", "Reboot Device", ISS_OFF);
71     IUFillSwitchVector(&RebootSP, RebootS, 1, getDeviceName(), "REBOOT_DEVICE", "Device", MAIN_CONTROL_TAB, IP_RW, ISR_ATMOST1,
72                        60, IPS_IDLE);
73 
74     // Power Sensors
75     IUFillNumber(&PowerSensorsN[SENSOR_VOLTAGE], "SENSOR_VOLTAGE", "Voltage (V)", "%4.2f", 0, 999, 100, 0);
76     IUFillNumber(&PowerSensorsN[SENSOR_CURRENT], "SENSOR_CURRENT", "Current (A)", "%4.2f", 0, 999, 100, 0);
77     IUFillNumberVector(&PowerSensorsNP, PowerSensorsN, 2, getDeviceName(), "POWER_SENSORS", "Sensors", MAIN_CONTROL_TAB, IP_RO,
78                        60, IPS_IDLE);
79 
80     ////////////////////////////////////////////////////////////////////////////
81     /// Power Group
82     ////////////////////////////////////////////////////////////////////////////
83 
84     // Power on Boot
85     IUFillSwitch(&PowerOnBootS[0], "POWER_PORT_1", "Port 1", ISS_ON);
86     IUFillSwitch(&PowerOnBootS[1], "POWER_PORT_2", "Port 2", ISS_ON);
87     IUFillSwitch(&PowerOnBootS[2], "POWER_PORT_3", "Port 3", ISS_ON);
88     IUFillSwitch(&PowerOnBootS[3], "POWER_PORT_4", "Port 4", ISS_ON);
89     IUFillSwitchVector(&PowerOnBootSP, PowerOnBootS, 4, getDeviceName(), "POWER_ON_BOOT", "Power On Boot", MAIN_CONTROL_TAB,
90                        IP_RW, ISR_NOFMANY, 60, IPS_IDLE);
91 
92     ////////////////////////////////////////////////////////////////////////////
93     /// Dew Group
94     ////////////////////////////////////////////////////////////////////////////
95 
96     // Automatic Dew
97     IUFillSwitch(&AutoDewS[INDI_ENABLED], "INDI_ENABLED", "Enabled", ISS_OFF);
98     IUFillSwitch(&AutoDewS[INDI_DISABLED], "INDI_DISABLED", "Disabled", ISS_ON);
99     IUFillSwitchVector(&AutoDewSP, AutoDewS, 2, getDeviceName(), "AUTO_DEW", "Auto Dew", DEW_TAB, IP_RW, ISR_1OFMANY, 60,
100                        IPS_IDLE);
101 
102     // Dew PWM
103     IUFillNumber(&DewPWMN[DEW_PWM_A], "DEW_A", "Dew A (%)", "%.2f", 0, 100, 10, 0);
104     IUFillNumber(&DewPWMN[DEW_PWM_B], "DEW_B", "Dew B (%)", "%.2f", 0, 100, 10, 0);
105     IUFillNumberVector(&DewPWMNP, DewPWMN, 2, getDeviceName(), "DEW_PWM", "Dew PWM", DEW_TAB, IP_RW, 60, IPS_IDLE);
106 
107     ////////////////////////////////////////////////////////////////////////////
108     /// Environment Group
109     ////////////////////////////////////////////////////////////////////////////
110     addParameter("WEATHER_TEMPERATURE", "Temperature (C)", -15, 35, 15);
111     addParameter("WEATHER_HUMIDITY", "Humidity %", 0, 100, 15);
112     addParameter("WEATHER_DEWPOINT", "Dew Point (C)", 0, 100, 15);
113     setCriticalParameter("WEATHER_TEMPERATURE");
114 
115     ////////////////////////////////////////////////////////////////////////////
116     /// Serial Connection
117     ////////////////////////////////////////////////////////////////////////////
118     serialConnection = new Connection::Serial(this);
119     serialConnection->registerHandshake([&]()
120     {
121         return Handshake();
122     });
123     registerConnection(serialConnection);
124 
125     return true;
126 }
127 
updateProperties()128 bool PegasusPPB::updateProperties()
129 {
130     INDI::DefaultDevice::updateProperties();
131 
132     if (isConnected())
133     {
134         // Main Control
135         defineProperty(&PowerCycleAllSP);
136         defineProperty(&DSLRPowerSP);
137         defineProperty(&PowerSensorsNP);
138         defineProperty(&PowerOnBootSP);
139         defineProperty(&RebootSP);
140 
141         // Dew
142         defineProperty(&AutoDewSP);
143         defineProperty(&DewPWMNP);
144 
145         WI::updateProperties();
146 
147         setupComplete = true;
148     }
149     else
150     {
151         // Main Control
152         deleteProperty(PowerCycleAllSP.name);
153         deleteProperty(DSLRPowerSP.name);
154         deleteProperty(PowerSensorsNP.name);
155         deleteProperty(PowerOnBootSP.name);
156         deleteProperty(RebootSP.name);
157 
158         // Dew
159         deleteProperty(AutoDewSP.name);
160         deleteProperty(DewPWMNP.name);
161 
162         WI::updateProperties();
163 
164         setupComplete = false;
165     }
166 
167     return true;
168 }
169 
getDefaultName()170 const char * PegasusPPB::getDefaultName()
171 {
172     return "Pegasus PPB";
173 }
174 
Handshake()175 bool PegasusPPB::Handshake()
176 {
177     int tty_rc = 0, nbytes_written = 0, nbytes_read = 0;
178     char command[PEGASUS_LEN] = {0}, response[PEGASUS_LEN] = {0};
179 
180     PortFD = serialConnection->getPortFD();
181 
182     LOG_DEBUG("CMD <P#>");
183 
184     tcflush(PortFD, TCIOFLUSH);
185     strncpy(command, "P#\n", PEGASUS_LEN);
186     if ( (tty_rc = tty_write_string(PortFD, command, &nbytes_written)) != TTY_OK)
187     {
188         char errorMessage[MAXRBUF];
189         tty_error_msg(tty_rc, errorMessage, MAXRBUF);
190         LOGF_ERROR("Serial write error: %s", errorMessage);
191         return false;
192     }
193 
194     // Try first with stopChar as the stop character
195     if ( (tty_rc = tty_nread_section(PortFD, response, PEGASUS_LEN, stopChar, 1, &nbytes_read)) != TTY_OK)
196     {
197         // Try 0xA as the stop character
198         if (tty_rc == TTY_OVERFLOW || tty_rc == TTY_TIME_OUT)
199         {
200             tcflush(PortFD, TCIOFLUSH);
201             tty_write_string(PortFD, command, &nbytes_written);
202             stopChar = 0xA;
203             tty_rc = tty_nread_section(PortFD, response, PEGASUS_LEN, stopChar, 1, &nbytes_read);
204         }
205 
206         if (tty_rc != TTY_OK)
207         {
208             char errorMessage[MAXRBUF];
209             tty_error_msg(tty_rc, errorMessage, MAXRBUF);
210             LOGF_ERROR("Serial read error: %s", errorMessage);
211             return false;
212         }
213     }
214 
215     tcflush(PortFD, TCIOFLUSH);
216     response[nbytes_read - 1] = '\0';
217     LOGF_DEBUG("RES <%s>", response);
218 
219     setupComplete = false;
220 
221     return !strcmp(response, "PPB_OK");
222 }
223 
ISNewSwitch(const char * dev,const char * name,ISState * states,char * names[],int n)224 bool PegasusPPB::ISNewSwitch(const char * dev, const char * name, ISState * states, char * names[], int n)
225 {
226     if (dev && !strcmp(dev, getDeviceName()))
227     {
228         // Cycle all power on or off
229         if (!strcmp(name, PowerCycleAllSP.name))
230         {
231             IUUpdateSwitch(&PowerCycleAllSP, states, names, n);
232 
233             PowerCycleAllSP.s = IPS_ALERT;
234             char cmd[PEGASUS_LEN] = {0}, res[PEGASUS_LEN] = {0};
235             snprintf(cmd, PEGASUS_LEN, "P1:%d", IUFindOnSwitchIndex(&PowerCycleAllSP));
236             if (sendCommand(cmd, res))
237             {
238                 PowerCycleAllSP.s = !strcmp(cmd, res) ? IPS_OK : IPS_ALERT;
239             }
240 
241             IUResetSwitch(&PowerCycleAllSP);
242             IDSetSwitch(&PowerCycleAllSP, nullptr);
243             return true;
244         }
245 
246         // DSLR
247         if (!strcmp(name, DSLRPowerSP.name))
248         {
249             IUUpdateSwitch(&DSLRPowerSP, states, names, n);
250 
251             DSLRPowerSP.s = IPS_ALERT;
252             char cmd[PEGASUS_LEN] = {0}, res[PEGASUS_LEN] = {0};
253             snprintf(cmd, PEGASUS_LEN, "P2:%d", IUFindOnSwitchIndex(&DSLRPowerSP));
254             if (sendCommand(cmd, res))
255             {
256                 DSLRPowerSP.s = !strcmp(cmd, res) ? IPS_OK : IPS_ALERT;
257             }
258 
259             IUResetSwitch(&DSLRPowerSP);
260             IDSetSwitch(&DSLRPowerSP, nullptr);
261             return true;
262         }
263 
264         // Reboot
265         if (!strcmp(name, RebootSP.name))
266         {
267             RebootSP.s = reboot() ? IPS_OK : IPS_ALERT;
268             IDSetSwitch(&RebootSP, nullptr);
269             LOG_INFO("Rebooting device...");
270             return true;
271         }
272 
273         // Power on boot
274         if (!strcmp(name, PowerOnBootSP.name))
275         {
276             IUUpdateSwitch(&PowerOnBootSP, states, names, n);
277             PowerOnBootSP.s = setPowerOnBoot() ? IPS_OK : IPS_ALERT;
278             IDSetSwitch(&PowerOnBootSP, nullptr);
279             saveConfig(true, PowerOnBootSP.name);
280             return true;
281         }
282 
283         // Auto Dew
284         if (!strcmp(name, AutoDewSP.name))
285         {
286             int prevIndex = IUFindOnSwitchIndex(&AutoDewSP);
287             IUUpdateSwitch(&AutoDewSP, states, names, n);
288             if (setAutoDewEnabled(AutoDewS[INDI_ENABLED].s == ISS_ON))
289             {
290                 AutoDewSP.s = IPS_OK;
291             }
292             else
293             {
294                 IUResetSwitch(&AutoDewSP);
295                 AutoDewS[prevIndex].s = ISS_ON;
296                 AutoDewSP.s = IPS_ALERT;
297             }
298 
299             IDSetSwitch(&AutoDewSP, nullptr);
300             return true;
301         }
302     }
303 
304     return DefaultDevice::ISNewSwitch(dev, name, states, names, n);
305 }
306 
ISNewNumber(const char * dev,const char * name,double values[],char * names[],int n)307 bool PegasusPPB::ISNewNumber(const char * dev, const char * name, double values[], char * names[], int n)
308 {
309     if (dev && !strcmp(dev, getDeviceName()))
310     {
311         // Dew PWM
312         if (!strcmp(name, DewPWMNP.name))
313         {
314             bool rc1 = false, rc2 = false;
315             for (int i = 0; i < n; i++)
316             {
317                 if (!strcmp(names[i], DewPWMN[DEW_PWM_A].name))
318                     rc1 = setDewPWM(3, static_cast<uint8_t>(values[i] / 100.0 * 255.0));
319                 else if (!strcmp(names[i], DewPWMN[DEW_PWM_B].name))
320                     rc2 = setDewPWM(4, static_cast<uint8_t>(values[i] / 100.0 * 255.0));
321             }
322 
323             DewPWMNP.s = (rc1 && rc2) ? IPS_OK : IPS_ALERT;
324             if (DewPWMNP.s == IPS_OK)
325                 IUUpdateNumber(&DewPWMNP, values, names, n);
326             IDSetNumber(&DewPWMNP, nullptr);
327             return true;
328         }
329 
330         if (strstr(name, "WEATHER_"))
331             return WI::processNumber(dev, name, values, names, n);
332     }
333     return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n);
334 }
335 
sendCommand(const char * cmd,char * res)336 bool PegasusPPB::sendCommand(const char * cmd, char * res)
337 {
338     int nbytes_read = 0, nbytes_written = 0, tty_rc = 0;
339     char command[PEGASUS_LEN] = {0};
340     LOGF_DEBUG("CMD <%s>", cmd);
341 
342     for (int i = 0; i < 2; i++)
343     {
344         tcflush(PortFD, TCIOFLUSH);
345         snprintf(command, PEGASUS_LEN, "%s\n", cmd);
346         if ( (tty_rc = tty_write_string(PortFD, command, &nbytes_written)) != TTY_OK)
347             continue;
348 
349         if (!res)
350         {
351             tcflush(PortFD, TCIOFLUSH);
352             return true;
353         }
354 
355         if ( (tty_rc = tty_nread_section(PortFD, res, PEGASUS_LEN, stopChar, PEGASUS_TIMEOUT, &nbytes_read)) != TTY_OK
356                 || nbytes_read == 1)
357             continue;
358 
359         tcflush(PortFD, TCIOFLUSH);
360         res[nbytes_read - 1] = '\0';
361         LOGF_DEBUG("RES <%s>", res);
362         return true;
363     }
364 
365     if (tty_rc != TTY_OK)
366     {
367         char errorMessage[MAXRBUF];
368         tty_error_msg(tty_rc, errorMessage, MAXRBUF);
369         LOGF_ERROR("Serial error: %s", errorMessage);
370     }
371 
372     return false;
373 }
374 
setAutoDewEnabled(bool enabled)375 bool PegasusPPB::setAutoDewEnabled(bool enabled)
376 {
377     char cmd[PEGASUS_LEN] = {0}, res[PEGASUS_LEN] = {0};
378     snprintf(cmd, PEGASUS_LEN, "PD:%d", enabled ? 1 : 0);
379     if (sendCommand(cmd, res))
380     {
381         return (!strcmp(res, cmd));
382     }
383 
384     return false;
385 }
386 
setPowerOnBoot()387 bool PegasusPPB::setPowerOnBoot()
388 {
389     char cmd[PEGASUS_LEN] = {0}, res[PEGASUS_LEN] = {0};
390     snprintf(cmd, PEGASUS_LEN, "PE:%d%d%d%d", PowerOnBootS[0].s == ISS_ON ? 1 : 0,
391              PowerOnBootS[1].s == ISS_ON ? 1 : 0,
392              PowerOnBootS[2].s == ISS_ON ? 1 : 0,
393              PowerOnBootS[3].s == ISS_ON ? 1 : 0);
394     if (sendCommand(cmd, res))
395     {
396         return (!strcmp(res, "PE:1"));
397     }
398 
399     return false;
400 }
401 
setDewPWM(uint8_t id,uint8_t value)402 bool PegasusPPB::setDewPWM(uint8_t id, uint8_t value)
403 {
404     char cmd[PEGASUS_LEN] = {0}, res[PEGASUS_LEN] = {0}, expected[PEGASUS_LEN] = {0};
405     snprintf(cmd, PEGASUS_LEN, "P%d:%03d", id, value);
406     snprintf(expected, PEGASUS_LEN, "P%d:%d", id, value);
407     if (sendCommand(cmd, res))
408     {
409         return (!strcmp(res, expected));
410     }
411 
412     return false;
413 }
414 
saveConfigItems(FILE * fp)415 bool PegasusPPB::saveConfigItems(FILE * fp)
416 {
417     INDI::DefaultDevice::saveConfigItems(fp);
418     WI::saveConfigItems(fp);
419     IUSaveConfigSwitch(fp, &AutoDewSP);
420 
421     return true;
422 }
423 
TimerHit()424 void PegasusPPB::TimerHit()
425 {
426     if (!isConnected() || setupComplete == false)
427     {
428         SetTimer(getCurrentPollingPeriod());
429         return;
430     }
431 
432     getSensorData();
433     SetTimer(getCurrentPollingPeriod());
434 }
435 
sendFirmware()436 bool PegasusPPB::sendFirmware()
437 {
438     char res[PEGASUS_LEN] = {0};
439     if (sendCommand("PV", res))
440     {
441         LOGF_INFO("Detected firmware %s", res);
442         return true;
443     }
444 
445     return false;
446 }
447 
getSensorData()448 bool PegasusPPB::getSensorData()
449 {
450     char res[PEGASUS_LEN] = {0};
451     if (sendCommand("PA", res))
452     {
453         std::vector<std::string> result = split(res, ":");
454         if (result.size() < PA_N)
455         {
456             LOG_WARN("Received wrong number of detailed sensor data. Retrying...");
457             return false;
458         }
459 
460         if (result == lastSensorData)
461             return true;
462 
463         // Power Sensors
464         PowerSensorsN[SENSOR_VOLTAGE].value = std::stod(result[PA_VOLTAGE]);
465         PowerSensorsN[SENSOR_CURRENT].value = std::stod(result[PA_CURRENT]) / 65.0;
466         PowerSensorsNP.s = IPS_OK;
467         if (lastSensorData[PA_VOLTAGE] != result[PA_VOLTAGE] || lastSensorData[PA_CURRENT] != result[PA_CURRENT])
468             IDSetNumber(&PowerSensorsNP, nullptr);
469 
470         // Environment Sensors
471         setParameterValue("WEATHER_TEMPERATURE", std::stod(result[PA_TEMPERATURE]));
472         setParameterValue("WEATHER_HUMIDITY", std::stod(result[PA_HUMIDITY]));
473         setParameterValue("WEATHER_DEWPOINT", std::stod(result[PA_DEW_POINT]));
474         if (lastSensorData[PA_TEMPERATURE] != result[PA_TEMPERATURE] ||
475                 lastSensorData[PA_HUMIDITY] != result[PA_HUMIDITY] ||
476                 lastSensorData[PA_DEW_POINT] != result[PA_DEW_POINT])
477         {
478             if (WI::syncCriticalParameters())
479                 IDSetLight(&critialParametersLP, nullptr);
480             ParametersNP.s = IPS_OK;
481             IDSetNumber(&ParametersNP, nullptr);
482         }
483 
484         // Power Status
485         PowerCycleAllS[POWER_CYCLE_ON].s = (std::stoi(result[PA_PORT_STATUS]) == 1) ? ISS_ON : ISS_OFF;
486         PowerCycleAllS[POWER_CYCLE_ON].s = (std::stoi(result[PA_PORT_STATUS]) == 0) ? ISS_ON : ISS_OFF;
487         PowerCycleAllSP.s = (std::stoi(result[6]) == 1) ? IPS_OK : IPS_IDLE;
488         if (lastSensorData[PA_PORT_STATUS] != result[PA_PORT_STATUS])
489             IDSetSwitch(&PowerCycleAllSP, nullptr);
490 
491         // DSLR Power Status
492         DSLRPowerS[POWER_CYCLE_ON].s = (std::stoi(result[PA_DSLR_STATUS]) == 1) ? ISS_ON : ISS_OFF;
493         DSLRPowerS[POWER_CYCLE_ON].s = (std::stoi(result[PA_DSLR_STATUS]) == 0) ? ISS_ON : ISS_OFF;
494         DSLRPowerSP.s = (std::stoi(result[PA_DSLR_STATUS]) == 1) ? IPS_OK : IPS_IDLE;
495         if (lastSensorData[PA_DSLR_STATUS] != result[PA_DSLR_STATUS])
496             IDSetSwitch(&DSLRPowerSP, nullptr);
497 
498         // Dew PWM
499         DewPWMN[0].value = std::stod(result[PA_DEW_1]) / 255.0 * 100.0;
500         DewPWMN[1].value = std::stod(result[PA_DEW_2]) / 255.0 * 100.0;
501         if (lastSensorData[PA_DEW_1] != result[PA_DEW_1] || lastSensorData[PA_DEW_2] != result[PA_DEW_2])
502             IDSetNumber(&DewPWMNP, nullptr);
503 
504         // Auto Dew
505         AutoDewS[INDI_ENABLED].s  = (std::stoi(result[PA_AUTO_DEW]) == 1) ? ISS_ON : ISS_OFF;
506         AutoDewS[INDI_DISABLED].s = (std::stoi(result[PA_AUTO_DEW]) == 1) ? ISS_OFF : ISS_ON;
507         if (lastSensorData[PA_AUTO_DEW] != result[PA_AUTO_DEW])
508             IDSetSwitch(&AutoDewSP, nullptr);
509 
510         lastSensorData = result;
511 
512         return true;
513     }
514 
515     return false;
516 }
517 
518 // Device Control
reboot()519 bool PegasusPPB::reboot()
520 {
521     return sendCommand("PF", nullptr);
522 }
523 
split(const std::string & input,const std::string & regex)524 std::vector<std::string> PegasusPPB::split(const std::string &input, const std::string &regex)
525 {
526     // passing -1 as the submatch index parameter performs splitting
527     std::regex re(regex);
528     std::sregex_token_iterator
529     first{input.begin(), input.end(), re, -1},
530           last;
531     return {first, last};
532 }
533 
534