Instrument Neutral Distributed Interface INDI 2.2.5
indi_alpaca_telescope.cpp
Go to the documentation of this file.
1/*
2 INDI alpaca Driver
3
4 Copyright (C) 2026 Gord Tulloch
5
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Lesser General Public
8 License as published by the Free Software Foundation; either
9 version 2.1 of the License, or (at your option) any later version.
10
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Lesser General Public License for more details.
15
16 You should have received a copy of the GNU Lesser General Public
17 License along with this library; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19*/
20
23#include <memory>
24#include <cstring>
25#include <cmath>
26#include <indicom.h>
27#include <libnova/julian_day.h>
28
29// We declare an auto pointer to alpacaTelescopeDriver instance
30std::unique_ptr<alpacaTelescopeDriver> alpaca(new alpacaTelescopeDriver());
31
32#define RA_AXIS 0
33#define DEC_AXIS 1
34
36{
37 DBG_SCOPE = static_cast<uint32_t>(INDI::Logger::getInstance().addDebugLevel("Scope Verbose", "SCOPE"));
38
39 setVersion(1, 0);
46 4
47 );
49 m_ClientID = getpid();
50 LOG_DEBUG("Initializing from alpacaTelescope device...");
51}
52
54{
55 return "Alpaca Telescope";
56}
57
59{
61
62 // Use built-in TCP connection with default alpaca.local:32323
63 tcpConnection->setDefaultHost("alpaca.local");
65
66 // Device info
67 DeviceInfoTP[DESCRIPTION].fill("DESCRIPTION", "Description", "");
68 DeviceInfoTP[DRIVER_INFO].fill("DRIVER_INFO", "Driver Info", "");
69 DeviceInfoTP[DRIVER_VERSION].fill("DRIVER_VERSION", "Driver Version", "");
70 DeviceInfoTP[INTERFACE_VERSION].fill("INTERFACE_VERSION", "Interface Version", "");
71 DeviceInfoTP.fill(getDeviceName(), "DEVICE_INFO", "Device Info", OPTIONS_TAB, IP_RO, 60, IPS_IDLE);
72
74
75 // RA is a rotating frame, while HA or Alt/Az is not
77
78 /* Add debug controls so we may debug driver if necessary */
80
81 return true;
82}
83
85{
87
88 if (isConnected())
89 {
90 defineProperty(DeviceInfoTP);
91
92 if (InitPark())
93 {
94 // Load park data if available
95 m_currentRA = ParkPositionNP[AXIS_RA].getValue();
96 m_currentDEC = ParkPositionNP[AXIS_DE].getValue();
99 }
100 else
101 {
102 SetAxis1Park(-6.);
103 SetAxis2Park(0.);
106 }
107 }
108 else
109 {
110 deleteProperty(DeviceInfoTP);
111 }
112
113 return true;
114}
115
117{
118 std::string host = tcpConnection->host();
119 int port = tcpConnection->port();
120
121 LOGF_INFO("Connecting to alpaca server at %s:%d", host.c_str(), port);
122
123 httpClient = std::make_unique<httplib::Client>(host.c_str(), port);
124 httpClient->set_connection_timeout(10);
125 httpClient->set_read_timeout(30); // Increased timeout for commands that may take time to respond
126
127 nlohmann::json response;
128 if (!sendAlpacaGET("/connected", response))
129 {
130 LOG_ERROR("Failed to connect to alpaca");
131 httpClient.reset();
132 return false;
133 }
134
135 LOGF_INFO("alpaca reachable, connected=%s",
136 response.value("Value", false) ? "true" : "false");
137
138 nlohmann::json request;
139 request["Connected"] = true;
140 if (!sendAlpacaPUT("/connected", request, response))
141 {
142 LOG_ERROR("Failed to set connected state");
143 httpClient.reset();
144 return false;
145 }
146
147 // Query device capabilities, location, time, and initial status now that the
148 // HTTP client is ready (Handshake() also defines TrackModeSP via AddTrackMode(),
149 // which must happen before updateProperties() is called)
150 Handshake();
151
152 // Start the timer
154
155 LOG_INFO("Successfully connected to alpaca");
156 return true;
157}
158
160{
161 if (httpClient)
162 {
163 nlohmann::json request, response;
164 request["Connected"] = false;
165 sendAlpacaPUT("/connected", request, response);
166 httpClient.reset();
167 }
168 LOG_INFO("Disconnected from alpaca");
169 return true;
170}
171
173{
174 nlohmann::json response;
175
176 if (sendAlpacaGET("/description", response) && response.contains("Value"))
177 DeviceInfoTP[DESCRIPTION].setText(response["Value"].get<std::string>().c_str());
178
179 if (sendAlpacaGET("/driverinfo", response) && response.contains("Value"))
180 DeviceInfoTP[DRIVER_INFO].setText(response["Value"].get<std::string>().c_str());
181
182 if (sendAlpacaGET("/driverversion", response) && response.contains("Value"))
183 DeviceInfoTP[DRIVER_VERSION].setText(response["Value"].get<std::string>().c_str());
184
185 if (sendAlpacaGET("/interfaceversion", response) && response.contains("Value"))
186 DeviceInfoTP[INTERFACE_VERSION].setText(std::to_string(response["Value"].get<int>()).c_str());
187
188 DeviceInfoTP.apply();
189
190 // Query alignment mode to determine mount behavior
191 // 0 = algPolar (equatorial), 1 = algAltAz, 2 = algGermanPolar
192 if (sendAlpacaGET("/alignmentmode", response) && response.contains("Value"))
193 {
194 int alignmentMode = response["Value"].get<int>();
195 if (alignmentMode == 1)
196 {
197 LOG_INFO("Telescope alignment mode: Alt-Az (no meridian flips)");
198 }
199 else if (alignmentMode == 0 || alignmentMode == 2)
200 {
201 LOG_INFO("Telescope alignment mode: Equatorial (Fork mount - no meridian flips)");
202 }
203 }
204
205 bool canPark = false;
206 if (sendAlpacaGET("/canpark", response) && response.contains("Value"))
207 canPark = response["Value"].get<bool>();
208
209 if (canPark)
210 {
212
213 // Initialize park position if not already set
214 // Default to zenith (RA=0, Dec=observer latitude)
215 if (!InitPark())
216 {
217 // Set default park position to zenith
218 SetAxis1Park(0);
219 SetAxis2Park(90);
220 LOG_INFO("Park position initialized to zenith");
221 }
222
223 // Get current park state
224 if (sendAlpacaGET("/atpark", response) && response.contains("Value"))
225 {
226 isParked = response["Value"].get<bool>();
227 SetParked(isParked);
228 }
229 }
230
231 // Get site location from device
232 double latitude = 0.0, longitude = 0.0;
233 bool locationValid = false;
234
235 if (sendAlpacaGET("/sitelatitude", response) && response.contains("Value"))
236 {
237 latitude = response["Value"].get<double>();
238 locationValid = true;
239 LOGF_INFO("Site latitude from device: %.6f°", latitude);
240 }
241 else
242 {
243 LOG_WARN("Failed to get site latitude from device");
244 }
245
246 if (sendAlpacaGET("/sitelongitude", response) && response.contains("Value"))
247 {
248 longitude = response["Value"].get<double>();
249 LOGF_INFO("Site longitude from device: %.6f°", longitude);
250 }
251 else
252 {
253 LOG_WARN("Failed to get site longitude from device");
254 locationValid = false;
255 }
256
257 // Site elevation is optional - default to 0 if the device doesn't report it
258 double elevation = 0.0;
259 if (sendAlpacaGET("/siteelevation", response) && response.contains("Value"))
260 {
261 elevation = response["Value"].get<double>();
262 LOGF_INFO("Site elevation from device: %.1fm", elevation);
263 }
264 else
265 {
266 LOG_WARN("Failed to get site elevation from device");
267 }
268
269 // Update INDI location property if we got valid data
270 if (locationValid)
271 {
272 // INDI uses: LOCATION_LATITUDE = latitude (positive north), LOCATION_LONGITUDE = longitude (positive east)
273 // Alpaca uses same convention
274 LocationNP[LOCATION_LATITUDE].setValue(latitude);
275 LocationNP[LOCATION_LONGITUDE].setValue(longitude);
276 LocationNP[LOCATION_ELEVATION].setValue(elevation);
279
280 // Calculate and cache sin/cos for coordinate transformations
281 double latRad = latitude * M_PI / 180.0;
282 m_sinLat = sin(latRad);
283 m_cosLat = cos(latRad);
284
285 LOGF_INFO("Site location set: Lat=%.6f° Long=%.6f°", latitude, longitude);
286 }
287
288 // Get UTC date/time from device
289 if (sendAlpacaGET("/utcdate", response) && response.contains("Value"))
290 {
291 std::string utcDate = response["Value"].get<std::string>();
292 TimeTP[UTC].setText(utcDate.c_str());
293 TimeTP[OFFSET].setText("0");
295 TimeTP.apply();
296 LOGF_INFO("Device UTC date/time: %s", utcDate.c_str());
297 }
298 else
299 {
300 LOG_WARN("Failed to get UTC date/time from device, using system time");
302 }
303
304 // Query tracking capability
305 if (sendAlpacaGET("/cansettracking", response) && response.contains("Value"))
306 {
307 if (response["Value"].get<bool>())
308 {
310 LOG_INFO("Telescope supports tracking control - TELESCOPE_TRACK_STATE enabled");
311 }
312 else
313 {
314 LOG_INFO("Telescope does not support tracking control");
315 }
316 }
317 else
318 {
319 LOG_WARN("Failed to query tracking capability from device");
320 }
321
322 // Query whether the mount supports the non-blocking slewtotargetasync/slewtoaltazasync
323 // commands. If not, fall back to the blocking slewtotarget/slewtoaltaz commands.
324 if (sendAlpacaGET("/canslewasync", response) && response.contains("Value"))
325 m_CanSlewAsync = response["Value"].get<bool>();
326 else
327 m_CanSlewAsync = false;
328 LOGF_INFO("Telescope supports async slew: %s", m_CanSlewAsync ? "Yes" : "No");
329
330 // Query the list of supported tracking rates (Alpaca DriveRate enum: 0=Sidereal,
331 // 1=Lunar, 2=Solar, 3=King) and build the TrackModeSP property accordingly.
332 m_TrackModeAlpacaRates.clear();
334 if (sendAlpacaGET("/trackingrates", response) && response.contains("Value") && response["Value"].is_array())
335 {
336 static const struct { int rate; const char *name; const char *label; } alpacaRates[] =
337 {
338 {0, "TRACK_SIDEREAL", "Sidereal"},
339 {1, "TRACK_LUNAR", "Lunar"},
340 {2, "TRACK_SOLAR", "Solar"},
341 {3, "TRACK_KING", "King"},
342 };
343
344 for (const auto &entry : alpacaRates)
345 {
346 for (const auto &value : response["Value"])
347 {
348 if (value.get<int>() == entry.rate)
349 {
350 AddTrackMode(entry.name, entry.label, entry.rate == 0);
351 m_TrackModeAlpacaRates.push_back(entry.rate);
352 break;
353 }
354 }
355 }
356 }
357
358 if (!m_TrackModeAlpacaRates.empty())
359 {
361 LOGF_INFO("Telescope supports %zu tracking rate(s)", m_TrackModeAlpacaRates.size());
362
363 // Sync the current tracking rate
364 if (sendAlpacaGET("/trackingrate", response) && response.contains("Value"))
365 {
366 int currentRate = response["Value"].get<int>();
367 for (size_t i = 0; i < m_TrackModeAlpacaRates.size(); i++)
368 {
369 if (m_TrackModeAlpacaRates[i] == currentRate)
370 {
373 break;
374 }
375 }
376 }
377 }
378 else
379 {
380 LOG_WARN("Failed to query supported tracking rates from device");
381 }
382
383 // Query support for custom (offset) tracking rates
384 m_CanSetRaRate = sendAlpacaGET("/cansetrightascensionrate", response) && response.contains("Value")
385 && response["Value"].get<bool>();
386 m_CanSetDecRate = sendAlpacaGET("/cansetdeclinationrate", response) && response.contains("Value")
387 && response["Value"].get<bool>();
388
389 if (m_CanSetRaRate && m_CanSetDecRate)
390 {
392 LOG_INFO("Telescope supports custom tracking rates - TELESCOPE_TRACK_RATE enabled");
393 }
394 else
395 {
396 LOG_INFO("Telescope does not support custom tracking rates");
397 }
398
399 // Get initial coordinates
401
402 LOG_INFO("alpaca connected successfully");
403 return true;
404}
405
407{
408 nlohmann::json response;
409
410 // Get RA/Dec
411 double newRA = m_currentRA, newDec = m_currentDEC;
412
413 // Get current coordinates from Alpaca API
414 bool raUpdated = false, decUpdated = false;
415 if (sendAlpacaGET("/rightascension", response) && response.contains("Value"))
416 {
417 newRA = response["Value"].get<double>();
418 raUpdated = true;
419 }
420 else
421 {
422 LOG_WARN("Failed to get RA from Alpaca API");
423 }
424
425 if (sendAlpacaGET("/declination", response) && response.contains("Value"))
426 {
427 newDec = response["Value"].get<double>();
428 decUpdated = true;
429 }
430 else
431 {
432 LOG_WARN("Failed to get Dec from Alpaca API");
433 }
434
435 // Update current coordinates
436 if (raUpdated && decUpdated)
437 {
438 m_currentRA = newRA;
439 m_currentDEC = newDec;
440
441 // Always call NewRaDec to update client display
442 // This is critical during slewing to show real-time position updates
443 NewRaDec(m_currentRA, m_currentDEC);
444 }
445 else
446 {
447 LOG_WARN("NewRaDec NOT called - coordinate update failed");
448 }
449
450 // Get slewing state
451 if (sendAlpacaGET("/slewing", response) && response.contains("Value"))
452 isSlewing = response["Value"].get<bool>();
453
454 // Get tracking state
455 if (sendAlpacaGET("/tracking", response) && response.contains("Value"))
456 isTracking = response["Value"].get<bool>();
457
458 // Keep TrackModeSP in sync with the device's current tracking rate
459 if (HasTrackMode() && sendAlpacaGET("/trackingrate", response) && response.contains("Value"))
460 {
461 int currentRate = response["Value"].get<int>();
462 int currentIndex = TrackModeSP.findOnSwitchIndex();
463 for (size_t i = 0; i < m_TrackModeAlpacaRates.size(); i++)
464 {
465 if (m_TrackModeAlpacaRates[i] == currentRate && static_cast<int>(i) != currentIndex)
466 {
471 break;
472 }
473 }
474 }
475
476 // Get park state
477 if (sendAlpacaGET("/atpark", response) && response.contains("Value"))
478 {
479 bool parked = response["Value"].get<bool>();
480 if (parked != isParked)
481 {
482 isParked = parked;
483 SetParked(isParked);
484 }
485 }
486
487 // Update telescope state
489 {
490 // If we're parking and slewing stopped, we've reached park position
491 if (!isSlewing)
492 {
494 LOG_INFO("Parking complete - telescope at park position");
495 }
496 // else keep SCOPE_PARKING state while slewing to park position
497 }
498 else if (isSlewing)
500 else if (isTracking)
502 else if (isParked)
504 else
506
507 return true;
508}
509
511{
512 // Check if telescope is parked - must unpark before slewing
513 if (isParked)
514 {
515 LOG_ERROR("Cannot GoTo while parked. Please unpark the telescope first.");
516 return false;
517 }
518
519 LOGF_INFO("GoTo command: RA=%f Dec=%f", ra, dec);
520
521 // Use ASCOM Alpaca pattern: set target coordinates first, then slew
522 // Step 1: Set target RA
523 nlohmann::json response;
524 nlohmann::json raRequest;
525 raRequest["TargetRightAscension"] = ra;
526 if (!sendAlpacaPUT("/targetrightascension", raRequest, response))
527 {
528 LOG_ERROR("Failed to set target RA");
529 return false;
530 }
531
532 // Step 2: Set target Dec
533 nlohmann::json decRequest;
534 decRequest["TargetDeclination"] = dec;
535 if (!sendAlpacaPUT("/targetdeclination", decRequest, response))
536 {
537 LOG_ERROR("Failed to set target Dec");
538 return false;
539 }
540
541 // Step 3: Start slew to target. Prefer the non-blocking /slewtotargetasync if the
542 // device supports it, since /slewtotarget blocks the HTTP request (and therefore
543 // this driver) until the slew completes.
544 nlohmann::json emptyRequest;
545 const char *slewEndpoint = m_CanSlewAsync ? "/slewtotargetasync" : "/slewtotarget";
546 if (!sendAlpacaPUT(slewEndpoint, emptyRequest, response))
547 {
548 LOG_ERROR("Failed to send GoTo command");
549 return false;
550 }
551
553 LOGF_INFO("GoTo command sent (%s) - slewing to target", m_CanSlewAsync ? "async" : "sync");
554 return true;
555}
556
558{
559 nlohmann::json request, response;
560 request["RightAscension"] = ra;
561 request["Declination"] = dec;
562
563 if (!sendAlpacaPUT("/synctocoordinates", request, response))
564 {
565 LOG_ERROR("Failed to sync");
566 return false;
567 }
568
569 LOG_INFO("Sync successful");
570 return true;
571}
572
574{
575 nlohmann::json request, response;
576
577 // Check if we're parking - parking cannot be aborted via AbortSlew
579 {
580 LOG_WARN("Cannot abort parking operation - park must complete");
581 return false;
582 }
583
584 if (!sendAlpacaPUT("/abortslew", request, response))
585 {
586 LOG_ERROR("Failed to abort");
587 return false;
588 }
589
591 LOG_INFO("Slew aborted");
592 return true;
593}
594
596{
597 nlohmann::json request, response;
598
599 // Abort any ongoing slew first
600 if (isSlewing)
601 {
602 LOG_INFO("Aborting slew before park");
603 nlohmann::json abortReq, abortResp;
604 sendAlpacaPUT("/abortslew", abortReq, abortResp);
605 }
606
607 LOG_INFO("Sending park command to Alpaca device");
608
609 // Use Alpaca park command
610 if (!sendAlpacaPUT("/park", request, response))
611 {
612 LOG_ERROR("Failed to send park command to Alpaca device");
613 return false;
614 }
615
616 // Mark as parked - set state to PARKED not PARKING since command is sent
617 isParked = true;
618 SetParked(true);
620 LOG_INFO("Park command sent - telescope parked");
621
622 return true;
623}
624
626{
627 nlohmann::json request, response;
628
629 LOG_INFO("Sending unpark command to Alpaca device");
630
631 // Use Alpaca unpark command
632 if (!sendAlpacaPUT("/unpark", request, response))
633 {
634 LOG_ERROR("Failed to send unpark command to Alpaca device");
635 return false;
636 }
637
638 // Immediately mark as unparked so INDI allows motion commands
639 // ReadScopeStatus will sync with actual device state
640 isParked = false;
641 SetParked(false);
643 LOG_INFO("Unpark command sent - telescope unparked");
644
645 return true;
646}
647
649{
650 // Save current RA/Dec position as park position
651 // This allows user to park at current position via "Park Options -> Set Current"
652
653 // Get current coordinates
654 nlohmann::json response;
655 double ra = 0, dec = 0;
656
657 if (sendAlpacaGET("/rightascension", response) && response.contains("Value"))
658 {
659 ra = response["Value"].get<double>();
660 }
661 else
662 {
663 LOG_ERROR("Failed to get current RA for SetCurrentPark");
664 return false;
665 }
666
667 if (sendAlpacaGET("/declination", response) && response.contains("Value"))
668 {
669 dec = response["Value"].get<double>();
670 }
671 else
672 {
673 LOG_ERROR("Failed to get current Dec for SetCurrentPark");
674 return false;
675 }
676
677 // Save current position as park position
678 SetAxis1Park(ra); // RA in hours
679 SetAxis2Park(dec); // Dec in degrees
680
681 LOGF_INFO("Park position set to current coordinates: RA=%.6f hours, Dec=%.6f degrees", ra, dec);
682
683 return true;
684}
685
687{
688 nlohmann::json request, response;
689 request["Tracking"] = enabled;
690
691 if (!sendAlpacaPUT("/tracking", request, response))
692 {
693 LOGF_ERROR("Failed to %s tracking", enabled ? "enable" : "disable");
694 return false;
695 }
696
697 isTracking = enabled;
698 LOGF_INFO("Tracking %s", enabled ? "enabled" : "disabled");
699 return true;
700}
701
703{
704 if (mode >= m_TrackModeAlpacaRates.size())
705 {
706 LOG_ERROR("Invalid track mode index");
707 return false;
708 }
709
710 nlohmann::json request, response;
711 request["TrackingRate"] = m_TrackModeAlpacaRates[mode];
712
713 if (!sendAlpacaPUT("/trackingrate", request, response))
714 {
715 LOG_ERROR("Failed to set track mode");
716 return false;
717 }
718
719 LOGF_INFO("Track mode set to %s", TrackModeSP[mode].getLabel());
720 return true;
721}
722
723bool alpacaTelescopeDriver::SetTrackRate(double raRate, double deRate)
724{
725 if (!m_CanSetRaRate || !m_CanSetDecRate)
726 {
727 LOG_WARN("Custom tracking rates are not supported by this telescope");
728 return false;
729 }
730
731 nlohmann::json request, response;
732
733 // Alpaca's RightAscensionRate is expressed in seconds of RA per sidereal second,
734 // while INDI's raRate is in arcseconds/second. 1 second of RA = 15 arcseconds, and
735 // the sidereal/solar second difference (~0.27%) is negligible for this offset.
736 request["RightAscensionRate"] = raRate / 15.0;
737 if (!sendAlpacaPUT("/rightascensionrate", request, response))
738 {
739 LOG_ERROR("Failed to set RA tracking rate");
740 return false;
741 }
742
743 request.clear();
744 request["DeclinationRate"] = deRate;
745 if (!sendAlpacaPUT("/declinationrate", request, response))
746 {
747 LOG_ERROR("Failed to set DEC tracking rate");
748 return false;
749 }
750
751 LOGF_INFO("Custom tracking rate set: RA=%.6f arcsec/s, DEC=%.6f arcsec/s", raRate, deRate);
752 return true;
753}
754
756{
757 nlohmann::json request, response;
758
759 // Secondary axis (Dec/Altitude): 1
760 // Positive rate = North/Up, Negative rate = South/Down
761 request["Axis"] = 1;
762
763 if (command == MOTION_START)
764 {
765 // Use moderate rate for manual movement (0.5 deg/sec)
766 // TODO: Query /axisrates to get valid rates and use client-selected slew rate
767 double rate = (dir == DIRECTION_NORTH) ? 0.5 : -0.5;
768 request["Rate"] = rate;
769 LOGF_INFO("Moving %s at rate %.2f deg/sec", (dir == DIRECTION_NORTH) ? "North" : "South", rate);
770 }
771 else
772 {
773 request["Rate"] = 0.0;
774 LOG_INFO("Stopping NS motion");
775 }
776
777 if (!sendAlpacaPUT("/moveaxis", request, response))
778 {
779 LOG_ERROR("Failed to move NS");
780 return false;
781 }
782
783 // Log response for debugging
784 if (response.contains("ErrorNumber"))
785 {
786 int errNum = response["ErrorNumber"].get<int>();
787 if (errNum != 0)
788 {
789 LOGF_WARN("MoveAxis NS returned error %d: %s", errNum,
790 response.value("ErrorMessage", "Unknown error").c_str());
791 }
792 }
793
794 return true;
795}
796
798{
799 nlohmann::json request, response;
800
801 // Primary axis (RA/Azimuth): 0
802 // Positive rate = East/Right, Negative rate = West/Left
803 request["Axis"] = 0;
804
805 if (command == MOTION_START)
806 {
807 // Use moderate rate for manual movement (0.5 deg/sec)
808 // TODO: Query /axisrates to get valid rates and use client-selected slew rate
809 double rate = (dir == DIRECTION_EAST) ? 0.5 : -0.5;
810 request["Rate"] = rate;
811 LOGF_INFO("Moving %s at rate %.2f deg/sec", (dir == DIRECTION_EAST) ? "East" : "West", rate);
812 }
813 else
814 {
815 request["Rate"] = 0.0;
816 LOG_INFO("Stopping WE motion");
817 }
818
819 if (!sendAlpacaPUT("/moveaxis", request, response))
820 {
821 LOG_ERROR("Failed to move WE");
822 return false;
823 }
824
825 // Log response for debugging
826 if (response.contains("ErrorNumber"))
827 {
828 int errNum = response["ErrorNumber"].get<int>();
829 if (errNum != 0)
830 {
831 LOGF_WARN("MoveAxis WE returned error %d: %s", errNum,
832 response.value("ErrorMessage", "Unknown error").c_str());
833 }
834 }
835
836 return true;
837}
838
840{
842 return true;
843}
844
845bool alpacaTelescopeDriver::updateLocation(double latitude, double longitude, double elevation)
846{
847 // Update location on the Alpaca device
848 nlohmann::json request, response;
849
850 // Set latitude
851 request["SiteLatitude"] = latitude;
852 if (!sendAlpacaPUT("/sitelatitude", request, response))
853 {
854 LOG_ERROR("Failed to set site latitude on device");
855 return false;
856 }
857
858 // Set longitude
859 request.clear();
860 request["SiteLongitude"] = longitude;
861 if (!sendAlpacaPUT("/sitelongitude", request, response))
862 {
863 LOG_ERROR("Failed to set site longitude on device");
864 return false;
865 }
866
867 // Set elevation (not all Alpaca devices support this, so just warn on failure)
868 request.clear();
869 request["SiteElevation"] = elevation;
870 if (!sendAlpacaPUT("/siteelevation", request, response))
871 LOG_WARN("Failed to set site elevation on device");
872
873 // Calculate and cache sin/cos for coordinate transformations
874 double latRad = latitude * M_PI / 180.0;
875 m_sinLat = sin(latRad);
876 m_cosLat = cos(latRad);
877
878 LOGF_INFO("Site location updated: Lat=%.6f° Long=%.6f°", latitude, longitude);
879
880 // Call base class to update INDI properties
881 return INDI::Telescope::updateLocation(latitude, longitude, elevation);
882}
883
884bool alpacaTelescopeDriver::updateTime(ln_date *utc, double utc_offset)
885{
886 INDI_UNUSED(utc_offset);
887
888 // Alpaca utcdate is always UTC, expressed as an ISO 8601 string
889 char isoDate[32];
890 snprintf(isoDate, sizeof(isoDate), "%04d-%02d-%02dT%02d:%02d:%06.3fZ",
891 utc->years, utc->months, utc->days, utc->hours, utc->minutes, utc->seconds);
892
893 nlohmann::json request, response;
894 request["UTCDate"] = isoDate;
895 if (!sendAlpacaPUT("/utcdate", request, response))
896 {
897 LOG_ERROR("Failed to set UTC date/time on device");
898 return false;
899 }
900
901 return true;
902}
903
904// Alpaca helper methods
905std::string alpacaTelescopeDriver::getAlpacaURL(const std::string& endpoint)
906{
907 return "/api/v1/telescope/" + std::to_string(m_DeviceNumber) + endpoint;
908}
909
910bool alpacaTelescopeDriver::sendAlpacaGET(const std::string& endpoint, nlohmann::json& response)
911{
912 if (!httpClient)
913 {
914 LOG_ERROR("HTTP client not initialized");
915 return false;
916 }
917
918 std::string url = getAlpacaURL(endpoint);
919 url += "?ClientID=" + std::to_string(m_ClientID) +
920 "&ClientTransactionID=" + std::to_string(getTransactionId());
921
922 auto result = httpClient->Get(url.c_str());
923
924 if (!result)
925 {
926 LOGF_ERROR("GET %s failed: %s", endpoint.c_str(),
927 httplib::to_string(result.error()).c_str());
928 return false;
929 }
930
931 if (result->status != 200)
932 {
933 LOGF_ERROR("GET %s HTTP %d", endpoint.c_str(), result->status);
934 return false;
935 }
936
937 try
938 {
939 response = nlohmann::json::parse(result->body);
940
941 if (response.contains("ErrorNumber") && response["ErrorNumber"].get<int>() != 0)
942 {
943 LOGF_ERROR("Alpaca error %s: %d - %s", endpoint.c_str(),
944 response["ErrorNumber"].get<int>(),
945 response.value("ErrorMessage", "").c_str());
946 return false;
947 }
948
949 return true;
950 }
951 catch (const nlohmann::json::exception& e)
952 {
953 LOGF_ERROR("JSON parse error %s: %s", endpoint.c_str(), e.what());
954 return false;
955 }
956}
957
958bool alpacaTelescopeDriver::sendAlpacaPUT(const std::string& endpoint, const nlohmann::json& request,
959 nlohmann::json& response)
960{
961 if (!httpClient)
962 {
963 LOG_ERROR("HTTP client not initialized");
964 return false;
965 }
966
967 std::string url = getAlpacaURL(endpoint);
968
969 // Build form data - Alpaca expects form-encoded parameters
970 httplib::Params params;
971
972 for (auto& [key, value] : request.items())
973 {
974 if (value.is_string())
975 params.emplace(key, value.get<std::string>());
976 else if (value.is_boolean())
977 params.emplace(key, value.get<bool>() ? "true" : "false");
978 else if (value.is_number_integer())
979 params.emplace(key, std::to_string(value.get<int>()));
980 else if (value.is_number())
981 params.emplace(key, std::to_string(value.get<double>()));
982 }
983
984 params.emplace("ClientID", std::to_string(m_ClientID));
985 params.emplace("ClientTransactionID", std::to_string(getTransactionId()));
986
987 auto result = httpClient->Put(url.c_str(), params);
988
989 if (!result)
990 {
991 // Some Alpaca devices (like Seestar) close connection immediately after
992 // receiving /slewtotarget command. Log as debug instead of error.
993 if (endpoint == "/slewtotarget")
994 {
995 LOGF_DEBUG("PUT %s: connection closed (expected for some devices)", endpoint.c_str());
996 }
997 else
998 {
999 LOGF_ERROR("PUT %s failed: %s", endpoint.c_str(),
1000 httplib::to_string(result.error()).c_str());
1001 }
1002 return false;
1003 }
1004
1005 if (result->status != 200)
1006 {
1007 LOGF_ERROR("PUT %s HTTP %d", endpoint.c_str(), result->status);
1008 return false;
1009 }
1010
1011 try
1012 {
1013 response = nlohmann::json::parse(result->body);
1014
1015 if (response.contains("ErrorNumber") && response["ErrorNumber"].get<int>() != 0)
1016 {
1017 int errorNum = response["ErrorNumber"].get<int>();
1018 std::string errorMsg = response.value("ErrorMessage", "");
1019 LOGF_ERROR("Alpaca error %s: %d - %s", endpoint.c_str(), errorNum, errorMsg.c_str());
1020 return false;
1021 }
1022
1023 return true;
1024 }
1025 catch (const nlohmann::json::exception& e)
1026 {
1027 LOGF_ERROR("JSON parse error %s: %s", endpoint.c_str(), e.what());
1028 return false;
1029 }
1030}
1031
1032void ISGetProperties(const char *dev)
1033{
1034 alpaca->ISGetProperties(dev);
1035}
1036
1037void ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
1038{
1039 alpaca->ISNewSwitch(dev, name, states, names, n);
1040}
1041
1042void ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
1043{
1044 alpaca->ISNewText(dev, name, texts, names, n);
1045}
1046
1047void ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
1048{
1049 alpaca->ISNewNumber(dev, name, values, names, n);
1050}
1051
1052void ISNewBLOB(const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[],
1053 char *names[], int n)
1054{
1055 alpaca->ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n);
1056}
1057
1059{
1060 alpaca->ISSnoopDevice(root);
1061}
void setDefaultHost(const char *addressHost)
void setDefaultPort(uint32_t addressPort)
virtual const char * host() const
virtual uint32_t port() const
bool isConnected() const
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.
void addDebugControl()
Add Debug control to the driver.
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
void resize(size_t size)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
TelescopeStatus TrackState
void SetAxis1Park(double value)
SetRAPark Set current RA/AZ parking position. The data park file (stored in ~/.indi/ParkData....
void SetAxis1ParkDefault(double steps)
SetRAPark Set default RA/AZ parking position.
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.
void setTelescopeConnection(const uint8_t &value)
setTelescopeConnection Set telescope connection mode. Child class should call this in the constructor...
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....
Connection::TCP * tcpConnection
virtual bool updateLocation(double latitude, double longitude, double elevation)
Update telescope location settings.
uint32_t GetTelescopeCapability() const
GetTelescopeCapability returns the capability of the Telescope.
void NewRaDec(double ra, double dec)
The child class calls this function when it has updates.
bool InitPark()
InitPark Loads parking data (stored in ~/.indi/ParkData.xml) that contains parking status and parking...
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
void SetAxis2Park(double steps)
SetDEPark Set current DEC/ALT parking position. The data park file (stored in ~/.indi/ParkData....
void SetParkDataType(TelescopeParkData type)
setParkDataType Sets the type of parking data stored in the park data file and presented to the user.
void SetAxis2ParkDefault(double steps)
SetDEParkDefault Set default DEC/ALT parking position.
virtual bool Goto(double ra, double dec) override
Move the scope to the supplied RA and DEC coordinates.
virtual bool Disconnect() override
Disconnect from device.
virtual bool SetTrackEnabled(bool enabled) override
SetTrackEnabled Engages or disengages mount tracking. If there are no tracking modes available,...
virtual bool ReadScopeStatus() override
Read telescope status.
virtual bool updateLocation(double latitude, double longitude, double elevation) override
Update telescope location settings.
virtual bool Abort() override
Abort any telescope motion including tracking if possible.
virtual const char * getDefaultName() override
virtual bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
virtual bool UnPark() override
Unpark the telescope if already parked.
virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override
Move the telescope in the direction dir.
virtual bool initProperties() override
Called to initialize basic properties required all the time.
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual bool Sync(double ra, double dec) override
Set the telescope current RA and DEC coordinates to the supplied RA and DEC coordinates.
virtual bool Handshake() override
perform handshake with device to check communication
virtual bool SetCurrentPark() override
SetCurrentPark Set current coordinates/encoders value as the desired parking position.
virtual bool SetTrackMode(uint8_t mode) override
SetTrackMode Set active tracking mode. Do not change track state.
virtual bool updateTime(ln_date *utc, double utc_offset) override
Update telescope time, date, and UTC offset.
virtual bool updateProperties() override
Called when connected state changes, to add/remove properties.
virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override
Start or Stop the telescope motion in the direction dir.
virtual bool Park() override
Park the telescope to its home position.
virtual bool SetTrackRate(double raRate, double deRate) override
SetTrackRate Set custom tracking rates.
a class to store JSON values
bool contains(const typename object_t::key_type &key) const
check the existence of an element in a JSON object
void clear() noexcept
clears the contents
constexpr bool is_array() const noexcept
return whether value is an array
std::decay< ValueType >::type value(const KeyType &key, ValueType &&default_value) const
access specified object element with default value
auto get() const noexcept(noexcept(std::declval< const basic_json_t & >().template get_impl< ValueType >(detail::priority_tag< 4 > {}))) -> decltype(std::declval< const basic_json_t & >().template get_impl< ValueType >(detail::priority_tag< 4 > {}))
get a (pointer) value (explicit)
general exception of the basic_json class
const char * what() const noexcept override
returns the explanatory string
const char * OPTIONS_TAB
OPTIONS_TAB Where all the driver's options are located. Those may include auxiliary controls,...
#define M_PI
Definition dsp.h:52
INDI::PropertySwitch TrackModeSP
INDI::PropertyText TimeTP
INDI::PropertyNumber LocationNP
INDI::PropertyNumber ParkPositionNP
double ra
double dec
std::unique_ptr< alpacaTelescopeDriver > alpaca(new alpacaTelescopeDriver())
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)
ISState
Switch state.
Definition indiapi.h:148
@ ISS_ON
Definition indiapi.h:150
@ IP_RO
Definition indiapi.h:182
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
@ AXIS_DE
@ AXIS_RA
INDI_DIR_WE
@ DIRECTION_EAST
INDI_DIR_NS
@ DIRECTION_NORTH
Implementations for common driver routines.
#define INDI_UNUSED(x)
Definition indidevapi.h:132
#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 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
std::string to_string(Error error)
Definition httplib.h:9275
std::multimap< std::string, std::string > Params
Definition httplib.h:609
static Logger & getInstance()
Method to get a reference to the object (i.e., Singleton) It is a static method.
int addDebugLevel(const char *debugLevelName, const char *LoggingLevelName)
Adds a new debugging level to the driver.