Instrument Neutral Distributed Interface INDI 2.2.5
mxhd_telescope.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2026 Makoto Kasahara All rights reserved.
3
4 This library is free software; you can redistribute it and/or
5 modify it under the terms of the GNU Library General Public
6 License version 2 as published by the Free Software Foundation.
7
8 This library is distributed in the hope that it will be useful,
9 but WITHOUT ANY WARRANTY; without even the implied warranty of
10 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 Library General Public License for more details.
12
13 You should have received a copy of the GNU Library General Public License
14 along with this library; see the file LICENSE. If not, write to
15 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 Boston, MA 02110-1301, USA.
17*******************************************************************************/
18#include "mxhd_telescope.h"
19#include "mxhd_serial.h"
20
21#include <algorithm>
22#include <cstdio>
23#include <chrono>
24#include <cmath>
25#include <cstdlib>
26#include <cstring>
27#include <ctime>
28#include <libnova/libnova.h>
29#include <memory>
30#include <thread>
31#include <regex>
32#include <unistd.h>
33#include <vector>
34
35static double clampDec(double d)
36{
37 if (d > 90.0) return 90.0;
38 if (d < -90.0) return -90.0;
39 return d;
40}
41
42static double normalizeHours(double h)
43{
44 while (h < 0.0) h += 24.0;
45 while (h >= 24.0) h -= 24.0;
46 return h;
47}
48
49static double normalizeSignedHours(double h)
50{
51 h = normalizeHours(h);
52 if (h >= 12.0)
53 h -= 24.0;
54 return h;
55}
56
57static bool extractNumbers(const std::string &s, std::vector<double> &out)
58{
59 out.clear();
60 static const std::regex re(R"([-+]?\d+(\.\d+)?)");
61 for (auto it = std::sregex_iterator(s.begin(), s.end(), re); it != std::sregex_iterator(); ++it)
62 {
63 try
64 {
65 out.push_back(std::stod((*it).str()));
66 }
67 catch (...)
68 {
69 }
70 }
71 return !out.empty();
72}
73
74static bool parseRaHours(const std::string &ra, double &hours)
75{
76 std::vector<double> nums;
77 if (!extractNumbers(ra, nums)) return false;
78 if (nums.size() == 1)
79 {
80 hours = normalizeHours(nums[0]);
81 return true;
82 }
83 const double hh = nums[0];
84 const double mm = nums.size() >= 2 ? nums[1] : 0.0;
85 const double ss = nums.size() >= 3 ? nums[2] : 0.0;
86 hours = normalizeHours(hh + mm / 60.0 + ss / 3600.0);
87 return true;
88}
89
90static bool parseDecDeg(const std::string &dec, double &deg)
91{
92 const auto firstDigit = dec.find_first_of("0123456789");
93 const bool explicitNegative = (firstDigit != std::string::npos &&
94 dec.find('-', 0) != std::string::npos &&
95 dec.find('-', 0) < firstDigit);
96
97 std::vector<double> nums;
98 if (!extractNumbers(dec, nums)) return false;
99 if (nums.size() == 1)
100 {
101 deg = clampDec(nums[0]);
102 return true;
103 }
104 const double dd = nums[0];
105 const double mm = nums.size() >= 2 ? std::fabs(nums[1]) : 0.0;
106 const double ss = nums.size() >= 3 ? std::fabs(nums[2]) : 0.0;
107 const double sign = (dd < 0.0 || explicitNegative) ? -1.0 : 1.0;
108 deg = clampDec(sign * (std::fabs(dd) + mm / 60.0 + ss / 3600.0));
109 return true;
110}
111
112static void normalizeSexagesimal(int &high, int &mid, int &low)
113{
114 if (low >= 60)
115 {
116 low -= 60;
117 ++mid;
118 }
119 if (mid >= 60)
120 {
121 mid -= 60;
122 ++high;
123 }
124}
125
126static int roundUtcOffsetHours(double utcOffsetHoursEast)
127{
128 return static_cast<int>(std::lround(utcOffsetHoursEast));
129}
130
131static std::string formatUtcOffsetForMount(int utcOffsetHoursEast)
132{
133 // MX-HD / LX200 expects the opposite sign of the INDI/ASCOM east-positive convention.
134 const int mountHours = std::max(-14, std::min(14, -utcOffsetHoursEast));
135 char buf[8];
136 std::snprintf(buf, sizeof(buf), "%c%02d", mountHours >= 0 ? '+' : '-', std::abs(mountHours));
137 return std::string(buf);
138}
139
140static std::string formatDateMdy(const ln_date &date)
141{
142 char buf[16];
143 std::snprintf(buf, sizeof(buf), "%02d/%02d/%02d",
144 date.months,
145 date.days,
146 date.years % 100);
147 return std::string(buf);
148}
149
150static bool makeLocalDateFromUtc(const ln_date *utc, double utcOffsetHoursEast, ln_date &localDate)
151{
152 if (!utc)
153 return false;
154
155 const double utcJd = ln_get_julian_day(const_cast<ln_date *>(utc));
156 ln_get_date(utcJd + (utcOffsetHoursEast / 24.0), &localDate);
157 return true;
158}
159
160static bool normalizeLocalDateTime(const ln_date &date, ln_date &normalized)
161{
162 std::tm tm {};
163 tm.tm_year = date.years - 1900;
164 tm.tm_mon = date.months - 1;
165 tm.tm_mday = date.days;
166 tm.tm_hour = date.hours;
167 tm.tm_min = date.minutes;
168 tm.tm_sec = static_cast<int>(std::lround(date.seconds));
169 tm.tm_isdst = -1;
170
171 time_t t = ::timegm(&tm);
172 if (t == static_cast<time_t>(-1))
173 return false;
174
175 std::tm tmNormalized {};
176 if (!::gmtime_r(&t, &tmNormalized))
177 return false;
178
179 normalized.years = tmNormalized.tm_year + 1900;
180 normalized.months = tmNormalized.tm_mon + 1;
181 normalized.days = tmNormalized.tm_mday;
182 normalized.hours = tmNormalized.tm_hour;
183 normalized.minutes = tmNormalized.tm_min;
184 normalized.seconds = tmNormalized.tm_sec;
185 return true;
186}
187
188static std::string formatTimeHms(const ln_date &date)
189{
190 char buf[16];
191 std::snprintf(buf, sizeof(buf), "%02d:%02d:%02d",
192 date.hours,
193 date.minutes,
194 static_cast<int>(std::lround(date.seconds)));
195 return std::string(buf);
196}
197
198static std::string formatLatDmm(double latitudeDegrees)
199{
200 const double lat = std::max(-90.0, std::min(90.0, latitudeDegrees));
201 const char sign = lat >= 0.0 ? '+' : '-';
202 const double absLat = std::fabs(lat);
203 int deg = static_cast<int>(std::floor(absLat));
204 int min = static_cast<int>(std::lround((absLat - deg) * 60.0));
205 if (min >= 60)
206 {
207 min -= 60;
208 ++deg;
209 }
210 if (deg > 90)
211 {
212 deg = 90;
213 min = 0;
214 }
215 char buf[16];
216 std::snprintf(buf, sizeof(buf), "%c%02d*%02d", sign, deg, min);
217 return std::string(buf);
218}
219
220static std::string formatLonDmmMountWestPositive(double longitudeDegreesEastPositive)
221{
222 double lonE = std::fmod(longitudeDegreesEastPositive, 360.0);
223 if (lonE < 0.0)
224 lonE += 360.0;
225
226 // Convert INDI/ASCOM east-positive longitude to the mount's west-positive form.
227 double lonW = std::fmod(360.0 - lonE, 360.0);
228 if (lonW < 0.0)
229 lonW += 360.0;
230
231 int totalMinutes = static_cast<int>(std::lround(lonW * 60.0));
232 totalMinutes %= (360 * 60);
233 if (totalMinutes < 0)
234 totalMinutes += (360 * 60);
235
236 const int deg = totalMinutes / 60;
237 const int min = totalMinutes % 60;
238 char buf[16];
239 std::snprintf(buf, sizeof(buf), "+%03d*%02d", deg, min);
240 return std::string(buf);
241}
242
243static double currentUtcJulianDay()
244{
245 const auto now = std::chrono::system_clock::now();
246 const auto epochSeconds = std::chrono::duration<double>(now.time_since_epoch()).count();
247 return 2440587.5 + (epochSeconds / 86400.0);
248}
249
250static double greenwichMeanSiderealHours(double julianDay)
251{
252 const double t = (julianDay - 2451545.0) / 36525.0;
253 double gmstDegrees = 280.46061837 +
254 360.98564736629 * (julianDay - 2451545.0) +
255 0.000387933 * t * t -
256 (t * t * t) / 38710000.0;
257
258 gmstDegrees = std::fmod(gmstDegrees, 360.0);
259 if (gmstDegrees < 0.0)
260 gmstDegrees += 360.0;
261
262 return gmstDegrees / 15.0;
263}
264
265static std::string formatRaSr(double raHours)
266{
267 // LX200 :Sr HH:MM:SS#
268 raHours = normalizeHours(raHours);
269 int hh = static_cast<int>(std::floor(raHours));
270 const double m = (raHours - hh) * 60.0;
271 int mm = static_cast<int>(std::floor(m));
272 int ss = static_cast<int>(std::round((m - mm) * 60.0));
273 normalizeSexagesimal(hh, mm, ss);
274 if (hh >= 24) hh -= 24;
275 char buf[32];
276 std::snprintf(buf, sizeof(buf), "%02d:%02d:%02d", hh, mm, ss);
277 return std::string(buf);
278}
279
280static std::string formatDecSd(double decDeg)
281{
282 // LX200 :Sd sDD*MM:SS#
283 const double d = clampDec(decDeg);
284 const char sign = (d < 0.0) ? '-' : '+';
285 const double ad = std::fabs(d);
286 int dd = static_cast<int>(std::floor(ad));
287 const double m = (ad - dd) * 60.0;
288 int mm = static_cast<int>(std::floor(m));
289 int ss = static_cast<int>(std::round((m - mm) * 60.0));
290 normalizeSexagesimal(dd, mm, ss);
291 if (dd > 90)
292 {
293 dd = 90;
294 mm = 0;
295 ss = 0;
296 }
297 char buf[32];
298 std::snprintf(buf, sizeof(buf), "%c%02d*%02d:%02d", sign, dd, mm, ss);
299 return std::string(buf);
300}
301
303{
304 setVersion(1, 0);
306 uint32_t capabilities =
315 /* MX-HD supports adjustable tracking rate in firmware, but this
316 driver version only exposes the standard sidereal/solar/lunar
317 track modes and does not implement arbitrary numeric track-rate
318 control yet. */
319 /* TELESCOPE_HAS_TRACK_RATE | */
322 0;
323 SetTelescopeCapability(capabilities, 4);
324}
325
327{
328 return "MX-HD Telescope";
329}
330
332{
336
337 AddTrackMode("TRACK_SIDEREAL", "Sidereal", true);
338 AddTrackMode("TRACK_SOLAR", "Solar");
339 AddTrackMode("TRACK_LUNAR", "Lunar");
340
341 // Home command (custom simple button)
342 IUFillSwitch(&HomeS[0], "FIND_HOME", "Home", ISS_OFF);
343 IUFillSwitchVector(&HomeSP, HomeS, 1, getDeviceName(), "MXHD_HOME", "Home", MAIN_CONTROL_TAB,
345
346 // Timed guide (INDI-compatible property names)
347 IUFillNumber(&GuideNSN[0], "TIMED_GUIDE_N", "North (ms)", "%.0f", 0, 600000, 0, 0);
348 IUFillNumber(&GuideNSN[1], "TIMED_GUIDE_S", "South (ms)", "%.0f", 0, 600000, 0, 0);
349 IUFillNumberVector(&GuideNSNP, GuideNSN, 2, getDeviceName(), "TELESCOPE_TIMED_GUIDE_NS", "Pulse Guide NS", MOTION_TAB,
350 IP_RW, 0, IPS_IDLE);
351
352 IUFillNumber(&GuideWEN[0], "TIMED_GUIDE_W", "West (ms)", "%.0f", 0, 600000, 0, 0);
353 IUFillNumber(&GuideWEN[1], "TIMED_GUIDE_E", "East (ms)", "%.0f", 0, 600000, 0, 0);
354 IUFillNumberVector(&GuideWENP, GuideWEN, 2, getDeviceName(), "TELESCOPE_TIMED_GUIDE_WE", "Pulse Guide WE", MOTION_TAB,
355 IP_RW, 0, IPS_IDLE);
356
357 return true;
358}
359
361{
363
364 if (isConnected())
365 {
366 defineProperty(&HomeSP);
367 defineProperty(&GuideNSNP);
368 defineProperty(&GuideWENP);
369 }
370 else
371 {
372 deleteProperty(HomeSP.name);
373 deleteProperty(GuideNSNP.name);
374 deleteProperty(GuideWENP.name);
375 }
376
377 return true;
378}
379
381{
382 // Best-effort: try reading status bytes. If it succeeds, we assume mount is present.
383 // Some MX-HD boots or link layers need a short settle time right after port open.
384 for (int attempt = 0; attempt < 3; ++attempt)
385 {
386 uint8_t b1 = 0, b2 = 0, b3 = 0;
387 if (mxhdReadStatus(b1, b2, b3))
388 {
389 decodeStatus(b1, b2, b3);
390 return true;
391 }
392 std::this_thread::sleep_for(std::chrono::milliseconds(200));
393 }
394 LOG_WARN("Handshake status read failed; continuing with connection so the client can retry.");
395 return true;
396}
397
399{
400 // Use base class serial connection (fd stored in PortFD)
402 return false;
403
404 // Wrap fd for helper I/O
405 {
406 std::lock_guard<std::mutex> lock(m_ioMutex);
407 delete m_io;
408 m_io = new MXHDSerial(PortFD);
409 }
410
411 // Default: tracking enabled on connect (we keep state, but mount behavior may differ).
412 m_trackingEnabled = true;
413
414 if (!Handshake())
415 return false;
416
417 if (!mxhdApplyCachedSiteTime())
418 {
419 LOG_ERROR("Failed to apply cached site/time information after connect.");
420 (void)mxhdSend(":Q#");
421 Disconnect();
422 return false;
423 }
424
425 return true;
426}
427
429{
430 // Invalidate any pending pulse-guide completion callbacks.
431 {
432 std::lock_guard<std::mutex> guideLock(m_guideMutex);
433 ++m_guideTokenGen;
434 m_guideActive = false;
435 m_guideDirection = 0;
436 }
437
438 // Best-effort stop in case guiding/manual motion was in progress.
439 (void)mxhdSend(":Q#");
440
441 {
442 std::lock_guard<std::mutex> lock(m_ioMutex);
443 delete m_io;
444 m_io = nullptr;
445 }
447}
448
449bool MXHDTelescope::updateLocation(double latitude, double longitude, double elevation)
450{
451 bool shouldSend = false;
452 {
453 std::lock_guard<std::mutex> lock(m_siteTimeMutex);
454 // INDI standard coordinates are latitude +N and longitude +E.
455 // Skip the default 0/0 placeholder unless we've already seen a real site.
456 if (!m_hasCachedSiteCoordinates &&
457 std::fabs(latitude) <= 1e-9 &&
458 std::fabs(longitude) <= 1e-9)
459 {
460 m_cachedSiteLatitudeDeg = latitude;
461 m_cachedSiteLongitudeDeg = longitude;
462 return true;
463 }
464
465 m_cachedSiteLatitudeDeg = latitude;
466 m_cachedSiteLongitudeDeg = longitude;
467 m_hasCachedSiteCoordinates = true;
468 (void)elevation;
469 shouldSend = isConnected();
470 }
471
472 if (!shouldSend)
473 return true;
474
475 if (!mxhdApplySiteCoordinates(latitude, longitude))
476 return false;
477
478 return true;
479}
480
481bool MXHDTelescope::updateTime(ln_date *utc, double utc_offset)
482{
483 if (!utc)
484 return false;
485
486 bool shouldSend = false;
487 {
488 std::lock_guard<std::mutex> lock(m_siteTimeMutex);
489 m_cachedUtcDate = *utc;
490 m_cachedUtcOffsetHoursEast = utc_offset;
491 m_hasCachedTime = true;
492 shouldSend = isConnected();
493 }
494
495 if (!shouldSend)
496 return true;
497
498 if (!mxhdApplyTime(utc, utc_offset))
499 return false;
500
501 return true;
502}
503
504bool MXHDTelescope::mxhdSend(const std::string &cmd)
505{
506 std::lock_guard<std::mutex> lock(m_ioMutex);
507 if (!m_io)
508 return false;
509
510 std::string err;
511 if (!m_io->writeString(cmd, err))
512 {
513 LOGF_ERROR("I/O send failed (%s): %s", cmd.c_str(), err.c_str());
514 return false;
515 }
516 return true;
517}
518
519bool MXHDTelescope::mxhdQueryHash(const std::string &cmd, std::string &resp)
520{
521 std::lock_guard<std::mutex> lock(m_ioMutex);
522 if (!m_io)
523 return false;
524
525 std::string err;
526 if (!m_io->queryHash(cmd, resp, 2000, err))
527 {
528 LOGF_ERROR("I/O query failed (%s): %s", cmd.c_str(), err.c_str());
529 return false;
530 }
531 return true;
532}
533
534bool MXHDTelescope::mxhdQueryAck(const std::string &cmd, char &ack, int timeoutMs)
535{
536 std::lock_guard<std::mutex> lock(m_ioMutex);
537 if (!m_io)
538 return false;
539
540 std::string err;
541 if (!m_io->queryAck(cmd, ack, timeoutMs, err))
542 {
543 LOGF_ERROR("I/O query failed (%s): %s", cmd.c_str(), err.c_str());
544 return false;
545 }
546 return true;
547}
548
549bool MXHDTelescope::mxhdApplySiteCoordinates(double latitudeDeg, double longitudeDeg)
550{
551 const std::string cmdSt = ":St" + formatLatDmm(latitudeDeg) + "#";
552 const std::string cmdSg = ":Sg" + formatLonDmmMountWestPositive(longitudeDeg) + "#";
553
554 char ack = 0;
555 if (!mxhdQueryAck(cmdSt, ack, 10000))
556 return false;
557 if (ack != '1')
558 {
559 LOGF_WARN("Unexpected :St response: '%c'", ack);
560 return false;
561 }
562
563 if (!mxhdQueryAck(cmdSg, ack, 10000))
564 return false;
565 if (ack != '1')
566 {
567 LOGF_WARN("Unexpected :Sg response: '%c'", ack);
568 return false;
569 }
570
571 return true;
572}
573
574bool MXHDTelescope::mxhdApplyTime(ln_date *utc, double utcOffsetHoursEast)
575{
576 if (!mxhdSendUtcOffset(utcOffsetHoursEast))
577 return false;
578 return mxhdSendLocalDateTime(utc, utcOffsetHoursEast);
579}
580
581bool MXHDTelescope::mxhdSendUtcOffset(double utcOffsetHoursEast, int timeoutMs)
582{
583 const int utcOffsetHoursRounded = roundUtcOffsetHours(utcOffsetHoursEast);
584 if (std::fabs(utcOffsetHoursEast - utcOffsetHoursRounded) > 0.01)
585 {
586 LOGF_WARN("UTC offset is fractional (%.3fh); rounded to %dh for MX-HD :SG.",
587 utcOffsetHoursEast, utcOffsetHoursRounded);
588 }
589
590 const std::string cmdSg = ":SG" + formatUtcOffsetForMount(utcOffsetHoursRounded) + "#";
591 char ack = 0;
592 if (!mxhdQueryAck(cmdSg, ack, timeoutMs))
593 return false;
594 if (ack != '1')
595 {
596 LOGF_WARN("Unexpected :SG response: '%c'", ack);
597 return false;
598 }
599 return true;
600}
601
602bool MXHDTelescope::mxhdSendLocalDateTime(ln_date *utc, double utcOffsetHoursEast, int timeoutMs)
603{
604 if (!utc)
605 return false;
606
607 ln_date localDate {};
608 if (!makeLocalDateFromUtc(utc, utcOffsetHoursEast, localDate))
609 return false;
610
611 ln_date normalizedLocalDate {};
612 if (!normalizeLocalDateTime(localDate, normalizedLocalDate))
613 {
614 LOG_ERROR("Failed to normalize local date/time for :SC/:SL.");
615 return false;
616 }
617
618 const std::string date = formatDateMdy(normalizedLocalDate);
619 const std::string time = formatTimeHms(normalizedLocalDate);
620 if (date.empty())
621 {
622 LOG_ERROR("Failed to format local date for :SC.");
623 return false;
624 }
625 if (time.empty())
626 {
627 LOG_ERROR("Failed to format local time for :SL.");
628 return false;
629 }
630
631 const std::string cmdSc = ":SC" + date + "#";
632 const std::string cmdSl = ":SL" + time + "#";
633
634 char ack = 0;
635 if (!mxhdQueryAck(cmdSc, ack, timeoutMs))
636 return false;
637 if (ack != '1')
638 {
639 LOGF_WARN("Unexpected :SC response: '%c'", ack);
640 return false;
641 }
642
643 if (!mxhdQueryAck(cmdSl, ack, timeoutMs))
644 return false;
645 if (ack != '1')
646 {
647 LOGF_WARN("Unexpected :SL response: '%c'", ack);
648 return false;
649 }
650
651 return true;
652}
653
654void MXHDTelescope::mxhdUpdatePierSide(double raHours)
655{
656 double longitudeDegEast = 0.0;
657 {
658 std::lock_guard<std::mutex> lock(m_siteTimeMutex);
659 if (!m_hasCachedSiteCoordinates)
660 return;
661 longitudeDegEast = m_cachedSiteLongitudeDeg;
662 }
663
664 const double lstHours = normalizeHours(greenwichMeanSiderealHours(currentUtcJulianDay()) + (longitudeDegEast / 15.0));
665 const double haHours = normalizeSignedHours(lstHours - raHours);
666
667 // Hold the previous side very close to the meridian to avoid flapping.
668 constexpr double kMeridianDeadbandHours = 1.0 / 60.0;
669 if (std::fabs(haHours) < kMeridianDeadbandHours)
670 {
671 if (!m_hasPierSideCache)
672 return;
673
674 setPierSide(m_pierSideEast ? PIER_EAST : PIER_WEST);
675 return;
676 }
677
678 // INDI definition:
679 // - PIER_EAST: mount on east side of pier, telescope pointing west (HA > 0)
680 // - PIER_WEST: mount on west side of pier, telescope pointing east (HA < 0)
681 m_pierSideEast = (haHours > 0.0);
682 m_hasPierSideCache = true;
683 setPierSide(m_pierSideEast ? PIER_EAST : PIER_WEST);
684}
685
686bool MXHDTelescope::mxhdApplyCachedSiteTime()
687{
688 bool haveSite = false;
689 bool haveTime = false;
690 double latitudeDeg = 0.0;
691 double longitudeDeg = 0.0;
692 ln_date utc {};
693 double utcOffsetHoursEast = 0.0;
694
695 {
696 std::lock_guard<std::mutex> lock(m_siteTimeMutex);
697 haveSite = m_hasCachedSiteCoordinates;
698 haveTime = m_hasCachedTime;
699 latitudeDeg = m_cachedSiteLatitudeDeg;
700 longitudeDeg = m_cachedSiteLongitudeDeg;
701 utc = m_cachedUtcDate;
702 utcOffsetHoursEast = m_cachedUtcOffsetHoursEast;
703 }
704
705 // ASCOM order:
706 // 1) SG (timezone)
707 // 2) St/Sg (site)
708 // 3) SC/SL (date/time)
709 if (haveTime)
710 {
711 if (!mxhdSendUtcOffset(utcOffsetHoursEast, 10000))
712 return false;
713 }
714
715 if (haveSite)
716 {
717 if (!mxhdApplySiteCoordinates(latitudeDeg, longitudeDeg))
718 return false;
719 }
720
721 if (haveTime)
722 {
723 if (!mxhdSendLocalDateTime(&utc, utcOffsetHoursEast, 10000))
724 return false;
725 }
726
727 if (!haveSite && !haveTime)
728 {
729 LOG_WARN("No cached INDI site/time values available at connect; skipping automatic :SG/:St/:Sg/:SC/:SL sync.");
730 }
731
732 return true;
733}
734
735bool MXHDTelescope::mxhdReadStatus(uint8_t &b1, uint8_t &b2, uint8_t &b3)
736{
737 std::lock_guard<std::mutex> lock(m_ioMutex);
738 if (!m_io)
739 return false;
740
741 std::string err;
742 if (!m_io->queryStatus3("@ST#", b1, b2, b3, 2000, err))
743 {
744 LOGF_WARN("Status read failed (@ST#): %s", err.c_str());
745 return false;
746 }
747 return true;
748}
749
750void MXHDTelescope::decodeStatus(uint8_t b1, uint8_t b2, uint8_t b3)
751{
752 m_st1 = b1;
753 m_st2 = b2;
754 m_st3 = b3;
755
756 m_isHoming = (b3 & 0x80) != 0;
757 const bool slewBit = (b3 & 0x40) != 0;
758 m_isSlewing = slewBit || m_isHoming;
759
760 m_voltageV = static_cast<double>(b2) / 10.0;
761
762 if (m_isHoming || m_isSlewing)
763 TrackState = m_parkCommandActive ? SCOPE_PARKING : SCOPE_SLEWING;
764 else if (isParked())
766 else
767 TrackState = m_trackingEnabled ? SCOPE_TRACKING : SCOPE_IDLE;
768}
769
770bool MXHDTelescope::mxhdQueryRaDec(double &raHours, double &decDeg)
771{
772 std::string raRaw, decRaw;
773 if (!mxhdQueryHash(":GR#", raRaw))
774 return false;
775 if (!mxhdQueryHash(":GD#", decRaw))
776 return false;
777
778 if (!parseRaHours(raRaw, raHours))
779 {
780 LOGF_ERROR("Invalid RA response: '%s'", raRaw.c_str());
781 return false;
782 }
783 if (!parseDecDeg(decRaw, decDeg))
784 {
785 LOGF_ERROR("Invalid Dec response: '%s'", decRaw.c_str());
786 return false;
787 }
788 return true;
789}
790
791bool MXHDTelescope::mxhdSetTargetRaDec(double raHours, double decDeg)
792{
793 // LX200: :Sr HH:MM:SS# and :Sd sDD*MM:SS#
794 const std::string ra = ":Sr" + formatRaSr(raHours) + "#";
795 const std::string dec = ":Sd" + formatDecSd(decDeg) + "#";
796
797 char ack = 0;
798 if (!mxhdQueryAck(ra, ack))
799 return false;
800 if (ack != '1')
801 {
802 LOGF_WARN("Unexpected :Sr response: '%c'", ack);
803 return false;
804 }
805
806 if (!mxhdQueryAck(dec, ack))
807 return false;
808 if (ack != '1')
809 {
810 LOGF_WARN("Unexpected :Sd response: '%c'", ack);
811 return false;
812 }
813 return true;
814}
815
816bool MXHDTelescope::Goto(double ra, double dec)
817{
818 // INDI uses hours for RA, degrees for Dec.
819 if (!mxhdSetTargetRaDec(ra, dec))
820 return false;
821
822 char ack = 0;
823 if (!mxhdQueryAck(":MS#", ack))
824 return false;
825
826 // MX-HD / LX200-style slew ack: '0' on success.
827 if (ack != '0')
828 LOGF_WARN("Unexpected :MS# response: '%c'", ack);
829
830 m_isSlewing = true;
832 return true;
833}
834
835bool MXHDTelescope::Sync(double ra, double dec)
836{
837 if (!mxhdSetTargetRaDec(ra, dec))
838 return false;
839
840 std::string resp;
841 if (!mxhdQueryHash(":CM#", resp))
842 return false;
843
844 // Old LX200 returns "Unknown name" on sync sometimes; treat any response as non-fatal.
845 NewRaDec(ra, dec);
846 return true;
847}
848
850{
851 // MX-HD Park: @Hm#
852 if (!mxhdSend("@Hm#"))
853 return false;
854
855 SetParked(false);
856 m_parkCommandActive = true;
857 m_unparkCommandActive = false;
858 m_homeCommandActive = false;
859 m_isSlewing = true;
861 return true;
862}
863
865{
866 if (!isParked())
867 {
868 LOG_INFO("Mount is already unparked.");
869 return true;
870 }
871
872 // MX-HD restores a valid solved pose for remote operation by returning to HOME first.
873 if (!mxhdSend("@OG#"))
874 return false;
875
876 m_parkCommandActive = false;
877 m_unparkCommandActive = true;
878 m_homeCommandActive = false;
879 m_isSlewing = true;
880 m_isHoming = true;
882 return true;
883}
884
886{
887 switch (index)
888 {
889 case SLEW_GUIDE:
890 return mxhdSend(":RG#");
891
892 case SLEW_CENTERING:
893 return mxhdSend(":RC#");
894
895 case SLEW_FIND:
896 return mxhdSend(":RM#");
897
898 case SLEW_MAX:
899 return mxhdSend(":RS#");
900
901 default:
902 LOGF_WARN("Unsupported slew rate index %d requested by client.", index);
903 return false;
904 }
905}
906
908{
909 // MX-HD tracking on/off: @FD1# / @FD0#
910 if (!mxhdSend(enabled ? "@FD1#" : "@FD0#"))
911 return false;
912 m_trackingEnabled = enabled;
913 return true;
914}
915
917{
918 TrackRateMode selectedMode = TrackRateMode::Sidereal;
919
920 switch (mode)
921 {
922 case TRACK_SIDEREAL:
923 selectedMode = TrackRateMode::Sidereal;
924 break;
925
926 case TRACK_SOLAR:
927 selectedMode = TrackRateMode::Solar;
928 break;
929
930 case TRACK_LUNAR:
931 selectedMode = TrackRateMode::Lunar;
932 break;
933
934 default:
935 LOGF_WARN("Unsupported track mode index %u requested by client.", mode);
936 return false;
937 }
938
939 if (!mxhdSetTrackRate(selectedMode))
940 return false;
941
942 m_trackRateMode = selectedMode;
943 return true;
944}
945
946bool MXHDTelescope::mxhdSetTrackRate(TrackRateMode mode)
947{
948 switch (mode)
949 {
950 case TrackRateMode::Sidereal:
951 return mxhdSend("@CE0#");
952 case TrackRateMode::Solar:
953 return mxhdSend("@LP1#");
954 case TrackRateMode::Lunar:
955 return mxhdSend("@LP4#");
956 default:
957 return false;
958 }
959}
960
962{
963 // Abort has mount-specific behavior:
964 // - If homing is active, :Q# does not stop it reliably; we use @ME0# and delayed finalize.
965 // - If slewing bit is set, :Q# can work.
966 uint8_t b1 = 0, b2 = 0, b3 = 0;
967 if (mxhdReadStatus(b1, b2, b3))
968 decodeStatus(b1, b2, b3);
969
970 m_abortRequestedAt = ::time(nullptr);
971 m_abortSawHomeCleared = false;
972 m_abortHomeClearedAt = 0;
973
974 if (m_isHoming)
975 {
976 // Home abort: @ME0# then after (home=0 && abort+180s) -> @FD0# then 1s -> @ME1#
977 if (!mxhdSend("@ME0#"))
978 return false;
979 if (HomeSP.s == IPS_BUSY)
980 {
981 HomeSP.s = IPS_ALERT;
982 IDSetSwitch(&HomeSP, nullptr);
983 }
984 m_unparkCommandActive = false;
985 m_homeCommandActive = false;
986 startAbortFinalize(180);
987 return true;
988 }
989
990 if (m_isSlewing)
991 {
992 std::string resp;
993 if (!mxhdQueryHash(":Q#", resp))
994 return false;
995 m_unparkCommandActive = false;
996 m_homeCommandActive = false;
997 return true;
998 }
999
1000 // Not slewing & not homing: do a safe stop sequence.
1001 if (!mxhdSend("@ME0#"))
1002 return false;
1003 m_unparkCommandActive = false;
1004 m_homeCommandActive = false;
1005 startAbortFinalize(90);
1006 return true;
1007}
1008
1009void MXHDTelescope::startAbortFinalize(uint32_t delaySeconds)
1010{
1011 m_abortFinalizeActive = true;
1012 m_abortDelaySeconds = delaySeconds;
1013 // Keep mount "busy" from the client perspective while abort finalization runs.
1014 // (No direct INDI state for "aborting", so we will just keep things consistent in ReadScopeStatus.)
1015}
1016
1017void MXHDTelescope::maybeAdvanceAbortFinalize()
1018{
1019 if (!m_abortFinalizeActive)
1020 return;
1021
1022 // Update status first.
1023 uint8_t b1 = 0, b2 = 0, b3 = 0;
1024 if (!mxhdReadStatus(b1, b2, b3))
1025 return;
1026 decodeStatus(b1, b2, b3);
1027
1028 const time_t now = ::time(nullptr);
1029
1030 if (!m_abortSawHomeCleared)
1031 {
1032 if (!m_isHoming)
1033 {
1034 m_abortSawHomeCleared = true;
1035 m_abortHomeClearedAt = now;
1036 }
1037 // Wait until home clears at least once.
1038 return;
1039 }
1040
1041 if (now < (m_abortRequestedAt + static_cast<time_t>(m_abortDelaySeconds)))
1042 return;
1043
1044 // Finalize: @FD0# then after 1s @ME1#
1045 (void)mxhdSend("@FD0#");
1046 // Sleep is acceptable here because TimerHit runs in driver thread and this is rare; keep it short.
1047 usleep(1000 * 1000);
1048 (void)mxhdSend("@ME1#");
1049
1050 m_abortFinalizeActive = false;
1051}
1052
1054{
1055 if (command == MOTION_START)
1056 {
1057 // LX200 manual move: :Mn# (north) / :Ms# (south)
1058 return mxhdSend(dir == DIRECTION_NORTH ? ":Mn#" : ":Ms#");
1059 }
1060 else
1061 {
1062 // Stop N/S: :Qn# / :Qs#
1063 return mxhdSend(dir == DIRECTION_NORTH ? ":Qn#" : ":Qs#");
1064 }
1065}
1066
1068{
1069 if (command == MOTION_START)
1070 {
1071 // LX200 manual move: :Me# (east) / :Mw# (west)
1072 return mxhdSend(dir == DIRECTION_EAST ? ":Me#" : ":Mw#");
1073 }
1074 else
1075 {
1076 // Stop E/W: :Qe# / :Qw#
1077 return mxhdSend(dir == DIRECTION_EAST ? ":Qe#" : ":Qw#");
1078 }
1079}
1080
1081IPState MXHDTelescope::startTimedGuidePulse(GuideAxis axis, int direction, uint32_t ms, const char *moveCmd)
1082{
1083 if (ms == 0)
1084 return IPS_OK;
1085
1086 if (!isConnected() || moveCmd == nullptr)
1087 return IPS_ALERT;
1088
1089 uint64_t token = 0;
1090 bool needStopFirst = false;
1091 {
1092 std::lock_guard<std::mutex> lock(m_guideMutex);
1093 needStopFirst = m_guideActive;
1094 token = ++m_guideTokenGen;
1095 m_guideActive = true;
1096 m_guideAxis = axis;
1097 m_guideDirection = direction;
1098 }
1099
1100 if (needStopFirst && !mxhdSend(":Q#"))
1101 {
1102 std::lock_guard<std::mutex> lock(m_guideMutex);
1103 if (token == m_guideTokenGen.load())
1104 {
1105 m_guideActive = false;
1106 m_guideDirection = 0;
1107 }
1108 return IPS_ALERT;
1109 }
1110
1111 if (!mxhdSend(moveCmd))
1112 {
1113 std::lock_guard<std::mutex> lock(m_guideMutex);
1114 if (token == m_guideTokenGen.load())
1115 {
1116 m_guideActive = false;
1117 m_guideDirection = 0;
1118 }
1119 return IPS_ALERT;
1120 }
1121
1122 std::thread([this, axis, token, ms]()
1123 {
1124 std::this_thread::sleep_for(std::chrono::milliseconds(ms));
1125 completeTimedGuidePulse(axis, token, true);
1126 }).detach();
1127
1128 return IPS_BUSY;
1129}
1130
1131void MXHDTelescope::setGuidePropertyIdle(GuideAxis axis, IPState state)
1132{
1133 if (axis == GuideAxis::RA)
1134 {
1135 GuideWEN[0].value = 0;
1136 GuideWEN[1].value = 0;
1137 GuideWENP.s = state;
1138 IDSetNumber(&GuideWENP, nullptr);
1139 }
1140 else
1141 {
1142 GuideNSN[0].value = 0;
1143 GuideNSN[1].value = 0;
1144 GuideNSNP.s = state;
1145 IDSetNumber(&GuideNSNP, nullptr);
1146 }
1147}
1148
1149void MXHDTelescope::completeTimedGuidePulse(GuideAxis axis, uint64_t token, bool sendStop)
1150{
1151 bool shouldStop = false;
1152 {
1153 std::lock_guard<std::mutex> lock(m_guideMutex);
1154 if (token != m_guideTokenGen.load() || !m_guideActive || axis != m_guideAxis)
1155 return;
1156
1157 m_guideActive = false;
1158 m_guideDirection = 0;
1159 ++m_guideTokenGen;
1160 shouldStop = sendStop && isConnected();
1161 }
1162
1163 if (shouldStop)
1164 (void)mxhdSend(":Q#");
1165
1166 setGuidePropertyIdle(axis, IPS_OK);
1167}
1168
1170{
1171 return startTimedGuidePulse(GuideAxis::DEC, +1, ms, "@mN#");
1172}
1173
1175{
1176 return startTimedGuidePulse(GuideAxis::DEC, -1, ms, "@mS#");
1177}
1178
1180{
1181 return startTimedGuidePulse(GuideAxis::RA, +1, ms, "@mE#");
1182}
1183
1185{
1186 return startTimedGuidePulse(GuideAxis::RA, -1, ms, "@mW#");
1187}
1188
1189bool MXHDTelescope::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
1190{
1191 if (dev && std::strcmp(dev, getDeviceName()) != 0)
1192 return false;
1193
1194 if (std::strcmp(name, HomeSP.name) == 0)
1195 {
1196 IUUpdateSwitch(&HomeSP, states, names, n);
1197 if (HomeS[0].s == ISS_ON)
1198 {
1199 if (!mxhdSend("@OG#"))
1200 {
1201 HomeSP.s = IPS_ALERT;
1202 }
1203 else
1204 {
1205 HomeSP.s = IPS_BUSY;
1206 m_homeCommandActive = true;
1207 m_unparkCommandActive = false;
1208 m_isSlewing = true;
1209 m_isHoming = true;
1211 }
1212 HomeS[0].s = ISS_OFF;
1213 IDSetSwitch(&HomeSP, nullptr);
1214 }
1215 return true;
1216 }
1217
1218 if (std::strcmp(name, "SIMULATE_PIER_SIDE") == 0)
1219 {
1220 setSimulatePierSide(true);
1221 LOG_INFO("MX-HD uses HA-based pier side, so SIMULATE_PIER_SIDE is forced to enabled.");
1222 return true;
1223 }
1224
1225 return INDI::Telescope::ISNewSwitch(dev, name, states, names, n);
1226}
1227
1228bool MXHDTelescope::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
1229{
1230 if (dev && std::strcmp(dev, getDeviceName()) != 0)
1231 return false;
1232
1233 if (std::strcmp(name, GuideNSNP.name) == 0)
1234 {
1235 IUUpdateNumber(&GuideNSNP, values, names, n);
1236 const uint32_t northMs = static_cast<uint32_t>(GuideNSN[0].value < 0 ? 0 : GuideNSN[0].value);
1237 const uint32_t southMs = static_cast<uint32_t>(GuideNSN[1].value < 0 ? 0 : GuideNSN[1].value);
1238
1239 if (northMs > 0 && southMs > 0)
1240 {
1241 setGuidePropertyIdle(GuideAxis::DEC, IPS_ALERT);
1242 return true;
1243 }
1244
1245 if (northMs > 0)
1246 GuideNSNP.s = GuideNorth(northMs);
1247 else if (southMs > 0)
1248 GuideNSNP.s = GuideSouth(southMs);
1249 else
1250 GuideNSNP.s = IPS_IDLE;
1251
1252 if (GuideNSNP.s != IPS_BUSY)
1253 setGuidePropertyIdle(GuideAxis::DEC, GuideNSNP.s);
1254 else
1255 IDSetNumber(&GuideNSNP, nullptr);
1256 return true;
1257 }
1258
1259 if (std::strcmp(name, GuideWENP.name) == 0)
1260 {
1261 IUUpdateNumber(&GuideWENP, values, names, n);
1262 const uint32_t westMs = static_cast<uint32_t>(GuideWEN[0].value < 0 ? 0 : GuideWEN[0].value);
1263 const uint32_t eastMs = static_cast<uint32_t>(GuideWEN[1].value < 0 ? 0 : GuideWEN[1].value);
1264
1265 if (westMs > 0 && eastMs > 0)
1266 {
1267 setGuidePropertyIdle(GuideAxis::RA, IPS_ALERT);
1268 return true;
1269 }
1270
1271 if (westMs > 0)
1272 GuideWENP.s = GuideWest(westMs);
1273 else if (eastMs > 0)
1274 GuideWENP.s = GuideEast(eastMs);
1275 else
1276 GuideWENP.s = IPS_IDLE;
1277
1278 if (GuideWENP.s != IPS_BUSY)
1279 setGuidePropertyIdle(GuideAxis::RA, GuideWENP.s);
1280 else
1281 IDSetNumber(&GuideWENP, nullptr);
1282 return true;
1283 }
1284
1285 return INDI::Telescope::ISNewNumber(dev, name, values, names, n);
1286}
1287
1289{
1290 // Status bytes
1291 uint8_t b1 = 0, b2 = 0, b3 = 0;
1292 if (mxhdReadStatus(b1, b2, b3))
1293 decodeStatus(b1, b2, b3);
1294
1295 // Abort finalization sequence advance (if any)
1296 maybeAdvanceAbortFinalize();
1297
1298 if (m_abortFinalizeActive)
1300 else if (m_isHoming || m_isSlewing)
1301 TrackState = m_parkCommandActive ? SCOPE_PARKING : SCOPE_SLEWING;
1302 else if (isParked())
1304 else
1305 TrackState = m_trackingEnabled ? SCOPE_TRACKING : SCOPE_IDLE;
1306
1307 // During slew/home motions MX-HD can temporarily stop answering :GR#/:GD#.
1308 // Avoid polling coordinates until the mount returns to an idle/tracking state.
1309 if (!m_abortFinalizeActive && !m_isHoming && !m_isSlewing)
1310 {
1311 double ra = 0.0, dec = 0.0;
1312 if (mxhdQueryRaDec(ra, dec))
1313 {
1314 // Push latest coordinates through the INDI telescope base helper.
1315 NewRaDec(ra, dec);
1316 mxhdUpdatePierSide(ra);
1317 }
1318 }
1319
1320 // Park state: when mount becomes idle after a park command, mark parked.
1321 if (m_parkCommandActive && !m_isSlewing && !m_isHoming)
1322 {
1323 SetParked(true);
1324 m_parkCommandActive = false;
1326 }
1327
1328 // Unpark is modeled as "go home, then clear parked" so the mount regains a known pose.
1329 if (m_unparkCommandActive && !m_isSlewing && !m_isHoming)
1330 {
1331 SetParked(false);
1332 m_unparkCommandActive = false;
1333 TrackState = m_trackingEnabled ? SCOPE_TRACKING : SCOPE_IDLE;
1334 }
1335
1336 // Home state: treat end of homing as OK.
1337 if (m_homeCommandActive && HomeSP.s == IPS_BUSY && !m_isHoming && !m_isSlewing)
1338 {
1339 m_homeCommandActive = false;
1340 HomeSP.s = IPS_OK;
1341 IDSetSwitch(&HomeSP, nullptr);
1342 }
1343
1344 return true;
1345}
1346
1348{
1349 if (!isConnected())
1350 return;
1351
1354}
1355
1356// INDI boilerplate entry points
1357static std::unique_ptr<MXHDTelescope> s_telescope;
1358
1359void ISGetProperties(const char *dev)
1360{
1361 if (!s_telescope)
1362 s_telescope.reset(new MXHDTelescope());
1363 s_telescope->ISGetProperties(dev);
1364}
1365
1366void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
1367{
1368 if (s_telescope)
1369 s_telescope->ISNewSwitch(dev, name, states, names, n);
1370}
1371
1372void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
1373{
1374 if (s_telescope)
1375 s_telescope->ISNewNumber(dev, name, values, names, n);
1376}
1377
1378void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
1379{
1380 if (s_telescope)
1381 s_telescope->ISNewText(dev, name, texts, names, n);
1382}
1383
1384void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n)
1385{
1386 if (s_telescope)
1387 s_telescope->ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n);
1388}
1389
1391{
1392 if (s_telescope)
1393 s_telescope->ISSnoopDevice(root);
1394}
bool isConnected() const
void detach()
indicates that the device is being removed
virtual bool Disconnect()
Disconnect from device.
void setDefaultPollingPeriod(uint32_t msec)
setDefaultPollingPeriod Change the default polling period to call TimerHit() function in the driver.
void setDriverInterface(uint32_t value)
setInterface Set driver interface. By default the driver interface is set to GENERAL_DEVICE....
void setVersion(uint16_t vMajor, uint16_t vMinor)
Set driver version information to be defined in DRIVER_INFO property as vMajor.vMinor.
virtual bool deleteProperty(const char *propertyName)
Delete a property and unregister it. It will also be deleted from all clients.
void defineProperty(INumberVectorProperty *property)
uint32_t getCurrentPollingPeriod() const
getCurrentPollingPeriod Return the current polling period.
int SetTimer(uint32_t ms)
Set a timer to call the function TimerHit after ms milliseconds.
virtual bool Connect()
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
TelescopeStatus TrackState
void SetTelescopeCapability(uint32_t cap, uint8_t slewRateCount)
SetTelescopeCapability sets the Telescope capabilities. All capabilities must be initialized.
virtual bool initProperties() override
Called to initialize basic properties required all the time.
bool isParked()
isParked is mount currently parked?
virtual int AddTrackMode(const char *name, const char *label, bool isDefault=false)
AddTrackMode.
virtual bool updateProperties() override
Called when connected state changes, to add/remove properties.
virtual void SetParked(bool isparked)
SetParked Change the mount parking status. The data park file (stored in ~/.indi/ParkData....
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
@ TELESCOPE_HAS_PIER_SIDE_SIMULATION
void NewRaDec(double ra, double dec)
The child class calls this function when it has updates.
void setPierSide(TelescopePierSide side)
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
bool queryStatus3(const std::string &cmd, uint8_t &b1, uint8_t &b2, uint8_t &b3, int timeoutMs, std::string &err)
bool queryHash(const std::string &cmd, std::string &response, int timeoutMs, std::string &err)
bool writeString(const std::string &s, std::string &err)
bool queryAck(const std::string &cmd, char &ack, int timeoutMs, std::string &err)
void TimerHit() override
Called when setTimer() time is up.
IPState GuideWest(uint32_t ms)
bool Disconnect() override
Disconnect from device.
bool ReadScopeStatus() override
Read telescope status.
IPState GuideNorth(uint32_t ms)
bool SetSlewRate(int index) override
SetSlewRate Set desired slew rate index.
bool Sync(double ra, double dec) override
Set the telescope current RA and DEC coordinates to the supplied RA and DEC coordinates.
bool updateLocation(double latitude, double longitude, double elevation) override
Update telescope location settings.
bool SetTrackMode(uint8_t mode) override
SetTrackMode Set active tracking mode. Do not change track state.
IPState GuideSouth(uint32_t ms)
bool Abort() override
Abort any telescope motion including tracking if possible.
bool Handshake() override
perform handshake with device to check communication
bool initProperties() override
Called to initialize basic properties required all the time.
bool updateTime(ln_date *utc, double utc_offset) override
Update telescope time, date, and UTC offset.
bool Park() override
Park the telescope to its home position.
const char * getDefaultName() override
bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override
Start or Stop the telescope motion in the direction dir.
IPState GuideEast(uint32_t ms)
bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
bool SetTrackEnabled(bool enabled) override
SetTrackEnabled Engages or disengages mount tracking. If there are no tracking modes available,...
bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override
Move the telescope in the direction dir.
bool updateProperties() override
Called when connected state changes, to add/remove properties.
bool Goto(double ra, double dec) override
Move the scope to the supplied RA and DEC coordinates.
bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
bool UnPark() override
Unpark the telescope if already parked.
const char * MAIN_CONTROL_TAB
MAIN_CONTROL_TAB Where all the primary controls for the device are located.
const char * MOTION_TAB
MOTION_TAB Where all the motion control properties of the device are located.
void setSimulatePierSide(bool value)
double ra
double dec
#define min(a, b)
ISState
Switch state.
Definition indiapi.h:148
@ ISS_OFF
Definition indiapi.h:149
@ ISS_ON
Definition indiapi.h:150
@ IP_RW
Definition indiapi.h:184
IPState
Property state.
Definition indiapi.h:158
@ IPS_BUSY
Definition indiapi.h:161
@ IPS_ALERT
Definition indiapi.h:162
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
@ ISR_ATMOST1
Definition indiapi.h:172
INDI_DIR_WE
@ DIRECTION_EAST
INDI_DIR_NS
@ DIRECTION_NORTH
void IUFillNumberVector(INumberVectorProperty *nvp, INumber *np, int nnp, const char *dev, const char *name, const char *label, const char *group, IPerm p, double timeout, IPState s)
Assign attributes for a number vector property. The vector's auxiliary elements will be set to NULL.
Definition indidevapi.c:490
void IUFillSwitch(ISwitch *sp, const char *name, const char *label, ISState s)
Assign attributes for a switch property. The switch's auxiliary elements will be set to NULL.
Definition indidevapi.c:376
void IUFillNumber(INumber *np, const char *name, const char *label, const char *format, double min, double max, double step, double value)
Assign attributes for a number property. The number's auxiliary elements will be set to NULL.
Definition indidevapi.c:398
void IUFillSwitchVector(ISwitchVectorProperty *svp, ISwitch *sp, int nsp, const char *dev, const char *name, const char *label, const char *group, IPerm p, ISRule r, double timeout, IPState s)
Assign attributes for a switch vector property. The vector's auxiliary elements will be set to NULL.
Definition indidevapi.c:453
int IUUpdateSwitch(ISwitchVectorProperty *svp, ISState *states, char *names[], int n)
Update all switches in a switch vector property.
void IDSetNumber(const INumberVectorProperty *nvp, const char *fmt,...)
void IDSetSwitch(const ISwitchVectorProperty *svp, const char *fmt,...)
int IUUpdateNumber(INumberVectorProperty *nvp, double values[], char *names[], int n)
Update all numbers in a number vector property.
#define LOGF_WARN(fmt,...)
Definition indilogger.h:81
#define LOG_WARN(txt)
Definition indilogger.h:73
#define LOG_ERROR(txt)
Shorter logging macros. In order to use these macros, the function (or method) "getDeviceName()" must...
Definition indilogger.h:72
#define LOGF_ERROR(fmt,...)
Definition indilogger.h:80
#define LOG_INFO(txt)
Definition indilogger.h:74
#define getDeviceName
void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n)
Update data of an existing blob vector property.
void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
Update the value of an existing switch vector property.
void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
Update the value of an existing text vector property.
void ISGetProperties(const char *dev)
Get Device Properties.
void ISSnoopDevice(XMLEle *root)
Function defined by Drivers that is called when another Driver it is snooping (by having previously c...
void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
uint8_t cmd[4]
Definition pwc-ioctl.h:2
char name[MAXINDINAME]
Definition indiapi.h:321
char name[MAXINDINAME]
Definition indiapi.h:369