Instrument Neutral Distributed Interface INDI  2.0.2
celestrondriver.cpp
Go to the documentation of this file.
1 /*
2  Celestron driver
3 
4  Copyright (C) 2015 Jasem Mutlaq
5  Copyright (C) 2017 Juan Menendez
6 
7  This library is free software; you can redistribute it and/or
8  modify it under the terms of the GNU Lesser General Public
9  License as published by the Free Software Foundation; either
10  version 2.1 of the License, or (at your option) any later version.
11 
12  This library is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15  Lesser General Public License for more details.
16 
17  You should have received a copy of the GNU Lesser General Public
18  License along with this library; if not, write to the Free Software
19  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20 */
21 
22 /*
23  Version with experimental pulse guide support. GC 04.12.2015
24 */
25 
26 #include "indicom.h"
27 #include "indilogger.h"
28 #include "celestrondriver.h"
29 
30 #include <libnova/julian_day.h>
31 
32 #include <map>
33 #include <cstring>
34 #include <cmath>
35 #include <termios.h>
36 #include <unistd.h>
37 
38 #define CELESTRON_TIMEOUT 5 /* FD timeout in seconds */
39 
40 using namespace Celestron;
41 
42 static char device_str[MAXINDIDEVICE] = "Celestron GPS";
43 
44 // Account for the quadrant in declination
45 double Celestron::trimDecAngle(double angle)
46 {
47  angle = angle - 360 * floor(angle / 360);
48  if (angle < 0)
49  angle += 360.0;
50 
51  if ((angle > 90.) && (angle <= 270.))
52  angle = 180. - angle;
53  else if ((angle > 270.) && (angle <= 360.))
54  angle = angle - 360.;
55 
56  return angle;
57 }
58 
59 // Convert decimal degrees to NexStar angle
60 uint16_t Celestron::dd2nex(double angle)
61 {
62  angle = angle - 360 * floor(angle / 360);
63  if (angle < 0)
64  angle += 360.0;
65 
66  return static_cast<uint16_t>(angle * 0x10000 / 360.0);
67 }
68 
69 // Convert decimal degrees to NexStar angle (precise)
70 uint32_t Celestron::dd2pnex(double angle)
71 {
72  angle = angle - 360 * floor(angle / 360);
73  if (angle < 0)
74  angle += 360.0;
75 
76  return static_cast<uint32_t>(angle * 0x100000000 / 360.0);
77 }
78 
79 // Convert NexStar angle to decimal degrees
80 double Celestron::nex2dd(uint32_t value)
81 {
82  return 360.0 * (static_cast<double>(value) / 0x10000);
83 }
84 
85 // Convert NexStar angle to decimal degrees (precise)
86 double Celestron::pnex2dd(uint32_t value)
87 {
88  return 360.0 * (static_cast<double>(value) / 0x100000000);
89 }
90 
91 void hex_dump(char *buf, const char *data, size_t size)
92 {
93  for (size_t i = 0; i < size; i++)
94  sprintf(buf + 3 * i, "%02X ", data[i]);
95 
96  if (size > 0)
97  buf[3 * size - 1] = '\0';
98 }
99 
100 // This method is required by the logging macros
102 {
103  return device_str;
104 }
105 
106 void CelestronDriver::set_device(const char *name)
107 {
108  strncpy(device_str, name, MAXINDIDEVICE);
109 }
110 
111 // Virtual method for testing
112 int CelestronDriver::serial_write(const char *cmd, int nbytes, int *nbytes_written)
113 {
114  tcflush(fd, TCIOFLUSH);
115  return tty_write(fd, cmd, nbytes, nbytes_written);
116 }
117 
118 // Virtual method for testing
119 int CelestronDriver::serial_read(int nbytes, int *nbytes_read)
120 {
121  return tty_read(fd, response, nbytes, CELESTRON_TIMEOUT, nbytes_read);
122 }
123 
124 // Virtual method for testing
125 int CelestronDriver::serial_read_section(char stop_char, int *nbytes_read)
126 {
127  return tty_nread_section(fd, response, MAX_RESP_SIZE, stop_char, CELESTRON_TIMEOUT, nbytes_read);
128 }
129 
130 // Set the expected response for a command in simulation mode
131 __attribute__((__format__ (__printf__, 2, 0)))
132 void CelestronDriver::set_sim_response(const char *fmt, ...)
133 {
134  if (simulation)
135  {
136  va_list args;
137  va_start(args, fmt);
138  vsprintf(response, fmt, args);
139  va_end(args);
140  }
141 }
142 
143 //static std::mutex tty_lock;
144 
145 // Send a command to the mount. Return the number of bytes received or 0 if
146 // case of error
147 size_t CelestronDriver::send_command(const char *cmd, size_t cmd_len, char *resp,
148  size_t resp_len, bool ascii_cmd, bool ascii_resp)
149 {
150  int err;
151  size_t nbytes = resp_len;
152  char errmsg[MAXRBUF];
153  char hexbuf[3 * MAX_RESP_SIZE];
154 
155  if (ascii_cmd)
156  LOGF_DEBUG("CMD <%s>", cmd);
157  else
158  {
159  // Non-ASCII commands should be represented as hex strings
160  hex_dump(hexbuf, cmd, cmd_len);
161  LOGF_DEBUG("CMD <%s>", hexbuf);
162  }
163 
164  if (!simulation && fd)
165  {
166  // lock the serial command, unlocks when this goes out of scope
167  //std::lock_guard<std::mutex> lockGuard(tty_lock);
168  if ((err = serial_write(cmd, static_cast<int>(cmd_len), reinterpret_cast<int *>(&nbytes))) != TTY_OK)
169  {
170  tty_error_msg(err, errmsg, MAXRBUF);
171  LOGF_ERROR("Serial write error: %s", errmsg);
172  return 0;
173  }
174 
175  if (resp_len > 0)
176  {
177  if (ascii_resp)
178  err = serial_read_section('#', reinterpret_cast<int *>(&nbytes));
179  else
180  {
181  err = serial_read(static_cast<int>(resp_len), reinterpret_cast<int *>(&nbytes));
182  // passthrough commands that fail will return an extra 0 then the terminator
183  while (err == TTY_OK && resp[nbytes - 1] != '#')
184  {
185  char m[1];
186  int n;
187  err = tty_read(fd, m, 1, CELESTRON_TIMEOUT, &n);
188  if (n == 1)
189  {
190  resp[nbytes] = m[0];
191  nbytes++;
192  }
193  }
194  }
195  if (err)
196  {
197  tty_error_msg(err, errmsg, MAXRBUF);
198  LOGF_ERROR("Serial read error: %s", errmsg);
199  return 0;
200  }
201  }
202  }
203 
204  if (nbytes != resp_len)
205  {
206  size_t max = nbytes > resp_len ? nbytes : resp_len;
207  hex_dump(hexbuf, resp, max);
208  LOGF_DEBUG("Received %d bytes, expected %d <%s>", nbytes, resp_len, hexbuf);
209  return max;
210  }
211 
212  if (resp_len == 0)
213  {
214  LOG_DEBUG("resp_len 0, no response expected");
215  return true;
216  }
217 
218  resp[nbytes] = '\0';
219 
220  if (ascii_resp)
221  LOGF_DEBUG("RES <%s>", resp);
222  else
223  {
224  // Non-ASCII commands should be represented as hex strings
225  hex_dump(hexbuf, resp, resp_len);
226  LOGF_DEBUG("RES <%s>", hexbuf);
227  }
228 
229  return nbytes;
230 }
231 
232 // Send a 'passthrough command' to the mount. Return the number of bytes
233 // received or 0 in case of error
234 size_t CelestronDriver::send_passthrough(int dest, int cmd_id, const char *payload,
235  size_t payload_len, char *resp, size_t response_len)
236 {
237  char cmd[8] = {0};
238 
239  cmd[0] = 0x50;
240  cmd[1] = static_cast<char>(payload_len + 1);
241  cmd[2] = static_cast<char>(dest);
242  cmd[3] = static_cast<char>(cmd_id);
243  cmd[7] = static_cast<char>(response_len);
244 
245  // payload_len must be <= 3 !
246  memcpy(cmd + 4, payload, payload_len);
247 
248  return send_command(cmd, 8, resp, response_len + 1, false, false);
249 }
250 
252 {
253  LOG_DEBUG("Initializing Celestron using Kx CMD...");
254 
255  for (int i = 0; i < 2; i++)
256  {
257  if (echo())
258  return true;
259 
260  usleep(50000);
261  }
262 
263  return false;
264 }
265 
267 {
268  char version[8], model[16], RAVersion[8], DEVersion[8];
269  bool isGem;
270  bool canPec;
271  bool hasHomeIndex;
272 
273  LOG_DEBUG("Getting controller version...");
274  if (!get_version(version, 8))
275  return false;
276  info->Version = version;
277  info->controllerVersion = atof(version);
278 
279  LOG_DEBUG("Getting controller variant...");
281  // variant is only available for NexStar + versions 5.28 or more and Starsense.
282  // StarSense versions are currently 1.9 so overlap the early NexStar versions.
283  // NS HCs before 2.0 will test and timeout
284  if (info->controllerVersion < 2.2 || info->controllerVersion >= 5.28)
285  {
286  get_variant(&(info->controllerVariant));
287  }
288 
289  if (((info->controllerVariant == ISSTARSENSE) &&
290  info->controllerVersion >= MINSTSENSVER) ||
291  (info->controllerVersion >= 2.2))
292  {
293  LOG_DEBUG("Getting controller model...");
294  if (!get_model(model, 16, &isGem, &canPec, &hasHomeIndex))
295  return false;
296  info->Model = model;
297  info->isGem = isGem;
298  info->canPec = canPec;
299  info->hasHomeIndex = hasHomeIndex;
300  }
301  else
302  {
303  info->Model = "Unknown";
304  info->isGem = false;
305  info->canPec = false;
306  info->hasHomeIndex = false;
307  }
308 
309  //LOG_DEBUG("Getting GPS firmware version...");
310  // char GPSVersion[8];
311  //if (!get_dev_firmware(CELESTRON_DEV_GPS, GPSVersion, 8))
312  //return false;
313  //info->GPSFirmware = GPSVersion;
314  //info->GPSFirmware = "0.0";
315 
316  LOG_DEBUG("Getting RA firmware version...");
317  if (!get_dev_firmware(CELESTRON_DEV_RA, RAVersion, 8))
318  return false;
319  info->RAFirmware = RAVersion;
320 
321  LOG_DEBUG("Getting DEC firmware version...");
322  if (!get_dev_firmware(CELESTRON_DEV_DEC, DEVersion, 8))
323  return false;
324  info->DEFirmware = DEVersion;
325 
326 
327  LOG_DEBUG("Getting focuser version...");
328  info->hasFocuser = foc_exists();
329 
330  LOGF_DEBUG("Firmware Info HC Ver %s model %s %s %s mount, HW Ver %s",
331  info->Version.c_str(),
332  info->Model.c_str(),
333  info->controllerVariant == ISSTARSENSE ? "StarSense" : "NexStar",
334  info->isGem ? "GEM" : "Fork",
335  info->RAFirmware.c_str());
336 
337  return true;
338 }
339 
341 {
342  set_sim_response("x#");
343 
344  if (!send_command("Kx", 2, response, 2, true, true))
345  return false;
346 
347  return !strcmp(response, "x#");
348 }
349 
350 bool CelestronDriver::get_version(char *version, size_t size)
351 {
352  set_sim_response("\x04\x29#");
353 
354  if (!send_command("V", 1, response, 3, true, false))
355  return false;
356 
357  snprintf(version, size, "%d.%02d", static_cast<uint8_t>(response[0]), static_cast<uint8_t>(response[1]));
358 
359  LOGF_INFO("Controller version: %s", version);
360  return true;
361 }
362 
363 //TODO: no critical errors for this command
364 bool CelestronDriver::get_variant(char *variant)
365 {
366  set_sim_response("\x11#");
367 
368  if (!send_command("v", 1, response, 2, true, false))
369  return false;
370 
371  *variant = static_cast<uint8_t>(response[0]);
372  return true;
373 }
374 
376 {
377  set_sim_response("%c#", 20); // AVX
378  if (!send_command("m", 1, response, 2, true, false))
379  return -1;
380 
381  return static_cast<uint8_t>(response[0]);
382 }
383 
384 bool CelestronDriver::get_model(char *model, size_t size, bool *isGem, bool *canPec, bool *hasHomeIndex)
385 {
386  // extended list of mounts
387  std::map<int, std::string> models =
388  {
389  {1, "GPS Series"},
390  {3, "i-Series"},
391  {4, "i-Series SE"},
392  {5, "CGE"},
393  {6, "Advanced GT"},
394  {7, "SLT"},
395  {9, "CPC"},
396  {10, "GT"},
397  {11, "4/5 SE"},
398  {12, "6/8 SE"},
399  {13, "CGE Pro"},
400  {14, "CGEM DX"},
401  {15, "LCM"},
402  {16, "Sky Prodigy"},
403  {17, "CPC Deluxe"},
404  {18, "GT 16"},
405  {19, "StarSeeker"},
406  {20, "AVX"},
407  {21, "Cosmos"},
408  {22, "Evolution"},
409  {23, "CGX"},
410  {24, "CGXL"},
411  {25, "Astrofi"},
412  {26, "SkyWatcher"},
413  };
414 
415  set_sim_response("\x14#"); // Simulated response, AVX
416 
417 
418  int m = CelestronDriver::model();
419  if (m < 0)
420  return false;
421 
422  if (models.find(m) != models.end())
423  {
424  strncpy(model, models[m].c_str(), size);
425  LOGF_INFO("Mount model: %s", model);
426  }
427  else
428  {
429  strncpy(model, "Unknown", size);
430  LOGF_WARN("Unrecognized model (%d).", model);
431  }
432 
433  // use model# to detect the GEMs amd if PEC can be done
434  // Only Gem mounts can report the pier side pointing state
435  switch(m)
436  {
437  // fork mounts with PEC index
438  case 1: // GPS
439  case 9: // CPC
440  case 17: // CPC Deluxe
441  case 22: // Evolution
442  *isGem = false;
443  *canPec = true;
444  break;
445 
446  // GEM with no PEC index
447  case 6: // AS-GT
448  *isGem = true;
449  *canPec = false;
450  break;
451 
452  // GEM with PEC
453  case 5: // CGE
454  case 13: // CGE 2
455  case 14: // EQ6
456  case 20: // AVX
457  case 23: // CGX
458  case 24: // CGXL
459  *isGem = true;
460  *canPec = true;
461  break;
462 
463  // the rest are fork mounte with no PEC
464  default:
465  *isGem = false;
466  *canPec = false;
467  break;
468  }
469 
470  // CGX and CGX-L mounts have home indexes
471  *hasHomeIndex = (m == 23 || m == 24);
472 
473  LOGF_DEBUG("get_model %s, %s mount, %s, %s", model, *isGem ? "GEM" : "Fork", *canPec ? "has PEC" : "no PEC",
474  *hasHomeIndex ? "has home indexes" : "no home indexes");
475 
476  return true;
477 }
478 
479 bool CelestronDriver::get_dev_firmware(int dev, char *version, size_t size)
480 {
481  set_sim_response("\x06\x10#");
482 
483  size_t rlen = send_passthrough(dev, GET_VER, nullptr, 0, response, 2);
484 
485  switch (rlen)
486  {
487  case 2:
488  snprintf(version, size, "%01d.0", static_cast<uint8_t>(response[0]));
489  break;
490  case 3:
491  snprintf(version, size, "%d.%02d", static_cast<uint8_t>(response[0]), static_cast<uint8_t>(response[1]));
492  break;
493  default:
494  return false;
495  }
496 
497  return true;
498 }
499 
500 /*****************************************************************
501  PulseGuide commands
502 ******************************************************************/
503 
504 /*****************************************************************
505  Send a guiding pulse to the mount in direction "dir".
506  "rate" should be an unsigned 8-bit integer in the range (0,100) that
507  represents the pulse velocity in % of sidereal.
508  "duration_csec" is an unsigned 8-bit integer (0,255) with the pulse
509  duration in centiseconds (i.e. 1/100 s = 10ms).
510  The max pulse duration is 2550 ms.
511 *******************************************)()***********************/
512 size_t CelestronDriver::send_pulse(CELESTRON_DIRECTION dir, unsigned char rate, unsigned char duration_csec)
513 {
514  char payload[2];
515  int dev = CELESTRON_DEV_RA;
516  switch (dir)
517  {
518  case CELESTRON_N:
519  dev = CELESTRON_DEV_DEC;
520  payload[0] = rate;
521  break;
522  case CELESTRON_S:
523  dev = CELESTRON_DEV_DEC;
524  payload[0] = -rate;
525  break;
526  case CELESTRON_W:
527  dev = CELESTRON_DEV_RA;
528  payload[0] = rate;
529  break;
530  case CELESTRON_E:
531  dev = CELESTRON_DEV_RA;
532  payload[0] = -rate;
533  break;
534  }
535  payload[1] = duration_csec;
536 
537  set_sim_response("#");
538  return send_passthrough(dev, MTR_AUX_GUIDE, payload, 2, response, 0);
539 }
540 
541 /*****************************************************************
542  Send the guiding pulse status check command to the mount for the motor
543  responsible for "dir". If a pulse is being executed, returns true,
544  otherwise false.
545  If the getting the status fails returns false.
546 ******************************************************************/
548 {
549  int dev = CELESTRON_DEV_RA;
550  //char payload[2] = {0, 0};
551  switch (dir)
552  {
553  case CELESTRON_N:
554  case CELESTRON_S:
555  dev = CELESTRON_DEV_DEC;
556  break;
557  case CELESTRON_W:
558  case CELESTRON_E:
559  dev = CELESTRON_DEV_RA;
560  break;
561  }
562  set_sim_response("%c#", 0);
563 
564  if (!send_passthrough(dev, MTR_IS_AUX_GUIDE_ACTIVE, nullptr, 0, response, 1))
565  return false;
566 
567  return static_cast<bool>(response[0]);
568 }
569 
570 /*****************************************************************
571  Get the guide rate from the mount for the axis.
572  rate is 0 to 255 representing 0 to 100% sidereal
573  If getting the rate fails returns false.
574 ******************************************************************/
576 {
577  int dev = CELESTRON_DEV_DEC;
578  switch (axis)
579  {
581  dev = CELESTRON_DEV_RA;
582  set_sim_response("%c#", sim_ra_guide_rate);
583  break;
585  dev = CELESTRON_DEV_DEC;
586  set_sim_response("%c#", sim_dec_guide_rate);
587  break;
588  }
589  if (!send_passthrough(dev, MC_GET_AUTOGUIDE_RATE, nullptr, 0, response, 1))
590  return false;
591  *rate = static_cast<uint8_t>(response[0]);
592  LOGF_DEBUG("get_guide_rate raw response (0-255) %i", *rate);
593  return true;
594 }
595 
596 /*****************************************************************
597  Set the guide rate for the axis.
598  rate is 0 to 255 representing 0 to 100% sidereal
599  If setting the rate fails returns false.
600 ******************************************************************/
602 {
603  int dev = CELESTRON_DEV_DEC;
604  switch (axis)
605  {
607  dev = CELESTRON_DEV_RA;
608  sim_ra_guide_rate = rate;
609  break;
611  dev = CELESTRON_DEV_DEC;
612  sim_dec_guide_rate = rate;
613  break;
614  }
615  char payload[1];
616  payload[0] = rate;
617  set_sim_response("#");
618  return send_passthrough(dev, MC_SET_AUTOGUIDE_RATE, payload, 1, response, 0);
619 }
620 
622 {
623  int dev = (dir == CELESTRON_N || dir == CELESTRON_S) ? CELESTRON_DEV_DEC : CELESTRON_DEV_RA;
624  int cmd_id = (dir == CELESTRON_N || dir == CELESTRON_W) ? MC_MOVE_POS : MC_MOVE_NEG;
625  char payload[1];
626  payload[0] = rate + 1;
627 
628  set_sim_response("#");
629  return send_passthrough(dev, cmd_id, payload, 1, response, 0);
630 }
631 
633 {
634  int dev = (dir == CELESTRON_N || dir == CELESTRON_S) ? CELESTRON_DEV_DEC : CELESTRON_DEV_RA;
635  char payload[] = { 0 };
636 
637  set_sim_response("#");
638  return send_passthrough(dev, MC_MOVE_POS, payload, 1, response, 0);
639 }
640 
642 {
643  set_sim_response("#");
644  return send_command("M", 1, response, 1, true, true);
645 }
646 
647 bool CelestronDriver::slew_radec(double ra, double dec, bool precise)
648 {
649  char RAStr[16], DecStr[16];
650  fs_sexa(RAStr, ra, 2, 3600);
651  fs_sexa(DecStr, dec, 2, 3600);
652 
653  LOGF_DEBUG("Goto RA-DEC(%s,%s)", RAStr, DecStr);
654 
655  set_sim_slewing(true);
656 
657  char cmd[20];
658  if (precise)
659  sprintf(cmd, "r%08X,%08X", dd2pnex(ra * 15), dd2pnex(dec));
660  else
661  sprintf(cmd, "R%04X,%04X", dd2nex(ra * 15), dd2nex(dec));
662 
663  set_sim_response("#");
664  return send_command(cmd, strlen(cmd), response, 1, true, true);
665 }
666 
667 bool CelestronDriver::slew_azalt(double az, double alt, bool precise)
668 {
669  char AzStr[16], AltStr[16];
670  fs_sexa(AzStr, az, 3, 3600);
671  fs_sexa(AltStr, alt, 2, 3600);
672 
673  LOGF_DEBUG("Goto AZM-ALT (%s,%s)", AzStr, AltStr);
674 
675  set_sim_slewing(true);
676 
677  char cmd[20];
678  if (precise)
679  sprintf(cmd, "b%08X,%08X", dd2pnex(az), dd2pnex(alt));
680  else
681  sprintf(cmd, "B%04X,%04X", dd2nex(az), dd2nex(alt));
682 
683  set_sim_response("#");
684  return send_command(cmd, strlen(cmd), response, 1, true, true);
685 }
686 
687 bool CelestronDriver::sync(double ra, double dec, bool precise)
688 {
689  char RAStr[16], DecStr[16];
690  fs_sexa(RAStr, ra, 2, 3600);
691  fs_sexa(DecStr, dec, 2, 3600);
692 
693  LOGF_DEBUG("Sync (%s,%s)", RAStr, DecStr);
694 
695  sim_data.ra = ra;
696  sim_data.dec = dec;
697 
698  char cmd[20];
699  if (precise)
700  sprintf(cmd, "s%08X,%08X", dd2pnex(ra * 15), dd2pnex(dec));
701  else
702  sprintf(cmd, "S%04X,%04X", dd2nex(ra * 15), dd2nex(dec));
703 
704  set_sim_response("#");
705  return send_command(cmd, strlen(cmd), response, 1, true, true);
706 }
707 
708 // NS+ 5.28 and more only, not StarSense
710 {
711  LOG_DEBUG("Unsync");
712  set_sim_response("#");
713  return send_command("u", 1, response, 1, true, true);
714 }
715 
716 void parseCoordsResponse(char *response, double *d1, double *d2, bool precise)
717 {
718  uint32_t d1_int = 0, d2_int = 0;
719 
720  sscanf(response, "%x,%x#", &d1_int, &d2_int);
721 
722  if (precise)
723  {
724  *d1 = pnex2dd(d1_int);
725  *d2 = pnex2dd(d2_int);
726  }
727  else
728  {
729  *d1 = nex2dd(d1_int);
730  *d2 = nex2dd(d2_int);
731  }
732 }
733 
734 bool CelestronDriver::get_radec(double *ra, double *dec, bool precise)
735 {
736  if (precise)
737  {
738  set_sim_response("%08X,%08X#", dd2pnex(sim_data.ra * 15), dd2pnex(sim_data.dec));
739 
740  if (!send_command("e", 1, response, 18, true, true))
741  return false;
742  }
743  else
744  {
745  set_sim_response("%04X,%04X#", dd2nex(sim_data.ra * 15), dd2nex(sim_data.dec));
746 
747  if (!send_command("E", 1, response, 10, true, true))
748  return false;
749  }
750 
751  parseCoordsResponse(response, ra, dec, precise);
752  *ra /= 15.0;
753  *dec = trimDecAngle(*dec);
754 
755  char RAStr[16], DecStr[16];
756  fs_sexa(RAStr, *ra, 2, 3600);
757  fs_sexa(DecStr, *dec, 2, 3600);
758 
759  LOGF_EXTRA1("RA-DEC (%s,%s)", RAStr, DecStr);
760  return true;
761 }
762 
763 bool CelestronDriver::get_azalt(double *az, double *alt, bool precise)
764 {
765  if (precise)
766  {
767  set_sim_response("%08X,%08X#", dd2pnex(sim_data.az), dd2pnex(sim_data.alt));
768 
769  if (!send_command("z", 1, response, 18, true, true))
770  return false;
771  }
772  else
773  {
774  set_sim_response("%04X,%04X#", dd2nex(sim_data.az), dd2nex(sim_data.alt));
775 
776  if (!send_command("Z", 1, response, 10, true, true))
777  return false;
778  }
779 
780  parseCoordsResponse(response, az, alt, precise);
781 
782  char AzStr[16], AltStr[16];
783  fs_sexa(AzStr, *az, 3, 3600);
784  fs_sexa(AltStr, *alt, 2, 3600);
785  LOGF_EXTRA1("RES <%s> ==> AZM-ALT (%s,%s)", response, AzStr, AltStr);
786  return true;
787 }
788 
789 bool CelestronDriver::set_location(double longitude, double latitude)
790 {
791  LOGF_DEBUG("Setting location (%.3f,%.3f)", longitude, latitude);
792 
793  // Convert from INDI standard to regular east/west -180 to 180
794  if (longitude > 180)
795  longitude -= 360;
796 
797  int lat_d, lat_m, lat_s;
798  int long_d, long_m, long_s;
799  getSexComponents(latitude, &lat_d, &lat_m, &lat_s);
800  getSexComponents(longitude, &long_d, &long_m, &long_s);
801 
802  char cmd[9];
803  cmd[0] = 'W';
804  cmd[1] = static_cast<char>(abs(lat_d));
805  cmd[2] = static_cast<char>(lat_m);
806  cmd[3] = static_cast<char>(lat_s);
807  cmd[4] = lat_d > 0 ? 0 : 1;
808  cmd[5] = static_cast<char>(abs(
809  long_d)); // not sure how the conversion from int to char will work for longtitudes > 127
810  cmd[6] = static_cast<char>(long_m);
811  cmd[7] = static_cast<char>(long_s);
812  cmd[8] = longitude > 0 ? 0 : 1; //Error fixed here (was cmd[8] = long_d >= 0 ? 0 : 1;) which was wrong for < 1 degree East.
813 
814  set_sim_response("#");
815  return send_command(cmd, 9, response, 1, false, true);
816 }
817 
818 bool CelestronDriver::get_location(double *longitude, double *latitude)
819 {
820  // Simulated response (lat_d lat_m lat_s N|S long_d long_m long_s E|W)
821  set_sim_response("%c%c%c%c%c%c%c%c#", 51, 36, 17, 0, 0, 43, 3, 1);
822 
823  if (!send_command("w", 1, response, 9, true, false))
824  return false;
825 
826  *latitude = response[0];
827  *latitude += response[1] / 60.0;
828  *latitude += response[2] / 3600.0;
829  if (response[3] != 0)
830  *latitude = -*latitude;
831 
832  *longitude = response[4];
833  *longitude += response[5] / 60.0;
834  *longitude += response[6] / 3600.0;
835  if(response[7] != 0)
836  *longitude = -*longitude;
837 
838  // convert longitude to INDI range 0 to 359.999
839  if (*longitude < 0)
840  *longitude += 360.0;
841 
842  return true;
843 }
844 
845 // there are newer time commands that have the utc offset in 15 minute increments
846 bool CelestronDriver::set_datetime(struct ln_date *utc, double utc_offset, bool dst, bool precise)
847 {
848  struct ln_zonedate local_date;
849 
850  // Celestron takes local time and DST but ln_zonedate doesn't have DST
851  ln_date_to_zonedate(utc, &local_date, static_cast<int>(utc_offset * 3600));
852 
853  char cmd[9];
854  cmd[0] = 'H';
855  cmd[1] = static_cast<char>(local_date.hours);
856  cmd[2] = static_cast<char>(local_date.minutes);
857  cmd[3] = static_cast<char>(local_date.seconds);
858  cmd[4] = static_cast<char>(local_date.months);
859  cmd[5] = static_cast<char>(local_date.days);
860  cmd[6] = static_cast<char>(local_date.years - 2000);
861 
862  int utc_int = static_cast<int>(utc_offset);
863 
864  // changes for HC versions that support the high precision time zone
865  if(precise)
866  {
867  cmd[0] = 'I';
868  utc_int *= 4;
869  }
870 
871  cmd[7] = static_cast<char>(utc_int & 0xFF);
872 
873  // set dst
874  cmd[8] = dst ? 1 : 0;
875 
876  set_sim_response("#");
877  return send_command(cmd, 9, response, 1, false, true);
878 }
879 
880 bool CelestronDriver::get_utc_date_time(double *utc_hours, int *yy, int *mm,
881  int *dd, int *hh, int *minute, int *ss, bool* dst, bool precise)
882 {
883  // Simulated response (HH MM SS MONTH DAY YEAR OFFSET DAYLIGHT)
884  // 2015-04-01T17:30:10 tz +3 dst 0
885  //set_sim_response("%c%c%c%c%c%c%c%c#", 17, 30, 10, 4, 1, 15, 3, 0);
886  // use current system time for simulator
887  time_t now = time(nullptr);
888  tm *ltm = localtime(&now);
889 
890  set_sim_response("%c%c%c%c%c%c%c%c#",
891  ltm->tm_hour, ltm->tm_min, ltm->tm_sec,
892  ltm->tm_mon, ltm->tm_mday, ltm->tm_year - 100,
893  precise ? ltm->tm_gmtoff / 900 : ltm->tm_gmtoff / 3600, ltm->tm_isdst);
894 
895  // the precise time reader reports the time zone in 15 minute steps
896 
897  // read the local time from the HC
898  if (!send_command(precise ? "i" : "h", 1, response, 9, true, false))
899  return false;
900 
901  // Celestron returns local time, offset and DST
902  ln_zonedate localTime;
903  ln_date utcTime;
904 
905  // HH MM SS MONTH DAY YEAR OFFSET DAYLIGHT
906  localTime.hours = response[0];
907  localTime.minutes = response[1];
908  localTime.seconds = response[2];
909  localTime.months = response[3];
910  localTime.days = response[4];
911  localTime.years = 2000 + response[5];
912  int gmtoff = response[6];
913  *dst = response[7] != 0;
914 
915  // make gmtoff signed
916  if (gmtoff > 50)
917  gmtoff -= 256;
918 
919  // precise returns offset in 15 minute steps
920  if (precise)
921  {
922  *utc_hours = gmtoff / 4;
923  localTime.gmtoff = gmtoff * 900;
924  }
925  else
926  {
927  *utc_hours = gmtoff;
928  localTime.gmtoff = gmtoff * 3600;
929  }
930 
931  if (*dst)
932  {
933  *utc_hours += 1;
934  localTime.gmtoff += 3600;
935  }
936 
937  // LOGF_DEBUG("LT %d-%d-%dT%d:%d:%f, gmtoff %d, dst %s",
938  // localTime.years, localTime.months, localTime.days, localTime.hours, localTime.minutes, localTime.seconds,
939  // localTime.gmtoff, *dst ? "On" : "Off");
940 
941  // convert to UTC
942  ln_zonedate_to_date(&localTime, &utcTime);
943 
944  *yy = utcTime.years;
945  *mm = utcTime.months;
946  *dd = utcTime.days;
947  *hh = utcTime.hours;
948  *minute = utcTime.minutes;
949  *ss = static_cast<int>(utcTime.seconds);
950 
951  // LOGF_DEBUG("UTC %d-%d-%dT%d:%d:%d utc_hours %f", *yy, *mm, *dd, *hh, *minute, *ss, *utc_hours);
952 
953  return true;
954 }
955 
956 bool CelestronDriver::is_slewing(bool *slewing)
957 {
958  set_sim_response("%d#", sim_data.isSlewing);
959 
960  if (!send_command("L", 1, response, 2, true, true))
961  return false;
962 
963  *slewing = response[0] != '0';
964  return true;
965 }
966 
968 {
969  set_sim_response("\02#");
970 
971  if (!send_command("t", 1, response, 2, true, false))
972  return false;
973 
974  *mode = static_cast<CELESTRON_TRACK_MODE>(response[0]);
975  return true;
976 }
977 
979 {
980  char cmd[3];
981  sprintf(cmd, "T%c", mode);
982  set_sim_response("#");
983 
984  return send_command(cmd, 2, response, 1, false, true);
985 }
986 
988 {
989  set_sim_response("#");
990  return send_command("x", 1, response, 1, true, true);
991 }
992 
993 // wakeup the mount
995 {
996  set_sim_response("#");
997  return send_command("y", 1, response, 1, true, true);
998 }
999 
1000 // do a last align, assumes the mount is at the index position
1002 {
1003  set_sim_response("#");
1004  return send_command("Y", 1, response, 1, true, true);
1005 }
1006 
1008 {
1009  if (!send_passthrough(CELESTRON_DEV_RA, MC_LEVEL_START, nullptr, 0, response, 0))
1010  return false;
1011  return send_passthrough(CELESTRON_DEV_DEC, MC_LEVEL_START, nullptr, 0, response, 0);
1012 }
1013 
1015 {
1016  if (!send_passthrough(CELESTRON_DEV_DEC, MC_LEVEL_DONE, nullptr, 0, response, 1))
1017  return false;
1018  bool atDecIndex = response[0] != '\00';
1019  if (!send_passthrough(CELESTRON_DEV_RA, MC_LEVEL_DONE, nullptr, 0, response, 1))
1020  return false;
1021  bool atRaIndex = response[0] != '\00';
1022  *atIndex = atDecIndex && atRaIndex;
1023  return true;
1024 }
1025 
1026 // Get pier side command, returns 'E' or 'W'
1027 bool CelestronDriver:: get_pier_side(char *side_of_pier)
1028 {
1029  set_sim_response("W#");
1030 
1031  if (!send_command("p", 1, response, 2, true, true))
1032  return false;
1033  *side_of_pier = response[0];
1034 
1035  return true;
1036 }
1037 
1038 // check if the mount is aligned using the mount J command
1039 bool CelestronDriver::check_aligned(bool *isAligned)
1040 {
1041  // returns 0x01 or 0x00
1042  set_sim_response("\x01#");
1043  if (!send_command("J", 1, response, 2, true, false))
1044  return false;
1045 
1046  *isAligned = response[0] == 0x01;
1047  return true;
1048 }
1049 
1051 {
1052  set_sim_response("#");
1053  char cmd;
1054  switch (mode)
1055  {
1056  case CTM_EQN:
1058  break;
1059  case CTM_EQS:
1061  break;
1062  default:
1063  return false;
1064  }
1065  char payload[] = {static_cast<char>(rate >> 8 & 0xff), static_cast<char>(rate & 0xff)};
1066  return send_passthrough(CELESTRON_DEV_RA, cmd, payload, 2, response, 0);
1067 }
1068 
1069 // focuser commands
1070 
1072 {
1073  char focVersion[16];
1074  int vernum = 0; // version as a number: 0xMMmmbbbb
1075  LOG_DEBUG("Does focuser exist...");
1076  size_t rlen = send_passthrough(CELESTRON_DEV_FOC, GET_VER, nullptr, 0, response, 4);
1077  switch (rlen)
1078  {
1079  case 2:
1080  case 3:
1081  snprintf(focVersion, 15, "%d.%02d", static_cast<uint8_t>(response[0]), static_cast<uint8_t>(response[1]));
1082  vernum = (static_cast<uint8_t>(response[0]) << 24) + (static_cast<uint8_t>(response[1]) << 16);
1083  break;
1084  case 4:
1085  case 5:
1086  snprintf(focVersion, 15, "%d.%02d.%d",
1087  static_cast<uint8_t>(response[0]), static_cast<uint8_t>(response[1]),
1088  static_cast<int>((static_cast<uint8_t>(response[2]) << 8) + static_cast<uint8_t>(response[3])));
1089  vernum = (static_cast<uint8_t>(response[0]) << 24) + (static_cast<uint8_t>(response[1]) << 16) + (static_cast<uint8_t>
1090  (response[2]) << 8) + static_cast<uint8_t>(response[3]);
1091  break;
1092  default:
1093  LOGF_DEBUG("No focuser found, %i", echo());
1094  return false;
1095  }
1096 
1097  LOGF_DEBUG("Focuser Version %s, exists %s", focVersion, vernum != 0 ? "true" : "false");
1098  return vernum != 0;
1099 }
1100 
1102 {
1103  if (simulation)
1104  {
1105  int offset = get_sim_foc_offset();
1106  if (offset > 250)
1107  move_sim_foc(250);
1108  else if (offset < -250)
1109  move_sim_foc(-250);
1110  else
1111  move_sim_foc(offset);
1112  }
1113  set_sim_response("%c%c%c#", sim_data.foc_position >> 16 & 0xff, sim_data.foc_position >> 8 & 0XFF,
1114  sim_data.foc_position & 0XFF);
1115 
1116  size_t rlen = send_passthrough(CELESTRON_DEV_FOC, MC_GET_POSITION, nullptr, 0, response, 3);
1117  if (rlen >= 3)
1118  {
1119  int pos = (static_cast<uint8_t>(response[0]) << 16) + (static_cast<uint8_t>(response[1]) << 8) + static_cast<uint8_t>
1120  (response[2]);
1121  LOGF_DEBUG("get focus position %d", pos);
1122  return pos;
1123  }
1124  LOG_DEBUG("get Focus position fail");
1125  return -1;
1126 }
1127 
1128 bool CelestronDriver::foc_move(uint32_t steps)
1129 {
1130  sim_data.foc_target = steps;
1131  LOGF_DEBUG("Focus move %d", steps);
1132  char payload[] = {static_cast<char>(steps >> 16 & 0xff), static_cast<char>(steps >> 8 & 0xff), static_cast<char>(steps & 0xff)};
1133  set_sim_response("#");
1134  size_t rlen = send_passthrough(CELESTRON_DEV_FOC, MC_GOTO_FAST, payload, 3, response, 0);
1135  return rlen > 0;
1136 }
1137 
1139 {
1140  set_sim_response("%c#", sim_data.foc_target == sim_data.foc_position ? 0xff : 0x00);
1141  size_t rlen = send_passthrough(CELESTRON_DEV_FOC, MC_SLEW_DONE, nullptr, 0, response, 1);
1142  if (rlen < 1 )
1143  return false;
1144  return response[0] != '\xff'; // use char comparison because some compilers object
1145 }
1146 
1147 bool CelestronDriver::foc_limits(int * low, int * high)
1148 {
1149  set_sim_response("%c%c%c%c%c%c%c%c#", 0, 0, 0x07, 0xd0, 0, 0, 0x9C, 0x40); // 2000, 40000
1150 
1151  size_t rlen = send_passthrough(CELESTRON_DEV_FOC, FOC_GET_HS_POSITIONS, nullptr, 0, response, 8);
1152  if (rlen < 8)
1153  return false;
1154 
1155  *low = (static_cast<uint8_t>(response[0]) << 24) + (static_cast<uint8_t>(response[1]) << 16) + (static_cast<uint8_t>
1156  (response[2]) << 8) + static_cast<uint8_t>(response[3]);
1157  *high = (static_cast<uint8_t>(response[4]) << 24) + (static_cast<uint8_t>(response[5]) << 16) + (static_cast<uint8_t>
1158  (response[6]) << 8) + static_cast<uint8_t>(response[7]);
1159 
1160  // check on integrity of values, they must be sensible and the range must be more than 2 turns
1161  if (*high - *low < 2000 || *high < 0 || *high > 60000 || *low < 0 || *low > 50000)
1162  {
1163  LOGF_INFO("Focus range %i to %i invalid, range not updated", *high, *low);
1164  return false;
1165  }
1166 
1167  LOGF_DEBUG("Focus Limits: Maximum (%i) Minimum (%i)", *high, *low);
1168  return true;
1169 }
1170 
1172 {
1173  if(simulation)
1174  {
1175  sim_data.foc_target = sim_data.foc_position;
1176  }
1177  set_sim_response("#");
1178 
1179  char payload[] = {0};
1180  size_t rlen = send_passthrough(CELESTRON_DEV_FOC, MC_MOVE_POS, payload, 1, response, 0);
1181  return rlen > 0;
1182 }
1183 
1185 // PEC Handling
1187 
1189 {
1190  if (pecState >= PEC_STATE::PEC_INDEXED)
1191  {
1192  LOG_DEBUG("PecSeekIndex - already found");
1193  return true;
1194  }
1195 
1196  set_sim_response("#");
1197 
1198  size_t rlen = send_passthrough(CELESTRON_DEV_RA, MC_SEEK_INDEX, nullptr, 0, response, 0);
1199  if (rlen < 1)
1200  {
1201  LOG_WARN("Start PEC seek index failed");
1202  return false;
1203  }
1204 
1205  pecState = PEC_STATE::PEC_SEEKING;
1206 
1207  simSeekIndex = true;
1208 
1209  LOGF_DEBUG("PecSeekIndex %s", PecStateStr());
1210 
1211  return true;
1212 }
1213 
1215 {
1216  if (pecState <= PEC_STATE::PEC_NOT_AVAILABLE)
1217  return false;
1218 
1219  if (!force && pecState >= PEC_STATE::PEC_INDEXED)
1220  return true;
1221 
1222  set_sim_response("%c#", simSeekIndex ? 0xFF : 0x00);
1223 
1224  size_t rlen = send_passthrough(CELESTRON_DEV_RA, MC_AT_INDEX, nullptr, 0, response, 1);
1225  if (rlen < 1)
1226  return false;
1227 
1228  bool indexed = (response[0] == '\xFF');
1229  // update the local PEC state
1230  if (indexed && pecState <= PEC_STATE::PEC_INDEXED)
1231  {
1232  pecState = PEC_STATE::PEC_INDEXED;
1233  LOG_INFO("PEC Index Found");
1234  }
1235 
1236  LOGF_DEBUG("isPecAtIndex? %s", indexed ? "yes" : "no");
1237 
1238  return indexed;
1239 }
1240 
1242 {
1243  if (pecState < PEC_STATE::PEC_AVAILABLE)
1244  {
1245  LOG_DEBUG("getPecNumBins - PEC not available");
1246  return 0;
1247  }
1248  set_sim_response("%c#", 88);
1249  char payload[] = { 0x3F };
1250  size_t rlen = send_passthrough(CELESTRON_DEV_RA, MC_PEC_READ_DATA, payload, 1, response, 1);
1251  if (rlen < 1)
1252  return 0;
1253 
1254  size_t numPecBins = response[0];
1255  LOGF_DEBUG("getPecNumBins %d", numPecBins);
1256  return numPecBins;
1257 }
1258 
1260 {
1261  if (simulation)
1262  {
1263  // increment the index each time we read it. Timing will be too fast, a good thing!
1264  simIndex++;
1265  if (simIndex >= 88)
1266  simIndex = 0;
1267  }
1268  set_sim_response("%c#", simIndex);
1269 
1270  size_t rlen = send_passthrough(CELESTRON_DEV_RA, MTR_PECBIN, nullptr, 0, response, 1);
1271  if (rlen < 1)
1272  return 0;
1273 
1274  return response[0];
1275 }
1276 
1278 {
1279  if (!(pecState == PEC_STATE::PEC_INDEXED || pecState == PEC_STATE::PEC_PLAYBACK))
1280  return false;
1281  char data[1];
1282  data[0] = start ? 0x01 : 0x00;
1283 
1284  set_sim_response("#");
1285 
1286  size_t rlen = send_passthrough(CELESTRON_DEV_RA, MC_PEC_PLAYBACK, data, 1, response, 0);
1287  if (rlen <= 0)
1288  {
1289  LOGF_WARN("PEC Playback %s failed", start ? "start" : "stop");
1290  return false;
1291  }
1292 
1293  // we can't read the PEC state so use the start state to set it
1294  pecState = start ? PEC_STATE::PEC_PLAYBACK : PEC_STATE::PEC_INDEXED;
1295 
1296  LOGF_DEBUG("PecPayback %s, pecState %s", start ? "start" : "stop", PecStateStr());
1297 
1298  return true;
1299 }
1300 
1302 {
1303  if (!(pecState == PEC_STATE::PEC_INDEXED || pecState == PEC_STATE::PEC_RECORDING))
1304  return false;
1305 
1306  int command = start ? MC_PEC_RECORD_START : MC_PEC_RECORD_STOP;
1307 
1308  set_sim_response("#");
1309  simRecordStart = simIndex;
1310 
1311  size_t rlen = send_passthrough(CELESTRON_DEV_RA, command, nullptr, 0, response, 0);
1312  if (rlen <= 0)
1313  {
1314  LOGF_WARN("PEC Record %s failed", start ? "start" : "stop");
1315  return false;
1316  }
1317 
1318  pecState = start ? PEC_STATE::PEC_RECORDING : PEC_STATE::PEC_INDEXED;
1319 
1320  LOGF_DEBUG("PecRecord %s, pecState %s", start ? "start" : "stop", PecStateStr());
1321  return true;
1322 }
1323 
1325 {
1326  if (pecState != PEC_STATE::PEC_RECORDING)
1327  return true;
1328 
1329  set_sim_response("%c#", simIndex == simRecordStart ? 1 : 0);
1330 
1331  size_t rlen = send_passthrough(CELESTRON_DEV_RA, MC_PEC_RECORD_DONE, nullptr, 0, response, 1);
1332  if (rlen < 1)
1333  return false;
1334 
1335  bool done = response[0] != 0x00;
1336  if (done)
1337  pecState = PEC_STATE::PEC_INDEXED;
1338 
1339  LOGF_DEBUG("isPecRecordDone %s", done ? "yes" : "no");
1340 
1341  return done;
1342 }
1343 
1345 {
1346  if (simulation)
1347  {
1348  // generate PEC value from index, range -100 to +100, 1 cycle
1349  int val = static_cast<int>(std::round(std::cos(index * 2.0 * 3.14192 / 87.0) * 100.0));
1350  if (val < 0) val = 256 + val;
1351  set_sim_response("%c#", val);
1352  }
1353  char data[] = { static_cast<char>(0x40 + index) };
1354  size_t rlen = send_passthrough(CELESTRON_DEV_RA, MC_PEC_READ_DATA, data, 1, response, 1);
1355  if (rlen < 1)
1356  return 0;
1357 
1358  // make result signed
1359  return response[0] <= '\127' ? response[0] : -256 + response[0];
1360 }
1361 
1362 bool CelestronDriver::setPecValue(size_t index, int data)
1363 {
1364  char payload[2];
1365  payload[0] = static_cast<char>(0x40 + index);
1366  payload[1] = static_cast<char>((data < 127) ? data : 256 - data);
1367  set_sim_response("#");
1368  return send_passthrough(CELESTRON_DEV_RA, MC_PEC_WRITE_DATA, payload, 2, response, 1) == 0;
1369 }
1370 
1372 {
1373  switch (pecState)
1374  {
1376  isPecAtIndex();
1377  break;
1379  isPecRecordDone();
1380  break;
1381  default:
1382  break;
1383  }
1384  return pecState;
1385 }
1386 
1388 {
1389  return PecStateStr(pecState);
1390 }
1391 
1393 {
1394  switch (state)
1395  {
1396  default:
1397  return "None";
1399  return "Not Available";
1401  return "Available";
1403  return "PEC Playback";
1405  return "seeking index";
1407  return "Index Found";
1409  return "PEC Recording";
1410  }
1411 }
1412 
1416 
1417 // constructor, generates test data
1419 {
1420  numBins = 88;
1421  for (size_t i = 0; i <= numBins; ++i)
1422  {
1423  double p = i * 2.0 * 3.14192 / numBins;
1424  data[i] = std::sin(p) * 5;
1425  }
1426  wormArcSeconds = 7200;
1427 }
1428 
1429 // Load PEC data from the mount
1431 {
1432  // get model # and use it to set wormArcSeconds and rateScale
1433  int mountType = driver->model();
1434  rateScale = (mountType <= 2) ? 512 : 1024;
1435  wormArcSeconds = mountType == 8 ? 3600 : 7200;
1436  //LOGF_DEBUG("rateScale %f, wormArcSeconds %f, SiderealRate %f", rateScale, wormArcSeconds, SIDEREAL_ARCSEC_PER_SEC);
1437 
1438  numBins = driver->getPecNumBins();
1439  if (numBins < 88 || numBins > 254)
1440  return false;
1441 
1442  double posError = 0;
1443  data[0] = 0.0;
1444  for (size_t i = 0; i < numBins; i++)
1445  {
1446  // this is ported from the Celestron PECTool VB6 code
1447  // ' We traveled at SIDEREAL + binRate arcsec/sec over a distance of wormArcseconds/numPecBins arcseconds.
1448  // ' We need to figure out how long that took to get the error in arcseconds...
1449  // ' ie., error = binRate * binTime
1450  // binRate = rawPecData(i)
1451  // binTime = wormArcseconds / numPecBins / (SIDEREAL_ARCSEC_PER_SEC + binRate)
1452  // posError = posError + binRate * binTime
1453  // currentPecData(i + 1, 0) = posError
1454 
1455  int rawPec = driver->getPecValue(i);
1456 
1457  double binRate = rawPec * SIDEREAL_ARCSEC_PER_SEC / rateScale;
1458  double binTime = (wormArcSeconds / numBins) / (SIDEREAL_ARCSEC_PER_SEC + binRate);
1459  posError += binRate * binTime;
1460  data[i + 1] = posError;
1461 
1462  LOGF_DEBUG("i %d, rawPec %d, binRate %f, binTime %f, data[%d] %f", i, rawPec, binRate, binTime, i + 1, data[i + 1]);
1463  }
1464  return true;
1465 }
1466 
1467 // PEC file format, this matches the format used by Celestron in their PECTool application
1468 // file format, one line for each entry:
1469 // line 0: numBins, currently 88
1470 // lines 1 to 90: double data[0] to data[numBins], numBins + 1 values, currently 89
1471 // line 91: wormArcSecs, currently 7200
1472 //
1473 
1474 // Load PEC data from file
1475 bool PecData::Load(const char *fileName)
1476 {
1477  std::ifstream pecFile(fileName);
1478 
1479  if (pecFile.is_open())
1480  {
1481  pecFile >> numBins;
1482  for (size_t i = 0; i <= numBins; i++)
1483  {
1484  pecFile >> data[i];
1485  }
1486  pecFile >> wormArcSeconds;
1487  LOGF_DEBUG("PEC Load File %s, numBins %d, wormarcsecs %d", fileName, numBins, wormArcSeconds);
1488  return true;
1489  }
1490  else
1491  {
1492  // report file open failure
1493  LOGF_WARN("Load PEC file %s, error %s", fileName, strerror(errno));
1494  }
1495  return false;
1496 }
1497 
1498 // Save the current PEC data to file
1499 // returns false if it fails
1500 bool PecData::Save(const char *filename)
1501 {
1502  std::ofstream pecFile(filename);
1503  if (!pecFile.is_open())
1504  return false;
1505 
1506  pecFile << numBins << "\n";
1507  for (size_t i = 0; i <= numBins; i++)
1508  {
1509  pecFile << data[i] << "\n";
1510  LOGF_DEBUG("data[%d] = %f", i, data[i]);
1511  }
1512  pecFile << wormArcSeconds << "\n";
1513  return true;
1514 }
1515 
1516 // Save the current PEC data to the mount
1518 {
1519  if (driver->getPecNumBins() != numBins)
1520  {
1521  return false;
1522  }
1523 
1524  double deltaDist;
1525  for (size_t i = 0; i < numBins; i++)
1526  {
1527  // this is ported from the Celestron PECTool VB6 code
1528  // deltaDist = currentPecData(i + 1, 0) - currentPecData(i, 0)
1529  // rawPecData(i) = deltaDist * SIDEREAL_ARCSEC_PER_SEC / (wormArcseconds / numPecBins - deltaDist)
1530 
1531  // get the offset in arcsecs per bin
1532  deltaDist = data[i + 1] - data[i];
1533  // convert to offset in arcsecs per second
1534  double rawPecData = deltaDist * SIDEREAL_ARCSEC_PER_SEC / (wormArcSeconds / numBins - deltaDist);
1535 
1536  int rawdata = static_cast<int>(std::round(rawPecData * rateScale / SIDEREAL_ARCSEC_PER_SEC));
1537  LOGF_DEBUG("i %d, deltaDist %f, rawPecdata %f, rawData %d", i, deltaDist, rawPecData, rawdata);
1538  if (rawdata < 0)
1539  rawdata += 256;
1540  driver->setPecValue(i, rawdata);
1541  }
1542  return true;
1543 }
1544 
1545 // Removes any drift over the PEC cycle
1547 {
1548  // this works by taking the offset in arcseconds over one PEC cycle and correcting the PEC values
1549  // linearly so the drift is eliminated.
1550  // It gives slightly different values to what the original drift removal does but the difference is
1551  // small
1552  double delta = (data[numBins] - data[0]) / numBins;
1553  double offset = data[0];
1554  for (size_t i = 0; i <= numBins; i++)
1555  {
1556  data[i] = data[i] - offset - delta * i;
1557  }
1558 }
1559 
1560 void PecData::Kalman(PecData newData, int num)
1561 {
1562  if (numBins != newData.numBins)
1563  {
1564  //throw new ApplicationException("Kalman not possible numBins do not match");
1565  return;
1566  }
1567  auto fraction = 1.0 / num;
1568  auto kf = 1 - fraction;
1569  //auto nd = newData.Data;
1570  for (size_t i = 0; i <= numBins; i++)
1571  {
1572  data[i] = data[i] * kf + newData.data[i] * fraction;
1573  }
1574 }
1575 
1576 // This method is required by the logging macros
1578 {
1579  return device_str;
1580 }
1581 
1582 
void parseCoordsResponse(char *response, double *d1, double *d2, bool precise)
void hex_dump(char *buf, const char *data, size_t size)
__attribute__((__format__(__printf__, 2, 0))) void CelestronDriver
#define CELESTRON_TIMEOUT
#define CELESTRON_DEV_RA
#define MC_PEC_RECORD_DONE
#define MC_SET_NEG_GUIDERATE
#define MC_SEEK_INDEX
#define MC_PEC_RECORD_STOP
CELESTRON_DIRECTION
@ CELESTRON_W
@ CELESTRON_N
@ CELESTRON_S
@ CELESTRON_E
#define MC_PEC_PLAYBACK
#define MC_GOTO_FAST
CELESTRON_SLEW_RATE
#define MC_PEC_RECORD_START
#define FOC_GET_HS_POSITIONS
CELESTRON_TRACK_MODE
@ CTM_EQN
@ CTM_EQS
#define MC_MOVE_NEG
#define MTR_AUX_GUIDE
#define ISSTARSENSE
#define MC_PEC_WRITE_DATA
#define MC_LEVEL_START
#define GET_VER
#define MINSTSENSVER
#define MC_LEVEL_DONE
#define MAX_RESP_SIZE
#define CELESTRON_DEV_FOC
#define MC_GET_POSITION
CELESTRON_TRACK_RATE
PEC_STATE
@ PEC_SEEKING
The PEC index is being searched for, goes to PEC_INDEXED when found
@ PEC_AVAILABLE
PEC is available but inactive, can seek index Seek index is only available command
@ PEC_PLAYBACK
PEC is being played back, stays in this state until stopped equivalent to TelescopePECState PEC_ON
@ PEC_RECORDING
PEC is being recorded, goes to PEC_INDEXED when completed
@ PEC_NOT_AVAILABLE
PEC is not available, hardware has been checked, no other state is possible
@ PEC_INDEXED
the PEC index has been found, can go to Playback or Recording this is equivalent to TelescopePECState...
#define CELESTRON_DEV_DEC
#define MC_SET_AUTOGUIDE_RATE
#define MC_PEC_READ_DATA
CELESTRON_AXIS
#define MC_MOVE_POS
#define MC_GET_AUTOGUIDE_RATE
#define MC_SET_POS_GUIDERATE
#define ISNEXSTAR
#define MTR_PECBIN
#define MC_AT_INDEX
#define MTR_IS_AUX_GUIDE_ACTIVE
#define MC_SLEW_DONE
bool slew_radec(double ra, double dec, bool precise)
bool get_azalt(double *az, double *alt, bool precise)
bool set_guide_rate(CELESTRON_AXIS axis, uint8_t rate)
bool indexreached(bool *atIndex)
bool check_aligned(bool *isAligned)
bool set_datetime(struct ln_date *utc, double utc_offset, bool dst=false, bool precise=false)
bool set_location(double longitude, double latitude)
bool stop_motion(CELESTRON_DIRECTION dir)
int getPecValue(size_t index)
bool get_firmware(FirmwareInfo *info)
bool sync(double ra, double dec, bool precise)
bool foc_limits(int *low, int *high)
bool PecPlayback(bool start)
bool get_dev_firmware(int dev, char *version, size_t size)
size_t send_pulse(CELESTRON_DIRECTION direction, unsigned char rate, unsigned char duration_msec)
size_t send_passthrough(int dest, int cmd_id, const char *payload, size_t payload_len, char *resp, size_t response_len)
bool get_radec(double *ra, double *dec, bool precise)
bool get_track_mode(CELESTRON_TRACK_MODE *mode)
void set_device(const char *name)
bool set_track_rate(CELESTRON_TRACK_RATE rate, CELESTRON_TRACK_MODE mode)
bool set_track_mode(CELESTRON_TRACK_MODE mode)
bool get_model(char *model, size_t size, bool *isGem, bool *canPec, bool *hasHomeIndex)
bool get_version(char *version, size_t size)
size_t send_command(const char *cmd, size_t cmd_len, char *resp, size_t resp_len, bool ascii_cmd, bool ascii_resp)
virtual int serial_read(int nbytes, int *nbytes_read)
const char * PecStateStr()
bool PecRecord(bool start)
bool isPecAtIndex(bool force=false)
bool get_variant(char *variant)
bool get_location(double *longitude, double *latitude)
const char * getDeviceName()
bool get_pulse_status(CELESTRON_DIRECTION direction)
virtual int serial_write(const char *cmd, int nbytes, int *nbytes_written)
PEC_STATE updatePecState()
virtual int serial_read_section(char stop_char, int *nbytes_read)
bool get_guide_rate(CELESTRON_AXIS axis, uint8_t *rate)
bool slew_azalt(double az, double alt, bool precise)
bool setPecValue(size_t index, int data)
bool foc_move(uint32_t steps)
bool get_pier_side(char *sop)
bool is_slewing(bool *slewing)
bool get_utc_date_time(double *utc_hours, int *yy, int *mm, int *dd, int *hh, int *minute, int *ss, bool *dst, bool precise)
bool start_motion(CELESTRON_DIRECTION dir, CELESTRON_SLEW_RATE rate)
PecData()
PecData class.
const char * getDeviceName()
void RemoveDrift()
bool Load(CelestronDriver *driver)
bool Save(const char *filename)
double max(void)
int errno
double ra
double dec
#define MAXINDIDEVICE
Definition: indiapi.h:193
int tty_write(int fd, const char *buf, int nbytes, int *nbytes_written)
Writes a buffer to fd.
Definition: indicom.c:424
void getSexComponents(double value, int *d, int *m, int *s)
Definition: indicom.c:254
int tty_read(int fd, char *buf, int nbytes, int timeout, int *nbytes_read)
read buffer from terminal
Definition: indicom.c:482
void tty_error_msg(int err_code, char *err_msg, int err_msg_len)
Retrieve the tty error message.
Definition: indicom.c:1167
int fs_sexa(char *out, double a, int w, int fracbase)
Converts a sexagesimal number to a string. sprint the variable a in sexagesimal format into out[].
Definition: indicom.c:141
int tty_nread_section(int fd, char *buf, int nsize, char stop_char, int timeout, int *nbytes_read)
read buffer from terminal with a delimiter
Definition: indicom.c:666
Implementations for common driver routines.
@ TTY_OK
Definition: indicom.h:150
#define LOGF_INFO(fmt,...)
Definition: indilogger.h:82
#define LOG_DEBUG(txt)
Definition: indilogger.h:75
#define LOGF_WARN(fmt,...)
Definition: indilogger.h:81
#define LOG_WARN(txt)
Definition: indilogger.h:73
#define LOGF_DEBUG(fmt,...)
Definition: indilogger.h:83
#define LOGF_ERROR(fmt,...)
Definition: indilogger.h:80
#define LOG_INFO(txt)
Definition: indilogger.h:74
#define LOGF_EXTRA1(fmt,...)
Definition: indilogger.h:84
#define MAXRBUF
Definition: indiserver.cpp:102
int fd
Definition: intelliscope.c:43
#define RA_AXIS
#define DEC_AXIS
double pnex2dd(uint32_t value)
uint32_t dd2pnex(double angle)
uint16_t dd2nex(double angle)
double trimDecAngle(double angle)
double nex2dd(uint32_t value)
__u8 cmd[4]
Definition: pwc-ioctl.h:2
double controllerVersion
std::string Version
std::string RAFirmware
std::string Model
std::string DEFirmware
double round(double value, int decimal_places)