Instrument Neutral Distributed Interface INDI 2.2.5
indi_alpaca_focuser.cpp
Go to the documentation of this file.
1/*
2 INDI alpaca Focuser Driver
3
4 Copyright (C) 2024 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
21#include "indi_alpaca_focuser.h"
22#include <cstring>
23#include <memory>
24#include <cmath>
25
26static std::unique_ptr<AlpacaFocuser> alpacaFocuser(new AlpacaFocuser());
27
34
38
40{
41 return "Alpaca Focuser";
42}
43
45{
47
48 // Use built-in TCP connection with default alpaca.local:32323
49 tcpConnection->setDefaultHost("alpaca.local");
52 {
53 return Handshake();
54 });
55
56 // Device information
57 DeviceInfoTP[DESCRIPTION].fill("DESCRIPTION", "Description", "");
58 DeviceInfoTP[DRIVER_INFO].fill("DRIVER_INFO", "Driver Info", "");
59 DeviceInfoTP[DRIVER_VERSION].fill("DRIVER_VERSION", "Driver Version", "");
60 DeviceInfoTP[INTERFACE_VERSION].fill("INTERFACE_VERSION", "Interface Version", "");
61 DeviceInfoTP.fill(getDeviceName(), "DEVICE_INFO", "Device Info", OPTIONS_TAB, IP_RO, 60, IPS_IDLE);
62
63 // Temperature monitoring (read-only)
64 TemperatureNP[0].fill("TEMPERATURE", "Temperature (°C)", "%.2f", -50, 100, 0, 0);
65 TemperatureNP.fill(getDeviceName(), "FOCUS_TEMPERATURE", "Temperature", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE);
66
67 // Temperature compensation switch (only defined if the device supports it)
68 TempCompSP[TEMP_COMP_ENABLE].fill("TEMP_COMP_ENABLE", "Enable", ISS_OFF);
69 TempCompSP[TEMP_COMP_DISABLE].fill("TEMP_COMP_DISABLE", "Disable", ISS_ON);
70 TempCompSP.fill(getDeviceName(), "FOCUS_TEMP_COMP", "Temp. Compensation", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60,
71 IPS_IDLE);
72
74 setDefaultPollingPeriod(500); // Poll every 500ms
75
76 return true;
77}
78
80{
82
83 if (isConnected())
84 {
85 defineProperty(DeviceInfoTP);
86 defineProperty(TemperatureNP);
87
88 if (m_TempCompAvailable)
89 defineProperty(TempCompSP);
90 }
91 else
92 {
93 deleteProperty(DeviceInfoTP);
94 deleteProperty(TemperatureNP);
95 deleteProperty(TempCompSP);
96 }
97
98 return true;
99}
100
102{
103 return INDI::Focuser::Connect();
104}
105
107{
108 LOG_INFO("Connecting to alpaca Focuser...");
109
110 // Get host and port from TCP connection
111 if (tcpConnection)
112 {
113 m_Host = tcpConnection->host();
114 m_Port = tcpConnection->port();
115 }
116
117 // Create HTTP client
118 m_AlpacaClient.reset(new httplib::Client(m_Host, m_Port));
119 if (!m_AlpacaClient)
120 {
121 LOG_ERROR("Failed to create HTTP client");
122 return false;
123 }
124
125 m_AlpacaClient->set_connection_timeout(5, 0);
126 m_AlpacaClient->set_read_timeout(10, 0);
127
128 // Test connection with /connected endpoint
129 nlohmann::json response;
130 if (!sendAlpacaGET("/connected", response))
131 {
132 LOG_ERROR("Failed to connect to Alpaca device");
133 return false;
134 }
135
136 LOG_INFO("Connected to Alpaca device");
137
138 // Connect the device
139 if (!sendAlpacaPUT("/connected", "Connected=true", response))
140 {
141 LOG_ERROR("Failed to set device connected state");
142 return false;
143 }
144
145 // Query device information
146 if (sendAlpacaGET("/description", response) && response.contains("Value"))
147 {
148 std::string desc = response["Value"].get<std::string>();
149 DeviceInfoTP[DESCRIPTION].setText(desc.c_str());
150 }
151
152 if (sendAlpacaGET("/driverinfo", response) && response.contains("Value"))
153 {
154 std::string info = response["Value"].get<std::string>();
155 DeviceInfoTP[DRIVER_INFO].setText(info.c_str());
156 }
157
158 if (sendAlpacaGET("/driverversion", response) && response.contains("Value"))
159 {
160 std::string ver = response["Value"].get<std::string>();
161 DeviceInfoTP[DRIVER_VERSION].setText(ver.c_str());
162 }
163
164 if (sendAlpacaGET("/interfaceversion", response) && response.contains("Value"))
165 {
166 int ifver = response["Value"].get<int>();
167 DeviceInfoTP[INTERFACE_VERSION].setText(std::to_string(ifver).c_str());
168 }
169
170 DeviceInfoTP.apply();
171
172 // Setup focuser
173 if (!setupFocuser())
174 {
175 LOG_ERROR("Failed to setup focuser");
176 return false;
177 }
178
179 LOG_INFO("alpaca Focuser connected successfully");
181 return true;
182}
183
185{
186 LOG_INFO("Disconnecting alpaca Focuser...");
187
188 // Disconnect the device
189 nlohmann::json response;
190 sendAlpacaPUT("/connected", "Connected=false", response);
191
192 m_AlpacaClient.reset();
193
194 LOG_INFO("alpaca Focuser disconnected");
195 return true;
196}
197
198bool AlpacaFocuser::setupFocuser()
199{
200 nlohmann::json response;
201
202 // Query maximum position
203 if (sendAlpacaGET("/maxstep", response) && response.contains("Value"))
204 {
205 int maxStep = response["Value"].get<int>();
206 FocusMaxPosNP[0].setValue(maxStep);
209 LOGF_INFO("Focuser max position: %d steps", maxStep);
210 }
211
212 // Query current position
213 int currentPos = getPosition();
214 if (currentPos >= 0)
215 {
216 FocusAbsPosNP[0].setValue(currentPos);
219 LOGF_INFO("Current focuser position: %d steps", currentPos);
220 }
221
222 // Query temperature
223 if (sendAlpacaGET("/temperature", response) && response.contains("Value"))
224 {
225 double temp = response["Value"].get<double>();
226 TemperatureNP[0].setValue(temp);
227 TemperatureNP.apply();
228 LOGF_INFO("Focuser temperature: %.2f°C", temp);
229 }
230
231 // Check if absolute positioning is supported (should be true)
232 if (sendAlpacaGET("/absolute", response) && response.contains("Value"))
233 {
234 bool absolute = response["Value"].get<bool>();
235 if (!absolute)
236 {
237 LOG_WARN("Focuser does not support absolute positioning!");
238 }
239 else
240 {
241 LOG_INFO("Absolute positioning confirmed");
242 }
243 }
244
245 // Check if temperature compensation is available
246 if (sendAlpacaGET("/tempcompavailable", response) && response.contains("Value"))
247 {
248 m_TempCompAvailable = response["Value"].get<bool>();
249 LOGF_INFO("Temperature compensation available: %s", m_TempCompAvailable ? "yes" : "no");
250
251 if (m_TempCompAvailable && sendAlpacaGET("/tempcomp", response) && response.contains("Value"))
252 {
253 bool tempComp = response["Value"].get<bool>();
254 TempCompSP.reset();
255 TempCompSP[tempComp ? TEMP_COMP_ENABLE : TEMP_COMP_DISABLE].setState(ISS_ON);
256 TempCompSP.setState(IPS_OK);
257 }
258 }
259
260 return true;
261}
262
264{
265 // Validate range
266 if (targetTicks > FocusMaxPosNP[0].getValue())
267 {
268 LOGF_ERROR("Target position %u exceeds maximum %u", targetTicks,
269 static_cast<uint32_t>(FocusMaxPosNP[0].getValue()));
270 return IPS_ALERT;
271 }
272
273 LOGF_INFO("Moving to absolute position: %u", targetTicks);
274
275 nlohmann::json response;
276 std::string data = "Position=" + std::to_string(targetTicks);
277
278 if (!sendAlpacaPUT("/move", data, response))
279 {
280 LOGF_ERROR("Failed to move to position %u", targetTicks);
281 return IPS_ALERT;
282 }
283
284 // Check for errors
285 if (response.contains("ErrorNumber"))
286 {
287 int errorNum = response["ErrorNumber"].get<int>();
288 if (errorNum != 0)
289 {
290 std::string errorMsg = response.contains("ErrorMessage") ?
291 response["ErrorMessage"].get<std::string>() : "Unknown error";
292 LOGF_ERROR("Error moving focuser: %d - %s", errorNum, errorMsg.c_str());
293 return IPS_ALERT;
294 }
295 }
296
297 m_TargetPosition = targetTicks;
298 m_Moving = true;
299
300 return IPS_BUSY;
301}
302
304{
305 LOG_INFO("Aborting focuser movement");
306
307 nlohmann::json response;
308
309 if (!sendAlpacaPUT("/halt", "", response))
310 {
311 LOG_ERROR("Failed to halt focuser");
312 return false;
313 }
314
315 m_Moving = false;
318
319 LOG_INFO("Focuser movement halted");
320 return true;
321}
322
324{
325 if (!isConnected())
326 return;
327
328 // Update temperature
329 nlohmann::json response;
330 if (sendAlpacaGET("/temperature", response) && response.contains("Value"))
331 {
332 double temp = response["Value"].get<double>();
333 if (std::abs(temp - TemperatureNP[0].getValue()) > 0.1)
334 {
335 TemperatureNP[0].setValue(temp);
336 TemperatureNP.apply();
337 }
338 }
339
340 // Update temperature compensation state
341 if (m_TempCompAvailable && sendAlpacaGET("/tempcomp", response) && response.contains("Value"))
342 {
343 bool tempComp = response["Value"].get<bool>();
344 int activeIndex = tempComp ? TEMP_COMP_ENABLE : TEMP_COMP_DISABLE;
345 if (TempCompSP[activeIndex].getState() != ISS_ON)
346 {
347 TempCompSP.reset();
348 TempCompSP[activeIndex].setState(ISS_ON);
349 TempCompSP.setState(IPS_OK);
350 TempCompSP.apply();
351 }
352 }
353
354 // Check if moving
355 if (m_Moving)
356 {
357 bool moving = isMoving();
358
359 if (!moving)
360 {
361 // Movement completed
362 m_Moving = false;
363
364 // Update current position
365 int currentPos = getPosition();
366 if (currentPos >= 0)
367 {
368 FocusAbsPosNP[0].setValue(currentPos);
371
372 LOGF_INFO("Focuser reached position: %d", currentPos);
373 }
374 }
375 else
376 {
377 // Still moving, update current position
378 int currentPos = getPosition();
379 if (currentPos >= 0)
380 {
381 FocusAbsPosNP[0].setValue(currentPos);
383 }
384 }
385 }
386
388}
389
390bool AlpacaFocuser::isMoving()
391{
392 nlohmann::json response;
393
394 if (!sendAlpacaGET("/ismoving", response))
395 {
396 return false;
397 }
398
399 if (response.contains("Value"))
400 {
401 return response["Value"].get<bool>();
402 }
403
404 return false;
405}
406
407int AlpacaFocuser::getPosition()
408{
409 nlohmann::json response;
410
411 if (!sendAlpacaGET("/position", response))
412 {
413 return -1;
414 }
415
416 if (response.contains("Value"))
417 {
418 return response["Value"].get<int>();
419 }
420
421 return -1;
422}
423
424bool AlpacaFocuser::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
425{
426 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
427 {
428 if (TempCompSP.isNameMatch(name))
429 {
430 TempCompSP.update(states, names, n);
431
432 bool enable = TempCompSP[TEMP_COMP_ENABLE].getState() == ISS_ON;
433
434 nlohmann::json response;
435 std::string data = std::string("TempComp=") + (enable ? "true" : "false");
436
437 if (!sendAlpacaPUT("/tempcomp", data, response))
438 {
439 LOG_ERROR("Failed to set temperature compensation");
440 TempCompSP.setState(IPS_ALERT);
441 TempCompSP.apply();
442 return false;
443 }
444
445 LOGF_INFO("Temperature compensation %s", enable ? "enabled" : "disabled");
446 TempCompSP.setState(IPS_OK);
447 TempCompSP.apply();
448 return true;
449 }
450 }
451
452 return INDI::Focuser::ISNewSwitch(dev, name, states, names, n);
453}
454
455bool AlpacaFocuser::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
456{
457 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
458 {
459 // Process parent first
460 }
461
462 return INDI::Focuser::ISNewText(dev, name, texts, names, n);
463}
464
465bool AlpacaFocuser::sendAlpacaGET(const std::string &endpoint, nlohmann::json &response)
466{
467 if (!m_AlpacaClient)
468 return false;
469
470 std::string url = "/api/v1/focuser/" + std::to_string(m_DeviceNumber) + endpoint;
471
472 LOGF_DEBUG("GET %s", url.c_str());
473
474 auto res = m_AlpacaClient->Get(url.c_str());
475
476 if (!res)
477 {
478 LOGF_ERROR("HTTP GET failed for %s", url.c_str());
479 return false;
480 }
481
482 if (res->status != 200)
483 {
484 LOGF_ERROR("HTTP GET returned status %d for %s", res->status, url.c_str());
485 return false;
486 }
487
488 try
489 {
490 response = nlohmann::json::parse(res->body);
491
492 // Check for Alpaca errors
493 if (response.contains("ErrorNumber"))
494 {
495 int errorNum = response["ErrorNumber"].get<int>();
496 if (errorNum != 0)
497 {
498 std::string errorMsg = response.contains("ErrorMessage") ?
499 response["ErrorMessage"].get<std::string>() : "Unknown error";
500 LOGF_WARN("Alpaca error %d: %s", errorNum, errorMsg.c_str());
501 return false;
502 }
503 }
504
505 return true;
506 }
507 catch (const nlohmann::json::exception &e)
508 {
509 LOGF_ERROR("JSON parse error: %s", e.what());
510 return false;
511 }
512}
513
514bool AlpacaFocuser::sendAlpacaPUT(const std::string &endpoint, const std::string &data, nlohmann::json &response)
515{
516 if (!m_AlpacaClient)
517 return false;
518
519 std::string url = "/api/v1/focuser/" + std::to_string(m_DeviceNumber) + endpoint;
520
521 // Add client info to data
522 std::string fullData = data;
523 if (!fullData.empty())
524 fullData += "&";
525 fullData += "ClientID=" + std::to_string(m_ClientID);
526 fullData += "&ClientTransactionID=" + std::to_string(++m_TransactionID);
527
528 LOGF_DEBUG("PUT %s: %s", url.c_str(), fullData.c_str());
529
530 auto res = m_AlpacaClient->Put(url.c_str(), fullData, "application/x-www-form-urlencoded");
531
532 if (!res)
533 {
534 LOGF_ERROR("HTTP PUT failed for %s", url.c_str());
535 return false;
536 }
537
538 if (res->status != 200)
539 {
540 LOGF_ERROR("HTTP PUT returned status %d for %s", res->status, url.c_str());
541 return false;
542 }
543
544 try
545 {
546 response = nlohmann::json::parse(res->body);
547
548 // Check for Alpaca errors
549 if (response.contains("ErrorNumber"))
550 {
551 int errorNum = response["ErrorNumber"].get<int>();
552 if (errorNum != 0)
553 {
554 std::string errorMsg = response.contains("ErrorMessage") ?
555 response["ErrorMessage"].get<std::string>() : "Unknown error";
556 LOGF_WARN("Alpaca error %d: %s", errorNum, errorMsg.c_str());
557 return false;
558 }
559 }
560
561 return true;
562 }
563 catch (const nlohmann::json::exception &e)
564 {
565 LOGF_ERROR("JSON parse error: %s", e.what());
566 return false;
567 }
568}
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Processes incoming new switch states from the client for this device. Handles updates to FocuserInter...
virtual const char * getDefaultName() override
virtual bool AbortFocuser() override
AbortFocuser all focus motion.
virtual bool updateProperties() override
Updates the state and definition of properties based on the current connection status....
virtual void TimerHit() override
Callback function to be called once SetTimer duration elapses.
virtual IPState MoveAbsFocuser(uint32_t targetTicks) override
MoveFocuser the focuser to an absolute position.
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Processes incoming new text values from the client for this device. Delegates processing to the contr...
virtual bool Disconnect() override
Disconnect from device.
virtual bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
virtual bool initProperties() override
Initializes the default properties for the focuser device. Sets up standard INDI properties,...
virtual bool Handshake() override
Performs a device-specific handshake after a connection is established. This method should be overrid...
void registerHandshake(std::function< bool()> callback)
registerHandshake Register a handshake function to be called once the initial connection to the devic...
void setDefaultHost(const char *addressHost)
void setDefaultPort(uint32_t addressPort)
virtual const char * host() const
virtual uint32_t port() const
bool isConnected() const
void setDefaultPollingPeriod(uint32_t msec)
setDefaultPollingPeriod Change the default polling period to call TimerHit() function in the driver.
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...
void addDebugControl()
Add Debug control to the driver.
void SetCapability(uint32_t cap)
FI::SetCapability sets the focuser capabilities. All capabilities must be initialized.
INDI::PropertyNumber FocusMaxPosNP
INDI::PropertyNumber FocusAbsPosNP
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Processes incoming new switch states from the client for this device. Handles updates to FocuserInter...
virtual bool updateProperties() override
Updates the state and definition of properties based on the current connection status....
virtual bool initProperties() override
Initializes the default properties for the focuser device. Sets up standard INDI properties,...
Connection::TCP * tcpConnection
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Processes incoming new text values from the client for this device. Delegates processing to the contr...
void setSupportedConnections(const uint8_t &value)
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
bool isNameMatch(const char *otherName) const
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
bool update(const ISState states[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, ISRule rule, double timeout, IPState state)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
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
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 * MAIN_CONTROL_TAB
MAIN_CONTROL_TAB Where all the primary controls for the device are located.
const char * OPTIONS_TAB
OPTIONS_TAB Where all the driver's options are located. Those may include auxiliary controls,...
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
@ IP_RO
Definition indiapi.h:182
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_1OFMANY
Definition indiapi.h:171
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#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