Instrument Neutral Distributed Interface INDI 2.2.5
indi_alpaca_filterwheel.cpp
Go to the documentation of this file.
1/*
2 INDI alpaca FilterWheel 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
22#include <cstring>
23#include <memory>
24
25static std::unique_ptr<AlpacaFilterWheel> alpacaFilterWheel(new AlpacaFilterWheel());
26
31
33{
34 return "Alpaca Filter Wheel";
35}
36
38{
40
41 // Set connection mode to TCP only
43
44 // Server address
45 ServerAddressTP[HOST_INDEX].fill("HOST", "Host", m_Host.c_str());
46 ServerAddressTP[PORT_INDEX].fill("PORT", "Port", std::to_string(m_Port).c_str());
47 ServerAddressTP.fill(getDeviceName(), "SERVER_ADDRESS", "Server", CONNECTION_TAB, IP_RW, 60, IPS_IDLE);
48 ServerAddressTP.load();
49
50 // Update m_Host and m_Port from loaded config
51 if (ServerAddressTP[HOST_INDEX].text != nullptr && strlen(ServerAddressTP[HOST_INDEX].text) > 0)
52 {
53 m_Host = ServerAddressTP[HOST_INDEX].text;
54 }
55 if (ServerAddressTP[PORT_INDEX].text != nullptr && strlen(ServerAddressTP[PORT_INDEX].text) > 0)
56 {
57 m_Port = std::stoi(ServerAddressTP[PORT_INDEX].text);
58 }
59
60 // Device information
61 DeviceInfoTP[DESCRIPTION].fill("DESCRIPTION", "Description", "");
62 DeviceInfoTP[DRIVER_INFO].fill("DRIVER_INFO", "Driver Info", "");
63 DeviceInfoTP[DRIVER_VERSION].fill("DRIVER_VERSION", "Driver Version", "");
64 DeviceInfoTP[INTERFACE_VERSION].fill("INTERFACE_VERSION", "Interface Version", "");
65 DeviceInfoTP.fill(getDeviceName(), "DEVICE_INFO", "Device Info", INFO_TAB, IP_RO, 60, IPS_IDLE);
66
67 // Focus offsets for each filter
68 FocusOffsetsNP[0].fill("OFFSET_0", "Dark Offset", "%.0f", -1000, 1000, 1, 0);
69 FocusOffsetsNP[1].fill("OFFSET_1", "IR Offset", "%.0f", -1000, 1000, 1, 0);
70 FocusOffsetsNP[2].fill("OFFSET_2", "LP Offset", "%.0f", -1000, 1000, 1, 0);
71 FocusOffsetsNP.fill(getDeviceName(), "FOCUS_OFFSETS", "Focus Offsets", FILTER_TAB, IP_RO, 60, IPS_IDLE);
72
74
75 return true;
76}
77
79{
81
82 // Always define the server address so it can be configured before connecting
83 defineProperty(ServerAddressTP);
84}
85
87{
89
90 if (isConnected())
91 {
92 defineProperty(DeviceInfoTP);
93 defineProperty(FocusOffsetsNP);
94 }
95 else
96 {
97 deleteProperty(DeviceInfoTP);
98 deleteProperty(FocusOffsetsNP);
99 }
100
101 return true;
102}
103
105{
106 LOG_INFO("Connecting to alpaca FilterWheel...");
107
108 // Create HTTP client
109 m_AlpacaClient.reset(new httplib::Client(m_Host, m_Port));
110 if (!m_AlpacaClient)
111 {
112 LOG_ERROR("Failed to create HTTP client");
113 return false;
114 }
115
116 m_AlpacaClient->set_connection_timeout(5, 0);
117 m_AlpacaClient->set_read_timeout(10, 0);
118
119 // Test connection with /connected endpoint
120 nlohmann::json response;
121 if (!sendAlpacaGET("/connected", response))
122 {
123 LOG_ERROR("Failed to connect to Alpaca device");
124 return false;
125 }
126
127 LOG_INFO("Connected to Alpaca device");
128
129 // Connect the device
130 if (!sendAlpacaPUT("/connected", "Connected=true", response))
131 {
132 LOG_ERROR("Failed to set device connected state");
133 return false;
134 }
135
136 // Query device information
137 if (sendAlpacaGET("/description", response) && response.contains("Value"))
138 {
139 std::string desc = response["Value"].get<std::string>();
140 DeviceInfoTP[DESCRIPTION].setText(desc.c_str());
141 }
142
143 if (sendAlpacaGET("/driverinfo", response) && response.contains("Value"))
144 {
145 std::string info = response["Value"].get<std::string>();
146 DeviceInfoTP[DRIVER_INFO].setText(info.c_str());
147 }
148
149 if (sendAlpacaGET("/driverversion", response) && response.contains("Value"))
150 {
151 std::string ver = response["Value"].get<std::string>();
152 DeviceInfoTP[DRIVER_VERSION].setText(ver.c_str());
153 }
154
155 if (sendAlpacaGET("/interfaceversion", response) && response.contains("Value"))
156 {
157 int ifver = response["Value"].get<int>();
158 DeviceInfoTP[INTERFACE_VERSION].setText(std::to_string(ifver).c_str());
159 }
160
161 DeviceInfoTP.apply();
162
163 // Setup filter wheel
164 if (!setupFilterWheel())
165 {
166 LOG_ERROR("Failed to setup filter wheel");
167 return false;
168 }
169
170 LOG_INFO("alpaca FilterWheel connected successfully");
171 return true;
172}
173
175{
176 LOG_INFO("Disconnecting alpaca FilterWheel...");
177
178 // Disconnect the device
179 nlohmann::json response;
180 sendAlpacaPUT("/connected", "Connected=false", response);
181
182 m_AlpacaClient.reset();
183
184 LOG_INFO("alpaca FilterWheel disconnected");
185 return true;
186}
187
188bool AlpacaFilterWheel::setupFilterWheel()
189{
190 nlohmann::json response;
191
192 // Query filter names
193 if (sendAlpacaGET("/names", response) && response.contains("Value"))
194 {
195 auto names = response["Value"];
196 if (names.is_array() && !names.empty())
197 {
198 // Rebuild the filter name property with properly named/labeled widgets.
199 // A plain resize() leaves new widgets without a "name" attribute, which
200 // causes the receiving end to discard them as "defTextVector with no
201 // valid members".
203
204 for (size_t i = 0; i < names.size(); i++)
205 {
206 std::string name = names[i].get<std::string>();
207
208 char filterName[MAXINDINAME];
209 char filterLabel[MAXINDILABEL];
210 snprintf(filterName, MAXINDINAME, "FILTER_SLOT_NAME_%zu", i + 1);
211 snprintf(filterLabel, MAXINDILABEL, "Filter#%zu", i + 1);
212
213 INDI::WidgetText oneText;
214 oneText.fill(filterName, filterLabel, name.c_str());
215 FilterNameTP.push(std::move(oneText));
216
217 LOGF_INFO("Filter %zu: %s", i, name.c_str());
218 }
219
220 FilterNameTP.fill(getDeviceName(), "FILTER_NAME", "Filter", FilterSlotNP.getGroupName(), IP_RW, 60, IPS_IDLE);
222
223 // Match the filter slot range to the number of filters reported
224 FilterSlotNP[0].setMax(static_cast<double>(names.size()));
225
226 if (isConnected())
228
229 LOGF_INFO("Found %zu filters", names.size());
230 }
231 }
232
233 // Query focus offsets
234 if (sendAlpacaGET("/focusoffsets", response) && response.contains("Value"))
235 {
236 auto offsets = response["Value"];
237 if (offsets.is_array())
238 {
239 for (size_t i = 0; i < offsets.size() && i < 3; i++)
240 {
241 int offset = offsets[i].get<int>();
242 FocusOffsetsNP[i].setValue(offset);
243 LOGF_INFO("Filter %zu focus offset: %d steps", i, offset);
244 }
245 FocusOffsetsNP.apply();
246 }
247 }
248
249 // Query current position
250 int currentPos = QueryFilter();
251 if (currentPos >= 0)
252 {
253 LOGF_INFO("Current filter position: %d", currentPos);
254 FilterSlotNP[0].setValue(currentPos + 1); // INDI uses 1-based indexing
256 }
257
258 return true;
259}
260
262{
263 // Convert from INDI 1-based to ASCOM 0-based
264 int targetPos = position - 1;
265
266 const char *filterName = (static_cast<size_t>(targetPos) < FilterNameTP.size()) ? FilterNameTP[targetPos].getText() : "Unknown";
267 LOGF_INFO("Selecting filter position %d (%s)", targetPos, filterName);
268
269 // Check if already at position to avoid error 1279
270 int currentPos = QueryFilter();
271 if (currentPos == targetPos)
272 {
273 LOGF_INFO("Already at position %d, no movement needed", targetPos);
274 SelectFilterDone(position);
275 return true;
276 }
277
278 nlohmann::json response;
279 std::string data = "Position=" + std::to_string(targetPos);
280
281 if (!sendAlpacaPUT("/position", data, response))
282 {
283 LOGF_ERROR("Failed to set filter position to %d", targetPos);
284 return false;
285 }
286
287 // Check for errors
288 if (response.contains("ErrorNumber"))
289 {
290 int errorNum = response["ErrorNumber"].get<int>();
291 if (errorNum != 0)
292 {
293 std::string errorMsg = response.contains("ErrorMessage") ?
294 response["ErrorMessage"].get<std::string>() : "Unknown error";
295 LOGF_ERROR("Error setting filter position: %d - %s", errorNum, errorMsg.c_str());
296 return false;
297 }
298 }
299
300 LOGF_INFO("Filter position set to %d", targetPos);
301
302 // Notify the client that the filter change has completed
303 SelectFilterDone(position);
304
305 return true;
306}
307
309{
310 nlohmann::json response;
311
312 if (!sendAlpacaGET("/position", response))
313 {
314 LOG_ERROR("Failed to query filter position");
315 return -1;
316 }
317
318 if (response.contains("Value"))
319 {
320 int pos = response["Value"].get<int>();
321 return pos; // Return 0-based position
322 }
323
324 return -1;
325}
326
328{
330
331 ServerAddressTP.save(fp);
332
333 return true;
334}
335
336bool AlpacaFilterWheel::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
337{
338 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
339 {
340 // Server address
341 if (ServerAddressTP.isNameMatch(name))
342 {
343 ServerAddressTP.update(texts, names, n);
344
345 m_Host = ServerAddressTP[HOST_INDEX].text;
346 m_Port = std::stoi(ServerAddressTP[PORT_INDEX].text);
347
348 ServerAddressTP.apply();
349 saveConfig(true, "SERVER_ADDRESS");
350
351 LOGF_INFO("Server address updated: %s:%d", m_Host.c_str(), m_Port);
352 return true;
353 }
354 }
355
356 return INDI::FilterWheel::ISNewText(dev, name, texts, names, n);
357}
358
359bool AlpacaFilterWheel::sendAlpacaGET(const std::string &endpoint, nlohmann::json &response)
360{
361 if (!m_AlpacaClient)
362 return false;
363
364 std::string url = "/api/v1/filterwheel/" + std::to_string(m_DeviceNumber) + endpoint;
365
366 LOGF_DEBUG("GET %s", url.c_str());
367
368 auto res = m_AlpacaClient->Get(url.c_str());
369
370 if (!res)
371 {
372 LOGF_ERROR("HTTP GET failed for %s", url.c_str());
373 return false;
374 }
375
376 if (res->status != 200)
377 {
378 LOGF_ERROR("HTTP GET returned status %d for %s", res->status, url.c_str());
379 return false;
380 }
381
382 try
383 {
384 response = nlohmann::json::parse(res->body);
385
386 // Check for Alpaca errors
387 if (response.contains("ErrorNumber"))
388 {
389 int errorNum = response["ErrorNumber"].get<int>();
390 if (errorNum != 0)
391 {
392 std::string errorMsg = response.contains("ErrorMessage") ?
393 response["ErrorMessage"].get<std::string>() : "Unknown error";
394 LOGF_WARN("Alpaca error %d: %s", errorNum, errorMsg.c_str());
395 return false;
396 }
397 }
398
399 return true;
400 }
401 catch (const nlohmann::json::exception &e)
402 {
403 LOGF_ERROR("JSON parse error: %s", e.what());
404 return false;
405 }
406}
407
408bool AlpacaFilterWheel::sendAlpacaPUT(const std::string &endpoint, const std::string &data, nlohmann::json &response)
409{
410 if (!m_AlpacaClient)
411 return false;
412
413 std::string url = "/api/v1/filterwheel/" + std::to_string(m_DeviceNumber) + endpoint;
414
415 // Add client info to data
416 std::string fullData = data;
417 if (!fullData.empty())
418 fullData += "&";
419 fullData += "ClientID=" + std::to_string(m_ClientID);
420 fullData += "&ClientTransactionID=" + std::to_string(++m_TransactionID);
421
422 LOGF_DEBUG("PUT %s: %s", url.c_str(), fullData.c_str());
423
424 auto res = m_AlpacaClient->Put(url.c_str(), fullData, "application/x-www-form-urlencoded");
425
426 if (!res)
427 {
428 LOGF_ERROR("HTTP PUT failed for %s", url.c_str());
429 return false;
430 }
431
432 if (res->status != 200)
433 {
434 LOGF_ERROR("HTTP PUT returned status %d for %s", res->status, url.c_str());
435 return false;
436 }
437
438 try
439 {
440 response = nlohmann::json::parse(res->body);
441
442 // Check for Alpaca errors (but ignore error 1279 - already at position)
443 if (response.contains("ErrorNumber"))
444 {
445 int errorNum = response["ErrorNumber"].get<int>();
446 if (errorNum != 0 && errorNum != 1279)
447 {
448 std::string errorMsg = response.contains("ErrorMessage") ?
449 response["ErrorMessage"].get<std::string>() : "Unknown error";
450 LOGF_WARN("Alpaca error %d: %s", errorNum, errorMsg.c_str());
451 return false;
452 }
453 }
454
455 return true;
456 }
457 catch (const nlohmann::json::exception &e)
458 {
459 LOGF_ERROR("JSON parse error: %s", e.what());
460 return false;
461 }
462}
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
virtual bool Disconnect() override
Disconnect from device.
virtual bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
virtual const char * getDefaultName() override
virtual void ISGetProperties(const char *dev) override
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
virtual bool SelectFilter(int position) override
Select a new filter position.
virtual bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual int QueryFilter() override
Return current filter position.
bool isConnected() const
INDI::PropertyText getText(const char *name) const
virtual bool saveConfig(bool silent=false, const char *property=nullptr)
Save the current properties in a configuration file.
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)
void addDebugControl()
Add Debug control to the driver.
INDI::PropertyNumber FilterSlotNP
INDI::PropertyText FilterNameTP
void SelectFilterDone(int newpos)
The child class calls this function when the hardware successfully finished selecting a new filter wh...
virtual void ISGetProperties(const char *dev) override
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
virtual bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
void setFilterConnection(const uint8_t &value)
setFilterConnection Set Filter connection mode. Child class should call this in the constructor befor...
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
void push(WidgetView< T > &&item)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
bool load()
load Attempt to load property values from configuration file.
void save(FILE *f) const
save Save property to configuration file.
void resize(size_t size)
bool isNameMatch(const char *otherName) const
const char * getGroupName() const
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, 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)
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 * CONNECTION_TAB
CONNECTION_TAB Where all device connection settings (serial, usb, ethernet) are defined and controlle...
const char * FILTER_TAB
FILTER_TAB Where all the properties for filter wheels are located.
const char * INFO_TAB
INFO_TAB Where all the properties for general information are located.
@ IP_RW
Definition indiapi.h:184
@ IP_RO
Definition indiapi.h:182
@ IPS_IDLE
Definition indiapi.h:159
#define MAXINDILABEL
Definition indiapi.h:190
#define MAXINDINAME
Definition indiapi.h:189
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOGF_WARN(fmt,...)
Definition indilogger.h:81
#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
void fill(const char *name, const char *label, const char *initialText)