Instrument Neutral Distributed Interface INDI 2.2.5
alpaca_discovery.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2025 Jasem Mutlaq. All rights reserved.
3
4 INDI Alpaca Discovery Protocol
5
6 This program is free software; you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by the Free
8 Software Foundation; either version 2 of the License, or (at your option)
9 any later version.
10
11 This program is distributed in the hope that it will be useful, but WITHOUT
12 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 more details.
15
16 You should have received a copy of the GNU Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
20
21 The full GNU General Public License is included in this distribution in the
22 file called LICENSE.
23*******************************************************************************/
24
25#include "alpaca_discovery.h"
26#include "indilogger.h"
27
28#include <unistd.h>
29#include <fcntl.h>
30#include <string.h>
31#include <arpa/inet.h>
32#include <netdb.h>
33#include <ifaddrs.h>
34#include <poll.h>
35
36#ifdef _USE_SYSTEM_JSONLIB
37#include <nlohmann/json.hpp>
38#else
39#include <indijson.hpp>
40#endif
41
43
44// Discovery message
45// According to the ASCOM Alpaca API Reference, the discovery message should be "alpacadiscovery1"
46// where "1" is the version number. However, we check only for the prefix "alpacadiscovery"
47// to be more flexible and handle future versions.
48static const char* DISCOVERY_MESSAGE = "alpacadiscovery";
49
50AlpacaDiscovery::AlpacaDiscovery(int discoveryPort, int alpacaPort)
51 : m_DiscoveryPort(discoveryPort)
52 , m_AlpacaPort(alpacaPort)
53 , m_Running(false)
54 , m_StopRequested(false)
55{
56 DEBUGDEVICE("INDI Alpaca Server", INDI::Logger::DBG_SESSION, "Alpaca discovery initialized");
57}
58
60{
61 // Stop the discovery server if it's running
62 if (m_Running)
63 stop();
64
65 DEBUGDEVICE("INDI Alpaca Server", INDI::Logger::DBG_SESSION, "Alpaca discovery destroyed");
66}
67
69{
70 if (m_Running)
71 {
72 DEBUGDEVICE("INDI Alpaca Server", INDI::Logger::DBG_WARNING, "Discovery server already running");
73 return true;
74 }
75
76 m_StopRequested = false;
77 m_DiscoveryThread = std::thread(&AlpacaDiscovery::discoveryThreadFunc, this);
78
79 // Wait a bit to make sure server starts
80 std::this_thread::sleep_for(std::chrono::milliseconds(100));
81
82 if (!m_Running)
83 {
84 DEBUGDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "Failed to start discovery server");
85 return false;
86 }
87
88 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_SESSION, "Discovery server started on port %d", m_DiscoveryPort);
89 return true;
90}
91
93{
94 if (!m_Running)
95 {
96 DEBUGDEVICE("INDI Alpaca Server", INDI::Logger::DBG_WARNING, "Discovery server not running");
97 return true;
98 }
99
100 // Signal thread to stop
101 m_StopRequested = true;
102
103 // Wait for thread to finish
104 if (m_DiscoveryThread.joinable())
105 m_DiscoveryThread.join();
106
107 // Close all sockets
108 for (int socket : m_Sockets)
109 {
110 if (socket >= 0)
111 close(socket);
112 }
113 m_Sockets.clear();
114
115 m_Running = false;
116 DEBUGDEVICE("INDI Alpaca Server", INDI::Logger::DBG_SESSION, "Discovery server stopped");
117 return true;
118}
119
121{
122 return m_Running;
123}
124
126{
127 if (port > 0 && port < 65536)
128 {
129 m_DiscoveryPort = port;
130 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_SESSION, "Discovery port set to %d", port);
131 }
132 else
133 {
134 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "Invalid discovery port: %d", port);
135 }
136}
137
139{
140 return m_DiscoveryPort;
141}
142
144{
145 if (port > 0 && port < 65536)
146 {
147 m_AlpacaPort = port;
148 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_SESSION, "Alpaca port set to %d", port);
149 }
150 else
151 {
152 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "Invalid Alpaca port: %d", port);
153 }
154}
155
157{
158 return m_AlpacaPort;
159}
160
161void AlpacaDiscovery::discoveryThreadFunc()
162{
163 DEBUGDEVICE("INDI Alpaca Server", INDI::Logger::DBG_SESSION, "Starting discovery thread");
164
165 // Create IPv4 socket
166 int ipv4Socket = createIPv4Socket();
167 if (ipv4Socket >= 0)
168 {
169 m_Sockets.push_back(ipv4Socket);
170 DEBUGDEVICE("INDI Alpaca Server", INDI::Logger::DBG_SESSION, "IPv4 socket created successfully");
171 }
172 else
173 {
174 DEBUGDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "Failed to create IPv4 socket");
175 }
176
177 // Create IPv6 socket
178 int ipv6Socket = createIPv6Socket();
179 if (ipv6Socket >= 0)
180 {
181 m_Sockets.push_back(ipv6Socket);
182 DEBUGDEVICE("INDI Alpaca Server", INDI::Logger::DBG_SESSION, "IPv6 socket created successfully");
183 }
184 else
185 {
186 DEBUGDEVICE("INDI Alpaca Server", INDI::Logger::DBG_WARNING, "Failed to create IPv6 socket");
187 }
188
189 // Check if we have at least one socket
190 if (m_Sockets.empty())
191 {
192 DEBUGDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "No sockets created, discovery server cannot start");
193 return;
194 }
195
196 // Set up poll structures
197 std::vector<struct pollfd> fds;
198 for (int socket : m_Sockets)
199 {
200 struct pollfd pfd;
201 pfd.fd = socket;
202 pfd.events = POLLIN;
203 fds.push_back(pfd);
204 }
205
206 // Buffer for incoming messages
207 char buffer[1024];
208
209 // Mark as running
210 m_Running = true;
211
212 // Main loop
213 while (!m_StopRequested)
214 {
215 // Wait for data on any socket
216 int result = poll(fds.data(), fds.size(), 1000); // 1 second timeout
217
218 if (result < 0)
219 {
220 // Error
221 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "Poll error: %s", strerror(errno));
222 break;
223 }
224 else if (result == 0)
225 {
226 // Timeout, continue
227 continue;
228 }
229
230 // Check each socket
231 for (size_t i = 0; i < fds.size(); i++)
232 {
233 if (fds[i].revents & POLLIN)
234 {
235 // Data available on this socket
236 struct sockaddr_storage senderAddr;
237 socklen_t senderAddrLen = sizeof(senderAddr);
238
239 // Receive data
240 ssize_t bytesRead = recvfrom(fds[i].fd, buffer, sizeof(buffer) - 1, 0,
241 (struct sockaddr*)&senderAddr, &senderAddrLen);
242
243 if (bytesRead > 0)
244 {
245 // Null-terminate the buffer
246 buffer[bytesRead] = '\0';
247
248 // Process the request
249 processDiscoveryRequest(fds[i].fd, buffer, bytesRead,
250 (struct sockaddr*)&senderAddr, senderAddrLen);
251 }
252 }
253 }
254 }
255
256 // Close all sockets
257 for (int socket : m_Sockets)
258 {
259 if (socket >= 0)
260 close(socket);
261 }
262 m_Sockets.clear();
263
264 m_Running = false;
265 DEBUGDEVICE("INDI Alpaca Server", INDI::Logger::DBG_SESSION, "Discovery thread stopped");
266}
267
268int AlpacaDiscovery::createIPv4Socket()
269{
270 // Create socket
271 int sock = socket(AF_INET, SOCK_DGRAM, 0);
272 if (sock < 0)
273 {
274 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "Failed to create IPv4 socket: %s", strerror(errno));
275 return -1;
276 }
277
278 // Set socket options
279 int optval = 1;
280 if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
281 {
282 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "Failed to set SO_REUSEADDR: %s", strerror(errno));
283 close(sock);
284 return -1;
285 }
286
287#ifdef SO_REUSEPORT
288 if (setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)) < 0)
289 {
290 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_WARNING, "Failed to set SO_REUSEPORT: %s", strerror(errno));
291 }
292#endif
293
294 // Bind to the discovery port
295 struct sockaddr_in addr;
296 memset(&addr, 0, sizeof(addr));
297 addr.sin_family = AF_INET;
298 addr.sin_addr.s_addr = htonl(INADDR_ANY);
299 addr.sin_port = htons(m_DiscoveryPort);
300
301 if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0)
302 {
303 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "Failed to bind IPv4 socket: %s", strerror(errno));
304 close(sock);
305 return -1;
306 }
307
308 return sock;
309}
310
311int AlpacaDiscovery::createIPv6Socket()
312{
313 // Create socket
314 int sock = socket(AF_INET6, SOCK_DGRAM, 0);
315 if (sock < 0)
316 {
317 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "Failed to create IPv6 socket: %s", strerror(errno));
318 return -1;
319 }
320
321 // Set socket options
322 int optval = 1;
323 if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
324 {
325 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "Failed to set SO_REUSEADDR: %s", strerror(errno));
326 close(sock);
327 return -1;
328 }
329
330#ifdef SO_REUSEPORT
331 if (setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)) < 0)
332 {
333 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_WARNING, "Failed to set SO_REUSEPORT: %s", strerror(errno));
334 }
335#endif
336
337 // Allow IPv4 connections on IPv6 socket
338 optval = 0;
339 if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &optval, sizeof(optval)) < 0)
340 {
341 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_WARNING, "Failed to clear IPV6_V6ONLY: %s", strerror(errno));
342 }
343
344 // Bind to the discovery port
345 struct sockaddr_in6 addr;
346 memset(&addr, 0, sizeof(addr));
347 addr.sin6_family = AF_INET6;
348 addr.sin6_addr = in6addr_any;
349 addr.sin6_port = htons(m_DiscoveryPort);
350
351 if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0)
352 {
353 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "Failed to bind IPv6 socket: %s", strerror(errno));
354 close(sock);
355 return -1;
356 }
357
358 // Join IPv6 multicast group
359 struct ipv6_mreq mreq;
360 memset(&mreq, 0, sizeof(mreq));
361
362 // Set the multicast address (ff12::a1:9aca - Alpaca IPv6 multicast address)
363 if (inet_pton(AF_INET6, "ff12::a1:9aca", &mreq.ipv6mr_multiaddr) <= 0)
364 {
365 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "Failed to set multicast address: %s", strerror(errno));
366 close(sock);
367 return -1;
368 }
369
370 // Set the interface index (0 = any interface)
371 mreq.ipv6mr_interface = 0;
372
373 if (setsockopt(sock, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) < 0)
374 {
375 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR, "Failed to join multicast group: %s", strerror(errno));
376 close(sock);
377 return -1;
378 }
379
380 return sock;
381}
382
383void AlpacaDiscovery::processDiscoveryRequest(int socket, const char* buffer, size_t length,
384 const struct sockaddr* senderAddr, socklen_t senderAddrLen)
385{
386 INDI_UNUSED(length);
387
388 // Check if the message is a discovery request
389 if (strncmp(buffer, DISCOVERY_MESSAGE, strlen(DISCOVERY_MESSAGE)) == 0)
390 {
391 // Log the request
392 char addrStr[INET6_ADDRSTRLEN];
393 uint16_t port = 0;
394
395 if (senderAddr->sa_family == AF_INET)
396 {
397 struct sockaddr_in* addr = (struct sockaddr_in*)senderAddr;
398 inet_ntop(AF_INET, &addr->sin_addr, addrStr, sizeof(addrStr));
399 port = ntohs(addr->sin_port);
400 }
401 else if (senderAddr->sa_family == AF_INET6)
402 {
403 struct sockaddr_in6* addr = (struct sockaddr_in6*)senderAddr;
404 inet_ntop(AF_INET6, &addr->sin6_addr, addrStr, sizeof(addrStr));
405 port = ntohs(addr->sin6_port);
406 }
407
408 // Extract version number if present
409 char version = '?';
410 if (length > strlen(DISCOVERY_MESSAGE))
411 version = buffer[strlen(DISCOVERY_MESSAGE)];
412
413 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_DEBUG,
414 "Received discovery request from %s:%d (message: %s, version: %c)",
415 addrStr, port, buffer, version);
416
417 // Send response
418 sendDiscoveryResponse(socket, senderAddr, senderAddrLen);
419 }
420}
421
422void AlpacaDiscovery::sendDiscoveryResponse(int socket, const struct sockaddr* senderAddr, socklen_t senderAddrLen)
423{
424 // Generate response message
425 std::string response = generateResponseMessage();
426
427 // Send response
428 ssize_t bytesSent = sendto(socket, response.c_str(), response.length(), 0, senderAddr, senderAddrLen);
429
430 if (bytesSent < 0)
431 {
432 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_ERROR,
433 "Failed to send discovery response: %s", strerror(errno));
434 }
435 else
436 {
437 DEBUGFDEVICE("INDI Alpaca Server", INDI::Logger::DBG_DEBUG,
438 "Sent discovery response: %s", response.c_str());
439 }
440}
441
442std::string AlpacaDiscovery::generateResponseMessage() const
443{
444 // Create JSON response
445 json response =
446 {
447 {"AlpacaPort", m_AlpacaPort}
448 };
449
450 return response.dump();
451}
AlpacaDiscovery(int discoveryPort=32227, int alpacaPort=11111)
Constructor.
void setDiscoveryPort(int port)
Set the discovery port.
bool start()
Start the discovery server.
~AlpacaDiscovery()
Destructor.
bool stop()
Stop the discovery server.
bool isRunning() const
Check if the discovery server is running.
void setAlpacaPort(int port)
Set the Alpaca API port.
int getDiscoveryPort() const
Get the discovery port.
int getAlpacaPort() const
Get the Alpaca API port.
a class to store JSON values
string_t dump(const int indent=-1, const char indent_char=' ', const bool ensure_ascii=false, const error_handler_t error_handler=error_handler_t::strict) const
serialization
int errno
#define INDI_UNUSED(x)
Definition indidevapi.h:132
#define DEBUGDEVICE(device, priority, msg)
Definition indilogger.h:60
#define DEBUGFDEVICE(device, priority, msg,...)
Definition indilogger.h:61
int fd
std::vector< uint8_t > buffer
basic_json<> json
default specialization