Instrument Neutral Distributed Interface INDI 2.2.5
indi_alpaca_server.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2025 Jasem Mutlaq. All rights reserved.
3
4 INDI Alpaca Server
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 "indi_alpaca_server.h"
26#include "alpaca_client.h"
27#include "device_manager.h"
28#include "alpaca_discovery.h"
29#include "indilogger.h"
30#include "inditimer.h"
31
32#include <memory>
33#include <thread>
34#include <chrono>
35
36// We declare an auto pointer to INDIAlpacaServer
37std::unique_ptr<INDIAlpacaServer> indiAlpacaServer(new INDIAlpacaServer());
38
43
45{
46 // Stop the server if it's running
47 if (m_ServerRunning)
48 stopAlpacaServer();
49}
50
52{
53 return "INDI Alpaca Server";
54}
55
57{
59
60 // Server settings
61 ServerSettingsTP[0].fill("HOST", "Host", "0.0.0.0");
62 ServerSettingsTP[1].fill("PORT", "Port", "11111");
63 ServerSettingsTP.fill(getDeviceName(), "SERVER_SETTINGS", "Server", OPTIONS_TAB, IP_RW, 60, IPS_IDLE);
64
65 // INDI server settings
66 INDIServerSettingsTP[0].fill("HOST", "Host", "localhost");
67 INDIServerSettingsTP[1].fill("PORT", "Port", "7624");
68 INDIServerSettingsTP.fill(getDeviceName(), "INDI_SERVER_SETTINGS", "INDI Server", OPTIONS_TAB, IP_RW, 60, IPS_IDLE);
69
70 // Server control
71 ServerControlSP[0].fill("START", "Start", ISS_OFF);
72 ServerControlSP[1].fill("STOP", "Stop", ISS_OFF);
73 ServerControlSP.fill(getDeviceName(), "SERVER_CONTROL", "Control", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 0, IPS_IDLE);
74
75 // Connection settings
76 ConnectionSettingsNP[0].fill("TIMEOUT", "Timeout (sec)", "%.0f", 1, 30, 1, 5);
77 ConnectionSettingsNP[1].fill("RETRIES", "Max Retries", "%.0f", 1, 10, 1, 3);
78 ConnectionSettingsNP[2].fill("RETRY_DELAY", "Retry Delay (ms)", "%.0f", 100, 5000, 100, 1000);
79 ConnectionSettingsNP.fill(getDeviceName(), "CONNECTION_SETTINGS", "Connection", CONNECTION_TAB, IP_RW, 60, IPS_IDLE);
80
81 // Discovery settings
82 DiscoverySettingsNP[0].fill("PORT", "Discovery Port", "%.0f", 1, 65535, 1, 32227);
83 DiscoverySettingsNP.fill(getDeviceName(), "DISCOVERY_SETTINGS", "Discovery", OPTIONS_TAB, IP_RW, 60, IPS_IDLE);
84
85 // Startup delay settings
86 StartupDelayNP[0].fill("DELAY", "Startup Delay (sec)", "%.0f", 1, 60, 1, 3);
87 StartupDelayNP.fill(getDeviceName(), "STARTUP_DELAY", "Startup Delay", OPTIONS_TAB, IP_RW, 60, IPS_IDLE);
88
89 // Get device manager singleton
90 m_DeviceManager = DeviceManager::getInstance();
91
92 // Create INDI client
93 m_Client = std::make_shared<AlpacaClient>(m_DeviceManager);
94
95 // Set the AlpacaClient in the DeviceManager
96 m_DeviceManager->setAlpacaClient(m_Client);
97
99
100 return true;
101}
102
104{
106
107 defineProperty(ServerSettingsTP);
108 defineProperty(INDIServerSettingsTP);
109 defineProperty(ServerControlSP);
110 defineProperty(ConnectionSettingsNP);
111 defineProperty(DiscoverySettingsNP);
112 defineProperty(StartupDelayNP);
113}
114
116{
118
119 if (isConnected())
120 {
121 // Nothing to do here
122 }
123 else
124 {
125 // Nothing to do here
126 }
127
128 return true;
129}
130
131bool INDIAlpacaServer::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
132{
133 if (dev && !strcmp(dev, getDeviceName()))
134 {
135 // Server settings
136 if (ServerSettingsTP.isNameMatch(name))
137 {
138 ServerSettingsTP.update(texts, names, n);
139 ServerSettingsTP.setState(IPS_OK);
140 ServerSettingsTP.apply();
141 saveConfig(true, ServerSettingsTP.getName());
142 return true;
143 }
144
145 // INDI server settings
146 if (INDIServerSettingsTP.isNameMatch(name))
147 {
148 INDIServerSettingsTP.update(texts, names, n);
149 INDIServerSettingsTP.setState(IPS_OK);
150 INDIServerSettingsTP.apply();
151 saveConfig(true, INDIServerSettingsTP.getName());
152 return true;
153 }
154 }
155
156 return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n);
157}
158
159bool INDIAlpacaServer::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
160{
161 if (dev && !strcmp(dev, getDeviceName()))
162 {
163 // Connection settings
164 if (ConnectionSettingsNP.isNameMatch(name))
165 {
166 ConnectionSettingsNP.update(values, names, n);
167 ConnectionSettingsNP.setState(IPS_OK);
168 ConnectionSettingsNP.apply();
169 saveConfig(true, ConnectionSettingsNP.getName());
170 return true;
171 }
172
173 // Discovery settings
174 if (DiscoverySettingsNP.isNameMatch(name))
175 {
176 DiscoverySettingsNP.update(values, names, n);
177 DiscoverySettingsNP.setState(IPS_OK);
178 DiscoverySettingsNP.apply();
179
180 // Update discovery port if discovery server is running
181 if (m_Discovery && m_Discovery->isRunning())
182 {
183 m_Discovery->setDiscoveryPort(static_cast<int>(DiscoverySettingsNP[0].getValue()));
184 LOGF_INFO("Discovery port updated to %d", static_cast<int>(DiscoverySettingsNP[0].getValue()));
185 }
186
187 saveConfig(DiscoverySettingsNP);
188 return true;
189 }
190
191 // Startup delay settings
192 if (StartupDelayNP.isNameMatch(name))
193 {
194 StartupDelayNP.update(values, names, n);
195 StartupDelayNP.setState(IPS_OK);
196 StartupDelayNP.apply();
197 LOGF_INFO("Startup delay updated to %d seconds", static_cast<int>(StartupDelayNP[0].getValue()));
198 saveConfig(StartupDelayNP);
199 return true;
200 }
201 }
202
203 return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n);
204}
205
206bool INDIAlpacaServer::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
207{
208 if (dev && !strcmp(dev, getDeviceName()))
209 {
210 // Server control
211 if (ServerControlSP.isNameMatch(name))
212 {
213 ServerControlSP.update(states, names, n);
214 int index = ServerControlSP.findOnSwitchIndex();
215
216 switch (index)
217 {
218 case 0: // Start
219 if (!m_ServerRunning)
220 {
221 if (startAlpacaServer())
222 {
223 ServerControlSP[0].setState(ISS_ON);
224 ServerControlSP[1].setState(ISS_OFF);
225 ServerControlSP.setState(IPS_OK);
226 LOG_INFO("Alpaca server started");
227 }
228 else
229 {
230 ServerControlSP[0].setState(ISS_OFF);
231 ServerControlSP[1].setState(ISS_OFF);
232 ServerControlSP.setState(IPS_ALERT);
233 LOG_ERROR("Failed to start Alpaca server");
234 }
235 }
236 else
237 {
238 LOG_INFO("Alpaca server is already running");
239 ServerControlSP[0].setState(ISS_ON);
240 ServerControlSP[1].setState(ISS_OFF);
241 ServerControlSP.setState(IPS_OK);
242 }
243 break;
244
245 case 1: // Stop
246 if (m_ServerRunning)
247 {
248 if (stopAlpacaServer())
249 {
250 ServerControlSP[0].setState(ISS_OFF);
251 ServerControlSP[1].setState(ISS_OFF);
252 ServerControlSP.setState(IPS_IDLE);
253 LOG_INFO("Alpaca server stopped");
254 }
255 else
256 {
257 ServerControlSP[0].setState(ISS_ON);
258 ServerControlSP[1].setState(ISS_OFF);
259 ServerControlSP.setState(IPS_ALERT);
260 LOG_ERROR("Failed to stop Alpaca server");
261 }
262 }
263 else
264 {
265 LOG_INFO("Alpaca server is not running");
266 ServerControlSP[0].setState(ISS_OFF);
267 ServerControlSP[1].setState(ISS_OFF);
268 ServerControlSP.setState(IPS_IDLE);
269 }
270 break;
271 }
272
273 ServerControlSP.apply();
274 return true;
275 }
276 }
277
278 return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n);
279}
280
282{
284
285 ServerSettingsTP.save(fp);
286 INDIServerSettingsTP.save(fp);
287 ConnectionSettingsNP.save(fp);
288 DiscoverySettingsNP.save(fp);
289 StartupDelayNP.save(fp);
290
291 return true;
292}
293
295{
296 // Get the startup delay
297 int startupDelay = static_cast<int>(StartupDelayNP[0].getValue());
298 LOGF_INFO("Waiting %d seconds before connecting to INDI server...", startupDelay);
299
300 // Use a single-shot timer to delay the connection
301 INDI::Timer::singleShot(startupDelay * 1000, [this]()
302 {
303 // Connect to INDI server
304 m_Client->setServer(INDIServerSettingsTP[0].getText(), std::stoi(INDIServerSettingsTP[1].getText()));
305
306 if (m_Client->connectServer())
307 {
308 LOG_INFO("Connected to INDI server");
309
310 // Automatically start the Alpaca server when the driver is connected
311 if (!m_ServerRunning)
312 {
313 if (startAlpacaServer())
314 {
315 ServerControlSP[0].setState(ISS_ON);
316 ServerControlSP[1].setState(ISS_OFF);
317 ServerControlSP.setState(IPS_OK);
318 ServerControlSP.apply();
319 LOG_INFO("Alpaca server started automatically");
320 }
321 else
322 {
323 LOG_ERROR("Failed to start Alpaca server automatically");
324 }
325 }
326
328 setConnected(true);
330 }
331 else
332 {
333 LOG_ERROR("Failed to connect to INDI server");
334 setConnected(false);
336 }
337 });
338
339 return true;
340}
341
343{
344 // Stop Alpaca server if running
345 if (m_ServerRunning)
346 stopAlpacaServer();
347
348 // Disconnect from INDI server
349 if (m_Client->disconnectServer())
350 {
351 LOG_INFO("Disconnected from INDI server");
352 return true;
353 }
354 else
355 {
356 LOG_ERROR("Failed to disconnect from INDI server");
357 return false;
358 }
359}
360
362{
363 // Nothing to do here for now
365}
366
367bool INDIAlpacaServer::startAlpacaServer()
368{
369 if (m_ServerRunning)
370 {
371 LOG_INFO("Alpaca server is already running");
372 return true;
373 }
374
375 // Create HTTP server
376 m_Server = std::make_unique<httplib::Server>();
377
378 // Create discovery server
379 int discoveryPort = static_cast<int>(DiscoverySettingsNP[0].getValue());
380 int alpacaPort = std::stoi(ServerSettingsTP[1].getText());
381 m_Discovery = std::make_unique<AlpacaDiscovery>(discoveryPort, alpacaPort);
382
383 // Set up routes
384 m_Server->Get("/management/(.*)", [this](const httplib::Request & req, httplib::Response & res)
385 {
386 m_DeviceManager->handleAlpacaRequest(req, res);
387 });
388
389 m_Server->Get("/api/v1/(.*)", [this](const httplib::Request & req, httplib::Response & res)
390 {
391 m_DeviceManager->handleAlpacaRequest(req, res);
392 });
393
394 m_Server->Put("/api/v1/(.*)", [this](const httplib::Request & req, httplib::Response & res)
395 {
396 m_DeviceManager->handleAlpacaRequest(req, res);
397 });
398
399 // Setup API
400 m_Server->Get("/setup/v1/(.*)", [this](const httplib::Request & req, httplib::Response & res)
401 {
402 m_DeviceManager->handleSetupRequest(req, res);
403 });
404
405 // Start server thread
406 m_ServerThread = std::thread(&INDIAlpacaServer::serverThreadFunc, this);
407
408 // Wait a bit to make sure server starts
409 std::this_thread::sleep_for(std::chrono::milliseconds(100));
410
411 // Start discovery server
412 if (!m_Discovery->start())
413 {
414 LOG_ERROR("Failed to start Alpaca discovery server");
415 // Continue anyway, as the HTTP server is more important
416 }
417 else
418 {
419 LOGF_INFO("Alpaca discovery server started on port %d", discoveryPort);
420 }
421
422 m_ServerRunning = true;
423 return true;
424}
425
426bool INDIAlpacaServer::stopAlpacaServer()
427{
428 if (!m_ServerRunning)
429 {
430 LOG_INFO("Alpaca server is not running");
431 return true;
432 }
433
434 // Stop discovery server
435 if (m_Discovery)
436 {
437 if (m_Discovery->isRunning())
438 {
439 if (!m_Discovery->stop())
440 {
441 LOG_ERROR("Failed to stop Alpaca discovery server");
442 // Continue anyway to stop the HTTP server
443 }
444 else
445 {
446 LOG_INFO("Alpaca discovery server stopped");
447 }
448 }
449 m_Discovery.reset();
450 }
451
452 // Stop HTTP server
453 if (m_Server)
454 {
455 m_Server->stop();
456
457 // Wait for thread to finish
458 if (m_ServerThread.joinable())
459 m_ServerThread.join();
460
461 m_Server.reset();
462 }
463
464 m_ServerRunning = false;
465 return true;
466}
467
468void INDIAlpacaServer::serverThreadFunc()
469{
470 LOG_INFO("Starting Alpaca server thread");
471
472 // Get host and port
473 std::string host = ServerSettingsTP[0].getText();
474 int port = std::stoi(ServerSettingsTP[1].getText());
475
476 LOGF_INFO("Alpaca server listening on %s:%d", host.c_str(), port);
477
478 // Start server
479 m_Server->listen(host.c_str(), port);
480
481 LOG_INFO("Alpaca server thread stopped");
482}
void handleAlpacaRequest(const httplib::Request &req, httplib::Response &res)
void handleSetupRequest(const httplib::Request &req, httplib::Response &res)
static DeviceManager * getInstance()
void setAlpacaClient(const std::shared_ptr< AlpacaClient > &client)
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
bool Disconnect() override
Disconnect from device.
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
void TimerHit() override
Callback function to be called once SetTimer duration elapses.
bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
void ISGetProperties(const char *dev) override
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
const char * getDefaultName() override
bool isConnected() const
const char * getDeviceName() const
INDI::PropertyText getText(const char *name) const
virtual bool updateProperties()
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
virtual bool saveConfig(bool silent=false, const char *property=nullptr)
Save the current properties in a configuration file.
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
Process the client newSwitch command.
virtual void ISGetProperties(const char *dev)
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
virtual void setConnected(bool status, IPState state=IPS_OK, const char *msg=nullptr)
Set connection switch status in the client.
void setVersion(uint16_t vMajor, uint16_t vMinor)
Set driver version information to be defined in DRIVER_INFO property as vMajor.vMinor.
void defineProperty(INumberVectorProperty *property)
virtual bool saveConfigItems(FILE *fp)
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
uint32_t getCurrentPollingPeriod() const
getCurrentPollingPeriod Return the current polling period.
virtual bool initProperties()
Initialize properties initial state and value. The child class must implement this function.
void addAuxControls()
Add Debug, Simulation, and Configuration options to the driver.
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
Process the client newNumber command.
int SetTimer(uint32_t ms)
Set a timer to call the function TimerHit after ms milliseconds.
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
Process the client newSwitch command.
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
void save(FILE *f) const
save Save property to configuration file.
const char * getName() const
bool isNameMatch(const char *otherName) const
bool update(const double values[], const char *const names[], int n)
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)
bool update(const char *const texts[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
INDI::PropertyViewText * getText() const
static void singleShot(int msec, const std::function< void()> &callback)
This static function calls a the given function after a given time interval.
const char * CONNECTION_TAB
CONNECTION_TAB Where all device connection settings (serial, usb, ethernet) are defined and controlle...
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,...
std::unique_ptr< INDIAlpacaServer > indiAlpacaServer(new INDIAlpacaServer())
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
@ 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 LOG_ERROR(txt)
Shorter logging macros. In order to use these macros, the function (or method) "getDeviceName()" must...
Definition indilogger.h:72
#define LOG_INFO(txt)
Definition indilogger.h:74