Instrument Neutral Distributed Interface INDI 2.2.5
ccd_simulator.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2017 Jasem Mutlaq. All rights reserved.
3 Copyright(c) 2010 Gerry Rozema. All rights reserved.
4 This library is free software; you can redistribute it and/or
5 modify it under the terms of the GNU Library General Public
6 License version 2 as published by the Free Software Foundation.
7 .
8 This library is distributed in the hope that it will be useful,
9 but WITHOUT ANY WARRANTY; without even the implied warranty of
10 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 Library General Public License for more details.
12 .
13 You should have received a copy of the GNU Library General Public License
14 along with this library; see the file COPYING.LIB. If not, write to
15 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 Boston, MA 02110-1301, USA.
17*******************************************************************************/
18
19#include "ccd_simulator.h"
20#include "indicom.h"
22
23#include <libnova/julian_day.h>
24#include <libastro.h>
25
26#ifdef HAVE_XISF
27#include <libxisf.h>
28#endif
29
30#include <cmath>
31#include <cstdio>
32#include <cstring>
33#include <strings.h>
34#include <unistd.h>
35#include <sys/types.h>
36#include <dirent.h>
37#include <sys/stat.h>
38#include <algorithm>
39#include <chrono>
40#include <random>
41#include <thread>
42
43static pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
44static pthread_mutex_t condMutex = PTHREAD_MUTEX_INITIALIZER;
45
46static std::unique_ptr<CCDSim> ccdsim(new CCDSim());
47
49{
50 currentRA = RA;
51 currentDE = Dec;
52
53 terminateThread = false;
54
55 time(&RunStart);
56
57 // Filter stuff
58 FilterSlotNP[0].setMin(1);
59 FilterSlotNP[0].setMax(8);
60}
61
63{
65 SimulatorSettingsNP[SIM_YRES].getValue(),
66 16,
68 SimulatorSettingsNP[SIM_YSIZE].getValue());
69
73 m_Bias = OffsetNP[0].getValue();
76 // An oag is offset this much from center of scope position (arcminutes);
83 // This is the rotation of the simulated camera respective to North.
84 // Because the simulated star field is calculated with their RA/DEC-coordinates
85 // (see DrawCcdFrame()) the origin angle of star field points north. So this value
86 // for EQ mounts normally simulate a certain camera offset and is a constant.
87 // For ALTAZ-mount this variable is altered consecutively by the value of the parallactic
88 // angle (transfered through a signal from KStars/skymapdrawabstract.cpp) and this way used
89 // to simulate the deviation of the camera orientation from N.
91
92 uint32_t nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * PrimaryCCD.getBPP() / 8;
94
95 if (!m_PlanetMode)
96 Streamer->setPixelFormat(INDI_MONO, 16);
98
99 return true;
100}
101
103{
104 streamPredicate = 0;
105 terminateThread = false;
106 pthread_create(&primary_thread, nullptr, &streamVideoHelper, this);
108 return true;
109}
110
112{
113 pthread_mutex_lock(&condMutex);
114 streamPredicate = 1;
115 terminateThread = true;
116 pthread_cond_signal(&cv);
117 pthread_mutex_unlock(&condMutex);
118
119 return true;
120}
121
123{
124 return "CCD Simulator";
125}
126
128{
130
132
133 // Constrain temperature range to match TEC physics: -15°C to ambient (25°C)
134 TemperatureNP[0].setMin(-15);
135 TemperatureNP[0].setMax(25);
136 TemperatureNP[0].setValue(0);
137
138 CaptureFormat format = {"INDI_MONO", "Mono", 16, true};
139 addCaptureFormat(format);
140
141 // Planet mode uses RGB capture format
142 CaptureFormat rgbFormat = {"INDI_RGB", "RGB", 16, false};
143 addCaptureFormat(rgbFormat);
144
145 SimulatorSettingsNP[SIM_XRES].fill("SIM_XRES", "X resolution", "%4.0f", 512, 8192, 512, 1280);
146 SimulatorSettingsNP[SIM_YRES].fill("SIM_YRES", "Y resolution", "%4.0f", 512, 8192, 512, 1024);
147 SimulatorSettingsNP[SIM_XSIZE].fill("SIM_XSIZE", "X Pixel Size", "%4.2f", 1, 30, 5, 5.2);
148 SimulatorSettingsNP[SIM_YSIZE].fill("SIM_YSIZE", "Y Pixel Size", "%4.2f", 1, 30, 5, 5.2);
149 SimulatorSettingsNP[SIM_MAXVAL].fill("SIM_MAXVAL", "Maximum ADU", "%4.0f", 255, 65000, 1000, 65000);
150 SimulatorSettingsNP[SIM_SATURATION].fill("SIM_SATURATION", "Saturation Mag", "%4.1f", 0, 20, 1, 1.0);
151 SimulatorSettingsNP[SIM_LIMITINGMAG].fill("SIM_LIMITINGMAG", "Limiting Mag", "%4.1f", 0, 20, 1, 17.0);
152 SimulatorSettingsNP[SIM_NOISE].fill("SIM_NOISE", "Noise", "%4.0f", 0, 6000, 500, 10);
153 SimulatorSettingsNP[SIM_SKYGLOW].fill("SIM_SKYGLOW", "Sky Glow (magnitudes)", "%4.1f", 0, 6000, 500, 19.5);
154 SimulatorSettingsNP[SIM_OAGOFFSET].fill("SIM_OAGOFFSET", "Oag Offset (arcminutes)", "%4.1f", 0, 6000, 500, 0);
155 SimulatorSettingsNP[SIM_POLAR].fill("SIM_POLAR", "PAE (arcminutes)", "%4.1f", -600, 600, 100, 0);
156 SimulatorSettingsNP[SIM_POLARDRIFT].fill("SIM_POLARDRIFT", "PAE Drift (minutes)", "%4.1f", 0, 60, 5, 0);
157 SimulatorSettingsNP[SIM_PE_PERIOD].fill("SIM_PEPERIOD", "PE Period (seconds)", "%4.1f", 0, 60, 5, 0);
158 SimulatorSettingsNP[SIM_PE_MAX].fill("SIM_PEMAX", "PE Max (arcsec)", "%4.1f", 0, 6000, 500, 0);
159 SimulatorSettingsNP[SIM_TIME_FACTOR].fill("SIM_TIME_FACTOR", "Time Factor (x)", "%.2f", 0.01, 100, 10, 1);
160 SimulatorSettingsNP[SIM_ROTATION].fill("SIM_ROTATION", "Rotation Offset", "%.2f", -180, 180, 10, 0);
161
162 SimulatorSettingsNP.fill(getDeviceName(), "SIMULATOR_SETTINGS",
163 "Settings", SIMULATOR_TAB, IP_RW, 60, IPS_IDLE);
164 // load() is important to fill all editfields with saved values also, so ISNewNumber() of one field
165 // doesn't update the other fields of the group with the "old" contents.
167
168 // Sim Tests
169 SimTestsSP[SIM_TESTS_BAYER].fill("BAYER", "Bayer", ISS_OFF);
170 SimTestsSP[SIM_TESTS_CRASH].fill("CRASH", "Crash", ISS_OFF);
171 SimTestsSP[SIM_TESTS_TIMEOUT].fill("TIMEOUT", "Timeout", ISS_OFF);
172 SimTestsSP.fill(getDeviceName(), "SIM_TESTS", "Sim Tests", SIMULATOR_TAB, IP_RW,
173 ISR_NOFMANY, 60, IPS_IDLE);
174
175 // Simulate focusing
176 FocusSimulationNP[SIM_FOCUS_POSITION].fill("SIM_FOCUS_POSITION", "Focus", "%.f", 0.0, 100000.0, 1.0, 36700.0);
177 FocusSimulationNP[SIM_FOCUS_MAX].fill("SIM_FOCUS_MAX", "Max. Position", "%.f", 0.0, 100000.0, 1.0, 100000.0);
178 FocusSimulationNP[SIM_SEEING].fill("SIM_SEEING", "Seeing (arcsec)", "%4.2f", 0, 60, 0, 3.5);
179 FocusSimulationNP.fill(getDeviceName(), "SIM_FOCUSING", "Focus Simulation",
181
182 // Simulate sensor tilt (extra defocus in arcsec at sensor edge)
183 TiltSimulationNP[SIM_TILT_LR].fill("SIM_TILT_LR", "Left-Right (arcsec)", "%4.2f", 0, 20, 0.5, 0);
184 TiltSimulationNP[SIM_TILT_TB].fill("SIM_TILT_TB", "Top-Bottom (arcsec)", "%4.2f", 0, 20, 0.5, 0);
185 TiltSimulationNP.fill(getDeviceName(), "SIM_TILT", "Tilt Simulation",
187
188 // Periodic Error
189 EqPENP[AXIS_RA].fill("RA_PE", "RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 0);
190 EqPENP[AXIS_DE].fill("DEC_PE", "DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0);
191 EqPENP.fill(getDeviceName(), "EQUATORIAL_PE", "EQ PE", SIMULATOR_TAB, IP_RW, 60,
192 IPS_IDLE);
193
194 // FWHM
196 IUFillNumber(&FWHMN[0], "SIM_FWHM", "FWHM (arcseconds)", "%4.2f", 0, 60, 0, 7.5);
197 IUFillNumberVector(&FWHMNP, FWHMN, 1, focuser, "FWHM", "FWHM", OPTIONS_TAB, IP_RO, 60, IPS_IDLE);
198
199 // Cooler
200 CoolerSP[INDI_ENABLED].fill("COOLER_ON", "ON", ISS_OFF);
201 CoolerSP[INDI_DISABLED].fill("COOLER_OFF", "OFF", ISS_ON);
202 CoolerSP.fill(getDeviceName(), "CCD_COOLER", "Cooler", MAIN_CONTROL_TAB, IP_WO,
204
205 // Gain
206 GainNP[0].fill("GAIN", "value", "%.f", 0, 300, 10, 90);
207 GainNP.fill(getDeviceName(), "CCD_GAIN", "Gain", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
208
209 // Offset
210 OffsetNP[0].fill("OFFSET", "value", "%.f", 0, 6000, 500, 0);
211 OffsetNP.fill(getDeviceName(), "CCD_OFFSET", "Offset", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
212
213 // Directory to read images from. This is useful to test real images captured by camera
214 // For each capture, one file is read (sorted by name) and is sent to client.
215 DirectoryTP[0].fill("LOCATION", "Location", getenv("HOME"));
216 DirectoryTP.fill(getDeviceName(), "CCD_DIRECTORY_LOCATION", "Directory", SIMULATOR_TAB, IP_RW, 60, IPS_IDLE);
218
219 // Toggle Directory Reading. If enabled. The simulator will just read images from the directory and not generate them.
220 DirectorySP[INDI_ENABLED].fill("INDI_ENABLED", "Enabled", ISS_OFF);
221 DirectorySP[INDI_DISABLED].fill("INDI_DISABLED", "Disabled", ISS_ON);
222 DirectorySP.fill(getDeviceName(), "CCD_DIRECTORY_TOGGLE", "Use Dir.", SIMULATOR_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
223
224 // Resolution
225 for (uint8_t i = 0; i < Resolutions.size(); i++)
226 {
227 std::ostringstream ss;
228 if (Resolutions[i].first > 0)
229 ss << Resolutions[i].first << " x " << Resolutions[i].second;
230 else
231 ss << "Custom";
232 ResolutionSP[i].fill(ss.str().c_str(), ss.str().c_str(), i == 0 ? ISS_ON : ISS_OFF);
233 }
234 ResolutionSP.fill(getDeviceName(), "CCD_RESOLUTION", "Resolution", SIMULATOR_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
235
237
238#ifdef USE_EQUATORIAL_PE
239 IDSnoopDevice(mount, "EQUATORIAL_PE");
240#else
241 IDSnoopDevice(mount, "EQUATORIAL_EOD_COORD");
242#endif
243
244 IDSnoopDevice(focuser, "FWHM");
245
246 uint32_t cap = 0;
247
248 cap |= CCD_CAN_ABORT;
249 cap |= CCD_CAN_BIN;
250 cap |= CCD_CAN_SUBFRAME;
251 cap |= CCD_HAS_COOLER;
252 cap |= CCD_HAS_GUIDE_HEAD;
253 cap |= CCD_HAS_SHUTTER;
254 cap |= CCD_HAS_ST4_PORT;
255 cap |= CCD_HAS_STREAMING;
256 cap |= CCD_HAS_DSP;
257
258 SetCCDCapability(cap);
259
260 // This should be called after the initial SetCCDCapability (above)
261 // as it modifies the capabilities.
263
265
266 FilterSlotNP[0].setMin(1);
267 FilterSlotNP[0].setMax(8);
268
269 // ===================== Planet Simulation =====================
270
271 // Planet mode toggle
272 PlanetModeSP[PLANET_ENABLED].fill("PLANET_ENABLED", "Enabled", ISS_OFF);
273 PlanetModeSP[PLANET_DISABLED].fill("PLANET_DISABLED", "Disabled", ISS_ON);
274 PlanetModeSP.fill(getDeviceName(), "PLANET_MODE", "Planet Mode", SIMULATOR_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
275
276 // Planet selection
277 PlanetSelectSP[PLANET_MERCURY].fill("PLANET_MERCURY", "Mercury", ISS_OFF);
278 PlanetSelectSP[PLANET_VENUS].fill("PLANET_VENUS", "Venus", ISS_OFF);
279 PlanetSelectSP[PLANET_MARS].fill("PLANET_MARS", "Mars", ISS_OFF);
280 PlanetSelectSP[PLANET_JUPITER].fill("PLANET_JUPITER", "Jupiter", ISS_ON);
281 PlanetSelectSP[PLANET_SATURN].fill("PLANET_SATURN", "Saturn", ISS_OFF);
282 PlanetSelectSP[PLANET_URANUS].fill("PLANET_URANUS", "Uranus", ISS_OFF);
283 PlanetSelectSP[PLANET_NEPTUNE].fill("PLANET_NEPTUNE", "Neptune", ISS_OFF);
284 PlanetSelectSP[PLANET_MOON].fill("PLANET_MOON", "Moon", ISS_OFF);
285 PlanetSelectSP[PLANET_SUN].fill("PLANET_SUN", "Sun", ISS_OFF);
286 PlanetSelectSP.fill(getDeviceName(), "PLANET_SELECT", "Planet", SIMULATOR_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
287
288 // Planet settings: refresh interval (minutes) and size override (arcsec)
289 PlanetSettingsNP[PLANET_REFRESH].fill("PLANET_REFRESH", "Refresh (min)", "%4.0f", 1, 1440, 10, 60);
290 PlanetSettingsNP[PLANET_SIZE].fill("PLANET_SIZE", "Size (arcsec, 0=auto)", "%4.1f", 0, 3600, 10, 0);
291 PlanetSettingsNP.fill(getDeviceName(), "PLANET_SETTINGS", "Planet Settings", SIMULATOR_TAB, IP_RW, 60, IPS_IDLE);
293
294 // xplanet executable and config paths
295 PlanetPathsTP[PLANET_XPLANET_PATH].fill("XPLANET_PATH", "xplanet binary", "xplanet");
296 PlanetPathsTP[PLANET_XPLANET_CONFIG].fill("XPLANET_CONFIG", "Config file", "/usr/share/xplanet/config/default");
297 PlanetPathsTP.fill(getDeviceName(), "PLANET_PATHS", "xplanet Paths", SIMULATOR_TAB, IP_RW, 60, IPS_IDLE);
299
301
303
304 return true;
305}
306
308{
309 if (onOff)
310 {
312 BayerTP[CFA_OFFSET_X].setText("0");
313 BayerTP[CFA_OFFSET_Y].setText("0");
314 BayerTP[CFA_TYPE].setText("RGGB");
315 }
316 else
317 {
319 }
320}
321
338
340{
342
344
345 if (isConnected())
346 {
347 if (HasCooler())
349
352
356
358
359 if (HasGuideHead())
360 {
361 SetGuiderParams(500, 290, 16, 9.8, 12.6);
363 }
364
365 // Define the Filter Slot and name properties
367 }
368 else
369 {
370 if (HasCooler())
372
378
380 }
381
382 return true;
383}
384
386{
388 if (std::abs(temperature - TemperatureNP[0].getValue()) < 0.1)
389 {
390 TemperatureNP[0].setValue(temperature);
391 return 1;
392 }
393
394 auto isCooling = temperature < TemperatureNP[0].getValue();
397 CoolerSP.setState(isCooling ? IPS_BUSY : IPS_IDLE);
398 CoolerSP.apply();
399 return 0;
400}
401
402bool CCDSim::StartExposure(float duration)
403{
404 // for the simulator, we can just draw the frame now
405 // and it will get returned at the right time
406 // by the timer routines
407 AbortPrimaryFrame = false;
408 ExposureRequest = duration;
409
411 gettimeofday(&ExpStart, nullptr);
412
413 // Planet mode: use cached xplanet rendering
415 {
416 time_t now;
417 time(&now);
418 double elapsedMinutes = difftime(now, m_LastPlanetRenderTime) / 60.0;
419 if (m_CachedPlanetBuffer.empty() || elapsedMinutes >= m_PlanetRefreshMinutes)
420 {
421 if (renderPlanet() == false)
422 {
423 // Fallback to blank frame on failure
425 }
426 }
427 else
428 {
429 // Ensure buffer is sized correctly (NAXIS=3 already set by SetCaptureFormat)
430 uint32_t rgbBufSize = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * 3 * 2;
431 if (PrimaryCCD.getFrameBufferSize() != static_cast<int>(rgbBufSize))
432 PrimaryCCD.setFrameBufferSize(rgbBufSize);
433 // Use cached planet buffer
434 size_t copySize = std::min(m_CachedPlanetBuffer.size(),
435 static_cast<size_t>(PrimaryCCD.getFrameBufferSize()));
436 memcpy(PrimaryCCD.getFrameBuffer(), m_CachedPlanetBuffer.data(), copySize);
437 LOGF_DEBUG("Using cached planet render (%.1f minutes since last render)", elapsedMinutes);
438 }
439 }
440 // When the directory is enabled, load every frame type (LIGHT, DARK, BIAS, FLAT)
441 // from the watched folder instead of generating a synthetic frame.
442 else if (DirectorySP[INDI_ENABLED].getState() == ISS_ON)
443 {
444 if (loadNextImage() == false)
445 return false;
446 }
447 else
448 {
449 // Reset to mono (NAXIS=2) if coming from planet mode
450 if (PrimaryCCD.getNAxis() == 3)
451 {
453 uint32_t nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * PrimaryCCD.getBPP() / 8;
455 // Re-enable Bayer if previously enabled (not applicable during planet mode)
457 }
459 }
460
461 // Now compress the actual wait time
462 ExposureRequest = duration * m_TimeFactor;
463 InExposure = true;
464
465 return true;
466}
467
469{
471 AbortGuideFrame = false;
474 gettimeofday(&GuideExpStart, nullptr);
475 InGuideExposure = true;
476 return true;
477}
478
480{
481 if (!InExposure)
482 return true;
483
484 AbortPrimaryFrame = true;
485
486 return true;
487}
488
490{
491 //IDLog("Enter AbortGuideExposure\n");
492 if (!InGuideExposure)
493 return true; // no need to abort if we aren't doing one
494 AbortGuideFrame = true;
495 return true;
496}
497
499{
500 uint32_t nextTimer = getCurrentPollingPeriod();
501
502 // No need to reset timer if we are not connected anymore
503 if (!isConnected())
504 return;
505
507 {
509 {
510 InExposure = false;
511 AbortPrimaryFrame = false;
512 }
513 else
514 {
515 float timeleft;
517
518 //IDLog("CCD Exposure left: %g - Request: %g\n", timeleft, ExposureRequest);
519 if (timeleft < 0)
520 timeleft = 0;
521
522 PrimaryCCD.setExposureLeft(timeleft);
523
524 if (timeleft < 1.0)
525 {
526 if (timeleft <= 0.001)
527 {
528 InExposure = false;
529 // Skip binning for planet mode (RGB data) and directory mode; only bin for generated star fields
530 if (!m_PlanetMode && DirectorySP[INDI_DISABLED].getState() == ISS_ON)
533 }
534 else
535 {
536 // set a shorter timer
537 nextTimer = timeleft * 1000;
538 }
539 }
540 }
541 }
542
543 if (InGuideExposure)
544 {
546 if (timeleft < 0)
547 timeleft = 0;
548
549 GuideCCD.setExposureLeft(timeleft);
550
551 if (timeleft < 1.0)
552 {
553 if (timeleft <= 0.001)
554 {
555 InGuideExposure = false;
556 if (!AbortGuideFrame)
557 {
560 if (InGuideExposure)
561 {
562 // the call to complete triggered another exposure
564 if (timeleft < 1.0)
565 {
566 nextTimer = timeleft * 1000;
567 }
568 }
569 }
570 else
571 {
572 //IDLog("Not sending guide frame cuz of abort\n");
573 }
574 AbortGuideFrame = false;
575 }
576 else
577 {
578 nextTimer = timeleft * 1000; // set a shorter timer
579 }
580 }
581 }
582
583 // Simulate realistic TEC cooler with ambient temperature of 25°C.
584 // Minimum achievable temperature: 40°C below ambient (-15°C).
585 // Warming up beyond ambient is not possible (TEC cannot heat).
586 constexpr double AMBIENT_TEMPERATURE = 25.0;
587 constexpr double MIN_TEC_TEMPERATURE = -15.0; // 40°C below ambient
588
589 const double currentTemp = TemperatureNP[0].getValue();
590 // Clamp target to achievable range
591 const double targetTemp = std::clamp(TemperatureRequest, MIN_TEC_TEMPERATURE, AMBIENT_TEMPERATURE);
592
593 // The base class declares the target as reached (IPS_OK) as soon as the temperature is within
594 // RAMP_THRESHOLD of the requested target. That is adequate for real hardware which keeps
595 // settling onto its setpoint, so keep simulating while the sensor has not settled onto the
596 // requested setpoint yet, even if the state is no longer BUSY. Otherwise the reported
597 // temperature would freeze at the threshold boundary (e.g. stop at -2.5°C for a -2°C request
598 // with a 0.5°C threshold) and never reach the actual setpoint.
599 if (TemperatureNP.getState() == IPS_BUSY || std::abs(targetTemp - currentTemp) > 0.01)
600 {
601 double newTemp = currentTemp;
602
603 if (targetTemp < newTemp)
604 {
605 // Cooling: approach target at -0.5°C per TimerHit cycle
606 newTemp = std::max(targetTemp, newTemp - 0.5);
607 }
608 else if (targetTemp > newTemp)
609 {
610 // Warming: approach target naturally (ambient), slowing as we get closer
611 // Simulate exponential decay toward ambient
612 double deltaT = AMBIENT_TEMPERATURE - newTemp;
613 // When below ambient, warm up by ~20% of remaining gap per cycle
614 // (roughly 0.4°C at 10°C delta, slowing to ~0.02°C near ambient)
615 double warmingStep = deltaT * 0.2;
616 if (warmingStep < 0.02)
617 warmingStep = 0.02;
618 if (warmingStep > 0.5)
619 warmingStep = 0.5;
620 newTemp = std::min(targetTemp, newTemp + warmingStep);
621 }
622
623 TemperatureNP[0].setValue(newTemp);
624
625 if (std::abs(newTemp - m_LastTemperature) > 0.1)
626 {
627 m_LastTemperature = newTemp;
629 }
630
631 // Cooler is effectively off when at or near ambient
632 if (newTemp >= AMBIENT_TEMPERATURE - 0.1 && CoolerSP.getState() != IPS_IDLE)
633 {
637 CoolerSP.apply();
638 }
639 }
640
641
642 SetTimer(nextTimer);
643}
644
646{
647 float exposure_time;
648
649 uint16_t * ptr = reinterpret_cast<uint16_t *>(targetChip->getFrameBuffer());
650
651 if (targetChip->getXRes() == 500)
652 exposure_time = GuideExposureRequest * 4;
653 else if (Streamer->isStreaming())
654 exposure_time = (ExposureRequest < 1) ? (ExposureRequest * 100) : ExposureRequest * 2;
655 else
656 exposure_time = ExposureRequest;
657
658 exposure_time *= (1 + sqrt(GainNP[0].getValue()));
659
660 auto targetFocalLength = ScopeInfoNP[FOCAL_LENGTH].getValue() > 0 ? ScopeInfoNP[FOCAL_LENGTH].getValue() :
662
663 if (ShowStarField)
664 {
665 float PEOffset {0};
666
667 if (m_PEPeriod > 0)
668 {
669 time_t now;
670 time(&now);
671 double timesince = difftime(now, RunStart);
672 double PESpot = timesince / m_PEPeriod * 2.0 * 3.14159;
673 PEOffset = m_PEMax * std::sin(PESpot) / 3600.0f;
674 }
675
676 // Camera rotation: manual offset + snooped rotator + pier flip
678 double theta = m_RotationOffset;
679 if (!std::isnan(RotatorAngle))
680 theta += RotatorAngle;
681 if (pierSide == 1)
682 theta -= 180;
683 theta = range360(theta);
684 LOGF_DEBUG("Rotator Angle: %f, Camera Rotation: %f", RotatorAngle, theta);
685
686#ifdef USE_EQUATORIAL_PE
687 if (usePE)
688 {
691 }
692 else
693 {
694 static bool s_warnedNoSnoop = false;
695 if (!s_warnedNoSnoop)
696 {
697 LOG_WARN("EQUATORIAL_PE not yet snooped -- rendering at raw EQUATORIAL_EOD_COORD (mount errors not active)");
698 s_warnedNoSnoop = true;
699 }
700#endif
701 currentRA = RA;
702 currentDE = Dec;
703
704 if (std::isnan(currentRA))
705 {
706 currentRA = 0;
707 currentDE = 0;
708 }
709
710 INDI::IEquatorialCoordinates epochPos { currentRA, currentDE }, J2000Pos { 0, 0 };
711 INDI::ObservedToJ2000(&epochPos, m_Clock.julianDate(), &J2000Pos);
712 currentRA = J2000Pos.rightascension;
713 currentDE = J2000Pos.declination;
714
717#ifdef USE_EQUATORIAL_PE
718 }
719#endif
720
721 // Field center: RA in degrees (with PE), Dec with OAG offset and polar drift
722 double ra_deg = currentRA * 15.0 + PEOffset;
723 double cameradec = currentDE + m_OAGOffset / 60.0;
724 double decr = cameradec * 0.0174532925;
725 double decDrift = (m_PolarDrift * m_PolarError * cos(decr)) / 3.81;
726 double dec_deg = cameradec + decDrift / 3600.0;
727
728 INDI::CCDChip::CCD_FRAME ftype = targetChip->getFrameType();
729 bool const isLight = (ftype == INDI::CCDChip::LIGHT_FRAME);
730 bool const isFlat = (ftype == INDI::CCDChip::FLAT_FRAME);
731
732 RenderConfig cfg;
733 cfg.maxVal = m_MaxVal;
736 cfg.seeing = m_Seeing;
737 cfg.tiltLR = TiltSimulationNP[SIM_TILT_LR].getValue();
738 cfg.tiltTB = TiltSimulationNP[SIM_TILT_TB].getValue();
739 // Sky glow: 30% boost for realistic light-frame backgrounds; flat frames use diffuser daylight level
740 cfg.skyGlow = isLight ? m_SkyGlow * 1.3f : m_SkyGlow / 10.0f;
742
743 std::unique_lock<std::mutex> guard(ccdBufferLock);
744
745 if (isLight || isFlat)
746 {
747 int drawn = m_Renderer.renderFrame(targetChip, ra_deg, dec_deg,
748 targetFocalLength, theta, exposure_time, isLight);
749 if (isLight && drawn < 0)
750 LOG_ERROR("Error launching gsc, is it installed with appropriate environment variables set?");
751 else if (isLight && drawn == 0)
752 LOG_WARN("No stars found in field -- check gsc catalog coverage for this region.");
753 }
754 else
755 {
756 memset(targetChip->getFrameBuffer(), 0, targetChip->getFrameBufferSize());
757 }
758
760 }
761 else
762 {
763 testvalue++;
764 if (testvalue > 255)
765 testvalue = 0;
766 uint16_t val = testvalue;
767
768 int nbuf = targetChip->getSubW() * targetChip->getSubH();
769
770 for (int x = 0; x < nbuf; x++)
771 {
772 *ptr = val++;
773 ptr++;
774 }
775 }
776 return 0;
777}
778
779float CCDSim::CalcTimeLeft(timeval start, float req)
780{
781 struct timeval now
782 {
783 0, 0
784 };
785 gettimeofday(&now, nullptr);
786 double const timesince = (now.tv_sec * 1000.0 + now.tv_usec / 1000.0)
787 - (start.tv_sec * 1000.0 + start.tv_usec / 1000.0);
788 return static_cast<float>(req - timesince / 1000.0);
789}
790
792{
793 guideNSOffset += v / 1000.0 * GuideRate / 3600;
794 return IPS_OK;
795}
796
798{
799 guideNSOffset += v / -1000.0 * GuideRate / 3600;
800 return IPS_OK;
801}
802
804{
805 float c = v / 1000.0 * GuideRate;
806 c = c / 3600.0 / 15.0;
807 c = c / (cos(currentDE * 0.0174532925));
808
809 guideWEOffset += c;
810
811 return IPS_OK;
812}
813
815{
816 float c = v / -1000.0 * GuideRate;
817 c = c / 3600.0 / 15.0;
818 c = c / (cos(currentDE * 0.0174532925));
819
820 guideWEOffset += c;
821
822 return IPS_OK;
823}
824
825bool CCDSim::ISNewText(const char * dev, const char * name, char * texts[], char * names[], int n)
826{
827 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
828 {
829 // This is for our device
830 // Now lets see if it's something we process here
831 if (INDI::FilterInterface::processText(dev, name, texts, names, n))
832 return true;
833
834 if (m_Clock.processText(name, texts, names, n))
835 return true;
836
837 if (DirectoryTP.isNameMatch(name))
838 {
839 DirectoryTP.update(texts, names, n);
840 if (DirectorySP[INDI_ENABLED].getState() == ISS_OFF || (DirectorySP[INDI_ENABLED].getState() == ISS_ON && watchDirectory()))
842 else
846 return true;
847 }
848
849 if (PlanetPathsTP.isNameMatch(name))
850 {
851 PlanetPathsTP.update(texts, names, n);
854 // Invalidate cache to force re-render with new paths
859 return true;
860 }
861 }
862
863 return INDI::CCD::ISNewText(dev, name, texts, names, n);
864}
865
866bool CCDSim::ISNewNumber(const char * dev, const char * name, double values[], char * names[], int n)
867{
868 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
869 {
870 if (INDI::FilterInterface::processNumber(dev, name, values, names, n))
871 return true;
872
873 if (GainNP.isNameMatch(name))
874 {
875 GainNP.update(values, names, n);
877 GainNP.apply();
878 return true;
879 }
880 if (OffsetNP.isNameMatch(name))
881 {
882 OffsetNP.update(values, names, n);
884 OffsetNP.apply();
885 m_Bias = OffsetNP[0].getValue();
886 return true;
887 }
888 else if (SimulatorSettingsNP.isNameMatch(name))
889 {
890 SimulatorSettingsNP.update(values, names, n);
892
893 // Reset our parameters now
895 // Invalidate planet cache since FOV/pixel scale may have changed
899 return true;
900 }
901 // Record PE EQ to simulate different position in the sky than actual mount coordinate
902 // This can be useful to simulate Periodic Error or cone error or any arbitrary error.
903 else if (EqPENP.isNameMatch(name))
904 {
905 EqPENP.update(values, names, n);
907
908 INDI::IEquatorialCoordinates epochPos { 0, 0 }, J2000Pos { 0, 0 };
909 epochPos.rightascension = EqPENP[AXIS_RA].getValue();
910 epochPos.declination = EqPENP[AXIS_DE].getValue();
911
912 RA = EqPENP[AXIS_RA].getValue();
913 Dec = EqPENP[AXIS_DE].getValue();
914
915 INDI::ObservedToJ2000(&epochPos, m_Clock.julianDate(), &J2000Pos);
916 currentRA = J2000Pos.rightascension;
917 currentDE = J2000Pos.declination;
918 usePE = true;
919
920 EqPENP.apply();
921 return true;
922 }
923 else if (FocusSimulationNP.isNameMatch(name))
924 {
925 // update focus simulation parameters
926 FocusSimulationNP.update(values, names, n);
929 }
930 else if (TiltSimulationNP.isNameMatch(name))
931 {
932 TiltSimulationNP.update(values, names, n);
935 }
936 else if (PlanetSettingsNP.isNameMatch(name))
937 {
938 PlanetSettingsNP.update(values, names, n);
941 // Invalidate cache so new settings take effect next exposure
946 return true;
947 }
948 }
949
950 return INDI::CCD::ISNewNumber(dev, name, values, names, n);
951}
952
953bool CCDSim::ISNewSwitch(const char * dev, const char * name, ISState * states, char * names[], int n)
954{
955 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
956 {
957 // Sim Tests
958 if (SimTestsSP.isNameMatch(name))
959 {
960 SimTestsSP.update(states, names, n);
961
962 // Bayer
963 if (SimTestsSP[SIM_TESTS_BAYER].getState() == ISS_ON)
964 {
965 m_SimulateBayer = true;
967 }
968 else
969 {
970 m_SimulateBayer = false;
972 }
974
975 // Crash
976 if (SimTestsSP[SIM_TESTS_CRASH].getState() == ISS_ON)
977 {
978 abort();
979 }
980
981 // Timeout
982 if (SimTestsSP[SIM_TESTS_TIMEOUT].getState() == ISS_ON)
983 {
984 m_SimulateTimeout = true;
986 }
987 else
988 {
989 m_SimulateTimeout = false;
991 }
992
995 return true;
996 }
997 else if (CoolerSP.isNameMatch(name))
998 {
999 CoolerSP.update(states, names, n);
1000
1001 if (CoolerSP[INDI_ENABLED].getState() == ISS_ON)
1003 else
1004 {
1006 // When cooler is turned off, warm up to ambient (25°C)
1007 constexpr double AMBIENT_TEMPERATURE = 25.0;
1008 TemperatureRequest = AMBIENT_TEMPERATURE;
1009 m_TargetTemperature = AMBIENT_TEMPERATURE;
1013 }
1014
1015 CoolerSP.apply();
1016
1017 return true;
1018 }
1019 else if (DirectorySP.isNameMatch(name))
1020 {
1021 DirectorySP.update(states, names, n);
1022 m_AllFiles.clear();
1023 m_RemainingFiles.clear();
1024 if (DirectorySP[INDI_ENABLED].getState() == ISS_ON)
1025 {
1026 if (watchDirectory() == false)
1027 {
1031 }
1032 }
1033 else
1034 {
1035 m_RemainingFiles.clear();
1038 LOG_INFO("Directory-based images are disabled.");
1039 }
1040 DirectorySP.apply(nullptr);
1041 return true;
1042 }
1043 else if (ResolutionSP.isNameMatch(name))
1044 {
1045 ResolutionSP.update(states, names, n);
1048
1049 int index = ResolutionSP.findOnSwitchIndex();
1050 if (index >= 0 && index < static_cast<int>(Resolutions.size() - 1))
1051 {
1052 SimulatorSettingsNP[SIM_XRES].setValue(Resolutions[index].first);
1053 SimulatorSettingsNP[SIM_YRES].setValue(Resolutions[index].second);
1055 SimulatorSettingsNP[SIM_YRES].getValue(),
1056 16,
1057 SimulatorSettingsNP[SIM_XSIZE].getValue(),
1058 SimulatorSettingsNP[SIM_YSIZE].getValue());
1060 uint32_t nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * PrimaryCCD.getBPP() / 8;
1062
1064
1065 // Invalidate planet cache since resolution changed
1067 if (m_PlanetMode)
1068 {
1069 SetCaptureFormat(1); // Re-apply RGB format with new buffer size
1070 }
1071 }
1072 return true;
1073 }
1074 // Planet mode toggle
1075 else if (PlanetModeSP.isNameMatch(name))
1076 {
1077 PlanetModeSP.update(states, names, n);
1079 if (m_PlanetMode)
1080 {
1081 // Switch to RGB capture format
1082 int rgbIndex = -1;
1083 for (size_t i = 0; i < m_CaptureFormats.size(); i++)
1084 {
1085 if (m_CaptureFormats[i].name == "INDI_RGB")
1086 {
1087 rgbIndex = static_cast<int>(i);
1088 break;
1089 }
1090 }
1091 if (rgbIndex >= 0 && CaptureFormatSP[rgbIndex].getState() != ISS_ON)
1092 {
1094 CaptureFormatSP[rgbIndex].setState(ISS_ON);
1096 SetCaptureFormat(rgbIndex);
1097 }
1098 // Disable directory mode when planet mode is active
1102 m_AllFiles.clear();
1103 m_RemainingFiles.clear();
1104 // Invalidate cached planet so it re-renders on next exposure
1106 LOGF_INFO("Planet simulation enabled for %s", m_SelectedPlanet.c_str());
1107 }
1108 else
1109 {
1110 // Switch back to Mono capture format
1111 int monoIndex = -1;
1112 for (size_t i = 0; i < m_CaptureFormats.size(); i++)
1113 {
1114 if (m_CaptureFormats[i].name == "INDI_MONO")
1115 {
1116 monoIndex = static_cast<int>(i);
1117 break;
1118 }
1119 }
1120 if (monoIndex >= 0 && CaptureFormatSP[monoIndex].getState() != ISS_ON)
1121 {
1123 CaptureFormatSP[monoIndex].setState(ISS_ON);
1125 SetCaptureFormat(monoIndex);
1126 }
1127 // Restore star field settings: pixel format, buffer, Bayer, streamer
1130 LOG_INFO("Planet simulation disabled. Returning to star field mode.");
1131 }
1133 PlanetModeSP.apply(nullptr);
1134 return true;
1135 }
1136 // Planet selection
1137 else if (PlanetSelectSP.isNameMatch(name))
1138 {
1139 PlanetSelectSP.update(states, names, n);
1140 int index = PlanetSelectSP.findOnSwitchIndex();
1141 static const char* planetNames[] =
1142 {
1143 "mercury", "venus", "mars", "jupiter", "saturn",
1144 "uranus", "neptune", "moon", "sun"
1145 };
1146 if (index >= 0 && index < 9)
1147 {
1148 m_SelectedPlanet = planetNames[index];
1149 // Invalidate cache to re-render with new planet
1151 LOGF_INFO("Selected planet: %s", m_SelectedPlanet.c_str());
1152 }
1154 PlanetSelectSP.apply(nullptr);
1155 return true;
1156 }
1157 }
1158
1159 // Nobody has claimed this, so, ignore it
1160 return INDI::CCD::ISNewSwitch(dev, name, states, names, n);
1161}
1162
1164{
1165 DIR* dirp = opendir(DirectoryTP[0].getText());
1166 if (dirp == nullptr)
1167 {
1168 LOGF_ERROR("Cannot monitor invalid directory %s", DirectoryTP[0].getText());
1169 return false;
1170 }
1171
1172 struct dirent * dp;
1173 std::string d_dir = std::string(DirectoryTP[0].getText());
1174 auto directory = DirectoryTP[0].getText();
1175 if (directory[std::string(directory).length() - 1] != '/')
1176 d_dir += "/";
1177
1178 // Accept FITS (.fit/.fits) and, when built with LibXISF support, XISF (.xisf) files.
1179 static const std::vector<std::string> supportedExtensions =
1180 {
1181 ".fits", ".fit",
1182#ifdef HAVE_XISF
1183 ".xisf",
1184#endif
1185 };
1186
1187 auto hasSupportedExtension = [](const std::string &name)
1188 {
1189 for (const auto &extension : supportedExtensions)
1190 {
1191 if (name.size() < extension.size())
1192 continue;
1193 if (strcasecmp(name.c_str() + name.size() - extension.size(), extension.c_str()) == 0)
1194 return true;
1195 }
1196 return false;
1197 };
1198
1199 while ((dp = readdir(dirp)) != NULL)
1200 {
1201 if (hasSupportedExtension(dp->d_name))
1202 m_AllFiles.push_back(d_dir + dp->d_name);
1203 }
1204 closedir(dirp);
1205
1206 if (m_AllFiles.empty())
1207 {
1208 LOGF_ERROR("No supported image files (.fit, .fits%s) found in directory %s",
1209#ifdef HAVE_XISF
1210 ", .xisf"
1211#else
1212 ""
1213#endif
1214 , directory);
1215 return false;
1216 }
1217 else
1218 {
1219 std::sort(m_AllFiles.begin(), m_AllFiles.end());
1221 LOGF_INFO("Directory-based images are enabled. Subsequent exposures will be loaded from directory %s",
1222 directory);
1223 }
1224
1225 return true;
1226}
1227
1229{
1230#ifdef USE_EQUATORIAL_PE
1232#endif
1233 IDSnoopDevice(ActiveDeviceTP[ACTIVE_TELESCOPE].getText(), "EQUATORIAL_EOD_COORD");
1235
1237}
1238
1240{
1241 if (IUSnoopNumber(root, &FWHMNP) == 0)
1242 {
1243 // we calculate the FWHM and do not snoop it from the focus simulator
1244 // m_Seeing = FWHMNP.np[0].value;
1245 return true;
1246 }
1247
1248 XMLEle * ep = nullptr;
1249 const char * propName = findXMLAttValu(root, "name");
1250
1251 if (!strcmp(propName, "ABS_FOCUS_POSITION"))
1252 {
1253 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
1254 {
1255 const char * name = findXMLAttValu(ep, "name");
1256
1257 if (!strcmp(name, "FOCUS_ABSOLUTE_POSITION"))
1258 {
1259 FocuserPos = atol(pcdataXMLEle(ep));
1260 LOGF_DEBUG("Snooped FocuserPosition %g", FocuserPos);
1261 // calculate FWHM
1262 double focus = FocusSimulationNP[SIM_FOCUS_POSITION].getValue();
1263 double max = FocusSimulationNP[SIM_FOCUS_MAX].getValue();
1264 double optimalFWHM = FocusSimulationNP[SIM_SEEING].getValue();
1265
1266 // limit to +/- 10
1267 double ticks = 20 * (FocuserPos - focus) / max;
1268
1269 m_Seeing = 0.5625 * ticks * ticks + optimalFWHM;
1270 return true;
1271 }
1272 }
1273 }
1274 else if (!strcmp(propName, "ABS_ROTATOR_ANGLE"))
1275 {
1276 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
1277 {
1278 const char * name = findXMLAttValu(ep, "name");
1279
1280 if (!strcmp(name, "ANGLE"))
1281 {
1282 RotatorAngle = atof(pcdataXMLEle(ep));
1283 LOGF_DEBUG("Snooped RotatorAngle %f", RotatorAngle);
1284 return true;
1285 }
1286 }
1287 }
1288 // We try to snoop EQUATORIAL_PE first (true pointing with mount errors injected);
1289 // if not found we fall through to the regular EQUATORIAL_EOD_COORD snoop below.
1290#ifdef USE_EQUATORIAL_PE
1291 if (!strcmp(propName, "EQUATORIAL_PE"))
1292 {
1293 int rc_ra = -1, rc_de = -1;
1294 double newra = 0, newdec = 0;
1295
1296 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
1297 {
1298 const char * elemName = findXMLAttValu(ep, "name");
1299
1300 if (!strcmp(elemName, "RA_PE"))
1301 rc_ra = f_scansexa(pcdataXMLEle(ep), &newra);
1302 else if (!strcmp(elemName, "DEC_PE"))
1303 rc_de = f_scansexa(pcdataXMLEle(ep), &newdec);
1304 }
1305
1306 if (rc_ra == 0 && rc_de == 0)
1307 {
1308 INDI::IEquatorialCoordinates epochPos { newra, newdec }, J2000Pos { 0, 0 };
1309 INDI::ObservedToJ2000(&epochPos, m_Clock.julianDate(), &J2000Pos);
1310 raPE = J2000Pos.rightascension;
1311 decPE = J2000Pos.declination;
1312
1313 // Only activate PE rendering once the telescope reports a valid (Ok) state.
1314 // The initial defNumberVector arrives with state Idle and values 0/0.
1315 const char * state = findXMLAttValu(root, "state");
1316 if (!usePE && strcmp(state, "Ok") == 0)
1317 {
1318 LOGF_INFO("EQUATORIAL_PE snoop active: JNow RA %.4f Dec %.4f -> J2000 RA %.4f Dec %.4f", newra, newdec, raPE, decPE);
1319 usePE = true;
1320 }
1321
1322 EqPENP[AXIS_RA].setValue(newra);
1323 EqPENP[AXIS_DE].setValue(newdec);
1324 EqPENP.apply();
1325
1326 LOGF_DEBUG("Snooped EQUATORIAL_PE JNow RA %g Dec %g -> J2000 RA %g Dec %g", newra, newdec, raPE, decPE);
1327 return true;
1328 }
1329 }
1330#endif
1331
1332 return INDI::CCD::ISSnoopDevice(root);
1333}
1334
1336{
1337 // Save CCD Config
1339
1340 // Save Filter Wheel Config
1342
1343 // Save CCD Simulator Config
1345
1346 // Gain
1347 GainNP.save(fp);
1348 OffsetNP.save(fp);
1349
1350 // Directory
1351 DirectoryTP.save(fp);
1352
1353 // Resolution
1354 ResolutionSP.save(fp);
1355
1356 // Focus simulation
1358
1359 // Tilt simulation
1361
1362 // Planet simulation
1364 PlanetPathsTP.save(fp);
1365
1366 return true;
1367}
1368
1370{
1371 // Sleep randomly between 1500 and 2000ms to simulate filter selection
1372 std::random_device rd;
1373 std::mt19937 gen(rd());
1374 std::uniform_int_distribution<> dist(1500, 2000);
1375 std::this_thread::sleep_for(std::chrono::milliseconds(dist(gen)));
1376
1377 CurrentFilter = f;
1379 return true;
1380}
1381
1383{
1384 return CurrentFilter;
1385}
1386
1388{
1389 ExposureRequest = 1.0 / Streamer->getTargetExposure();
1390
1391 // Planet mode: stream pre-compressed JPEG directly (INDI_JPG format, like gphoto_ccd)
1392 if (m_PlanetMode)
1393 {
1394 Streamer->setPixelFormat(INDI_JPG, 8);
1396 }
1397 else
1398 {
1399 Streamer->setPixelFormat(INDI_MONO, 16);
1401 }
1402
1403 pthread_mutex_lock(&condMutex);
1404 streamPredicate = 1;
1405 pthread_mutex_unlock(&condMutex);
1406 pthread_cond_signal(&cv);
1407
1408 return true;
1409}
1410
1412{
1413 pthread_mutex_lock(&condMutex);
1414 streamPredicate = 0;
1415 pthread_mutex_unlock(&condMutex);
1416 pthread_cond_signal(&cv);
1417
1418 return true;
1419}
1420
1421bool CCDSim::UpdateCCDFrame(int x, int y, int w, int h)
1422{
1423 long bin_width = w / PrimaryCCD.getBinX();
1424 long bin_height = h / PrimaryCCD.getBinY();
1425
1426 bin_width = bin_width - (bin_width % 2);
1427 bin_height = bin_height - (bin_height % 2);
1428
1429 Streamer->setSize(bin_width, bin_height);
1430
1431 return INDI::CCD::UpdateCCDFrame(x, y, w, h);
1432}
1433
1434bool CCDSim::UpdateCCDBin(int hor, int ver)
1435{
1436 // Binning is not supported in planet mode (RGB data)
1437 if (m_PlanetMode && (hor != 1 || ver != 1))
1438 {
1439 LOG_ERROR("Binning is not supported in planet mode.");
1440 return false;
1441 }
1442
1443 if (PrimaryCCD.getSubW() % hor != 0 || PrimaryCCD.getSubH() % ver != 0)
1444 {
1445 LOGF_ERROR("%dx%d binning is not supported.", hor, ver);
1446 return false;
1447 }
1448
1449 uint32_t bin_width = PrimaryCCD.getSubW() / hor;
1450 uint32_t bin_height = PrimaryCCD.getSubH() / ver;
1451 Streamer->setSize(bin_width, bin_height);
1452
1453 return INDI::CCD::UpdateCCDBin(hor, ver);
1454}
1455
1456bool CCDSim::UpdateGuiderBin(int hor, int ver)
1457{
1458 if (GuideCCD.getSubW() % hor != 0 || GuideCCD.getSubH() % ver != 0)
1459 {
1460 LOGF_ERROR("%dx%d binning is not supported.", hor, ver);
1461 return false;
1462 }
1463
1464 return INDI::CCD::UpdateGuiderBin(hor, ver);
1465}
1466
1467void * CCDSim::streamVideoHelper(void * context)
1468{
1469 return static_cast<CCDSim *>(context)->streamVideo();
1470}
1471
1473{
1474 auto start = std::chrono::high_resolution_clock::now();
1475
1476 while (true)
1477 {
1478 pthread_mutex_lock(&condMutex);
1479
1480 while (streamPredicate == 0)
1481 {
1482 pthread_cond_wait(&cv, &condMutex);
1483 ExposureRequest = Streamer->getTargetExposure();
1484 }
1485
1486 if (terminateThread)
1487 break;
1488
1489 // release condMutex
1490 pthread_mutex_unlock(&condMutex);
1491
1492
1493 // Planet mode: stream cached PNG directly (no encoding, like gphoto_ccd JPEG)
1494 if (m_PlanetMode)
1495 {
1496 renderPlanet(); // updates both caches if needed
1497 if (!m_CachedStreamBuffer.empty())
1498 {
1499 auto finish = std::chrono::high_resolution_clock::now();
1500 std::chrono::duration<double> elapsed = finish - start;
1501 if (elapsed.count() < ExposureRequest)
1502 usleep(fabs(ExposureRequest - elapsed.count()) * 1e6);
1503 Streamer->newFrame(m_CachedStreamBuffer.data(), m_CachedStreamBuffer.size());
1504 }
1505 }
1506 else
1507 {
1509 // Software binning for mono star field mode only
1510 if (DirectorySP[INDI_DISABLED].getState() == ISS_ON)
1512
1513 auto finish = std::chrono::high_resolution_clock::now();
1514 std::chrono::duration<double> elapsed = finish - start;
1515 if (elapsed.count() < ExposureRequest)
1516 usleep(fabs(ExposureRequest - elapsed.count()) * 1e6);
1517
1518 uint32_t size = PrimaryCCD.getFrameBufferSize();
1519 Streamer->newFrame(PrimaryCCD.getFrameBuffer(), size);
1520 }
1521
1522 start = std::chrono::high_resolution_clock::now();
1523 }
1524
1525 pthread_mutex_unlock(&condMutex);
1526 return nullptr;
1527}
1528
1530{
1531 // Get current CCD dimensions
1532 uint32_t width = PrimaryCCD.getXRes();
1533 uint32_t height = PrimaryCCD.getYRes();
1534
1535 // Compute pixel scale: arcsec per pixel = 206.265 * pixel_size_um / focal_length_mm
1536 float pixelSizeX = SimulatorSettingsNP[SIM_XSIZE].getValue();
1537 float pixelSizeY = SimulatorSettingsNP[SIM_YSIZE].getValue();
1538 float focalLength = ScopeInfoNP[FOCAL_LENGTH].getValue() > 0 ?
1540 if (focalLength <= 0)
1541 focalLength = 1000.0f; // default fallback
1542 float arcsecPerPixelX = 206.265f * pixelSizeX / focalLength;
1543 float arcsecPerPixelY = 206.265f * pixelSizeY / focalLength;
1544
1545 // Determine planet angular diameter
1546 float planetDiameterArcsec = 0;
1547 if (m_PlanetSizeOverride > 0)
1548 {
1549 planetDiameterArcsec = m_PlanetSizeOverride;
1550 }
1551 else
1552 {
1554 if (it != PlanetAngularDiameter.end())
1555 planetDiameterArcsec = it->second;
1556 else
1557 planetDiameterArcsec = 42.0f; // fallback
1558 }
1559
1560 // How many sensor pixels the planet spans (use average of X and Y pixel scales)
1561 float planetPixelsX = planetDiameterArcsec / arcsecPerPixelX;
1562 float planetPixelsY = planetDiameterArcsec / arcsecPerPixelY;
1563 float avgPlanetPixels = (planetPixelsX + planetPixelsY) / 2.0f;
1564
1565 // Render xplanet at high resolution (1024x1024) with visible black margin.
1566 // Use -range (camera distance in degrees) - NOT -radius which is angular FOV
1567 // and causes planet to fill entire frame under orthographic projection.
1568 // Range of 0.05° gives ~40% black border for Jupiter (~0.0117° angular diam).
1569 const int renderSize = 1024;
1570 const float planetRange = 0.05f; // degrees
1571
1572 // Compute planet disk pixel radius from angular size and range
1573 float planetDiamDeg = planetDiameterArcsec / 3600.0f;
1574 float planetFracOfFrame = planetDiamDeg / planetRange;
1575 float srcPlanetRadius = planetFracOfFrame * renderSize / 2.0f;
1576 if (srcPlanetRadius < 10.0f) srcPlanetRadius = 10.0f;
1577
1578 // Build xplanet command. We output PNG — same file used for both FITS rendering
1579 // and direct streaming (cached as-is, like gphoto_ccd sends JPEG).
1580 std::string searchDir;
1581 size_t configPos = m_XPlanetConfig.rfind("/config/");
1582 if (configPos != std::string::npos)
1583 searchDir = m_XPlanetConfig.substr(0, configPos);
1584
1585 const char *pngPath = "/tmp/indi_planet.png";
1586 char cmd[2048];
1587 const bool useCustomConfig = (m_XPlanetConfig != "/usr/share/xplanet/config/default");
1588
1589 if (!searchDir.empty() && useCustomConfig)
1590 {
1591 snprintf(cmd, sizeof(cmd),
1592 "%s -body %s -geometry %dx%d -range %.4f "
1593 "-num_times 1 -projection orthographic "
1594 "-config %s -searchdir %s "
1595 "-output %s 2>/dev/null",
1596 m_XPlanetBinary.c_str(), m_SelectedPlanet.c_str(),
1597 renderSize, renderSize, static_cast<double>(planetRange),
1598 m_XPlanetConfig.c_str(), searchDir.c_str(), pngPath);
1599 }
1600 else if (!searchDir.empty())
1601 {
1602 snprintf(cmd, sizeof(cmd),
1603 "%s -body %s -geometry %dx%d -range %.4f "
1604 "-num_times 1 -projection orthographic "
1605 "-searchdir %s "
1606 "-output %s 2>/dev/null",
1607 m_XPlanetBinary.c_str(), m_SelectedPlanet.c_str(),
1608 renderSize, renderSize, static_cast<double>(planetRange),
1609 searchDir.c_str(), pngPath);
1610 }
1611 else
1612 {
1613 snprintf(cmd, sizeof(cmd),
1614 "%s -body %s -geometry %dx%d -range %.4f "
1615 "-num_times 1 -projection orthographic "
1616 "-output %s 2>/dev/null",
1617 m_XPlanetBinary.c_str(), m_SelectedPlanet.c_str(),
1618 renderSize, renderSize, static_cast<double>(planetRange), pngPath);
1619 }
1620
1621 // Ensure buffer is sized for 3-channel 16-bit planar RGB
1622 uint32_t rgbBufSize = width * height * 3 * 2;
1623 if (PrimaryCCD.getFrameBufferSize() != static_cast<int>(rgbBufSize))
1624 PrimaryCCD.setFrameBufferSize(rgbBufSize);
1625
1626 // Check if cache needs invalidation before spawning any external processes
1630 m_CachedFocalLength != focalLength ||
1631 m_CachedPixelSizeX != pixelSizeX ||
1632 m_CachedWidth != width))
1633 {
1634 LOG_DEBUG("Cache params changed, re-rendering");
1635 m_CachedPlanetBuffer.clear();
1637 }
1638
1639 // Only spawn xplanet if cache is empty (fresh render needed)
1640 if (m_CachedPlanetBuffer.empty())
1641 {
1643 LOGF_DEBUG("Running: %s", cmd);
1644 int ret = system(cmd);
1645 if (ret != 0)
1646 {
1647 LOGF_ERROR("xplanet failed with exit code %d. Is xplanet installed?", ret);
1648 return false;
1649 }
1650
1651 // Generate CCD-resolution JPEG for streaming: resize planet to correct pixel size,
1652 // center on black background matching CCD dimensions
1653 {
1654 char jpgCmd[2048];
1655 int planetRenderPixels = static_cast<int>(avgPlanetPixels);
1656 if (planetRenderPixels < 1) planetRenderPixels = 1;
1657 snprintf(jpgCmd, sizeof(jpgCmd),
1658 "convert %s -resize %dx%d -background black "
1659 "-gravity center -extent %dx%d -quality 90 /tmp/indi_planet.jpg 2>/dev/null",
1660 pngPath, planetRenderPixels, planetRenderPixels,
1661 static_cast<int>(width), static_cast<int>(height));
1662 ret = system(jpgCmd);
1663 if (ret != 0)
1664 {
1665 LOG_ERROR("Failed to generate CCD-resolution JPEG. Is ImageMagick installed?");
1666 return false;
1667 }
1668 }
1669
1670 // Cache the CCD-resolution JPEG for direct streaming
1671 FILE *jpgFile = fopen("/tmp/indi_planet.jpg", "rb");
1672 if (jpgFile)
1673 {
1674 fseek(jpgFile, 0, SEEK_END);
1675 long jpgSize = ftell(jpgFile);
1676 fseek(jpgFile, 0, SEEK_SET);
1677 m_CachedStreamBuffer.resize(jpgSize);
1678 size_t bytesRead = fread(m_CachedStreamBuffer.data(), 1, jpgSize, jpgFile);
1679 INDI_UNUSED(bytesRead);
1680 fclose(jpgFile);
1681 }
1682
1683 ret = system("convert /tmp/indi_planet.png /tmp/indi_planet.ppm 2>/dev/null");
1684 if (ret != 0)
1685 {
1686 LOG_ERROR("Failed to convert PNG to PPM. Is ImageMagick installed?");
1687 return false;
1688 }
1689
1690 // Parse the PPM file
1691 FILE *fp = fopen("/tmp/indi_planet.ppm", "rb");
1692 if (!fp)
1693 {
1694 LOG_ERROR("Failed to open /tmp/indi_planet.ppm");
1695 return false;
1696 }
1697
1698 char magic[3];
1699 if (fscanf(fp, "%2s\n", magic) != 1 || strcmp(magic, "P6") != 0)
1700 {
1701 LOG_ERROR("PPM file is not P6 format (binary RGB)");
1702 fclose(fp);
1703 return false;
1704 }
1705
1706 int ppmWidth, ppmHeight, maxVal;
1707 // Skip comments
1708 int c = fgetc(fp);
1709 while (c == '#')
1710 {
1711 while (fgetc(fp) != '\n');
1712 c = fgetc(fp);
1713 }
1714 ungetc(c, fp);
1715 if (fscanf(fp, "%d %d\n%d\n", &ppmWidth, &ppmHeight, &maxVal) != 3)
1716 {
1717 LOG_ERROR("Failed to parse PPM header");
1718 fclose(fp);
1719 return false;
1720 }
1721
1722 std::vector<uint8_t> rgbData(ppmWidth * ppmHeight * 3);
1723 size_t bytesRead = fread(rgbData.data(), 1, rgbData.size(), fp);
1724 fclose(fp);
1725
1726 if (bytesRead != rgbData.size())
1727 {
1728 LOGF_ERROR("PPM read error: expected %zu bytes, got %zu", rgbData.size(), bytesRead);
1729 return false;
1730 }
1731
1732 // Allocate cached planet buffer at CCD resolution (16-bit RGB, 3-channel PLANAR layout for FITS)
1733 m_CachedPlanetBuffer.resize(rgbBufSize);
1734 m_CachedPlanetWidth = width;
1735 m_CachedPlanetHeight = height;
1736 uint16_t *buffer = reinterpret_cast<uint16_t *>(m_CachedPlanetBuffer.data());
1737
1738 // Clear to black
1739 memset(buffer, 0, rgbBufSize);
1740
1741 // Plane offsets within buffer (FITS NAXIS=3 planar: R_plane | G_plane | B_plane)
1742 size_t planeSize = static_cast<size_t>(width) * static_cast<size_t>(height);
1743 uint16_t *rPlane = buffer;
1744 uint16_t *gPlane = buffer + planeSize;
1745 uint16_t *bPlane = buffer + planeSize * 2;
1746
1747 // Center the planet in the CCD frame
1748 float centerX = width / 2.0f;
1749 float centerY = height / 2.0f;
1750
1751 // Scale factor from xplanet render to planet pixel size
1752 float dstPlanetRadius = avgPlanetPixels / 2.0f;
1753 if (dstPlanetRadius < 1.0f)
1754 dstPlanetRadius = 1.0f;
1755 float scale = srcPlanetRadius / dstPlanetRadius;
1756
1757 // Map each CCD pixel to the source PPM, writing planar 16-bit data.
1758 // Use simple linear scaling (8-bit × 256 → 16-bit) so the planet's
1759 // 8-bit perceptual brightness maps to the upper 16-bit range, making
1760 // auto-stretch work naturally without crushing mid-tones.
1761 for (uint32_t y = 0; y < height; y++)
1762 {
1763 for (uint32_t x = 0; x < width; x++)
1764 {
1765 float sx = (x - centerX) * scale + ppmWidth / 2.0f;
1766 float sy = (y - centerY) * scale + ppmHeight / 2.0f;
1767
1768 float dx = (sx - ppmWidth / 2.0f);
1769 float dy = (sy - ppmHeight / 2.0f);
1770 if (sqrtf(dx * dx + dy * dy) > srcPlanetRadius)
1771 continue;
1772
1773 int ix = static_cast<int>(sx);
1774 int iy = static_cast<int>(sy);
1775 if (ix < 0 || ix >= ppmWidth || iy < 0 || iy >= ppmHeight)
1776 continue;
1777
1778 int srcIdx = (iy * ppmWidth + ix) * 3;
1779 uint8_t r8 = rgbData[srcIdx];
1780 uint8_t g8 = rgbData[srcIdx + 1];
1781 uint8_t b8 = rgbData[srcIdx + 2];
1782
1783 // Linear 8→16-bit scaling: perceptual brightness preserved
1784 uint16_t r16 = static_cast<uint16_t>(r8) * 256;
1785 uint16_t g16 = static_cast<uint16_t>(g8) * 256;
1786 uint16_t b16 = static_cast<uint16_t>(b8) * 256;
1787
1788 if (r16 > static_cast<uint32_t>(m_MaxVal)) r16 = m_MaxVal;
1789 if (g16 > static_cast<uint32_t>(m_MaxVal)) g16 = m_MaxVal;
1790 if (b16 > static_cast<uint32_t>(m_MaxVal)) b16 = m_MaxVal;
1791
1792 size_t px = y * width + x;
1793 rPlane[px] = r16;
1794 gPlane[px] = g16;
1795 bPlane[px] = b16;
1796 }
1797 }
1798
1799 // Store FOV params so we can detect changes on cache check
1801 m_CachedFocalLength = focalLength;
1802 m_CachedPixelSizeX = pixelSizeX;
1803 m_CachedWidth = width;
1804 }
1805 else
1806 {
1807 // Cache hit: PPM already parsed and scaled, just copy to frame buffer
1808 // (NAXIS and buffer size already set by SetCaptureFormat when planet mode enabled)
1809 }
1810
1811 // Copy the cached buffer into PrimaryCCD's frame buffer
1812 size_t copySize = std::min(m_CachedPlanetBuffer.size(), static_cast<size_t>(rgbBufSize));
1813 memcpy(PrimaryCCD.getFrameBuffer(), m_CachedPlanetBuffer.data(), copySize);
1814
1816 // Only log on fresh render, not cache hits
1818 {
1819 LOGF_INFO("Rendered planet %s: diameter=%.1f arcsec, pixels=%.1f, scale=%.1f arcsec/px",
1820 m_SelectedPlanet.c_str(), planetDiameterArcsec, planetPixelsX, arcsecPerPixelX);
1821 }
1822
1823 return true;
1824}
1825
1826void CCDSim::addFITSKeywords(INDI::CCDChip *targetChip, std::vector<INDI::FITSRecord> &fitsKeyword)
1827{
1828 INDI::CCD::addFITSKeywords(targetChip, fitsKeyword);
1829
1830 fitsKeyword.push_back({"GAIN", GainNP[0].getValue(), 3, "Gain"});
1831
1832 if (m_PlanetMode)
1833 {
1834 fitsKeyword.emplace_back("OBJECT", m_SelectedPlanet.c_str(), "Target planet");
1835 fitsKeyword.emplace_back("XPLANET", static_cast<int64_t>(1), "Generated by xplanet");
1836 fitsKeyword.emplace_back("COLORSPC", "RGB", "Color space");
1837 }
1838}
1839
1841{
1842 if (m_RemainingFiles.empty())
1844 const std::string filename = m_RemainingFiles[0];
1845 m_RemainingFiles.pop_front();
1846
1847#ifdef HAVE_XISF
1848 if (filename.size() >= 5 && strcasecmp(filename.c_str() + filename.size() - 5, ".xisf") == 0)
1849 return loadNextXISFImage(filename);
1850#endif
1851
1852 return loadNextFITSImage(filename);
1853}
1854
1855bool CCDSim::loadNextFITSImage(const std::string &filename)
1856{
1857 char comment[512] = {0}, bayer_pattern[16] = {0};
1858 int status = 0, anynull = 0;
1859 double pixel_size = 5.2;
1860 int ndim {2}, bitpix {8};
1861 long naxes[3];
1862 fitsfile *fptr = nullptr;
1863
1864 if (fits_open_diskfile(&fptr, filename.c_str(), READONLY, &status))
1865 {
1866 char error_status[512] = {0};
1867 fits_get_errstatus(status, error_status);
1868 LOGF_WARN("Error opening file %s due to error %s", filename.c_str(), error_status);
1869 return false;
1870 }
1871
1872 if (fits_get_img_param(fptr, 3, &bitpix, &ndim, naxes, &status))
1873 {
1874 char error_status[512] = {0};
1875 fits_get_errstatus(status, error_status);
1876 LOGF_WARN("Error reading image parameters from %s due to error %s", filename.c_str(), error_status);
1877 fits_close_file(fptr, &status);
1878 return false;
1879 }
1880
1881 if (ndim < 3)
1882 naxes[2] = 1;
1883 else
1885 int samples_per_channel = naxes[0] * naxes[1];
1886 int channels = naxes[2];
1887 int elements = samples_per_channel * channels;
1888 int size = elements * bitpix / 8;
1890
1891 if (fits_read_img(fptr, bitpix == 8 ? TBYTE : TUSHORT, 1, elements, 0, PrimaryCCD.getFrameBuffer(), &anynull, &status))
1892 {
1893 char error_status[512] = {0};
1894 fits_get_errstatus(status, error_status);
1895 LOGF_WARN("Error reading pixel data from %s due to error %s", filename.c_str(), error_status);
1896 fits_close_file(fptr, &status);
1897 return false;
1898 }
1899
1900 // Pixel size is informational only. Different capture applications use different keywords
1901 // for it (e.g. INDI writes PIXSIZE1 while others like N.I.N.A/SharpCap write XPIXSZ), and some
1902 // omit it entirely, so fall back gracefully instead of rejecting the whole image over it.
1903 status = 0;
1904 if (fits_read_key_dbl(fptr, "PIXSIZE1", &pixel_size, comment, &status))
1905 {
1906 status = 0;
1907 if (fits_read_key_dbl(fptr, "XPIXSZ", &pixel_size, comment, &status))
1908 {
1909 LOGF_DEBUG("No PIXSIZE1 or XPIXSZ keyword found in %s, defaulting pixel size to %.2f microns",
1910 filename.c_str(), pixel_size);
1911 }
1912 }
1913
1914 status = 0;
1915 if (fits_read_key_str(fptr, "BAYERPAT", bayer_pattern, comment, &status))
1916 {
1917 char error_status[512] = {0};
1918 fits_get_errstatus(status, error_status);
1919 LOGF_DEBUG("No BAYERPAT keyword found in %s (%s)", filename.c_str(), error_status);
1920 }
1921 SetCCDParams(naxes[0], naxes[1], bitpix, pixel_size, pixel_size);
1922
1923 // Check if MONO or Bayer
1924 if (channels == 1 && strlen(bayer_pattern) == 4)
1925 {
1927 BayerTP[CFA_OFFSET_X].setText("0");
1928 BayerTP[CFA_OFFSET_Y].setText("0");
1929 BayerTP[CFA_TYPE].setText(bayer_pattern);
1930 }
1931 else
1932 {
1934 }
1935
1936 status = 0;
1937 fits_close_file(fptr, &status);
1938 return true;
1939}
1940
1941#ifdef HAVE_XISF
1942bool CCDSim::loadNextXISFImage(const std::string &filename)
1943{
1944 try
1945 {
1946 LibXISF::XISFReader reader;
1947 reader.open(filename);
1948
1949 if (reader.imagesCount() < 1)
1950 {
1951 LOGF_WARN("No images found in XISF file %s", filename.c_str());
1952 return false;
1953 }
1954
1955 // Work on a local copy so we can normalize pixel storage before copying it out.
1956 LibXISF::Image image = reader.getImage(0);
1957
1958 const auto sampleFormat = image.sampleFormat();
1959 const bool isFloat = (sampleFormat == LibXISF::Image::Float32 || sampleFormat == LibXISF::Image::Float64);
1960 int bitpix = 0;
1961 switch (sampleFormat)
1962 {
1963 case LibXISF::Image::UInt8:
1964 bitpix = 8;
1965 break;
1966 case LibXISF::Image::UInt16:
1967 bitpix = 16;
1968 break;
1969 case LibXISF::Image::Float32:
1970 case LibXISF::Image::Float64:
1971 // Processed/master XISF files (e.g. exported from PixInsight) are commonly stored as
1972 // normalized [0,1] floating point samples per the XISF spec. Scale them to 16-bit so
1973 // they can flow through the same integer pipeline as camera-native captures.
1974 bitpix = 16;
1975 break;
1976 default:
1977 LOGF_WARN("Unsupported XISF sample format '%s' in %s. Only 8/16-bit integer and 32/64-bit "
1978 "normalized float samples are supported.",
1979 LibXISF::Image::sampleFormatString(sampleFormat).c_str(), filename.c_str());
1980 return false;
1981 }
1982
1983 int channels = static_cast<int>(image.channelCount());
1984 PrimaryCCD.setNAxis(channels >= 3 ? 3 : 2);
1985
1986 // The CCD frame buffer expects channel-planar data (as FITS image cubes store it),
1987 // matching how this driver (and INDI in general) writes XISF images.
1988 if (image.pixelStorage() != LibXISF::Image::Planar)
1989 image.convertPixelStorageTo(LibXISF::Image::Planar);
1990
1991 const size_t elementCount = image.width() * image.height() * channels;
1992 if (isFloat)
1993 {
1994 PrimaryCCD.setFrameBufferSize(elementCount * 2);
1995 auto * dest = reinterpret_cast<uint16_t *>(PrimaryCCD.getFrameBuffer());
1996 auto scaleClamp = [](double value) -> uint16_t
1997 {
1998 if (value <= 0.0)
1999 return 0;
2000 if (value >= 1.0)
2001 return 65535;
2002 return static_cast<uint16_t>(std::lround(value * 65535.0));
2003 };
2004
2005 if (sampleFormat == LibXISF::Image::Float32)
2006 {
2007 const float * src = image.imageData<float>();
2008 for (size_t i = 0; i < elementCount; i++)
2009 dest[i] = scaleClamp(src[i]);
2010 }
2011 else
2012 {
2013 const double * src = image.imageData<double>();
2014 for (size_t i = 0; i < elementCount; i++)
2015 dest[i] = scaleClamp(src[i]);
2016 }
2017 }
2018 else
2019 {
2020 PrimaryCCD.setFrameBufferSize(image.imageDataSize());
2021 memcpy(PrimaryCCD.getFrameBuffer(), image.imageData(), image.imageDataSize());
2022 }
2023
2024 // Pixel size is informational only; fall back to a default if not present, same as the FITS path.
2025 double pixel_size = 5.2;
2026 bool foundPixelSize = false;
2027 for (const auto &keyword : image.fitsKeywords())
2028 {
2029 if (keyword.name == "PIXSIZE1" || keyword.name == "XPIXSZ")
2030 {
2031 pixel_size = std::stod(keyword.value);
2032 foundPixelSize = true;
2033 break;
2034 }
2035 }
2036 if (!foundPixelSize)
2037 LOGF_DEBUG("No PIXSIZE1 or XPIXSZ keyword found in %s, defaulting pixel size to %.2f microns",
2038 filename.c_str(), pixel_size);
2039
2040 SetCCDParams(image.width(), image.height(), bitpix, pixel_size, pixel_size);
2041
2042 // Check if MONO or Bayer
2043 const auto cfa = image.colorFilterArray();
2044 if (channels == 1 && cfa.pattern.size() == 4)
2045 {
2047 BayerTP[CFA_OFFSET_X].setText("0");
2048 BayerTP[CFA_OFFSET_Y].setText("0");
2049 BayerTP[CFA_TYPE].setText(cfa.pattern.c_str());
2050 }
2051 else
2052 {
2054 }
2055 }
2056 catch (LibXISF::Error &error)
2057 {
2058 LOGF_WARN("Error reading XISF file %s due to error: %s", filename.c_str(), error.what());
2059 return false;
2060 }
2061
2062 return true;
2063}
2064#endif
2065
2066bool CCDSim::SetCaptureFormat(uint8_t index)
2067{
2068 // Get the selected format
2069 if (index >= m_CaptureFormats.size())
2070 return false;
2071
2072 const auto &format = m_CaptureFormats[index];
2073 bool isRGB = (format.name == "INDI_RGB");
2074
2075 // Set NAXIS and buffer size for RGB vs Mono
2076 // The base CCD class handles CaptureFormatSP state, we just need to configure the chip
2077 uint32_t nbuf;
2078 if (isRGB)
2079 {
2081 nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * 3 * (PrimaryCCD.getBPP() / 8);
2083 }
2084 else
2085 {
2087 nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * (PrimaryCCD.getBPP() / 8);
2089 }
2091 Streamer->setPixelFormat(isRGB ? INDI_RGB : INDI_MONO, 16);
2092 m_IsRGBFormat = isRGB;
2093 return true;
2094}
double temperature
The CCDSim class provides an advanced simulator for a CCD that includes a dedicated on-board guide ch...
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....
bool m_IsRGBFormat
INDI::PropertyNumber TiltSimulationNP
float m_CachedPixelSizeX
bool m_SimulateBayer
double m_LastTemperature
uint32_t m_CachedPlanetHeight
bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
float guideWEOffset
void setBayerEnabled(bool onOff)
INDI::PropertyNumber FocusSimulationNP
bool StartExposure(float duration) override
Start exposing primary CCD chip.
@ SIM_SATURATION
@ SIM_LIMITINGMAG
@ SIM_TIME_FACTOR
@ SIM_PE_PERIOD
@ SIM_OAGOFFSET
@ SIM_POLARDRIFT
bool loadNextFITSImage(const std::string &filename)
virtual IPState GuideSouth(uint32_t) override
Guide southward for ms milliseconds.
INDI::PropertyText PlanetPathsTP
INDI::PropertyText DirectoryTP
INDI::PropertyNumber GainNP
bool ShowStarField
static const constexpr char * SIMULATOR_TAB
std::string m_XPlanetConfig
bool watchDirectory()
float GuideRate
Guide rate is 7 arcseconds per second.
bool AbortGuideFrame
float m_RotationOffset
bool Disconnect() override
Disconnect from device.
float m_SkyGlow
INDI::PropertyNumber EqPENP
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 ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
void * streamVideo()
float guideNSOffset
bool StartGuideExposure(float) override
Start exposing guide CCD chip.
std::vector< uint8_t > m_CachedPlanetBuffer
virtual bool SetCaptureFormat(uint8_t index) override
SetCaptureFormat Set Active Capture format.
INDI::PropertySwitch SimTestsSP
uint32_t m_CachedWidth
float m_PolarDrift
@ SIM_TESTS_TIMEOUT
@ SIM_TESTS_BAYER
@ SIM_TESTS_CRASH
pthread_t primary_thread
std::string m_XPlanetBinary
float m_PEMax
void TimerHit() override
Callback function to be called once SetTimer duration elapses.
@ PLANET_DISABLED
virtual int SetTemperature(double temperature) override
Set CCD temperature.
static const std::map< std::string, float > PlanetAngularDiameter
uint32_t m_CachedPlanetWidth
const char * getDefaultName() override
std::deque< std::string > m_AllFiles
INDI::PropertySwitch CoolerSP
virtual bool StartStreaming() override
StartStreaming Start live video streaming.
INDI::PropertyNumber SimulatorSettingsNP
static void * streamVideoHelper(void *context)
virtual bool ISSnoopDevice(XMLEle *root) override
Process a snoop event from INDI server. This function is called when a snooped property is updated in...
float m_PolarError
bool setupParameters()
float m_PlanetRefreshMinutes
SkyRenderer m_Renderer
virtual IPState GuideEast(uint32_t) override
Guide easward for ms milliseconds.
bool m_PlanetMode
time_t RunStart
float m_PlanetSizeOverride
double raPE
virtual IPState GuideWest(uint32_t) override
Guide westward for ms milliseconds.
float m_OAGOffset
bool renderPlanet()
int QueryFilter() override
Return current filter position.
INumber FWHMN[1]
virtual bool StopStreaming() override
StopStreaming Stop live video streaming.
float m_PEPeriod
PEPeriod in minutes.
bool SelectFilter(int) override
Select a new filter position.
bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
double currentDE
@ PLANET_XPLANET_PATH
@ PLANET_XPLANET_CONFIG
INDI::PropertyNumber OffsetNP
bool loadNextImage()
std::vector< uint8_t > m_CachedStreamBuffer
bool AbortPrimaryFrame
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 ...
int testvalue
float m_LimitingMag
static const std::vector< std::pair< uint32_t, uint32_t > > Resolutions
INDI::PropertySwitch ResolutionSP
INumberVectorProperty FWHMNP
std::string m_SelectedPlanet
virtual IPState GuideNorth(uint32_t) override
Guide northward for ms milliseconds.
bool AbortExposure() override
Abort ongoing exposure.
INDI::PropertyNumber PlanetSettingsNP
INDI::PropertySwitch PlanetSelectSP
bool m_SimulateTimeout
float m_CachedFocalLength
float m_TimeFactor
float m_SaturationMag
int streamPredicate
double decPE
std::string m_CachedPlanet
double TemperatureRequest
@ SIM_FOCUS_POSITION
bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
INDI::PropertySwitch DirectorySP
SimulatorClock m_Clock
virtual void activeDevicesUpdated() override
activeDevicesUpdated Inform children that ActiveDevices property was updated so they can snoop on the...
double currentRA
time_t m_LastPlanetRenderTime
float ExposureRequest
int m_MaxNoise
std::deque< std::string > m_RemainingFiles
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
INDI::PropertySwitch PlanetModeSP
bool terminateThread
float CalcTimeLeft(timeval start, float req)
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save configuration items in XML file.
virtual bool UpdateGuiderBin(int hor, int ver) override
CCD calls this function when Guide head binning is updated by the client. Derived classes should impl...
float m_Seeing
bool m_LastRenderWasCacheHit
float GuideExposureRequest
virtual void addFITSKeywords(INDI::CCDChip *targetChip, std::vector< INDI::FITSRecord > &fitsKeyword) override
Generate FITS keywords that will be added to FIST/XISF file.
bool AbortGuideExposure() override
Abort ongoing exposure.
int DrawCcdFrame(INDI::CCDChip *targetChip)
Provides interface to implement Filter Wheel functionality.
bool isConnected() const
INDI::PropertyText getText(const char *name) const
The CCDChip class provides functionality of a CCD Chip within a CCD.
Definition indiccdchip.h:39
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.
int getXRes() const
getXRes Get the horizontal resolution in pixels of the CCD Chip.
Definition indiccdchip.h:81
int getBinX() const
getBinX Get horizontal binning of the CCD chip.
uint8_t * getFrameBuffer()
getFrameBuffer Get raw frame buffer of the CCD chip.
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 getFrameBufferSize() const
getFrameBufferSize Get allocated frame buffer size to hold the CCD image frame.
int getNAxis() const
int getYRes() const
Get the vertical resolution in pixels of the CCD Chip.
Definition indiccdchip.h:90
void binFrame()
binFrame Perform software binning on the CCD frame. Only use this function if hardware binning is not...
void setNAxis(int value)
setNAxis Set FITS number of axis
bool InGuideExposure
Definition indiccd.h:567
CCDChip PrimaryCCD
Definition indiccd.h:621
uint32_t GetCCDCapability() const
GetCCDCapability returns the CCD capabilities.
Definition indiccd.h:161
@ CCD_CAN_SUBFRAME
Definition indiccd.h:116
@ CCD_HAS_DSP
Definition indiccd.h:124
@ CCD_CAN_ABORT
Definition indiccd.h:117
@ CCD_HAS_GUIDE_HEAD
Definition indiccd.h:118
@ CCD_HAS_SHUTTER
Definition indiccd.h:120
@ CCD_HAS_BAYER
Definition indiccd.h:122
@ CCD_CAN_BIN
Definition indiccd.h:115
@ CCD_HAS_STREAMING
Definition indiccd.h:123
@ CCD_HAS_ST4_PORT
Definition indiccd.h:119
@ CCD_HAS_COOLER
Definition indiccd.h:121
@ ACTIVE_TELESCOPE
Definition indiccd.h:659
@ ACTIVE_FOCUSER
Definition indiccd.h:661
bool HasGuideHead()
Definition indiccd.h:199
std::unique_ptr< StreamManager > Streamer
Definition indiccd.h:619
bool HasCooler()
Definition indiccd.h:223
bool InExposure
Definition indiccd.h:566
INDI::Timer m_TemperatureCheckTimer
Definition indiccd.h:603
INDI::PropertyText ActiveDeviceTP
ActiveDeviceTP defines 4 devices the camera driver can listen to (snoop) for properties of interest s...
Definition indiccd.h:656
virtual bool UpdateCCDFrame(int x, int y, int w, int h)
CCD calls this function when CCD Frame dimension needs to be updated in the hardware....
Definition indiccd.cpp:1988
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
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
std::mutex ccdBufferLock
Definition indiccd.h:612
INDI::PropertyNumber ScopeInfoNP
Definition indiccd.h:720
virtual bool UpdateCCDBin(int hor, int ver)
CCD calls this function when CCD Binning needs to be updated in the hardware. Derived classes should ...
Definition indiccd.cpp:2007
long FocuserPos
Definition indiccd.h:591
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
int pierSide
Definition indiccd.h:554
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
double RotatorAngle
Definition indiccd.h:585
virtual void SetGuiderParams(int x, int y, int bpp, float xf, float yf)
Setup CCD parameters for guide head CCD. Child classes call this function to update CCD parameters.
Definition indiccd.cpp:2901
INDI::ElapsedTimer m_TemperatureElapsedTimer
Definition indiccd.h:604
virtual bool UpdateGuiderBin(int hor, int ver)
CCD calls this function when Guide head binning is updated by the client. Derived classes should impl...
Definition indiccd.cpp:2025
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save configuration items in XML file.
Definition indiccd.cpp:2914
virtual bool ISSnoopDevice(XMLEle *root) override
Process a snoop event from INDI server. This function is called when a snooped property is updated in...
Definition indiccd.cpp:729
INDI::PropertySwitch CaptureFormatSP
Specifies Camera NATIVE capture format (e.g. Mono, RGB, RAW8..etc).
Definition indiccd.h:699
void SetCCDCapability(uint32_t cap)
SetCCDCapability Set the CCD capabilities. All fields must be initialized.
Definition indiccd.cpp:140
@ FOCAL_LENGTH
Definition indiccd.h:723
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
CCDChip GuideCCD
Definition indiccd.h:622
@ CFA_OFFSET_Y
Definition indiccd.h:688
@ CFA_OFFSET_X
Definition indiccd.h:687
double RA
Definition indiccd.h:551
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
double snoopedFocalLength
Definition indiccd.h:565
std::vector< CaptureFormat > m_CaptureFormats
Definition indiccd.h:617
double m_TargetTemperature
Definition indiccd.h:602
double Dec
Definition indiccd.h:551
virtual bool saveConfig(bool silent=false, const char *property=nullptr)
Save the current properties in a configuration file.
void setDriverInterface(uint32_t value)
setInterface Set driver interface. By default the driver interface is set to GENERAL_DEVICE....
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.
uint32_t getDriverInterface() const
int SetTimer(uint32_t ms)
Set a timer to call the function TimerHit after ms milliseconds.
void addDebugControl()
Add Debug control to the driver.
void start()
Starts this timer. Once started, a timer value can be checked with elapsed().
bool processText(const char *dev, const char *name, char *texts[], char *names[], int n)
Process text properties.
INDI::PropertyNumber FilterSlotNP
bool updateProperties()
updateProperties Defines or Delete properties based on default device connection status
bool processNumber(const char *dev, const char *name, double values[], char *names[], int n)
Process number properties.
void SelectFilterDone(int newpos)
The child class calls this function when the hardware successfully finished selecting a new filter wh...
void initProperties(const char *groupName)
Initialize filter wheel properties. It is recommended to call this function within initProperties() o...
bool saveConfigItems(FILE *fp)
saveConfigItems save Filter Names in config file
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.
const char * getName() const
bool isNameMatch(const char *otherName) const
bool update(const double values[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
bool update(const ISState states[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, ISRule rule, double timeout, IPState state)
bool update(const char *const texts[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
INDI::PropertyViewText * getText() const
void start()
Starts or restarts the timer with the timeout specified in interval.
Definition inditimer.cpp:82
bool processText(const char *name, char *texts[], char *names[], int n)
void updateProperties(INDI::DefaultDevice *device, bool connected)
double julianDate() const
void initProperties(const char *deviceName, const char *group)
int renderFrame(INDI::CCDChip *chip, double ra_j2000_deg, double dec_j2000_deg, double focal_length_mm, double rotation_deg, float exposure_s, bool renderStars=true, double minSearchRadiusArcmin=0)
void applyReadoutNoise(INDI::CCDChip *chip, int bias, int maxNoise)
void setConfig(const RenderConfig &cfg)
const char * FILTER_TAB
FILTER_TAB Where all the properties for filter wheels are located.
const char * MAIN_CONTROL_TAB
MAIN_CONTROL_TAB Where all the primary controls for the device are located.
const char * SITE_TAB
SITE_TAB Where all site information setting are located.
const char * OPTIONS_TAB
OPTIONS_TAB Where all the driver's options are located. Those may include auxiliary controls,...
double max(void)
ISState
Switch state.
Definition indiapi.h:148
@ ISS_OFF
Definition indiapi.h:149
@ ISS_ON
Definition indiapi.h:150
#define MAXINDIDEVICE
Definition indiapi.h:191
@ 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
@ ISR_NOFMANY
Definition indiapi.h:173
@ AXIS_DE
@ AXIS_RA
@ INDI_MONO
@ INDI_RGB
@ INDI_JPG
int f_scansexa(const char *str0, double *dp)
convert sexagesimal string str AxBxC to double. x can be anything non-numeric. Any missing A,...
Definition indicom.c:194
double range360(double r)
range360 Limits an angle to be between 0-360 degrees.
Definition indicom.c:1321
Implementations for common driver routines.
void IUFillNumberVector(INumberVectorProperty *nvp, INumber *np, int nnp, const char *dev, const char *name, const char *label, const char *group, IPerm p, double timeout, IPState s)
Assign attributes for a number vector property. The vector's auxiliary elements will be set to NULL.
Definition indidevapi.c:490
int IUSnoopNumber(XMLEle *root, INumberVectorProperty *nvp)
Update a snooped number vector property from the given XML root element.
Definition indidevapi.c:555
void IUFillNumber(INumber *np, const char *name, const char *label, const char *format, double min, double max, double step, double value)
Assign attributes for a number property. The number's auxiliary elements will be set to NULL.
Definition indidevapi.c:398
#define INDI_UNUSED(x)
Definition indidevapi.h:132
void IDSnoopDevice(const char *snooped_device, const char *snooped_property)
Function a Driver calls to snoop on another Device. Snooped messages will then arrive via ISSnoopDevi...
Definition indidriver.c:143
#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
const char * findXMLAttValu(XMLEle *ep, const char *name)
Find an XML element's attribute value.
Definition lilxml.cpp:644
XMLEle * nextXMLEle(XMLEle *ep, int init)
Iterate an XML element for a list of nesetd XML elements.
Definition lilxml.cpp:555
char * pcdataXMLEle(XMLEle *ep)
Return the pcdata of an XML element.
Definition lilxml.cpp:606
Namespace to encapsulate INDI client, drivers, and mediator classes.
void ObservedToJ2000(IEquatorialCoordinates *observed, double jd, IEquatorialCoordinates *J2000pos)
ObservedToJ2000 converts an observed position to a J2000 catalogue position removes aberration,...
Definition libastro.cpp:51
@ error
throw a parse_error exception in case of a tag
uint8_t cmd[4]
Definition pwc-ioctl.h:2
float limitingMag
Definition sky_renderer.h:9
float saturationMag
char device[MAXINDIDEVICE]
Definition indiapi.h:319