Instrument Neutral Distributed Interface INDI 2.2.5
indi_alpaca_ccd.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2025 Jasem Mutlaq. All rights reserved.
3
4 ASCOM Alpaca Camera INDI Driver
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_ccd.h"
26
27#include "indicom.h"
28#include <httplib.h>
29#include <string.h>
30#include <chrono>
31#include <thread>
32#include <cmath>
33
34#ifdef _USE_SYSTEM_JSONLIB
35#include <nlohmann/json.hpp>
36#else
37#include <indijson.hpp>
38#endif
39
40// We declare an auto pointer to AlpacaCCD
41#ifndef INDI_ALPACA_CCD_NO_INSTANCE
42std::unique_ptr<AlpacaCCD> alpaca_ccd(new AlpacaCCD());
43#endif
44
46{
47 // Set initial CCD capabilities based on ASCOM Alpaca Camera API
48 // CCD_CAN_ABORT, CCD_HAS_COOLER and CCD_HAS_SHUTTER are refined in Connect()
49 // once the device's CanAbortExposure/CanStopExposure/CanSetCCDTemperature/
50 // HasShutter properties are known.
53}
54
55void AlpacaCCD::setDefaultServerAddress(const char *host, const char *port, bool force)
56{
57 // Only apply defaults if no value was loaded from saved config (mirrors TCP plugin pattern).
58 // m_ConfigHost/m_ConfigPort are empty when no config file entry exists for this device,
59 // meaning this is either a first run or the user has never saved settings.
60 if (host != nullptr)
61 {
62 if (force || m_ConfigHost.empty())
63 ServerAddressTP[0].setText(host);
64 }
65
66 if (port != nullptr)
67 {
68 if (force || m_ConfigPort.empty())
69 ServerAddressTP[1].setText(port);
70 }
71}
72
74{
75 // Call base class initProperties
77
78 // Cooler properties
79 CoolerSP[INDI_ENABLED].fill("COOLER_ON", "ON", ISS_OFF);
80 CoolerSP[INDI_DISABLED].fill("COOLER_OFF", "OFF", ISS_ON);
81 CoolerSP.fill(getDeviceName(), "CCD_COOLER", "Cooler", MAIN_CONTROL_TAB, IP_WO, ISR_1OFMANY, 0, IPS_IDLE);
82
83 // Try to load server address from saved config (mirrors TCP plugin pattern).
84 // m_ConfigHost/m_ConfigPort are empty if no config was previously saved.
85 char configHost[MAXINDINAME] = {0};
86 char configPort[MAXINDINAME] = {0};
87 if (IUGetConfigText(getDeviceName(), "SERVER_ADDRESS", "HOST", configHost, MAXINDINAME) == 0)
88 m_ConfigHost = configHost;
89 if (IUGetConfigText(getDeviceName(), "SERVER_ADDRESS", "PORT", configPort, MAXINDINAME) == 0)
90 m_ConfigPort = configPort;
91
92 // Setup server address properties using config values (empty string if no config saved).
93 // Subclasses call setDefaultServerAddress() after initProperties() to set their own defaults,
94 // which will only take effect if m_ConfigHost/m_ConfigPort are still empty.
95 ServerAddressTP[0].fill("HOST", "Host", m_ConfigHost.c_str());
96 ServerAddressTP[1].fill("PORT", "Port", m_ConfigPort.c_str());
97 ServerAddressTP.fill(getDeviceName(), "SERVER_ADDRESS", "Server", CONNECTION_TAB, IP_RW, 60, IPS_IDLE);
98
99 // Setup device number property
100 DeviceNumberNP[0].fill("DEVICE_NUMBER", "Device Number", "%.0f", 0, 10, 1, 0);
101 DeviceNumberNP.fill(getDeviceName(), "DEVICE_NUMBER", "Alpaca Device", CONNECTION_TAB, IP_RW, 60, IPS_IDLE);
102 DeviceNumberNP.load();
103
104 // Setup connection settings properties
105 ConnectionSettingsNP[0].fill("TIMEOUT", "Timeout (sec)", "%.0f", 1, 30, 1, 5);
106 ConnectionSettingsNP[1].fill("RETRIES", "Max Retries", "%.0f", 1, 10, 1, 3);
107 ConnectionSettingsNP[2].fill("RETRY_DELAY", "Retry Delay (ms)", "%.0f", 100, 5000, 100, 1000);
108 ConnectionSettingsNP.fill(getDeviceName(), "CONNECTION_SETTINGS", "Connection", CONNECTION_TAB, IP_RW, 60, IPS_IDLE);
109 ConnectionSettingsNP.load();
110
111 // Gain property
112 GainNP[0].fill("GAIN", "Gain", "%.0f", 0, 1000, 1, 0); // Max gain can vary, set a reasonable default
113 GainNP.fill(getDeviceName(), "CCD_GAIN", "Gain", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
114
115 // Offset property
116 OffsetNP[0].fill("OFFSET", "Offset", "%.0f", 0, 10000, 1, 0); // Max offset can vary
117 OffsetNP.fill(getDeviceName(), "CCD_OFFSET", "Offset", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
118
119 // Cooler Power property
120 CoolerPowerNP[0].fill("CCD_COOLER_VALUE", "Power (%)", "%.0f", 0, 100, 1, 0);
121 CoolerPowerNP.fill(getDeviceName(), "CCD_COOLER_POWER", "Cooler Power", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE);
122
123 // Fast readout mode (only defined if the device reports CanFastReadout=true)
124 FastReadoutSP[FAST_READOUT_ENABLE].fill("FAST_READOUT_ENABLE", "Enable", ISS_OFF);
125 FastReadoutSP[FAST_READOUT_DISABLE].fill("FAST_READOUT_DISABLE", "Disable", ISS_ON);
126 FastReadoutSP.fill(getDeviceName(), "CCD_FAST_READOUT", "Fast Readout", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60,
127 IPS_IDLE);
128
129 // Device Info property
130 DeviceInfoTP[0].fill("DESCRIPTION", "Description", "");
131 DeviceInfoTP[1].fill("DRIVER_INFO", "Driver Info", "");
132 DeviceInfoTP[2].fill("DRIVER_VERSION", "Driver Version", "");
133 DeviceInfoTP[3].fill("NAME", "Name", "");
134 DeviceInfoTP.fill(getDeviceName(), "DEVICE_INFO", "Device Info", CONNECTION_TAB, IP_RO, 60, IPS_IDLE);
135
136 // Camera State property
137 CameraStateTP[0].fill("STATE", "State", "Idle");
138 CameraStateTP.fill(getDeviceName(), "CCD_CAMERA_STATE", "Camera State", MAIN_CONTROL_TAB, IP_RO, 60, IPS_IDLE);
139
141
142 // Default server address (subclasses may override via setDefaultServerAddress()).
143 setDefaultServerAddress("alpaca.local", "32323");
144
145 return true;
146}
147
148void AlpacaCCD::ISGetProperties(const char *dev)
149{
151
152 // Always define these properties
153 defineProperty(ServerAddressTP);
154 defineProperty(DeviceNumberNP);
155 defineProperty(ConnectionSettingsNP);
156}
157
159{
160 if (ServerAddressTP[0].isEmpty() || ServerAddressTP[1].isEmpty())
161 {
162 LOG_ERROR("Server address or port is not set.");
163 return false;
164 }
165
166 // Initialize HTTP client
167 httpClient = std::make_unique<httplib::Client>(ServerAddressTP[0].getText(), std::stoi(ServerAddressTP[1].getText()));
168 httpClient->set_connection_timeout(static_cast<int>(ConnectionSettingsNP[0].getValue()));
169 httpClient->set_read_timeout(static_cast<int>(ConnectionSettingsNP[0].getValue()));
170
171 // Test connection by getting camera status
172 nlohmann::json response;
173 bool success = sendAlpacaGET("/connected", response);
174
175 if (!success)
176 {
177 LOG_ERROR("Failed to connect to Alpaca camera. Please check server address and port.");
178 return false;
179 }
180
181 // Set connected state
182 nlohmann::json body = {{"Connected", true}};
183 success = sendAlpacaPUT("/connected", body, response);
184
185 if (!success)
186 {
187 LOG_ERROR("Failed to set connected state on Alpaca camera.");
188 return false;
189 }
190
191 LOG_INFO("Successfully connected to Alpaca camera.");
192
193 // Get current CCD capabilities
194 uint32_t cap = GetCCDCapability();
195
196 // Check if camera supports Gain by trying to get gain value
197 success = sendAlpacaGET("/gain", response);
198 if (success)
199 {
200 m_HasGain = true;
201 LOGF_INFO("Camera supports Gain: Yes (current value: %.0f)", response["Value"].get<double>());
202 }
203 else
204 {
205 LOG_INFO("Camera does not support Gain (failed to get gain value).");
206 m_HasGain = false;
207 }
208
209 // Check if camera supports Offset by trying to get offset value
210 success = sendAlpacaGET("/offset", response);
211 if (success)
212 {
213 m_HasOffset = true;
214 LOGF_INFO("Camera supports Offset: Yes (current value: %.0f)", response["Value"].get<double>());
215 }
216 else
217 {
218 LOG_INFO("Camera does not support Offset (failed to get offset value).");
219 m_HasOffset = false;
220 }
221
222 // Check if camera supports setting CCD temperature
223 success = sendAlpacaGET("/cansetccdtemperature", response);
224 if (success && response["Value"].get<bool>())
225 {
227 LOG_INFO("Camera supports CCD temperature control.");
228 }
229 else
230 {
231 cap &= ~INDI::CCD::CCD_HAS_COOLER;
232 LOG_INFO("Camera does not support CCD temperature control.");
233 }
234
235 // Check pulse guide capability
236 success = sendAlpacaGET("/canpulseguide", response);
237 if (success)
238 {
239 m_CanPulseGuide = response["Value"].get<bool>();
240 if (m_CanPulseGuide)
241 {
243 LOG_INFO("Camera supports pulse guiding.");
244 }
245 else
246 {
247 LOG_INFO("Camera does not support pulse guiding.");
248 }
249 }
250 else
251 {
252 LOG_WARN("Failed to query CanPulseGuide, assuming no pulse guide support.");
253 m_CanPulseGuide = false;
254 }
255
256 // Check abort/stop exposure capability
257 success = sendAlpacaGET("/canabortexposure", response);
258 m_CanAbortExposure = success && response["Value"].get<bool>();
259
260 success = sendAlpacaGET("/canstopexposure", response);
261 if (success)
262 {
263 m_CanStopExposure = response["Value"].get<bool>();
264 if (m_CanStopExposure)
265 {
266 LOG_INFO("Camera supports stopping exposure.");
267 }
268 else
269 {
270 LOG_INFO("Camera does not support stopping exposure.");
271 }
272 }
273 else
274 {
275 LOG_WARN("Failed to query CanStopExposure, assuming no stop exposure support.");
276 m_CanStopExposure = false;
277 }
278
279 if (m_CanAbortExposure || m_CanStopExposure)
280 {
282 LOG_INFO("Camera supports aborting exposure.");
283 }
284 else
285 {
286 cap &= ~INDI::CCD::CCD_CAN_ABORT;
287 LOG_INFO("Camera does not support aborting exposure.");
288 }
289
290 // Check if camera has a mechanical shutter
291 success = sendAlpacaGET("/hasshutter", response);
292 if (success && response["Value"].get<bool>())
293 {
295 LOG_INFO("Camera has a mechanical shutter.");
296 }
297 else
298 {
299 cap &= ~INDI::CCD::CCD_HAS_SHUTTER;
300 LOG_INFO("Camera does not have a mechanical shutter.");
301 }
302
303 // Check if camera can report cooler power
304 success = sendAlpacaGET("/cangetcoolerpower", response);
305 m_HasCoolerPower = success && response["Value"].get<bool>();
306 LOGF_INFO("Camera supports reading cooler power: %s", m_HasCoolerPower ? "Yes" : "No");
307
308 // Check if camera supports asymmetric binning
309 success = sendAlpacaGET("/canasymmetricbin", response);
310 m_CanAsymmetricBin = success && response["Value"].get<bool>();
311 LOGF_INFO("Camera supports asymmetric binning: %s", m_CanAsymmetricBin ? "Yes" : "No");
312
313 // Check if camera supports a fast readout mode
314 success = sendAlpacaGET("/canfastreadout", response);
315 m_HasFastReadout = success && response["Value"].get<bool>();
316 LOGF_INFO("Camera supports fast readout mode: %s", m_HasFastReadout ? "Yes" : "No");
317
318 // Check camera sensor type for Bayer pattern support
319 success = sendAlpacaGET("/sensortype", response);
320 if (success)
321 {
322 m_SensorType = response["Value"].get<uint8_t>();
323 std::string sensorTypeStr = getSensorTypeString(m_SensorType);
324 LOGF_INFO("Camera sensor type: %d (%s)", m_SensorType, sensorTypeStr.c_str());
325
326 // Sensor types: 0=Monochrome, 1=Color, 2=RGGB Bayer, 3=CMYG Bayer, etc.
327 bool isColorCamWithBayer = (m_SensorType >= 2); // Bayer patterns start at type 2
328
329 if (isColorCamWithBayer)
330 {
332 LOG_INFO("Camera has Bayer color sensor - enabling Bayer capability.");
333 }
334 else if (m_SensorType == 1)
335 {
336 LOG_INFO("Camera has color sensor (non-Bayer).");
337 }
338 else
339 {
340 LOG_INFO("Camera has monochrome sensor.");
341 }
342 }
343 else
344 {
345 LOG_WARN("Failed to query sensor type, assuming monochrome sensor.");
346 }
347
348
349 // Get sensor type to determine available capture formats
350 std::string sensorTypeStr = getSensorTypeString(m_SensorType);
351 LOGF_INFO("Setting up capture formats for sensor type: %d (%s)", m_SensorType, sensorTypeStr.c_str());
352
353 // Clear existing capture formats from base class
354 m_CaptureFormats.clear();
355
356 // Add capture formats based on sensor type and typical Alpaca camera capabilities
357 // Most Alpaca cameras support both 8-bit and 16-bit modes
358 switch (m_SensorType)
359 {
360 case 0: // Monochrome
361 addCaptureFormat({"MONO_8", "Mono 8-bit", 8, false});
362 addCaptureFormat({"MONO_16", "Mono 16-bit", 16, true});
363 break;
364 case 1: // Color (RGB)
365 addCaptureFormat({"RGB_8", "RGB 8-bit", 8, true});
366 addCaptureFormat({"RGB_16", "RGB 16-bit", 16, false});
367 break;
368 case 2: // RGGB Bayer
369 BayerTP[CFA_TYPE].setText("RGGB");
370 BayerTP.apply();
371 [[fallthrough]];
372 case 3: // CMYG Bayer
373 addCaptureFormat({"RAW_8", "Raw 8-bit", 8, false});
374 addCaptureFormat({"RAW_16", "Raw 16-bit", 16, true});
375 cap |= CCD_HAS_BAYER;
376 break;
377 default:
378 LOG_WARN("Unknown sensor type, defaulting to mono formats.");
379 addCaptureFormat({"MONO_8", "Mono 8-bit", 8, true});
380 addCaptureFormat({"MONO_16", "Mono 16-bit", 16, false});
381 break;
382 }
383
384 // Update CCD capabilities
385 SetCCDCapability(cap);
386
387 // Start temperature monitoring timer
388 mTimerTemperature.callOnTimeout(std::bind(&AlpacaCCD::temperatureTimerTimeout, this));
389 mTimerTemperature.start(TEMP_TIMER_MS);
390
392 return true;
393}
394
395void AlpacaCCD::updateCameraCapabilities()
396{
397 if (!isConnected())
398 {
399 LOG_WARN("Not connected to Alpaca camera, cannot update camera capabilities.");
400 return;
401 }
402
403 nlohmann::json response;
404 bool success;
405
406 // Get camera capabilities and update INDI properties
407 success = sendAlpacaGET("/cameraxsize", response);
408 if (success) m_CameraXSize = response["Value"].get<int>();
409
410 success = sendAlpacaGET("/cameraysize", response);
411 if (success) m_CameraYSize = response["Value"].get<int>();
412
413 success = sendAlpacaGET("/pixelsizex", response);
414 if (success) m_PixelSizeX = response["Value"].get<double>();
415
416 success = sendAlpacaGET("/pixelsizey", response);
417 if (success) m_PixelSizeY = response["Value"].get<double>();
418
419 // Update CCDParams
420 SetCCDParams(m_CameraXSize, m_CameraYSize, PrimaryCCD.getBPP(), m_PixelSizeX, m_PixelSizeY);
421
422 // Update binning limits (HOR_BIN/VER_BIN default to a max of 4)
423 double maxBinX = 1, maxBinY = 1;
424 success = sendAlpacaGET("/maxbinx", response);
425 if (success) maxBinX = response["Value"].get<double>();
426
427 success = sendAlpacaGET("/maxbiny", response);
428 if (success) maxBinY = response["Value"].get<double>();
429
430 PrimaryCCD.setMinMaxStep("CCD_BINNING", "HOR_BIN", 1, maxBinX, 1, false);
431 PrimaryCCD.setMinMaxStep("CCD_BINNING", "VER_BIN", 1, maxBinY, 1, false);
432
433 // Update exposure limits from the device (defaults are 0.01-3600s, step 1.0)
434 double expMin = 0.01, expMax = 3600, expStep = 1.0;
435 success = sendAlpacaGET("/exposuremin", response);
436 if (success) expMin = response["Value"].get<double>();
437
438 success = sendAlpacaGET("/exposuremax", response);
439 if (success) expMax = response["Value"].get<double>();
440
441 success = sendAlpacaGET("/exposureresolution", response);
442 if (success) expStep = response["Value"].get<double>();
443
444 PrimaryCCD.setMinMaxStep("CCD_EXPOSURE", "CCD_EXPOSURE_VALUE", expMin, expMax, expStep, false);
445
446 success = sendAlpacaGET("/description", response);
447 if (success) m_Description = response["Value"].get<std::string>();
448
449 success = sendAlpacaGET("/driverinfo", response);
450 if (success) m_DriverInfo = response["Value"].get<std::string>();
451
452 success = sendAlpacaGET("/driverversion", response);
453 if (success) m_DriverVersion = response["Value"].get<std::string>();
454
455 success = sendAlpacaGET("/name", response);
456 if (success) m_CameraName = response["Value"].get<std::string>();
457
458 if (m_HasGain)
459 {
460 // Some cameras expose Gain as a numeric range (GainMin/GainMax), others as a list of
461 // named presets (Gains). Per the Alpaca spec, only one of the two is implemented.
462 success = sendAlpacaGET("/gains", response);
463 if (success && response.contains("Value") && response["Value"].is_array() && !response["Value"].empty())
464 {
465 m_GainIsList = true;
466 auto gains = response["Value"];
467 GainSP.resize(gains.size());
468 for (size_t i = 0; i < gains.size(); ++i)
469 {
470 std::string gainName = gains[i].get<std::string>();
471 char nameBuf[MAXINDINAME];
472 snprintf(nameBuf, sizeof(nameBuf), "GAIN_%zu", i);
473 GainSP[i].fill(nameBuf, gainName.c_str(), ISS_OFF);
474 }
475 GainSP.fill(getDeviceName(), "CCD_GAIN_PRESET", "Gain", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
476 }
477 else
478 {
479 m_GainIsList = false;
480
481 success = sendAlpacaGET("/gainmin", response);
482 if (success) m_GainMin = response["Value"].get<double>();
483
484 success = sendAlpacaGET("/gainmax", response);
485 if (success) m_GainMax = response["Value"].get<double>();
486 }
487 }
488
489 if (m_HasOffset)
490 {
491 // As with Gain, Offset is either a numeric range or a list of named presets, never both.
492 success = sendAlpacaGET("/offsets", response);
493 if (success && response.contains("Value") && response["Value"].is_array() && !response["Value"].empty())
494 {
495 m_OffsetIsList = true;
496 auto offsets = response["Value"];
497 OffsetSP.resize(offsets.size());
498 for (size_t i = 0; i < offsets.size(); ++i)
499 {
500 std::string offsetName = offsets[i].get<std::string>();
501 char nameBuf[MAXINDINAME];
502 snprintf(nameBuf, sizeof(nameBuf), "OFFSET_%zu", i);
503 OffsetSP[i].fill(nameBuf, offsetName.c_str(), ISS_OFF);
504 }
505 OffsetSP.fill(getDeviceName(), "CCD_OFFSET_PRESET", "Offset", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
506 }
507 else
508 {
509 m_OffsetIsList = false;
510 }
511 }
512
513 success = sendAlpacaGET("/maxadu", response);
514 if (success) m_MaxADU = response["Value"].get<uint32_t>();
515
516 success = sendAlpacaGET("/electronsperadu", response);
517 if (success) m_ElectronsPerADU = response["Value"].get<double>();
518
519 success = sendAlpacaGET("/fullwellcapacity", response);
520 if (success) m_FullWellCapacity = response["Value"].get<double>();
521
522 if (m_SensorType >= 2)
523 {
524 if (sendAlpacaGET("/bayeroffsetx", response))
525 m_BayerOffsetX = response["Value"].get<uint8_t>();
526
527 if (sendAlpacaGET("/bayeroffsety", response))
528 m_BayerOffsetY = response["Value"].get<uint8_t>();
529 }
530}
531
533{
534 // Stop the worker thread
535 mWorker.quit();
536
537 // Stop temperature monitoring timer
538 mTimerTemperature.stop();
539
540 if (isConnected())
541 {
542 nlohmann::json response;
543 nlohmann::json body = {{"Connected", false}};
544 bool success = sendAlpacaPUT("/connected", body, response);
545
546 if (!success)
547 {
548 LOG_ERROR("Failed to set disconnected state on Alpaca camera.");
549 return false;
550 }
551 }
552
553 LOG_INFO("Disconnected from Alpaca camera.");
554 httpClient.reset(); // Destroy HTTP client
555 return true;
556}
557
559{
560 return "Alpaca Camera";
561}
562
563bool AlpacaCCD::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
564{
565 if (isDeviceNameMatch(dev) && ServerAddressTP.isNameMatch(name))
566 {
567 ServerAddressTP.update(texts, names, n);
568 ServerAddressTP.setState(IPS_OK);
569 ServerAddressTP.apply();
570 // Save configuration after update
571 saveConfig();
572 return true;
573 }
574
575 return INDI::CCD::ISNewText(dev, name, texts, names, n);
576}
577
578bool AlpacaCCD::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
579{
580 if (isDeviceNameMatch(dev))
581 {
582 if (ConnectionSettingsNP.isNameMatch(name))
583 {
584 ConnectionSettingsNP.update(values, names, n);
585 ConnectionSettingsNP.setState(IPS_OK);
586 ConnectionSettingsNP.apply();
587 saveConfig();
588 LOG_INFO("Connection settings updated.");
589 return true;
590 }
591 else if (DeviceNumberNP.isNameMatch(name))
592 {
593 if (!isConnected())
594 {
595 DeviceNumberNP.update(values, names, n);
596 DeviceNumberNP.setState(IPS_OK);
597 saveConfig();
598 LOG_INFO("Alpaca device number updated.");
599 }
600 else
601 DeviceNumberNP.setState(IPS_IDLE);
602
603 DeviceNumberNP.apply();
604 return true;
605 }
606 else if (GainNP.isNameMatch(name))
607 {
608 updateProperty(GainNP, values, names, n, [this, values]()
609 {
610 nlohmann::json response;
611 nlohmann::json body = {{"Gain", static_cast<int>(values[0])}};
612 return sendAlpacaPUT("/gain", body, response);
613 }, true);
614 return true;
615 }
616 else if (OffsetNP.isNameMatch(name))
617 {
618 updateProperty(OffsetNP, values, names, n, [this, values]()
619 {
620 nlohmann::json response;
621 nlohmann::json body = {{"Offset", static_cast<int>(values[0])}};
622 return sendAlpacaPUT("/offset", body, response);
623 }, true);
624 return true;
625 }
626 }
627
628 return INDI::CCD::ISNewNumber(dev, name, values, names, n);
629}
630
634bool AlpacaCCD::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
635{
636 if (isDeviceNameMatch(dev))
637 {
638 if (CoolerSP.isNameMatch(name))
639 {
640 updateProperty(CoolerSP, states, names, n, [this, states]()
641 {
642 bool coolerOn = states[0] == ISS_ON;
643 nlohmann::json response;
644 nlohmann::json body = {{"CoolerOn", coolerOn}};
645 return sendAlpacaPUT("/cooleron", body, response);
646 });
647 return true;
648 }
649 else if (ReadoutModeSP.isNameMatch(name))
650 {
651 if (ReadoutModeSP.update(states, names, n) == false)
652 {
653 ReadoutModeSP.setState(IPS_ALERT);
654 ReadoutModeSP.apply();
655 return true;
656 }
657
658 int index = ReadoutModeSP.findOnSwitchIndex();
659 if (index != -1)
660 {
661 // Set the readout mode on the Alpaca camera
662 nlohmann::json response;
663 nlohmann::json body = {{"ReadoutMode", index}};
664 bool success = sendAlpacaPUT("/readoutmode", body, response);
665
666 if (success)
667 {
668 m_CurrentReadoutModeIndex = index;
669 ReadoutModeSP.setState(IPS_OK);
670 ReadoutModeSP.apply();
671 LOGF_DEBUG("Readout mode set to index %d: %s", index, ReadoutModeSP[index].getLabel());
672 return true;
673 }
674 else
675 {
676 LOG_ERROR("Failed to set readout mode.");
677 }
678 }
679 ReadoutModeSP.setState(IPS_ALERT);
680 ReadoutModeSP.apply();
681 return true;
682 }
683 else if (m_GainIsList && GainSP.isNameMatch(name))
684 {
685 if (GainSP.update(states, names, n) == false)
686 {
687 GainSP.setState(IPS_ALERT);
688 GainSP.apply();
689 return true;
690 }
691
692 int index = GainSP.findOnSwitchIndex();
693 nlohmann::json response;
694 nlohmann::json body = {{"Gain", index}};
695 if (index != -1 && sendAlpacaPUT("/gain", body, response))
696 {
697 GainSP.setState(IPS_OK);
698 LOGF_INFO("Gain set to %s", GainSP[index].getLabel());
699 }
700 else
701 {
702 LOG_ERROR("Failed to set gain.");
703 GainSP.setState(IPS_ALERT);
704 }
705 GainSP.apply();
706 return true;
707 }
708 else if (m_OffsetIsList && OffsetSP.isNameMatch(name))
709 {
710 if (OffsetSP.update(states, names, n) == false)
711 {
712 OffsetSP.setState(IPS_ALERT);
713 OffsetSP.apply();
714 return true;
715 }
716
717 int index = OffsetSP.findOnSwitchIndex();
718 nlohmann::json response;
719 nlohmann::json body = {{"Offset", index}};
720 if (index != -1 && sendAlpacaPUT("/offset", body, response))
721 {
722 OffsetSP.setState(IPS_OK);
723 LOGF_INFO("Offset set to %s", OffsetSP[index].getLabel());
724 }
725 else
726 {
727 LOG_ERROR("Failed to set offset.");
728 OffsetSP.setState(IPS_ALERT);
729 }
730 OffsetSP.apply();
731 return true;
732 }
733 else if (FastReadoutSP.isNameMatch(name))
734 {
735 updateProperty(FastReadoutSP, states, names, n, [this, states]()
736 {
737 bool enable = states[0] == ISS_ON;
738 nlohmann::json response;
739 nlohmann::json body = {{"FastReadout", enable}};
740 return sendAlpacaPUT("/fastreadout", body, response);
741 });
742 return true;
743 }
744 }
745 return INDI::CCD::ISNewSwitch(dev, name, states, names, n);
746}
747
749{
750 // Update capabilities first
751 if (isConnected())
752 updateCameraCapabilities();
753
755
756 if (isConnected())
757 {
758 nlohmann::json response;
759 bool success;
760 defineProperty(CameraStateTP);
761
762 // Device Info properties
763 defineProperty(DeviceInfoTP);
764 DeviceInfoTP[0].setText(m_Description.c_str());
765 DeviceInfoTP[1].setText(m_DriverInfo.c_str());
766 DeviceInfoTP[2].setText(m_DriverVersion.c_str());
767 DeviceInfoTP[3].setText(m_CameraName.c_str());
768 DeviceInfoTP.setState(IPS_OK);
769 DeviceInfoTP.apply();
770
771 if (m_HasGain)
772 {
773 // Get current gain (a preset index if m_GainIsList, otherwise a raw value)
774 success = sendAlpacaGET("/gain", response);
775 if (m_GainIsList)
776 {
777 defineProperty(GainSP);
778 if (success)
779 {
780 int index = response["Value"].get<int>();
781 if (index >= 0 && static_cast<size_t>(index) < GainSP.count())
782 {
783 GainSP.reset();
784 GainSP[index].setState(ISS_ON);
785 }
786 }
787 GainSP.setState(IPS_OK);
788 GainSP.apply();
789 }
790 else
791 {
792 if (success)
793 {
794 defineProperty(GainNP);
795 GainNP[0].setValue(response["Value"].get<double>());
796 GainNP.setState(IPS_OK);
797 GainNP.apply();
798 }
799 else
800 {
801 LOG_WARN("Failed to get gain.");
802 }
803
804 // Set gain min/max from cached values
805 GainNP[0].setMin(m_GainMin);
806 GainNP[0].setMax(m_GainMax);
807 }
808 }
809
810 if (m_HasOffset)
811 {
812 // Get current offset (a preset index if m_OffsetIsList, otherwise a raw value)
813 success = sendAlpacaGET("/offset", response);
814 if (m_OffsetIsList)
815 {
816 defineProperty(OffsetSP);
817 if (success)
818 {
819 int index = response["Value"].get<int>();
820 if (index >= 0 && static_cast<size_t>(index) < OffsetSP.count())
821 {
822 OffsetSP.reset();
823 OffsetSP[index].setState(ISS_ON);
824 }
825 }
826 OffsetSP.setState(IPS_OK);
827 OffsetSP.apply();
828 }
829 else
830 {
831 if (success)
832 {
833 defineProperty(OffsetNP);
834 OffsetNP[0].setValue(response["Value"].get<double>());
835 OffsetNP.setState(IPS_OK);
836 OffsetNP.apply();
837 }
838 else
839 {
840 LOG_WARN("Failed to get offset.");
841 }
842 }
843 }
844
845 // Cooler properties
846 defineProperty(CoolerSP);
848 if (m_HasCoolerPower)
849 defineProperty(CoolerPowerNP);
850
851 // Fast readout mode
852 if (m_HasFastReadout)
853 {
854 success = sendAlpacaGET("/fastreadout", response);
855 if (success)
856 {
857 bool fastReadout = response["Value"].get<bool>();
858 FastReadoutSP.reset();
859 FastReadoutSP[fastReadout ? FAST_READOUT_ENABLE : FAST_READOUT_DISABLE].setState(ISS_ON);
860 FastReadoutSP.setState(IPS_OK);
861 }
862 defineProperty(FastReadoutSP);
863 }
864
865 defineProperty(ReadoutModeSP);
866 updateReadoutModes();
867
868 if (m_SensorType >= 2)
869 {
870 char offsetX[2], offsetY[2];
871 snprintf(offsetX, sizeof(offsetX), "%d", m_BayerOffsetX);
872 snprintf(offsetY, sizeof(offsetY), "%d", m_BayerOffsetY);
873 BayerTP[CFA_OFFSET_X].setText(offsetX);
874 BayerTP[CFA_OFFSET_Y].setText(offsetY);
875 BayerTP.apply();
876 LOGF_DEBUG("Bayer offsets: X=%s, Y=%s", BayerTP[CFA_OFFSET_X].getText(), BayerTP[CFA_OFFSET_Y].getText());
877 }
878 }
879 else
880 {
881 LOG_INFO("Alpaca camera is disconnected.");
882 deleteProperty(CoolerSP);
884 deleteProperty(GainNP);
885 deleteProperty(GainSP);
886 deleteProperty(OffsetNP);
887 deleteProperty(OffsetSP);
888 deleteProperty(CoolerPowerNP);
889 deleteProperty(FastReadoutSP);
890 deleteProperty(DeviceInfoTP);
891 deleteProperty(ReadoutModeSP);
892 deleteProperty(CameraStateTP);
893 }
894
895 return true;
896}
897
899{
900 if (!isConnected())
901 return;
902
903 // Worker thread now handles exposure timing, so we just update status
904 updateStatus();
906}
907
909{
911
912 ServerAddressTP.save(fp);
913 DeviceNumberNP.save(fp);
914 ConnectionSettingsNP.save(fp);
915 if (m_HasGain && !m_GainIsList)
916 {
917 GainNP.save(fp);
918 }
919 if (m_HasOffset && !m_OffsetIsList)
920 {
921 OffsetNP.save(fp);
922 }
923
924 return true;
925}
926
927bool AlpacaCCD::StartExposure(float duration)
928{
929 if (!isConnected())
930 {
931 LOG_ERROR("Not connected to Alpaca camera.");
932 return false;
933 }
934
935 // Start the worker thread to handle the exposure
936 mWorker.start(std::bind(&AlpacaCCD::workerExposure, this, std::placeholders::_1, duration));
937 return true;
938}
939
941{
942 LOG_DEBUG("Aborting exposure...");
943
944 // Stop the worker thread
945 mWorker.quit();
946
947 // Prefer AbortExposure (discards the image); fall back to StopExposure
948 // (reads out the image normally) if that's all the device supports.
949 nlohmann::json response;
950 bool success = false;
951
952 if (m_CanAbortExposure)
953 success = sendAlpacaPUT("/abortexposure", {}, response);
954 else if (m_CanStopExposure)
955 success = sendAlpacaPUT("/stopexposure", {}, response);
956 else
957 LOG_WARN("Camera does not support aborting or stopping exposures.");
958
959 if (success)
960 {
961 LOG_INFO("Exposure aborted.");
962 }
963 else if (m_CanAbortExposure || m_CanStopExposure)
964 {
965 LOG_ERROR("Failed to send abort/stop command to Alpaca camera.");
966 }
967
968 return true;
969}
970
972{
973 if (!isConnected())
974 {
975 LOG_ERROR("Not connected to Alpaca camera.");
976 return -1;
977 }
978
979 // If there difference, for example, is less than 0.25 degrees, let's immediately return OK.
980 if (std::abs(temperature - m_CurrentTemperature) < TEMP_THRESHOLD)
981 return 1;
982
983 // Activate cooler if not already on
984 if (CoolerSP[INDI_ENABLED].getState() != ISS_ON)
985 {
986 nlohmann::json response;
987 nlohmann::json body = {{"CoolerOn", true}};
988 bool success = sendAlpacaPUT("/cooleron", body, response);
989
990 if (!success)
991 {
992 LOG_ERROR("Failed to activate cooler.");
993 return -1;
994 }
995
996 CoolerSP[INDI_ENABLED].setState(ISS_ON);
997 CoolerSP[INDI_DISABLED].setState(ISS_OFF);
998 CoolerSP.setState(IPS_OK);
999 CoolerSP.apply();
1000 }
1001
1002 m_TargetTemperature = temperature;
1003
1004 nlohmann::json response;
1005 nlohmann::json body = {{"SetCCDTemperature", temperature}};
1006 bool success = sendAlpacaPUT("/setccdtemperature", body, response);
1007
1008 if (success)
1009 {
1010 LOGF_DEBUG("Setting temperature to %.2f C.", temperature);
1011 return 0;
1012 }
1013
1014 LOGF_ERROR("Failed to set target temperature to %.2f C.", temperature);
1015 return -1;
1016}
1017
1018bool AlpacaCCD::UpdateCCDFrame(int x, int y, int w, int h)
1019{
1020 if (!isConnected())
1021 {
1022 LOG_ERROR("Not connected to Alpaca camera.");
1023 return false;
1024 }
1025
1026 nlohmann::json response;
1027 nlohmann::json body;
1028 bool success;
1029
1030 body = {{"StartX", x}};
1031 success = sendAlpacaPUT("/startx", body, response);
1032 if (!success) return false;
1033
1034 body = {{"StartY", y}};
1035 success = sendAlpacaPUT("/starty", body, response);
1036 if (!success) return false;
1037
1038 body = {{"NumX", w / PrimaryCCD.getBinX()}};
1039 success = sendAlpacaPUT("/numx", body, response);
1040 if (!success) return false;
1041
1042 body = {{"NumY", h / PrimaryCCD.getBinY()}};
1043 return sendAlpacaPUT("/numy", body, response);
1044}
1045
1046bool AlpacaCCD::UpdateCCDBin(int hor, int ver)
1047{
1048 if (!isConnected())
1049 {
1050 LOG_ERROR("Not connected to Alpaca camera.");
1051 return false;
1052 }
1053
1054 if (!m_CanAsymmetricBin && hor != ver)
1055 {
1056 LOG_WARN("Camera does not support asymmetric binning, using the horizontal value for both axes.");
1057 ver = hor;
1058 }
1059
1060 nlohmann::json response;
1061 nlohmann::json body;
1062 bool success;
1063
1064 body = {{"BinX", hor}};
1065 success = sendAlpacaPUT("/binx", body, response);
1066 if (!success) return false;
1067
1068 body = {{"BinY", ver}};
1069 success = sendAlpacaPUT("/biny", body, response);
1070 // We need to update ROI *after* setting binning in Alpaca (at least for Pegasus SmartEye for now)
1071 if (success)
1073 return false;
1074}
1075
1077{
1078 if (!isConnected())
1079 {
1080 LOG_ERROR("Not connected to Alpaca camera.");
1081 return false;
1082 }
1083
1084 // Get the capture format from the base class
1085 if (index >= m_CaptureFormats.size())
1086 {
1087 LOGF_ERROR("Invalid capture format index: %d", index);
1088 return false;
1089 }
1090
1091 const auto& format = m_CaptureFormats[index];
1092
1093 // Set the bit depth based on the selected format
1094 uint8_t bpp = format.bitsPerPixel;
1095 PrimaryCCD.setBPP(bpp);
1096
1097 // Update the frame buffer size
1098 int nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * PrimaryCCD.getBPP() / 8;
1100
1101 LOGF_DEBUG("Capture format set to %s (%d-bit)", format.label.c_str(), bpp);
1102 return true;
1103}
1104
1105// Helper function for pulse guiding
1106bool AlpacaCCD::sendPulseGuide(ALPACA_GUIDE_DIRECTION direction, long duration)
1107{
1108 if (!isConnected())
1109 {
1110 LOG_ERROR("Not connected to Alpaca camera.");
1111 return false;
1112 }
1113
1114 if (!m_CanPulseGuide)
1115 {
1116 LOG_ERROR("Camera does not support pulse guiding.");
1117 return false;
1118 }
1119
1120 nlohmann::json response;
1121 nlohmann::json body;
1122 int alpacaDirection;
1123
1124 switch (direction)
1125 {
1126 case ALPACA_GUIDE_NORTH:
1127 alpacaDirection = 0; // guideNorth
1128 break;
1129 case ALPACA_GUIDE_SOUTH:
1130 alpacaDirection = 1; // guideSouth
1131 break;
1132 case ALPACA_GUIDE_EAST:
1133 alpacaDirection = 2; // guideEast
1134 break;
1135 case ALPACA_GUIDE_WEST:
1136 alpacaDirection = 3; // guideWest
1137 break;
1138 default:
1139 LOG_ERROR("Invalid guide direction.");
1140 return false;
1141 }
1142
1143 body = {{"Direction", alpacaDirection}, {"Duration", duration}};
1144 return sendAlpacaPUT("/pulseguide", body, response);
1145
1146}
1147
1152{
1153 if (!sendPulseGuide(ALPACA_GUIDE_NORTH, ms))
1154 return IPS_ALERT;
1155
1156 m_PulseGuideAxis = AXIS_DE;
1157 m_PulseGuiding = true;
1158 return IPS_BUSY;
1159}
1160
1165{
1166 if (!sendPulseGuide(ALPACA_GUIDE_SOUTH, ms))
1167 return IPS_ALERT;
1168
1169 m_PulseGuideAxis = AXIS_DE;
1170 m_PulseGuiding = true;
1171 return IPS_BUSY;
1172}
1173
1178{
1179 if (!sendPulseGuide(ALPACA_GUIDE_EAST, ms))
1180 return IPS_ALERT;
1181
1182 m_PulseGuideAxis = AXIS_RA;
1183 m_PulseGuiding = true;
1184 return IPS_BUSY;
1185}
1186
1191{
1192 if (!sendPulseGuide(ALPACA_GUIDE_WEST, ms))
1193 return IPS_ALERT;
1194
1195 m_PulseGuideAxis = AXIS_RA;
1196 m_PulseGuiding = true;
1197 return IPS_BUSY;
1198}
1199
1203std::string AlpacaCCD::getAlpacaURL(const std::string& endpoint)
1204{
1205 return "/api/v1/camera/" + std::to_string(static_cast<int>(DeviceNumberNP[0].getValue())) + endpoint;
1206}
1207
1211std::string AlpacaCCD::getSensorTypeString(uint8_t sensorType)
1212{
1213 switch (sensorType)
1214 {
1215 case 0:
1216 return "Monochrome";
1217 case 1:
1218 return "Color";
1219 case 2:
1220 return "RGGB Bayer";
1221 case 3:
1222 return "CMYG Bayer";
1223 case 4:
1224 return "CMYG2 Bayer";
1225 case 5:
1226 return "LRGB Truesense";
1227 default:
1228 return "Unknown";
1229 }
1230}
1231
1235bool AlpacaCCD::sendAlpacaGET(const std::string& endpoint, nlohmann::json& response)
1236{
1237 if (!httpClient)
1238 {
1239 LOG_ERROR("HTTP client not initialized.");
1240 return false;
1241 }
1242
1243 std::string url = getAlpacaURL(endpoint);
1244 url += "?ClientID=" + std::to_string(getpid()) + "&ClientTransactionID=" + std::to_string(getTransactionId());
1245
1246 // 5 seconds timeout
1247 httpClient->set_read_timeout(5, 0);
1248 auto result = httpClient->Get(url.c_str());
1249
1250 if (!result)
1251 {
1252 LOGF_ERROR("HTTP GET failed for %s: %s", endpoint.c_str(),
1253 httplib::to_string(result.error()).c_str());
1254 return false;
1255 }
1256
1257 if (result->status != 200)
1258 {
1259 LOGF_ERROR("HTTP GET %s returned status %d", endpoint.c_str(), result->status);
1260 return false;
1261 }
1262
1263 try
1264 {
1265 response = nlohmann::json::parse(result->body);
1266
1267 // Check for Alpaca errors
1268 if (response.contains("ErrorNumber") && response["ErrorNumber"].get<int>() != 0)
1269 {
1270 LOGF_ERROR("Alpaca error in %s: %d - %s", endpoint.c_str(),
1271 response["ErrorNumber"].get<int>(),
1272 response["ErrorMessage"].get<std::string>().c_str());
1273 return false;
1274 }
1275
1276 return true;
1277
1278 }
1279 catch (const nlohmann::json::exception& e)
1280 {
1281 LOGF_ERROR("JSON parse error for %s: %s", endpoint.c_str(), e.what());
1282 return false;
1283 }
1284}
1285
1289bool AlpacaCCD::sendAlpacaPUT(const std::string& endpoint, const nlohmann::json& request, nlohmann::json& response)
1290{
1291 if (!httpClient)
1292 {
1293 LOG_ERROR("HTTP client not initialized.");
1294 return false;
1295 }
1296
1297 std::string url = getAlpacaURL(endpoint);
1298 std::string form_data = buildAlpacaFormData(request);
1299
1300 auto result = httpClient->Put(url, form_data, "application/x-www-form-urlencoded");
1301
1302 if (!result)
1303 {
1304 LOGF_ERROR("HTTP PUT failed for %s: %s", endpoint.c_str(),
1305 httplib::to_string(result.error()).c_str());
1306 return false;
1307 }
1308
1309 if (result->status != 200)
1310 {
1311 LOGF_ERROR("HTTP PUT %s returned status %d", endpoint.c_str(), result->status);
1312 return false;
1313 }
1314
1315 try
1316 {
1317 response = nlohmann::json::parse(result->body);
1318
1319 // Check for Alpaca errors
1320 if (response.contains("ErrorNumber") && response["ErrorNumber"].get<int>() != 0)
1321 {
1322
1323 // JM 2025.08.29: For some reason, I always get error 1025 when setting temperature
1324 // even though value is in range. Ignore this error for now.
1325 // if (response["ErrorNumber"].get<int>() == 1025)
1326 // return true;
1327
1328 LOGF_ERROR("Alpaca error in %s: %d - %s", endpoint.c_str(),
1329 response["ErrorNumber"].get<int>(),
1330 response["ErrorMessage"].get<std::string>().c_str());
1331 return false;
1332 }
1333
1334 return true;
1335
1336 }
1337 catch (const nlohmann::json::exception& e)
1338 {
1339 LOGF_ERROR("JSON parse error for %s: %s", endpoint.c_str(), e.what());
1340 return false;
1341 }
1342}
1343
1347bool AlpacaCCD::alpacaGetImageReady()
1348{
1349 nlohmann::json response;
1350 if (!sendAlpacaGET("/imageready", response))
1351 {
1352 return false;
1353 }
1354 return response["Value"].get<bool>();
1355}
1356
1360bool AlpacaCCD::alpacaGetImageArrayImageBytes(uint8_t** buffer, size_t* buffer_size, ImageBytesMetadata* metadata)
1361{
1362 if (!httpClient)
1363 {
1364 LOG_ERROR("HTTP client not initialized.");
1365 return false;
1366 }
1367
1368 // Set up headers for ImageBytes protocol as per ASCOM Alpaca API v10 section 8.5.1
1369 httplib::Headers headers =
1370 {
1371 {"Accept", "application/imagebytes"}
1372 };
1373
1374 std::string url = getAlpacaURL("/imagearray");
1375 url += "?ClientID=" + std::to_string(getpid()) + "&ClientTransactionID=" + std::to_string(getTransactionId());
1376
1377 auto result = httpClient->Get(url.c_str(), headers);
1378
1379 if (!result)
1380 {
1381 LOGF_ERROR("Failed to get image array: %s", httplib::to_string(result.error()).c_str());
1382 return false;
1383 }
1384
1385 if (result->status != 200)
1386 {
1387 LOGF_ERROR("Image array request returned status %d", result->status);
1388 return false;
1389 }
1390
1391 // Check Content-Type header as per ASCOM Alpaca API v10 section 8.5.3
1392 auto content_type = result->get_header_value("Content-Type");
1393 if (content_type.find("application/imagebytes") == std::string::npos)
1394 {
1395 LOGF_DEBUG("Server returned Content-Type: %s, falling back to JSON", content_type.c_str());
1396 return false; // Caller should try JSON fallback
1397 }
1398
1399 // Parse ImageBytes binary format as per ASCOM Alpaca API v10 section 8.6
1400 if (result->body.size() < sizeof(ImageBytesMetadata))
1401 {
1402 LOGF_ERROR("Response too small for ImageBytes metadata: %zu bytes", result->body.size());
1403 return false;
1404 }
1405
1406 // Extract metadata (44 bytes) with little-endian byte ordering as per section 8.8.2
1407 std::memcpy(metadata, result->body.data(), sizeof(ImageBytesMetadata));
1408
1409 // Convert from little-endian if needed (most systems are little-endian already)
1410 // Note: The spec requires little-endian format for maximum compatibility
1411 if (!parseImageBytesMetadata(metadata, result->body.size()))
1412 return false;
1413
1414 // Calculate expected data size based on transmission element type
1415 size_t bytes_per_element;
1416 switch (metadata->TransmissionElementType)
1417 {
1418 case 6: // Byte
1419 bytes_per_element = 1;
1420 break;
1421 case 1: // Int16
1422 case 8: // UInt16
1423 bytes_per_element = 2;
1424 break;
1425 case 2: // Int32
1426 case 9: // UInt32
1427 case 4: // Single (float)
1428 bytes_per_element = 4;
1429 break;
1430 case 3: // Double
1431 case 5: // UInt64
1432 case 7: // Int64
1433 bytes_per_element = 8;
1434 break;
1435 default:
1436 LOGF_ERROR("Unsupported transmission element type: %d", metadata->TransmissionElementType);
1437 return false;
1438 }
1439
1440 uint32_t planes = (metadata->Rank == 3) ? metadata->Dimension3 : 1;
1441 size_t expected_data_size = metadata->Dimension1 * metadata->Dimension2 * planes * bytes_per_element;
1442 size_t actual_data_size = result->body.size() - metadata->DataStart;
1443
1444 if (actual_data_size != expected_data_size)
1445 {
1446 LOGF_ERROR("Image data size mismatch: expected %zu bytes, got %zu bytes",
1447 expected_data_size, actual_data_size);
1448 return false;
1449 }
1450
1451 // Allocate buffer for image data
1452 *buffer_size = expected_data_size;
1453 PrimaryCCD.setFrameBufferSize(*buffer_size);
1455 if (!*buffer)
1456 {
1457 LOG_ERROR("Failed to allocate image buffer");
1458 return false;
1459 }
1460
1461 // Copy image data from response
1462 std::memcpy(*buffer, result->body.data() + metadata->DataStart, *buffer_size);
1463
1464 LOGF_DEBUG("ImageBytes: %dx%dx%d, type %d->%d, %zu bytes",
1465 metadata->Dimension1, metadata->Dimension2, planes,
1466 metadata->ImageElementType, metadata->TransmissionElementType, *buffer_size);
1467
1468 return true;
1469}
1470
1472{
1473 return ++m_ClientTransactionID;
1474}
1475
1477{
1478 // Convert JSON to form data for Alpaca compatibility
1479 std::string form_data;
1480 for (auto & [key, value] : request.items())
1481 {
1482 if (!form_data.empty()) form_data += "&";
1483
1484 if (value.is_string())
1485 {
1486 form_data += key + "=" + value.get<std::string>();
1487 }
1488 else if (value.is_number_integer())
1489 {
1490 form_data += key + "=" + std::to_string(value.get<int>());
1491 }
1492 else if (value.is_number_float())
1493 {
1494 form_data += key + "=" + std::to_string(value.get<double>());
1495 }
1496 else if (value.is_boolean())
1497 {
1498 form_data += key + "=" + (value.get<bool>() ? "true" : "false");
1499 }
1500 }
1501
1502 // Add ClientID and ClientTransactionID to form data
1503 if (!form_data.empty()) form_data += "&";
1504 form_data += "ClientID=" + std::to_string(getpid());
1505 form_data += "&ClientTransactionID=" + std::to_string(getTransactionId());
1506
1507 return form_data;
1508}
1509
1511{
1512 // Validate metadata version as per section 8.7.1
1513 if (metadata->MetadataVersion != 1)
1514 {
1515 LOGF_ERROR("Unsupported ImageBytes metadata version: %d", metadata->MetadataVersion);
1516 return false;
1517 }
1518
1519 // Check for errors as per section 8.9
1520 if (metadata->ErrorNumber != 0)
1521 {
1522 LOGF_ERROR("Alpaca ImageBytes error %d", metadata->ErrorNumber);
1523 return false;
1524 }
1525
1526 // Validate dimensions
1527 if (metadata->Rank < 2 || metadata->Rank > 3)
1528 {
1529 LOGF_ERROR("Invalid image rank: %d (must be 2 or 3)", metadata->Rank);
1530 return false;
1531 }
1532
1533 if (metadata->Dimension1 <= 0 || metadata->Dimension2 <= 0)
1534 {
1535 LOGF_ERROR("Invalid image dimensions: %dx%d", metadata->Dimension1, metadata->Dimension2);
1536 return false;
1537 }
1538
1539 if (metadata->DataStart < 0 || static_cast<size_t>(metadata->DataStart) > body_size)
1540 {
1541 LOGF_ERROR("Invalid data start offset: %d", metadata->DataStart);
1542 return false;
1543 }
1544
1545 return true;
1546}
1547
1551bool AlpacaCCD::alpacaGetImageArrayJSON(ImageMetadata& meta, uint8_t** buffer, size_t* buffer_size)
1552{
1553 // Fallback to standard JSON ImageArray method
1554 nlohmann::json response;
1555 if (!sendAlpacaGET("/imagearray", response))
1556 {
1557 LOG_ERROR("Failed to get image array via JSON");
1558 return false;
1559 }
1560
1561 if (!response.contains("Value") || !response["Value"].is_array())
1562 {
1563 LOG_ERROR("Invalid JSON image array response");
1564 return false;
1565 }
1566
1567 auto imageArray = response["Value"];
1568
1569 // Determine array dimensions
1570 if (imageArray.empty() || !imageArray[0].is_array())
1571 {
1572 LOG_ERROR("Invalid image array structure");
1573 return false;
1574 }
1575
1576 meta.width = imageArray.size();
1577 meta.height = imageArray[0].size();
1578 meta.rank = 2;
1579 meta.planes = 0;
1580
1581 // Check if it's a 3D array (color)
1582 if (!imageArray[0][0].is_number())
1583 {
1584 if (imageArray[0][0].is_array())
1585 {
1586 meta.rank = 3;
1587 meta.planes = imageArray[0][0].size();
1588 }
1589 else
1590 {
1591 LOG_ERROR("Unsupported image array element type");
1592 return false;
1593 }
1594 }
1595
1596 // Allocate buffer (assume 32-bit integers as per ASCOM standard)
1597 size_t pixel_count = meta.width * meta.height * (meta.planes > 0 ? meta.planes : 1);
1598 *buffer_size = pixel_count * sizeof(int32_t);
1599 PrimaryCCD.setFrameBufferSize(*buffer_size);
1601 if (!*buffer)
1602 {
1603 LOG_ERROR("Failed to allocate image buffer");
1604 return false;
1605 }
1606
1607 int32_t* int_buffer = reinterpret_cast<int32_t*>(*buffer);
1608 size_t index = 0;
1609
1610 // Copy data with proper ordering as per ASCOM specification
1611 if (meta.rank == 2)
1612 {
1613 // 2D monochrome array
1614 for (uint32_t x = 0; x < meta.width; x++)
1615 {
1616 for (uint32_t y = 0; y < meta.height; y++)
1617 {
1618 int_buffer[index++] = imageArray[x][y].get<int32_t>();
1619 }
1620 }
1621 }
1622 else
1623 {
1624 // 3D color array
1625 for (uint32_t x = 0; x < meta.width; x++)
1626 {
1627 for (uint32_t y = 0; y < meta.height; y++)
1628 {
1629 for (uint32_t p = 0; p < meta.planes; p++)
1630 {
1631 int_buffer[index++] = imageArray[x][y][p].get<int32_t>();
1632 }
1633 }
1634 }
1635 }
1636
1637 meta.type = 2; // Int32
1638
1639 // Get additional metadata for FITS headers
1640 meta.max_adu = m_MaxADU;
1641 meta.sensor_type = m_SensorType;
1642 if (meta.sensor_type >= 2)
1643 {
1644 meta.bayer_offset_x = atoi(BayerTP[CFA_OFFSET_X].getText());
1645 meta.bayer_offset_y = atoi(BayerTP[CFA_OFFSET_Y].getText());
1646 }
1647
1648 LOG_INFO("Downloaded image via JSON fallback");
1649 return true;
1650}
1651
1655bool AlpacaCCD::downloadImage()
1656{
1657 uint8_t* image_buffer = nullptr;
1658 size_t buffer_size = 0;
1659 bool success = false;
1660
1661 LOG_DEBUG("Starting image download...");
1662
1663 // Try ImageBytes protocol first (ASCOM Alpaca API v10 section 8)
1664 ImageBytesMetadata imagebytes_meta;
1665 if (alpacaGetImageArrayImageBytes(&image_buffer, &buffer_size, &imagebytes_meta))
1666 {
1667 LOGF_DEBUG("ImageBytes metadata: %dx%dx%d, rank=%d, image_type=%d, transmission_type=%d",
1668 imagebytes_meta.Dimension1, imagebytes_meta.Dimension2,
1669 (imagebytes_meta.Rank == 3) ? imagebytes_meta.Dimension3 : 1,
1670 imagebytes_meta.Rank, imagebytes_meta.ImageElementType, imagebytes_meta.TransmissionElementType);
1671 LOGF_DEBUG("Raw buffer size: %zu bytes", buffer_size);
1672
1673 // Convert ImageBytes metadata to our internal format
1674 m_CurrentImage.width = imagebytes_meta.Dimension1;
1675 m_CurrentImage.height = imagebytes_meta.Dimension2;
1676 m_CurrentImage.planes = (imagebytes_meta.Rank == 3) ? imagebytes_meta.Dimension3 : 0;
1677 m_CurrentImage.rank = imagebytes_meta.Rank;
1678
1679 // Map transmission element type to our type system
1680 switch (imagebytes_meta.TransmissionElementType)
1681 {
1682 case 6: // Byte
1683 m_CurrentImage.type = 1;
1684 LOG_DEBUG("Transmission type: Byte (8-bit) -> Internal type 1");
1685 break;
1686 case 1: // Int16
1687 case 8: // UInt16
1688 m_CurrentImage.type = 2;
1689 LOGF_DEBUG("Transmission type: %s (16-bit) -> Internal type 2",
1690 (imagebytes_meta.TransmissionElementType == 1) ? "Int16" : "UInt16");
1691 break;
1692 case 2: // Int32
1693 case 9: // UInt32
1694 case 4: // Single (float)
1695 m_CurrentImage.type = 3;
1696 LOGF_DEBUG("Transmission type: %s (32-bit) -> Internal type 3",
1697 (imagebytes_meta.TransmissionElementType == 2) ? "Int32" :
1698 (imagebytes_meta.TransmissionElementType == 9) ? "UInt32" : "Single");
1699 break;
1700 case 3: // Double
1701 case 5: // UInt64
1702 case 7: // Int64
1703 m_CurrentImage.type = 4;
1704 LOGF_DEBUG("Transmission type: %s (64-bit) -> Internal type 4",
1705 (imagebytes_meta.TransmissionElementType == 3) ? "Double" :
1706 (imagebytes_meta.TransmissionElementType == 5) ? "UInt64" : "Int64");
1707 break;
1708 default:
1709 m_CurrentImage.type = 3; // Default to Int32
1710 LOGF_WARN("Unknown transmission type %d, defaulting to Internal type 3",
1711 imagebytes_meta.TransmissionElementType);
1712 break;
1713 }
1714
1715 // Get additional metadata for FITS headers
1716 m_CurrentImage.max_adu = m_MaxADU;
1717 m_CurrentImage.sensor_type = m_SensorType;
1718 if (m_CurrentImage.sensor_type >= 2)
1719 {
1720 m_CurrentImage.bayer_offset_x = atoi(BayerTP[CFA_OFFSET_X].getText());
1721 m_CurrentImage.bayer_offset_y = atoi(BayerTP[CFA_OFFSET_Y].getText());
1722 LOGF_DEBUG("Bayer offsets: X=%d, Y=%d", m_CurrentImage.bayer_offset_x, m_CurrentImage.bayer_offset_y);
1723 }
1724
1725 LOGF_DEBUG("Final image metadata: %dx%d, planes=%d, rank=%d, type=%d",
1726 m_CurrentImage.width, m_CurrentImage.height, m_CurrentImage.planes,
1727 m_CurrentImage.rank, m_CurrentImage.type);
1728
1729 // Convert ImageBytes data format to match our processing expectations
1730 // ImageBytes uses row-major ordering as per ASCOM spec section 8.8.1
1731 // We need to handle the data according to transmission element type
1732 success = processImageBytesData(image_buffer, buffer_size, imagebytes_meta);
1733 }
1734 else
1735 {
1736 // Fallback to JSON ImageArray method
1737 LOG_DEBUG("ImageBytes not supported, falling back to JSON ImageArray");
1738 if (alpacaGetImageArrayJSON(m_CurrentImage, &image_buffer, &buffer_size))
1739 {
1740 LOGF_DEBUG("JSON image metadata: %dx%d, planes=%d, rank=%d, type=%d",
1741 m_CurrentImage.width, m_CurrentImage.height, m_CurrentImage.planes,
1742 m_CurrentImage.rank, m_CurrentImage.type);
1743 LOGF_DEBUG("JSON buffer size: %zu bytes", buffer_size);
1744
1745 // Translate coordinate system (ASCOM top-left to INDI bottom-left)
1746 translateCoordinates(image_buffer, m_CurrentImage);
1747
1748 // Process the image
1749 if (m_CurrentImage.planes == 0 || m_CurrentImage.planes == 1)
1750 {
1751 success = processMonoImage(image_buffer);
1752 }
1753 else
1754 {
1755 success = processColorImage(image_buffer);
1756 }
1757 }
1758 else
1759 {
1760 LOG_ERROR("Failed to download image via both ImageBytes and JSON methods");
1761 }
1762 }
1763
1764 // Clean up
1765 if (image_buffer)
1766 {
1767 // Since we are using the PrimaryCCD buffer, we don't need to free it here.
1768 // The INDI::CCD class will handle the memory management.
1769 }
1770
1771 m_ExposureInProgress = false;
1772 LOGF_DEBUG("Image download completed: %s", success ? "SUCCESS" : "FAILED");
1773 return success;
1774}
1775
1779void AlpacaCCD::updateReadoutModes()
1780{
1781 if (!isConnected())
1782 {
1783 LOG_WARN("Not connected to Alpaca camera, cannot update readout modes.");
1784 return;
1785 }
1786
1787 nlohmann::json response;
1788
1789 // Now handle readout modes separately (these are camera-specific settings)
1790 if (sendAlpacaGET("/readoutmodes", response))
1791 {
1792 if (response.contains("Value") && response["Value"].is_array())
1793 {
1794 auto readoutModes = response["Value"];
1795 if (!readoutModes.empty())
1796 {
1797 // Create switches for each readout mode
1798 ReadoutModeSP.resize(readoutModes.size());
1799 for (size_t i = 0; i < readoutModes.size(); ++i)
1800 {
1801 std::string modeName = readoutModes[i].get<std::string>();
1802 char nameBuf[MAXINDINAME];
1803 snprintf(nameBuf, sizeof(nameBuf), "MODE_%zu", i);
1804 ReadoutModeSP[i].fill(nameBuf, modeName.c_str(), ISS_OFF);
1805 }
1806
1807 ReadoutModeSP.fill(getDeviceName(), "READOUT_MODE", "Readout Mode", MAIN_CONTROL_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
1808
1809 // Get current readout mode
1810 if (sendAlpacaGET("/readoutmode", response))
1811 {
1812 m_CurrentReadoutModeIndex = response["Value"].get<int>();
1813 if (m_CurrentReadoutModeIndex >= 0 && static_cast<size_t>(m_CurrentReadoutModeIndex) < ReadoutModeSP.count())
1814 {
1815 ReadoutModeSP[m_CurrentReadoutModeIndex].setState(ISS_ON);
1816 }
1817 }
1818 else
1819 {
1820 LOG_WARN("Failed to get current readout mode, defaulting to first mode.");
1821 ReadoutModeSP[0].setState(ISS_ON);
1822 m_CurrentReadoutModeIndex = 0;
1823 }
1824
1825 ReadoutModeSP.setState(IPS_OK);
1826 ReadoutModeSP.apply();
1827 }
1828 }
1829 }
1830 else
1831 {
1832 LOG_WARN("Failed to get readout modes from Alpaca camera.");
1833 }
1834}
1835
1839void AlpacaCCD::updateStatus()
1840{
1841 if (!isConnected())
1842 return;
1843
1844 // Update camera state
1845 if (PrimaryCCD.isExposing())
1846 updateCameraState();
1847
1848 // Poll for completion of an in-progress pulse guide command
1849 if (m_PulseGuiding)
1850 {
1851 nlohmann::json response;
1852 if (!sendAlpacaGET("/ispulseguiding", response) || !response["Value"].get<bool>())
1853 {
1854 m_PulseGuiding = false;
1855 GuideComplete(m_PulseGuideAxis);
1856 }
1857 }
1858}
1859
1863void AlpacaCCD::workerExposure(const std::atomic_bool &isAboutToQuit, float duration)
1864{
1866
1867 LOGF_DEBUG("StartExposure->setexp : %.3fs", duration);
1868
1869 // Start the exposure on the Alpaca camera
1870 nlohmann::json response;
1871 nlohmann::json body =
1872 {
1873 {"Duration", duration},
1875 };
1876
1877 bool success = sendAlpacaPUT("/startexposure", body, response);
1878 if (!success)
1879 {
1880 LOG_ERROR("Failed to start exposure on Alpaca camera.");
1882 return;
1883 }
1884
1885 INDI::ElapsedTimer exposureTimer;
1886 LOGF_INFO("Taking a %.3f seconds frame...", duration);
1887
1888 // Wait for the exposure duration using INDI's own timer
1889 do
1890 {
1891 float delay = 0.1;
1892 float timeLeft = std::max(duration - exposureTimer.elapsed() / 1000.0, 0.0);
1893
1894 // Check the status every second until the time left is about one second,
1895 // after which decrease the poll interval
1896 if (timeLeft > 1.1)
1897 {
1898 delay = std::max(timeLeft - std::trunc(timeLeft), 0.005f);
1899 timeLeft = std::round(timeLeft);
1900 }
1901
1902 if (timeLeft > 0)
1903 {
1904 PrimaryCCD.setExposureLeft(timeLeft);
1905 }
1906
1907 // Check if we should quit
1908 if (isAboutToQuit)
1909 return;
1910
1911 // Sleep for the calculated delay
1912 usleep(delay * 1000 * 1000);
1913 }
1914 while (exposureTimer.elapsed() / 1000.0 < duration);
1915
1916 // Exposure time complete, now wait for image to be ready
1918 LOG_INFO("Exposure time complete, waiting for image...");
1919
1920 if (m_HasLastExposureDuration)
1921 {
1922 if (sendAlpacaGET("/lastexposureduration", response))
1923 {
1924 double actualDuration = response["Value"].get<double>();
1925 LOGF_DEBUG("Last exposure duration: %.3fs (requested %.3fs)", actualDuration, duration);
1926 }
1927 else
1928 m_HasLastExposureDuration = false;
1929 }
1930
1931 // Poll for image ready status
1932 int maxWaitTime = 30; // Maximum 30 seconds to wait for image
1933 int waitCount = 0;
1934
1935 while (waitCount < maxWaitTime * 10) // Check every 100ms
1936 {
1937 if (isAboutToQuit)
1938 return;
1939
1940 if (alpacaGetImageReady())
1941 {
1942 LOG_INFO("Image ready, downloading...");
1943 downloadImage();
1944 return;
1945 }
1946
1947 // Report download progress every second, if the camera supports it
1948 if (m_HasPercentCompleted && waitCount % 10 == 0)
1949 {
1950 if (sendAlpacaGET("/percentcompleted", response))
1951 LOGF_DEBUG("Percent completed: %d%%", response["Value"].get<int>());
1952 else
1953 m_HasPercentCompleted = false;
1954 }
1955
1956 usleep(100 * 1000); // Wait 100ms
1957 waitCount++;
1958 }
1959
1960 LOG_ERROR("Timeout waiting for image to be ready.");
1962}
1963
1967void AlpacaCCD::updateCoolerStatus()
1968{
1969 if (!isConnected())
1970 return;
1971
1972 nlohmann::json response;
1973 bool success;
1974
1975 // Get CoolerOn status
1976 success = sendAlpacaGET("/cooleron", response);
1977 if (success)
1978 {
1979 bool coolerOn = response["Value"].get<bool>();
1980 CoolerSP[INDI_ENABLED].setState(coolerOn ? ISS_ON : ISS_OFF);
1981 CoolerSP[INDI_DISABLED].setState(coolerOn ? ISS_OFF : ISS_ON);
1982 CoolerSP.setState(IPS_OK);
1983 CoolerSP.apply();
1984 }
1985 else
1986 {
1987 LOG_WARN("Failed to get cooler status.");
1988 }
1989
1990 // Get CoolerPower
1991 if (m_HasCoolerPower)
1992 {
1993 success = sendAlpacaGET("/coolerpower", response);
1994 if (success)
1995 {
1996 CoolerPowerNP[0].setValue(response["Value"].get<double>());
1997 CoolerPowerNP.setState(IPS_OK);
1998 CoolerPowerNP.apply();
1999 }
2000 else
2001 {
2002 LOG_WARN("Failed to get cooler power.");
2003 }
2004 }
2005}
2006
2010void AlpacaCCD::updateCameraState()
2011{
2012 if (!isConnected())
2013 return;
2014
2015 nlohmann::json response;
2016 if (sendAlpacaGET("/camerastate", response))
2017 {
2018 int state = response["Value"].get<int>();
2019 std::string previousState = CameraStateTP[0].getText();
2020 switch (state)
2021 {
2022 case 0: // CameraState_Idle
2023 CameraStateTP[0].setText("Idle");
2024 CameraStateTP.setState(IPS_IDLE);
2025 break;
2026 case 1: // CameraState_Waiting
2027 CameraStateTP[0].setText("Waiting");
2028 CameraStateTP.setState(IPS_BUSY);
2029 break;
2030 case 2: // CameraState_Exposing
2031 CameraStateTP[0].setText("Exposing");
2032 CameraStateTP.setState(IPS_BUSY);
2033 break;
2034 case 3: // CameraState_Reading
2035 CameraStateTP[0].setText("Reading");
2036 CameraStateTP.setState(IPS_BUSY);
2037 break;
2038 case 4: // CameraState_Download
2039 CameraStateTP[0].setText("Downloading");
2040 CameraStateTP.setState(IPS_BUSY);
2041 break;
2042 case 5: // CameraState_Error
2043 CameraStateTP[0].setText("Error");
2044 CameraStateTP.setState(IPS_ALERT);
2045 break;
2046 default:
2047 CameraStateTP[0].setText("Unknown");
2048 break;
2049 }
2050 // Only update if necessary
2051 if (previousState != CameraStateTP[0].getText())
2052 {
2053 CameraStateTP.apply();
2054 }
2055 }
2056 else
2057 {
2058 LOG_WARN("Failed to get camera state.");
2059 }
2060}
2061
2065bool AlpacaCCD::processImageBytesData(uint8_t* buffer, size_t buffer_size, const ImageBytesMetadata& metadata)
2066{
2067 // Handle ImageBytes data format as per ASCOM Alpaca API v10 section 8.8
2068 // ImageBytes uses row-major ordering with specific element ordering rules
2069
2070 LOGF_DEBUG("Processing ImageBytes data: %dx%d, rank=%d, transmission_type=%d",
2071 metadata.Dimension1, metadata.Dimension2, metadata.Rank, metadata.TransmissionElementType);
2072
2073 INDI::CCDChip* primary_chip = &PrimaryCCD;
2074 primary_chip->setImageExtension("fits");
2075
2076 uint32_t width = metadata.Dimension1;
2077 uint32_t height = metadata.Dimension2;
2078 uint32_t planes = (metadata.Rank == 3) ? metadata.Dimension3 : 1;
2079 size_t pixel_count = width * height * planes;
2080
2081 LOGF_DEBUG("Image dimensions: %dx%d, planes=%d, pixel_count=%zu", width, height, planes, pixel_count);
2082
2083 // Determine bytes per element based on transmission type
2084 size_t bytes_per_element;
2085 switch (metadata.TransmissionElementType)
2086 {
2087 case 6: // Byte
2088 bytes_per_element = 1;
2089 break;
2090 case 1: // Int16
2091 case 8: // UInt16
2092 bytes_per_element = 2;
2093 break;
2094 case 2: // Int32
2095 case 9: // UInt32
2096 case 4: // Single (float)
2097 bytes_per_element = 4;
2098 break;
2099 case 3: // Double
2100 case 5: // UInt64
2101 case 7: // Int64
2102 bytes_per_element = 8;
2103 break;
2104 default:
2105 LOGF_ERROR("Unsupported transmission element type: %d", metadata.TransmissionElementType);
2106 return false;
2107 }
2108
2109 LOGF_DEBUG("Bytes per element: %zu", bytes_per_element);
2110
2111 // Validate buffer size
2112 size_t expected_size = pixel_count * bytes_per_element;
2113 if (buffer_size != expected_size)
2114 {
2115 LOGF_ERROR("Buffer size mismatch: expected %zu, got %zu", expected_size, buffer_size);
2116 return false;
2117 }
2118
2119 // Copy source data before reallocating the frame buffer, since `buffer`
2120 // points into the current PrimaryCCD frame buffer and setFrameBufferSize
2121 // may realloc/free it.
2122 std::vector<uint8_t> src_copy(buffer, buffer + buffer_size);
2123
2124 // Convert ImageBytes data to INDI format (typically 16-bit)
2125 size_t indi_buffer_size = width * height * sizeof(uint16_t);
2126 PrimaryCCD.setFrameBufferSize(indi_buffer_size);
2127 uint16_t* indi_buffer = reinterpret_cast<uint16_t*>(PrimaryCCD.getFrameBuffer());
2128 if (!indi_buffer)
2129 {
2130 LOG_ERROR("Failed to allocate INDI buffer");
2131 return false;
2132 }
2133
2134 LOGF_DEBUG("Allocated INDI buffer: %zu bytes (%dx%d * 2 bytes/pixel)",
2135 indi_buffer_size, width, height);
2136
2137 // ImageBytes uses row-major ordering as per section 8.8.1
2138 // For 2D arrays: rightmost dimension (height) changes most quickly
2139 // For 3D arrays: rightmost dimension (color plane) changes most quickly
2140
2141 if (metadata.Rank == 2)
2142 {
2143 // 2D monochrome/Bayer image
2144 LOG_DEBUG("Converting 2D image data");
2145 convertImageBytesToINDI2D(src_copy.data(), indi_buffer, width, height, metadata.TransmissionElementType);
2146 }
2147 else if (metadata.Rank == 3)
2148 {
2149 // 3D color image - for now, just take the first plane or convert to grayscale
2150 // This is a simplified implementation; full color support would require more work
2151 LOGF_DEBUG("Converting 3D image data (averaging %d planes to grayscale)", planes);
2152 convertImageBytesToINDI3D(src_copy.data(), indi_buffer, width, height, planes, metadata.TransmissionElementType);
2153 }
2154 else
2155 {
2156 LOGF_ERROR("Unsupported image rank: %d", metadata.Rank);
2157 return false;
2158 }
2159
2160 // ImageBytes data is already in the correct coordinate system (FITS standard)
2161 // No need to flip coordinates like we do for JSON data
2162
2163 // Upload to INDI
2164 primary_chip->setFrame(0, 0, width, height);
2165 primary_chip->setFrameBuffer(reinterpret_cast<uint8_t*>(indi_buffer));
2166 primary_chip->setFrameBufferSize(indi_buffer_size, false);
2167
2168 LOGF_DEBUG("Set INDI frame buffer: %dx%d, size=%zu bytes", width, height, indi_buffer_size);
2169
2170 ExposureComplete(primary_chip);
2171 LOG_DEBUG("Image processing completed successfully");
2172 return true;
2173}
2174
2178bool AlpacaCCD::processMonoImage(uint8_t* buffer)
2179{
2180 // Set up INDI CCD buffer
2181 INDI::CCDChip* primary_chip = &PrimaryCCD;
2182
2183 primary_chip->setImageExtension("fits");
2184
2185 // Convert data type if needed (INDI typically expects 16-bit)
2186 uint16_t* indi_buffer = nullptr;
2187 size_t pixel_count = m_CurrentImage.width * m_CurrentImage.height;
2188 size_t indi_buffer_size = pixel_count * sizeof(uint16_t);
2189 PrimaryCCD.setFrameBufferSize(indi_buffer_size);
2190 indi_buffer = reinterpret_cast<uint16_t*>(PrimaryCCD.getFrameBuffer());
2191
2192 switch (m_CurrentImage.type)
2193 {
2194 case 1: // 8-bit to 16-bit
2195 for (size_t i = 0; i < pixel_count; i++)
2196 {
2197 indi_buffer[i] = static_cast<uint16_t>(buffer[i]) << 8; // Scale to 16-bit
2198 }
2199 break;
2200
2201 case 2: // 16-bit (direct copy)
2202 memcpy(indi_buffer, buffer, pixel_count * sizeof(uint16_t));
2203 break;
2204
2205 case 3: // 32-bit to 16-bit (scale down)
2206 {
2207 uint32_t* src32 = reinterpret_cast<uint32_t*>(buffer);
2208 for (size_t i = 0; i < pixel_count; i++)
2209 {
2210 indi_buffer[i] = static_cast<uint16_t>(src32[i] >> 16); // Scale down
2211 }
2212 break;
2213 }
2214 default:
2215 LOGF_ERROR("Unsupported Alpaca image type: %d", m_CurrentImage.type);
2216 return false;
2217 }
2218
2219 if (!indi_buffer)
2220 {
2221 LOG_ERROR("Failed to convert image data");
2222 return false;
2223 }
2224
2225 // Upload to INDI
2226 primary_chip->setFrame(0, 0, m_CurrentImage.width, m_CurrentImage.height);
2227 primary_chip->setFrameBuffer(reinterpret_cast<uint8_t*>(indi_buffer)); // Cast to uint8_t*
2228 primary_chip->setFrameBufferSize(pixel_count * sizeof(uint16_t), false);
2229
2230 ExposureComplete(primary_chip);
2231
2232 return true;
2233}
2234
2238bool AlpacaCCD::processColorImage(uint8_t* /*buffer*/)
2239{
2240 // Placeholder for color image processing
2241 LOG_WARN("Color image processing not yet implemented.");
2242 return false;
2243}
2244
2248void AlpacaCCD::translateCoordinates(uint8_t* buffer, const ImageMetadata& meta)
2249{
2250 // ASCOM: (0,0) = top-left, row-major memory
2251 // INDI: (0,0) = bottom-left, expects FITS standard
2252
2253 size_t bytes_per_pixel = (meta.type == 1) ? 1 : (meta.type == 2) ? 2 : (meta.type == 3) ? 4 : 8;
2254 size_t row_size = meta.width * bytes_per_pixel;
2255
2256 // For multi-plane images, flip each plane
2257 uint32_t planes = (meta.planes > 0) ? meta.planes : 1;
2258
2259 for (uint32_t plane = 0; plane < planes; plane++)
2260 {
2261 uint8_t* plane_start = buffer + (plane * meta.width * meta.height * bytes_per_pixel);
2262
2263 // Flip image vertically (reverse row order)
2264 for (uint32_t y = 0; y < meta.height / 2; y++)
2265 {
2266 uint8_t* top_row = plane_start + (y * row_size);
2267 uint8_t* bottom_row = plane_start + ((meta.height - 1 - y) * row_size);
2268
2269 // Swap rows
2270 for (size_t i = 0; i < row_size; i++)
2271 {
2272 uint8_t temp = top_row[i];
2273 top_row[i] = bottom_row[i];
2274 bottom_row[i] = temp;
2275 }
2276 }
2277 }
2278}
2279
2283void AlpacaCCD::temperatureTimerTimeout()
2284{
2285 if (!isConnected())
2286 return;
2287
2288 nlohmann::json response;
2289 bool success;
2290 IPState newState = TemperatureNP.getState();
2291
2292 // Get current temperature
2293 success = sendAlpacaGET("/ccdtemperature", response);
2294 if (success)
2295 {
2296 m_CurrentTemperature = response["Value"].get<double>();
2297
2298 // If cooling is active and target temperature is set, show goal status
2299 if (TemperatureNP.getState() == IPS_BUSY && !std::isnan(m_TargetTemperature)
2300 && std::abs(m_CurrentTemperature - m_TargetTemperature) <= TEMP_THRESHOLD)
2301 newState = IPS_OK;
2302 }
2303 else
2304 {
2305 LOG_WARN("Failed to get temperature from Alpaca camera.");
2306 newState = IPS_ALERT;
2307 }
2308
2309 // Update if there is a change
2310 if (std::abs(m_CurrentTemperature - TemperatureNP[0].getValue()) > 0.05 ||
2311 TemperatureNP.getState() != newState)
2312 {
2313 TemperatureNP.setState(newState);
2314 TemperatureNP[0].setValue(m_CurrentTemperature);
2316 }
2317
2318 if (HasCooler())
2319 updateCoolerStatus();
2320}
2321
2325void AlpacaCCD::convertImageBytesToINDI2D(uint8_t* src_buffer, uint16_t* dst_buffer, uint32_t width, uint32_t height,
2326 int32_t transmission_type)
2327{
2328 // ASCOM ImageBytes stores 2D data in column-major order: the rightmost
2329 // dimension (Dimension2 = height/rows) varies fastest, so each contiguous
2330 // block of `height` elements is one column.
2331 // FITS/INDI expects row-major order: each contiguous block of `width`
2332 // elements is one row. We transpose here: src[x*height+y] -> dst[y*width+x].
2333
2334 switch (transmission_type)
2335 {
2336 case 6: // Byte (8-bit)
2337 {
2338 uint8_t* src = src_buffer;
2339 for (uint32_t x = 0; x < width; x++)
2340 {
2341 for (uint32_t y = 0; y < height; y++)
2342 {
2343 dst_buffer[y * width + x] = static_cast<uint16_t>(src[x * height + y]) << 8;
2344 }
2345 }
2346 break;
2347 }
2348 case 1: // Int16
2349 {
2350 int16_t* src = reinterpret_cast<int16_t*>(src_buffer);
2351 for (uint32_t x = 0; x < width; x++)
2352 {
2353 for (uint32_t y = 0; y < height; y++)
2354 {
2355 int16_t val = src[x * height + y];
2356 dst_buffer[y * width + x] = static_cast<uint16_t>(val + 32768);
2357 }
2358 }
2359 break;
2360 }
2361 case 8: // UInt16
2362 {
2363 uint16_t* src = reinterpret_cast<uint16_t*>(src_buffer);
2364 for (uint32_t x = 0; x < width; x++)
2365 {
2366 for (uint32_t y = 0; y < height; y++)
2367 {
2368 dst_buffer[y * width + x] = src[x * height + y];
2369 }
2370 }
2371 break;
2372 }
2373 case 2: // Int32
2374 {
2375 int32_t* src = reinterpret_cast<int32_t*>(src_buffer);
2376 for (uint32_t x = 0; x < width; x++)
2377 {
2378 for (uint32_t y = 0; y < height; y++)
2379 {
2380 int32_t val = src[x * height + y];
2381 dst_buffer[y * width + x] = static_cast<uint16_t>(val);
2382 }
2383 }
2384 break;
2385 }
2386 case 9: // UInt32
2387 {
2388 uint32_t* src = reinterpret_cast<uint32_t*>(src_buffer);
2389 for (uint32_t x = 0; x < width; x++)
2390 {
2391 for (uint32_t y = 0; y < height; y++)
2392 {
2393 uint32_t val = src[x * height + y];
2394 dst_buffer[y * width + x] = static_cast<uint16_t>(val);
2395 }
2396 }
2397 break;
2398 }
2399 case 4: // Single (float)
2400 {
2401 float* src = reinterpret_cast<float*>(src_buffer);
2402 for (uint32_t x = 0; x < width; x++)
2403 {
2404 for (uint32_t y = 0; y < height; y++)
2405 {
2406 float val = src[x * height + y];
2407 dst_buffer[y * width + x] = static_cast<uint16_t>(val * 65535.0f);
2408 }
2409 }
2410 break;
2411 }
2412 default:
2413 LOGF_ERROR("Unsupported transmission type for 2D conversion: %d", transmission_type);
2414 memset(dst_buffer, 0, width * height * sizeof(uint16_t));
2415 break;
2416 }
2417}
2418
2419void AlpacaCCD::convertImageBytesToINDI3D(uint8_t* src_buffer, uint16_t* dst_buffer, uint32_t width, uint32_t height,
2420 uint32_t planes,
2421 int32_t transmission_type)
2422{
2423 // ASCOM ImageBytes 3D data: column-major with color plane as the fastest
2424 // dimension. Transpose to FITS row-major (grayscale average of planes).
2425
2426 switch (transmission_type)
2427 {
2428 case 6: // Byte (8-bit)
2429 {
2430 uint8_t* src = src_buffer;
2431 for (uint32_t x = 0; x < width; x++)
2432 {
2433 for (uint32_t y = 0; y < height; y++)
2434 {
2435 uint32_t sum = 0;
2436 for (uint32_t p = 0; p < planes; p++)
2437 {
2438 sum += src[(x * height + y) * planes + p];
2439 }
2440 uint16_t avg = static_cast<uint16_t>((sum / planes)) << 8;
2441 dst_buffer[y * width + x] = avg;
2442 }
2443 }
2444 break;
2445 }
2446 case 1: // Int16
2447 {
2448 int16_t* src = reinterpret_cast<int16_t*>(src_buffer);
2449 for (uint32_t x = 0; x < width; x++)
2450 {
2451 for (uint32_t y = 0; y < height; y++)
2452 {
2453 int32_t sum = 0;
2454 for (uint32_t p = 0; p < planes; p++)
2455 {
2456 sum += src[(x * height + y) * planes + p];
2457 }
2458 int16_t avg = static_cast<int16_t>(sum / planes);
2459 dst_buffer[y * width + x] = static_cast<uint16_t>(avg + 32768);
2460 }
2461 }
2462 break;
2463 }
2464 case 8: // UInt16
2465 {
2466 uint16_t* src = reinterpret_cast<uint16_t*>(src_buffer);
2467 for (uint32_t x = 0; x < width; x++)
2468 {
2469 for (uint32_t y = 0; y < height; y++)
2470 {
2471 uint32_t sum = 0;
2472 for (uint32_t p = 0; p < planes; p++)
2473 {
2474 sum += src[(x * height + y) * planes + p];
2475 }
2476 dst_buffer[y * width + x] = static_cast<uint16_t>(sum / planes);
2477 }
2478 }
2479 break;
2480 }
2481 case 2: // Int32
2482 {
2483 int32_t* src = reinterpret_cast<int32_t*>(src_buffer);
2484 for (uint32_t x = 0; x < width; x++)
2485 {
2486 for (uint32_t y = 0; y < height; y++)
2487 {
2488 int64_t sum = 0;
2489 for (uint32_t p = 0; p < planes; p++)
2490 {
2491 sum += src[(x * height + y) * planes + p];
2492 }
2493 int32_t avg = static_cast<int32_t>(sum / planes);
2494 dst_buffer[y * width + x] = static_cast<uint16_t>(avg >> 16);
2495 }
2496 }
2497 break;
2498 }
2499 case 9: // UInt32
2500 {
2501 uint32_t* src = reinterpret_cast<uint32_t*>(src_buffer);
2502 for (uint32_t x = 0; x < width; x++)
2503 {
2504 for (uint32_t y = 0; y < height; y++)
2505 {
2506 uint64_t sum = 0;
2507 for (uint32_t p = 0; p < planes; p++)
2508 {
2509 sum += src[(x * height + y) * planes + p];
2510 }
2511 uint32_t avg = static_cast<uint32_t>(sum / planes);
2512 dst_buffer[y * width + x] = static_cast<uint16_t>(avg >> 16);
2513 }
2514 }
2515 break;
2516 }
2517 case 4: // Single (float)
2518 {
2519 float* src = reinterpret_cast<float*>(src_buffer);
2520 for (uint32_t x = 0; x < width; x++)
2521 {
2522 for (uint32_t y = 0; y < height; y++)
2523 {
2524 float sum = 0.0f;
2525 for (uint32_t p = 0; p < planes; p++)
2526 {
2527 sum += src[(x * height + y) * planes + p];
2528 }
2529 float avg = sum / planes;
2530 dst_buffer[y * width + x] = static_cast<uint16_t>(avg * 65535.0f);
2531 }
2532 }
2533 break;
2534 }
2535 default:
2536 LOGF_ERROR("Unsupported transmission type for 3D conversion: %d", transmission_type);
2537 memset(dst_buffer, 0, width * height * sizeof(uint16_t));
2538 break;
2539 }
2540}
2541
2542void AlpacaCCD::addFITSKeywords(INDI::CCDChip * targetChip, std::vector<INDI::FITSRecord> &fitsKeywords)
2543{
2544 // Call base class to add standard INDI FITS keywords and custom keywords
2545 INDI::CCD::addFITSKeywords(targetChip, fitsKeywords);
2546
2547 // Add Alpaca-specific FITS keywords that are not covered by the base class or provide more precise information.
2548
2549 // MAXADU (Unique to Alpaca)
2550 fitsKeywords.push_back({"MAXADU", static_cast<int>(m_CurrentImage.max_adu), "Maximum ADU value"});
2551
2552 // Color/Bayer info (more specific than generic INDI BayerTP)
2553 switch (m_CurrentImage.sensor_type)
2554 {
2555 case 0: // Monochrome
2556 fitsKeywords.push_back({"COLORTYP", "MONOCHROME", "Sensor color type"});
2557 break;
2558 case 1: // Color
2559 fitsKeywords.push_back({"COLORTYP", "COLOR", "Sensor color type"});
2560 break;
2561 case 2: // RGGB Bayer
2562 fitsKeywords.push_back({"COLORTYP", "RGGB", "Bayer matrix pattern"});
2563 fitsKeywords.push_back({"XBAYROFF", static_cast<int>(m_CurrentImage.bayer_offset_x), "Bayer X offset"});
2564 fitsKeywords.push_back({"YBAYROFF", static_cast<int>(m_CurrentImage.bayer_offset_y), "Bayer Y offset"});
2565 break;
2566 case 3: // CMYG
2567 fitsKeywords.push_back({"COLORTYP", "CMYG", "Bayer matrix pattern"});
2568 fitsKeywords.push_back({"XBAYROFF", static_cast<int>(m_CurrentImage.bayer_offset_x), "Bayer X offset"});
2569 fitsKeywords.push_back({"YBAYROFF", static_cast<int>(m_CurrentImage.bayer_offset_y), "Bayer Y offset"});
2570 break;
2571 default:
2572 LOGF_WARN("Unknown sensor type: %d", m_CurrentImage.sensor_type);
2573 break;
2574 }
2575
2576 // Alpaca-specific identifiers
2577 fitsKeywords.push_back({"ALPACA", "TRUE", "Image from ASCOM Alpaca camera"});
2578 fitsKeywords.push_back({"ALPTYPE", static_cast<int>(m_CurrentImage.type), "Alpaca data type code"});
2579
2580 // Gain and Offset (if supported by the camera and not already added by base class)
2581 if (m_HasGain)
2582 {
2583 if (m_GainIsList)
2584 {
2585 int index = GainSP.findOnSwitchIndex();
2586 if (index != -1)
2587 fitsKeywords.push_back({"GAIN", GainSP[index].getLabel(), "Camera Gain setting"});
2588 }
2589 else
2590 {
2591 fitsKeywords.push_back({"GAIN", GainNP[0].getValue(), 3, "Camera Gain setting"});
2592 }
2593 }
2594 if (m_HasOffset)
2595 {
2596 if (m_OffsetIsList)
2597 {
2598 int index = OffsetSP.findOnSwitchIndex();
2599 if (index != -1)
2600 fitsKeywords.push_back({"OFFSET", OffsetSP[index].getLabel(), "Camera Offset setting"});
2601 }
2602 else
2603 {
2604 fitsKeywords.push_back({"OFFSET", OffsetNP[0].getValue(), 3, "Camera Offset setting"});
2605 }
2606 }
2607
2608 // Electrons per ADU and full well capacity (if reported by the camera)
2609 if (m_ElectronsPerADU > 0)
2610 {
2611 fitsKeywords.push_back({"EGAIN", m_ElectronsPerADU, 3, "Electrons per ADU"});
2612 }
2613 if (m_FullWellCapacity > 0)
2614 {
2615 fitsKeywords.push_back({"FULLWELL", m_FullWellCapacity, 3, "Full well capacity in electrons"});
2616 }
2617}
double temperature
virtual bool SetCaptureFormat(uint8_t index) override
SetCaptureFormat Set Active Capture format.
virtual bool UpdateCCDFrame(int x, int y, int w, int h) override
CCD calls this function when CCD Frame dimension needs to be updated in the hardware....
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
virtual bool AbortExposure() override
Abort ongoing exposure.
const char * getDefaultName() override
virtual IPState GuideNorth(uint32_t ms) override
Guide northward for ms milliseconds.
virtual IPState GuideEast(uint32_t ms) override
Guide easward for ms milliseconds.
void TimerHit() override
Callback function to be called once SetTimer duration elapses.
uint32_t getTransactionId()
void setDefaultServerAddress(const char *host, const char *port, bool force=false)
bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
virtual bool parseImageBytesMetadata(ImageBytesMetadata *metadata, size_t body_size)
virtual int SetTemperature(double temperature) override
Set CCD temperature.
virtual bool StartExposure(float duration) override
Start exposing primary CCD chip.
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
virtual IPState GuideSouth(uint32_t ms) override
Guide southward for ms milliseconds.
bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
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 ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
virtual bool UpdateCCDBin(int hor, int ver) override
CCD calls this function when CCD Binning needs to be updated in the hardware. Derived classes should ...
bool Disconnect() override
Disconnect from device.
virtual std::string buildAlpacaFormData(const nlohmann::json &request)
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save configuration items in XML file.
virtual IPState GuideWest(uint32_t ms) override
Guide westward for ms milliseconds.
bool isConnected() const
INDI::PropertyText getText(const char *name) const
bool isDeviceNameMatch(const char *otherName) const
Check that the device name matches the argument.
The CCDChip class provides functionality of a CCD Chip within a CCD.
Definition indiccdchip.h:39
void setFrameBuffer(uint8_t *buffer)
setFrameBuffer Set raw frame buffer pointer.
int getSubX() const
getSubX Get the starting left coordinates (X) of the frame.
Definition indiccdchip.h:99
int getSubY() const
getSubY Get the starting top coordinates (Y) of the frame.
void setExposureDuration(double duration)
setExposureDuration Set desired CCD frame exposure duration for next exposure. You must call this fun...
int getSubH() const
getSubH Get the height of the frame
int getBPP() const
getBPP Get CCD Chip depth (bits per pixel).
CCD_FRAME getFrameType() const
isInterlaced
int getBinY() const
getBinY Get vertical binning of the CCD chip.
void setFrame(uint32_t subx, uint32_t suby, uint32_t subw, uint32_t subh)
setFrame Set desired frame resolutoin for an exposure.
void setMinMaxStep(const char *property, const char *element, double min, double max, double step, bool sendToClient=true)
setMinMaxStep for a number property element
int getXRes() const
getXRes Get the horizontal resolution in pixels of the CCD Chip.
Definition indiccdchip.h:81
bool isExposing() const
int getBinX() const
getBinX Get horizontal binning of the CCD chip.
void setImageExtension(const char *ext)
setImageExtension Set image extension
uint8_t * getFrameBuffer()
getFrameBuffer Get raw frame buffer of the CCD chip.
void setExposureFailed()
setExposureFailed Alert the client that the exposure failed.
void setExposureLeft(double duration)
setExposureLeft Update exposure time left. Inform the client of the new exposure time left value.
int getSubW() const
getSubW Get the width of the frame
void setFrameBufferSize(uint32_t nbuf, bool allocMem=true)
setFrameBufferSize Set desired frame buffer size. The function will allocate memory accordingly....
int getYRes() const
Get the vertical resolution in pixels of the CCD Chip.
Definition indiccdchip.h:90
void setBPP(uint8_t bpp)
setBPP Set depth of CCD chip.
CCDChip PrimaryCCD
Definition indiccd.h:621
uint32_t GetCCDCapability() const
GetCCDCapability returns the CCD capabilities.
Definition indiccd.h:161
bool HasCooler()
Definition indiccd.h:223
@ CFA_OFFSET_Y
Definition indiccd.h:688
@ CFA_OFFSET_X
Definition indiccd.h:687
virtual void addFITSKeywords(CCDChip *targetChip, std::vector< FITSRecord > &fitsKeywords)
Generate FITS keywords that will be added to FIST/XISF file.
Definition indiccd.cpp:2055
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
Definition indiccd.cpp:1554
@ CCD_CAN_SUBFRAME
Definition indiccd.h:116
@ CCD_CAN_ABORT
Definition indiccd.h:117
@ CCD_HAS_SHUTTER
Definition indiccd.h:120
@ CCD_HAS_BAYER
Definition indiccd.h:122
@ CCD_CAN_BIN
Definition indiccd.h:115
@ CCD_HAS_ST4_PORT
Definition indiccd.h:119
@ CCD_HAS_COOLER
Definition indiccd.h:121
virtual bool ExposureComplete(CCDChip *targetChip)
Uploads target Chip exposed buffer as FITS to the client. Derived classes should class this function ...
Definition indiccd.cpp:2361
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...
Definition indiccd.cpp:525
INDI::PropertyNumber TemperatureNP
TemperatureNP Camera Temperature in Celcius.
Definition indiccd.h:669
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
Definition indiccd.cpp:157
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
Definition indiccd.cpp:910
INDI::PropertyText BayerTP
BayerTP Bayer pattern offset and type.
Definition indiccd.h:684
virtual bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
Definition indiccd.cpp:540
virtual void addCaptureFormat(const CaptureFormat &format)
addCaptureFormat Add a supported camera native capture format (e.g. Mono, Bayer8.....
Definition indiccd.cpp:3264
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save configuration items in XML file.
Definition indiccd.cpp:2914
virtual void GuideComplete(INDI_EQ_AXIS axis) override
GuideComplete Signal guide pulse completion.
Definition indiccd.cpp:3158
void SetCCDCapability(uint32_t cap)
SetCCDCapability Set the CCD capabilities. All fields must be initialized.
Definition indiccd.cpp:140
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
Definition indiccd.cpp:1143
virtual void SetCCDParams(int x, int y, int bpp, float xf, float yf)
Setup CCD parameters for primary CCD. Child classes call this function to update CCD parameters.
Definition indiccd.cpp:2888
std::vector< CaptureFormat > m_CaptureFormats
Definition indiccd.h:617
bool updateProperty(PropertyType &property, ValueType *values, char *names[], int n, std::function< bool()> updater, bool saveConfig=false)
Generic helper function to update an INDI property based on an external operation.
virtual bool saveConfig(bool silent=false, const char *property=nullptr)
Save the current properties in a configuration file.
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.
void addAuxControls()
Add Debug, Simulation, and Configuration options to the driver.
int SetTimer(uint32_t ms)
Set a timer to call the function TimerHit after ms milliseconds.
The ElapsedTimer class provides a fast way to calculate elapsed times.
int64_t elapsed() const
Returns the number of milliseconds since this ElapsedTimer was last started.
void setState(IPState state)
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 * getLabel() 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
void quit()
Sets the 'isAboutToClose' flag to 'true' and waits for the end of running function.
void start(const std::function< void(const std::atomic_bool &isAboutToClose)> &functionToRun)
Reserves a thread and uses it to run functionToRun. A running function can check the 'isAboutToClose'...
void callOnTimeout(const std::function< void()> &callback)
Definition inditimer.cpp:76
void start()
Starts or restarts the timer with the timeout specified in interval.
Definition inditimer.cpp:82
void stop()
Stops the timer.
Definition inditimer.cpp:97
a class to store JSON values
iteration_proxy< iterator > items() noexcept
helper to access iterator member functions in range-based for
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.
std::unique_ptr< AlpacaCCD > alpaca_ccd(new AlpacaCCD())
int32_t TransmissionElementType
int32_t ImageElementType
int32_t Rank
ALPACA_GUIDE_DIRECTION
@ ALPACA_GUIDE_SOUTH
@ ALPACA_GUIDE_WEST
@ ALPACA_GUIDE_EAST
@ ALPACA_GUIDE_NORTH
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
@ IP_WO
Definition indiapi.h:183
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 MAXINDINAME
Definition indiapi.h:189
@ AXIS_DE
@ AXIS_RA
Implementations for common driver routines.
int IUGetConfigText(const char *dev, const char *property, const char *member, char *value, int len)
IUGetConfigText Opens configuration file and reads single text property.
Definition indidriver.c:908
#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::vector< uint8_t > buffer
Namespace to encapsulate INDI client, drivers, and mediator classes.
std::string to_string(Error error)
Definition httplib.h:9275
std::unordered_multimap< std::string, std::string, detail::case_ignore::hash, detail::case_ignore::equal_to > Headers
Definition httplib.h:607
uint16_t type
Definition pwc-ioctl.h:0