1 /*
2     SestoSenso Focuser
3     Copyright (C) 2018 Jasem Mutlaq (mutlaqja@ikarustech.com)
4 
5     This library is free software; you can redistribute it and/or
6     modify it under the terms of the GNU Lesser General Public
7     License as published by the Free Software Foundation; either
8     version 2.1 of the License, or (at your option) any later version.
9 
10     This library is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13     Lesser General Public License for more details.
14 
15     You should have received a copy of the GNU Lesser General Public
16     License along with this library; if not, write to the Free Software
17     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18 
19     Commands and responses:
20 
21     Only use the SM/Sm commands during calibration. Will cause direction reversal!
22     #Sm;xxxxxxx! Set xxxxxxx as min value
23     #SM!	Set current position as max
24     #SM;xxxxxxx!	Set xxxxxxx as max value (xxxxxxx between 0 to 2097152)
25 
26     #SPxxxx! Set_current_position as xxxx
27     #SC;HOLD;RUN;ACC;DEC! Shell_set_current_supply in HOLD, RUN, ACC, DEC situations (Value must be from 0 to 24, maximum hold value 10)
28     #QM! Query max value
29     #Qm! Query min value
30     #QT! Qeury temperature
31     #QF! Query firmware version
32     #QN! Read the device name	-> reply	QN;SESTOSENSO!
33     #QP! Query_position
34     #FI! Fast_inward
35     #FO! Fast_outward
36     #SI! Slow_inward
37     #SO! Slow_outward
38     #GTxxxx! Go_to absolute position xxxx
39     #MA! Motion_abort and hold position
40     #MF!	Motor free
41     #PS! param_save save current position for next power ON and currents supply
42     #PD! param_to_default , and position to zero
43 
44     Response examples:
45 
46     #QF! 14.06\r
47     #QT! -10.34\r
48     #FI! FIok!\r
49     #FO! FOok!\r
50     #SI! SIok!\r
51     #SO! SOok!\r
52     #GTxxxx! 100\r 200\r 300\r xxxx\r GTok!\r
53     #MA! MAok!\r
54     #MF!	MFok!\r
55     #QP! 1530\r
56     #SPxxxx! SPok!\r
57     #SC;HOLD;RUN;ACC;DEC! SCok!\r
58     #PS! PSok!\r
59     #PD! PDok!\r
60 
61     Before to disconnect the COM port, send the #PS! command in order to save the position on internal memory
62 
63 */
64 
65 #include "sestosenso.h"
66 
67 #include "indicom.h"
68 
69 #include <cmath>
70 #include <cstring>
71 #include <memory>
72 
73 #include <termios.h>
74 #include <unistd.h>
75 
76 static std::unique_ptr<SestoSenso> sesto(new SestoSenso());
77 
SestoSenso()78 SestoSenso::SestoSenso()
79 {
80     setVersion(1, 4);
81     // Can move in Absolute & Relative motions, can AbortFocuser motion.
82     FI::SetCapability(FOCUSER_CAN_ABS_MOVE | FOCUSER_CAN_REL_MOVE | FOCUSER_CAN_ABORT);
83 }
84 
initProperties()85 bool SestoSenso::initProperties()
86 {
87     INDI::Focuser::initProperties();
88 
89     // Firmware Information
90     IUFillText(&FirmwareT[0], "VERSION", "Version", "");
91     IUFillTextVector(&FirmwareTP, FirmwareT, 1, getDeviceName(), "FOCUS_FIRMWARE", "Firmware", MAIN_CONTROL_TAB, IP_RO, 0,
92                      IPS_IDLE);
93 
94     // Focuser temperature
95     IUFillNumber(&TemperatureN[0], "TEMPERATURE", "Celsius", "%6.2f", -50, 70., 0., 0.);
96     IUFillNumberVector(&TemperatureNP, TemperatureN, 1, getDeviceName(), "FOCUS_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB,
97                        IP_RO, 0, IPS_IDLE);
98 
99     // Focuser calibration
100     IUFillText(&CalibrationMessageT[0], "CALIBRATION", "Calibration stage", "");
101     IUFillTextVector(&CalibrationMessageTP, CalibrationMessageT, 1, getDeviceName(), "CALIBRATION_MESSAGE", "Calibration",
102                      MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE);
103 
104     IUFillSwitch(&CalibrationS[CALIBRATION_START], "CALIBRATION_START", "Start", ISS_OFF);
105     IUFillSwitch(&CalibrationS[CALIBRATION_NEXT], "CALIBRATION_NEXT", "Next", ISS_OFF);
106     IUFillSwitchVector(&CalibrationSP, CalibrationS, 2, getDeviceName(), "FOCUS_CALIBRATION", "Calibration", MAIN_CONTROL_TAB,
107                        IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
108 
109     IUFillSwitch(&FastMoveS[FASTMOVE_IN], "FASTMOVE_IN", "Move In", ISS_OFF);
110     IUFillSwitch(&FastMoveS[FASTMOVE_OUT], "FASTMOVE_OUT", "Move out", ISS_OFF);
111     IUFillSwitch(&FastMoveS[FASTMOVE_STOP], "FASTMOVE_STOP", "Stop", ISS_OFF);
112     IUFillSwitchVector(&FastMoveSP, FastMoveS, 3, getDeviceName(), "FAST_MOVE", "Calibration Move", MAIN_CONTROL_TAB, IP_RW,
113                        ISR_1OFMANY, 0, IPS_IDLE);
114 
115     //
116     // Override the default Max. Position to make it Read-Only
117     IUFillNumberVector(&FocusMaxPosNP, FocusMaxPosN, 1, getDeviceName(), "FOCUS_MAX", "Max. Position", MAIN_CONTROL_TAB, IP_RO,
118                        0, IPS_IDLE);
119 
120     // Relative and absolute movement
121     FocusRelPosN[0].min   = 0.;
122     FocusRelPosN[0].max   = 50000.;
123     FocusRelPosN[0].value = 0;
124     FocusRelPosN[0].step  = 1000;
125 
126     FocusAbsPosN[0].min   = 0.;
127     FocusAbsPosN[0].max   = 200000.;
128     FocusAbsPosN[0].value = 0;
129     FocusAbsPosN[0].step  = 1000;
130 
131     FocusMaxPosN[0].value = 2097152;
132 
133     addAuxControls();
134 
135     setDefaultPollingPeriod(500);
136 
137     m_MotionProgressTimer.callOnTimeout(std::bind(&SestoSenso::checkMotionProgressCallback, this));
138     m_MotionProgressTimer.setSingleShot(true);
139 
140     return true;
141 }
142 
updateProperties()143 bool SestoSenso::updateProperties()
144 {
145     INDI::Focuser::updateProperties();
146 
147     if (isConnected())
148     {
149         // Only define temperature if there is a probe
150         if (updateTemperature())
151             defineProperty(&TemperatureNP);
152         defineProperty(&FirmwareTP);
153         IUSaveText(&CalibrationMessageT[0], "Press START to begin the Calibration");
154         defineProperty(&CalibrationMessageTP);
155         defineProperty(&CalibrationSP);
156 
157         if (getStartupValues())
158             LOG_INFO("SestoSenso parameters updated, focuser ready for use.");
159         else
160             LOG_WARN("Failed to inquire parameters. Check logs.");
161     }
162     else
163     {
164         if (TemperatureNP.s == IPS_OK)
165             deleteProperty(TemperatureNP.name);
166         deleteProperty(FirmwareTP.name);
167         deleteProperty(CalibrationMessageTP.name);
168         deleteProperty(CalibrationSP.name);
169     }
170 
171     return true;
172 }
173 
Handshake()174 bool SestoSenso::Handshake()
175 {
176     if (Ack())
177     {
178         LOG_INFO("SestoSenso is online. Getting focus parameters...");
179         return true;
180     }
181 
182     LOG_INFO(
183         "Error retrieving data from SestoSenso, please ensure SestoSenso controller is powered and the port is correct.");
184     return false;
185 }
186 
Disconnect()187 bool SestoSenso::Disconnect()
188 {
189     // Save current position to memory.
190     if (isSimulation() == false)
191         sendCommand("#PS!");
192 
193     return INDI::Focuser::Disconnect();
194 }
195 
getDefaultName()196 const char *SestoSenso::getDefaultName()
197 {
198     return "Sesto Senso";
199 }
200 
Ack()201 bool SestoSenso::Ack()
202 {
203     char res[SESTO_LEN] = {0};
204 
205     if (isSimulation())
206         strncpy(res, "1.0 Simulation", SESTO_LEN);
207     else if (sendCommand("#QF!", res) == false)
208         return false;
209 
210     IUSaveText(&FirmwareT[0], res);
211 
212     return true;
213 }
214 
updateTemperature()215 bool SestoSenso::updateTemperature()
216 {
217     char res[SESTO_LEN] = {0};
218     double temperature = 0;
219 
220     if (isSimulation())
221         strncpy(res, "23.45", SESTO_LEN);
222     else if (sendCommand("#QT!", res) == false)
223         return false;
224 
225     try
226     {
227         temperature = std::stod(res);
228     }
229     catch(...)
230     {
231         LOGF_WARN("Failed to process temperature response: %s (%d bytes)", res, strlen(res));
232         return false;
233     }
234 
235     if (temperature > 90)
236         return false;
237 
238     TemperatureN[0].value = temperature;
239     TemperatureNP.s = IPS_OK;
240 
241     return true;
242 }
243 
updateMaxLimit()244 bool SestoSenso::updateMaxLimit()
245 {
246     char res[SESTO_LEN] = {0};
247 
248     if (isSimulation())
249         return true;
250 
251     if (sendCommand("#QM!", res) == false)
252         return false;
253 
254     int maxLimit = 0;
255 
256     sscanf(res, "QM;%d!", &maxLimit);
257 
258     if (maxLimit > 0)
259     {
260         FocusMaxPosN[0].max = maxLimit;
261         if (FocusMaxPosN[0].value > maxLimit)
262             FocusMaxPosN[0].value = maxLimit;
263 
264         FocusAbsPosN[0].min   = 0;
265         FocusAbsPosN[0].max   = maxLimit;
266         FocusAbsPosN[0].value = 0;
267         FocusAbsPosN[0].step  = (FocusAbsPosN[0].max - FocusAbsPosN[0].min) / 50.0;
268 
269         FocusRelPosN[0].min   = 0.;
270         FocusRelPosN[0].max   = FocusAbsPosN[0].step * 10;
271         FocusRelPosN[0].value = 0;
272         FocusRelPosN[0].step  = FocusAbsPosN[0].step;
273 
274         IUUpdateMinMax(&FocusAbsPosNP);
275         IUUpdateMinMax(&FocusRelPosNP);
276 
277         FocusMaxPosNP.s = IPS_OK;
278         IUUpdateMinMax(&FocusMaxPosNP);
279         return true;
280     }
281 
282     FocusMaxPosNP.s = IPS_ALERT;
283     return false;
284 }
285 
updatePosition()286 bool SestoSenso::updatePosition()
287 {
288     char res[SESTO_LEN] = {0};
289     if (isSimulation())
290         snprintf(res, SESTO_LEN, "%d", static_cast<uint32_t>(FocusAbsPosN[0].value));
291     else if (sendCommand("#QP!", res) == false)
292         return false;
293 
294     try
295     {
296         FocusAbsPosN[0].value = std::stoi(res);
297         FocusAbsPosNP.s = IPS_OK;
298         return true;
299     }
300     catch(...)
301     {
302         LOGF_WARN("Failed to process position response: %s (%d bytes)", res, strlen(res));
303         FocusAbsPosNP.s = IPS_ALERT;
304         return false;
305     }
306 }
307 
isMotionComplete()308 bool SestoSenso::isMotionComplete()
309 {
310     char res[SESTO_LEN] = {0};
311 
312     if (isSimulation())
313     {
314         int32_t nextPos = FocusAbsPosN[0].value;
315         int32_t targPos = static_cast<int32_t>(targetPos);
316 
317         if (targPos > nextPos)
318             nextPos += 250;
319         else if (targPos < nextPos)
320             nextPos -= 250;
321 
322         if (abs(nextPos - targPos) < 250)
323             nextPos = targetPos;
324         else if (nextPos < 0)
325             nextPos = 0;
326         else if (nextPos > FocusAbsPosN[0].max)
327             nextPos = FocusAbsPosN[0].max;
328 
329         snprintf(res, SESTO_LEN, "%d", nextPos);
330     }
331     else
332     {
333         int nbytes_read = 0;
334 
335         //while (rc != TTY_TIME_OUT)
336         //{
337         int rc = tty_read_section(PortFD, res, SESTO_STOP_CHAR, 1, &nbytes_read);
338         if (rc == TTY_OK)
339         {
340             res[nbytes_read - 1] = 0;
341 
342 
343             if (!strcmp(res, "GTok!"))
344                 return true;
345 
346             try
347             {
348                 uint32_t newPos = std::stoi(res);
349                 FocusAbsPosN[0].value = newPos;
350             }
351             catch (...)
352             {
353                 LOGF_WARN("Failed to process motion response: %s (%d bytes)", res, strlen(res));
354             }
355         }
356         //}
357     }
358 
359     return false;
360 }
361 
ISNewSwitch(const char * dev,const char * name,ISState * states,char * names[],int n)362 bool SestoSenso::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
363 {
364     if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
365     {
366 
367         // Calibrate focuser
368         if (!strcmp(name, CalibrationSP.name))
369         {
370             char res[SESTO_LEN] = {0};
371             int current_switch = 0;
372 
373             CalibrationSP.s = IPS_BUSY;
374             //IDSetSwitch(&CalibrationSP, nullptr);
375             IUUpdateSwitch(&CalibrationSP, states, names, n);
376 
377             current_switch = IUFindOnSwitchIndex(&CalibrationSP);
378             CalibrationS[current_switch].s = ISS_ON;
379             IDSetSwitch(&CalibrationSP, nullptr);
380 
381             if (current_switch == CALIBRATION_START)
382             {
383                 if (cStage == Idle || cStage == Complete )
384                 {
385                     // Start the calibration process
386                     LOG_INFO("Start Calibration");
387                     CalibrationSP.s = IPS_BUSY;
388                     IDSetSwitch(&CalibrationSP, nullptr);
389 
390                     //
391                     // Unlock the motor to allow manual movement of the focuser
392                     //
393                     if (sendCommand("#MF!") == false)
394                         return false;
395 
396                     IUSaveText(&CalibrationMessageT[0], "Move focuser manually to the middle then press NEXT");
397                     IDSetText(&CalibrationMessageTP, nullptr);
398 
399                     // Set next step
400                     cStage = GoToMiddle;
401                 }
402                 else
403                 {
404                     LOG_INFO("Already started calibration. Proceed to next step.");
405                     IUSaveText(&CalibrationMessageT[0], "Already started. Proceed to NEXT.");
406                     IDSetText(&CalibrationMessageTP, nullptr);
407                 }
408             }
409             else if (current_switch == CALIBRATION_NEXT)
410             {
411                 if (cStage == GoToMiddle)
412                 {
413                     defineProperty(&FastMoveSP);
414                     IUSaveText(&CalibrationMessageT[0], "Move In/Move Out/Stop to MIN position then press NEXT");
415                     IDSetText(&CalibrationMessageTP, nullptr);
416                     cStage = GoMinimum;
417                 }
418                 else if (cStage == GoMinimum)
419                 {
420                     // Minimum position needs setting
421                     if (sendCommand("#Sm;0!") == false)
422                         return false;
423 
424                     IUSaveText(&CalibrationMessageT[0], "Move In/Move Out/Stop to MAX position then press NEXT");
425                     IDSetText(&CalibrationMessageTP, nullptr);
426                     cStage = GoMaximum;
427                 }
428                 else if (cStage == GoMaximum)
429                 {
430                     // Maximum position needs setting and save
431                     // Do not split these commands.
432 
433                     if (sendCommand("#SM!", res) == false)
434                         return false;
435                     if (sendCommand("#PS!") == false)
436                         return false;
437                     //
438                     // MAX value is in maxLimit
439                     // MIN value is 0
440                     //
441                     int maxLimit = 0;
442                     sscanf(res, "SM;%d!", &maxLimit);
443                     LOGF_INFO("MAX setting is %d", maxLimit);
444 
445                     FocusMaxPosN[0].max = maxLimit;
446                     FocusMaxPosN[0].value = maxLimit;
447 
448                     FocusAbsPosN[0].min   = 0;
449                     FocusAbsPosN[0].max   = maxLimit;
450                     FocusAbsPosN[0].value = maxLimit;
451                     FocusAbsPosN[0].step  = (FocusAbsPosN[0].max - FocusAbsPosN[0].min) / 50.0;
452 
453                     FocusRelPosN[0].min   = 0.;
454                     FocusRelPosN[0].max   = FocusAbsPosN[0].step * 10;
455                     FocusRelPosN[0].value = 0;
456                     FocusRelPosN[0].step  = FocusAbsPosN[0].step;
457 
458                     IUUpdateMinMax(&FocusAbsPosNP);
459                     IUUpdateMinMax(&FocusRelPosNP);
460                     FocusMaxPosNP.s = IPS_OK;
461                     IUUpdateMinMax(&FocusMaxPosNP);
462 
463                     IUSaveText(&CalibrationMessageT[0], "Calibration Completed.");
464                     IDSetText(&CalibrationMessageTP, nullptr);
465 
466                     deleteProperty(FastMoveSP.name);
467                     cStage = Complete;
468 
469                     LOG_INFO("Calibration completed");
470                     CalibrationSP.s = IPS_OK;
471                     IDSetSwitch(&CalibrationSP, nullptr);
472                     CalibrationS[current_switch].s = ISS_OFF;
473                     IDSetSwitch(&CalibrationSP, nullptr);
474                 }
475                 else
476                 {
477                     IUSaveText(&CalibrationMessageT[0], "Calibration not in process");
478                     IDSetText(&CalibrationMessageTP, nullptr);
479                 }
480 
481             }
482             return true;
483         }
484         else if (!strcmp(name, FastMoveSP.name))
485         {
486             IUUpdateSwitch(&FastMoveSP, states, names, n);
487             int current_switch = IUFindOnSwitchIndex(&FastMoveSP);
488 
489             switch (current_switch)
490             {
491                 case FASTMOVE_IN:
492                     if (sendCommand("#FI!") == false)
493                     {
494                         return false;
495                     }
496                     break;
497                 case FASTMOVE_OUT:
498                     if (sendCommand("#FO!") == false)
499                     {
500                         return false;
501                     }
502                     break;
503                 case FASTMOVE_STOP:
504                     if (sendCommand("#MA!") == false)
505                     {
506                         return false;
507                     }
508                     break;
509                 default:
510                     break;
511             }
512 
513             FastMoveSP.s = IPS_BUSY;
514             IDSetSwitch(&FastMoveSP, nullptr);
515             return true;
516         }
517 
518     }
519     return INDI::Focuser::ISNewSwitch(dev, name, states, names, n);
520 }
521 
MoveAbsFocuser(uint32_t targetTicks)522 IPState SestoSenso::MoveAbsFocuser(uint32_t targetTicks)
523 {
524     targetPos = targetTicks;
525 
526     char cmd[SESTO_LEN] = {0};
527     snprintf(cmd, 16, "#GT%u!", targetTicks);
528     if (isSimulation() == false)
529     {
530         if (sendCommand(cmd) == false)
531             return IPS_ALERT;
532     }
533 
534     m_MotionProgressTimer.start(10);
535     return IPS_BUSY;
536 }
537 
MoveRelFocuser(FocusDirection dir,uint32_t ticks)538 IPState SestoSenso::MoveRelFocuser(FocusDirection dir, uint32_t ticks)
539 {
540     int reversed = (IUFindOnSwitchIndex(&FocusReverseSP) == INDI_ENABLED) ? -1 : 1;
541     int relativeTicks =  ((dir == FOCUS_INWARD) ? -ticks : ticks) * reversed;
542     double newPosition = FocusAbsPosN[0].value + relativeTicks;
543 
544     bool rc = MoveAbsFocuser(newPosition);
545 
546     return (rc ? IPS_BUSY : IPS_ALERT);
547 }
548 
AbortFocuser()549 bool SestoSenso::AbortFocuser()
550 {
551     m_MotionProgressTimer.stop();
552 
553     if (isSimulation())
554         return true;
555 
556     return sendCommand("#MA!");
557 }
558 
559 //
560 // This timer function is initiated when a GT command has been issued
561 // A timer will call this function on a regular interval during the motion
562 // Modified the code to exit when motion is complete
563 //
checkMotionProgressCallback()564 void SestoSenso::checkMotionProgressCallback()
565 {
566     if (isMotionComplete())
567     {
568         FocusAbsPosNP.s = IPS_OK;
569         FocusRelPosNP.s = IPS_OK;
570         IDSetNumber(&FocusRelPosNP, nullptr);
571         IDSetNumber(&FocusAbsPosNP, nullptr);
572         lastPos = FocusAbsPosN[0].value;
573         LOG_INFO("Focuser reached requested position.");
574         return;
575     }
576     else
577         IDSetNumber(&FocusAbsPosNP, nullptr);
578 
579     lastPos = FocusAbsPosN[0].value;
580 
581     m_MotionProgressTimer.start(250);
582 }
583 
TimerHit()584 void SestoSenso::TimerHit()
585 {
586     if (!isConnected() || FocusAbsPosNP.s == IPS_BUSY || FocusRelPosNP.s == IPS_BUSY || CalibrationSP.s == IPS_BUSY)
587     {
588         SetTimer(getCurrentPollingPeriod());
589         return;
590     }
591 
592     bool rc = updatePosition();
593     if (rc)
594     {
595         if (fabs(lastPos - FocusAbsPosN[0].value) > 0)
596         {
597             IDSetNumber(&FocusAbsPosNP, nullptr);
598             lastPos = FocusAbsPosN[0].value;
599         }
600     }
601 
602     if (m_TemperatureCounter++ == SESTO_TEMPERATURE_FREQ)
603     {
604         rc = updateTemperature();
605         if (rc)
606         {
607             if (fabs(lastTemperature - TemperatureN[0].value) >= 0.1)
608             {
609                 IDSetNumber(&TemperatureNP, nullptr);
610                 lastTemperature = TemperatureN[0].value;
611             }
612         }
613         m_TemperatureCounter = 0;   // Reset the counter
614     }
615 
616     SetTimer(getCurrentPollingPeriod());
617 }
618 
getStartupValues()619 bool SestoSenso::getStartupValues()
620 {
621     bool rc1 = updatePosition();
622     if (rc1)
623         IDSetNumber(&FocusAbsPosNP, nullptr);
624 
625     if (updateMaxLimit() == false)
626         LOG_WARN("Check you have the latest SestoSenso firmware. Focuser requires calibration.");
627 
628     return (rc1);
629 }
630 
sendCommand(const char * cmd,char * res,int cmd_len,int res_len)631 bool SestoSenso::sendCommand(const char * cmd, char * res, int cmd_len, int res_len)
632 {
633     int nbytes_written = 0, nbytes_read = 0, rc = -1;
634 
635     tcflush(PortFD, TCIOFLUSH);
636 
637     if (cmd_len > 0)
638     {
639         char hex_cmd[SESTO_LEN * 3] = {0};
640         hexDump(hex_cmd, cmd, cmd_len);
641         LOGF_DEBUG("CMD <%s>", hex_cmd);
642         rc = tty_write(PortFD, cmd, cmd_len, &nbytes_written);
643     }
644     else
645     {
646         LOGF_DEBUG("CMD <%s>", cmd);
647         rc = tty_write_string(PortFD, cmd, &nbytes_written);
648     }
649 
650     if (rc != TTY_OK)
651     {
652         char errstr[MAXRBUF] = {0};
653         tty_error_msg(rc, errstr, MAXRBUF);
654         LOGF_ERROR("Serial write error: %s.", errstr);
655         return false;
656     }
657 
658     if (res == nullptr)
659         return true;
660 
661     if (res_len > 0)
662         rc = tty_read(PortFD, res, res_len, SESTO_TIMEOUT, &nbytes_read);
663     else
664     {
665         rc = tty_nread_section(PortFD, res, SESTO_LEN, SESTO_STOP_CHAR, SESTO_TIMEOUT, &nbytes_read);
666         res[nbytes_read - 1] = 0;
667     }
668 
669     if (rc != TTY_OK)
670     {
671         char errstr[MAXRBUF] = {0};
672         tty_error_msg(rc, errstr, MAXRBUF);
673         LOGF_ERROR("Serial read error: %s.", errstr);
674         return false;
675     }
676 
677     if (res_len > 0)
678     {
679         char hex_res[SESTO_LEN * 3] = {0};
680         hexDump(hex_res, res, res_len);
681         LOGF_DEBUG("RES <%s>", hex_res);
682     }
683     else
684     {
685         LOGF_DEBUG("RES <%s>", res);
686     }
687 
688     tcflush(PortFD, TCIOFLUSH);
689 
690     return true;
691 }
692 
hexDump(char * buf,const char * data,int size)693 void SestoSenso::hexDump(char * buf, const char * data, int size)
694 {
695     for (int i = 0; i < size; i++)
696         sprintf(buf + 3 * i, "%02X ", static_cast<uint8_t>(data[i]));
697 
698     if (size > 0)
699         buf[3 * size - 1] = '\0';
700 }
701 
ReverseFocuser(bool enable)702 bool SestoSenso::ReverseFocuser(bool enable)
703 {
704     INDI_UNUSED(enable);
705     return false;
706 }
707