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
131 // Constrain temperature range to match TEC physics: -15°C to ambient (25°C)
132 TemperatureNP[0].setMin(-15);
133 TemperatureNP[0].setMax(25);
134 TemperatureNP[0].setValue(0);
135
136 CaptureFormat format = {"INDI_MONO", "Mono", 16, true};
137 addCaptureFormat(format);
138
139 // Planet mode uses RGB capture format
140 CaptureFormat rgbFormat = {"INDI_RGB", "RGB", 16, false};
141 addCaptureFormat(rgbFormat);
142
143 SimulatorSettingsNP[SIM_XRES].fill("SIM_XRES", "X resolution", "%4.0f", 512, 8192, 512, 1280);
144 SimulatorSettingsNP[SIM_YRES].fill("SIM_YRES", "Y resolution", "%4.0f", 512, 8192, 512, 1024);
145 SimulatorSettingsNP[SIM_XSIZE].fill("SIM_XSIZE", "X Pixel Size", "%4.2f", 1, 30, 5, 5.2);
146 SimulatorSettingsNP[SIM_YSIZE].fill("SIM_YSIZE", "Y Pixel Size", "%4.2f", 1, 30, 5, 5.2);
147 SimulatorSettingsNP[SIM_MAXVAL].fill("SIM_MAXVAL", "Maximum ADU", "%4.0f", 255, 65000, 1000, 65000);
148 SimulatorSettingsNP[SIM_SATURATION].fill("SIM_SATURATION", "Saturation Mag", "%4.1f", 0, 20, 1, 1.0);
149 SimulatorSettingsNP[SIM_LIMITINGMAG].fill("SIM_LIMITINGMAG", "Limiting Mag", "%4.1f", 0, 20, 1, 17.0);
150 SimulatorSettingsNP[SIM_NOISE].fill("SIM_NOISE", "Noise", "%4.0f", 0, 6000, 500, 10);
151 SimulatorSettingsNP[SIM_SKYGLOW].fill("SIM_SKYGLOW", "Sky Glow (magnitudes)", "%4.1f", 0, 6000, 500, 19.5);
152 SimulatorSettingsNP[SIM_OAGOFFSET].fill("SIM_OAGOFFSET", "Oag Offset (arcminutes)", "%4.1f", 0, 6000, 500, 0);
153 SimulatorSettingsNP[SIM_POLAR].fill("SIM_POLAR", "PAE (arcminutes)", "%4.1f", -600, 600, 100, 0);
154 SimulatorSettingsNP[SIM_POLARDRIFT].fill("SIM_POLARDRIFT", "PAE Drift (minutes)", "%4.1f", 0, 60, 5, 0);
155 SimulatorSettingsNP[SIM_PE_PERIOD].fill("SIM_PEPERIOD", "PE Period (seconds)", "%4.1f", 0, 60, 5, 0);
156 SimulatorSettingsNP[SIM_PE_MAX].fill("SIM_PEMAX", "PE Max (arcsec)", "%4.1f", 0, 6000, 500, 0);
157 SimulatorSettingsNP[SIM_TIME_FACTOR].fill("SIM_TIME_FACTOR", "Time Factor (x)", "%.2f", 0.01, 100, 10, 1);
158 SimulatorSettingsNP[SIM_ROTATION].fill("SIM_ROTATION", "Rotation Offset", "%.2f", -180, 180, 10, 0);
159
160 SimulatorSettingsNP.fill(getDeviceName(), "SIMULATOR_SETTINGS",
161 "Settings", SIMULATOR_TAB, IP_RW, 60, IPS_IDLE);
162 // load() is important to fill all editfields with saved values also, so ISNewNumber() of one field
163 // doesn't update the other fields of the group with the "old" contents.
165
166 // Sim Tests
167 SimTestsSP[SIM_TESTS_BAYER].fill("BAYER", "Bayer", ISS_OFF);
168 SimTestsSP[SIM_TESTS_CRASH].fill("CRASH", "Crash", ISS_OFF);
169 SimTestsSP[SIM_TESTS_TIMEOUT].fill("TIMEOUT", "Timeout", ISS_OFF);
170 SimTestsSP.fill(getDeviceName(), "SIM_TESTS", "Sim Tests", SIMULATOR_TAB, IP_RW,
171 ISR_NOFMANY, 60, IPS_IDLE);
172
173 // Simulate focusing
174 FocusSimulationNP[SIM_FOCUS_POSITION].fill("SIM_FOCUS_POSITION", "Focus", "%.f", 0.0, 100000.0, 1.0, 36700.0);
175 FocusSimulationNP[SIM_FOCUS_MAX].fill("SIM_FOCUS_MAX", "Max. Position", "%.f", 0.0, 100000.0, 1.0, 100000.0);
176 FocusSimulationNP[SIM_SEEING].fill("SIM_SEEING", "Seeing (arcsec)", "%4.2f", 0, 60, 0, 3.5);
177 FocusSimulationNP.fill(getDeviceName(), "SIM_FOCUSING", "Focus Simulation",
179
180 // Simulate sensor tilt (extra defocus in arcsec at sensor edge)
181 TiltSimulationNP[SIM_TILT_LR].fill("SIM_TILT_LR", "Left-Right (arcsec)", "%4.2f", 0, 20, 0.5, 0);
182 TiltSimulationNP[SIM_TILT_TB].fill("SIM_TILT_TB", "Top-Bottom (arcsec)", "%4.2f", 0, 20, 0.5, 0);
183 TiltSimulationNP.fill(getDeviceName(), "SIM_TILT", "Tilt Simulation",
185
186 // Periodic Error
187 EqPENP[AXIS_RA].fill("RA_PE", "RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 0);
188 EqPENP[AXIS_DE].fill("DEC_PE", "DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0);
189 EqPENP.fill(getDeviceName(), "EQUATORIAL_PE", "EQ PE", SIMULATOR_TAB, IP_RW, 60,
190 IPS_IDLE);
191
192 // FWHM
194 IUFillNumber(&FWHMN[0], "SIM_FWHM", "FWHM (arcseconds)", "%4.2f", 0, 60, 0, 7.5);
195 IUFillNumberVector(&FWHMNP, FWHMN, 1, focuser, "FWHM", "FWHM", OPTIONS_TAB, IP_RO, 60, IPS_IDLE);
196
197 // Cooler
198 CoolerSP[INDI_ENABLED].fill("COOLER_ON", "ON", ISS_OFF);
199 CoolerSP[INDI_DISABLED].fill("COOLER_OFF", "OFF", ISS_ON);
200 CoolerSP.fill(getDeviceName(), "CCD_COOLER", "Cooler", MAIN_CONTROL_TAB, IP_WO,
202
203 // Gain
204 GainNP[0].fill("GAIN", "value", "%.f", 0, 300, 10, 90);
205 GainNP.fill(getDeviceName(), "CCD_GAIN", "Gain", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
206
207 // Offset
208 OffsetNP[0].fill("OFFSET", "value", "%.f", 0, 6000, 500, 0);
209 OffsetNP.fill(getDeviceName(), "CCD_OFFSET", "Offset", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
210
211 // Directory to read images from. This is useful to test real images captured by camera
212 // For each capture, one file is read (sorted by name) and is sent to client.
213 DirectoryTP[0].fill("LOCATION", "Location", getenv("HOME"));
214 DirectoryTP.fill(getDeviceName(), "CCD_DIRECTORY_LOCATION", "Directory", SIMULATOR_TAB, IP_RW, 60, IPS_IDLE);
216
217 // Toggle Directory Reading. If enabled. The simulator will just read images from the directory and not generate them.
218 DirectorySP[INDI_ENABLED].fill("INDI_ENABLED", "Enabled", ISS_OFF);
219 DirectorySP[INDI_DISABLED].fill("INDI_DISABLED", "Disabled", ISS_ON);
220 DirectorySP.fill(getDeviceName(), "CCD_DIRECTORY_TOGGLE", "Use Dir.", SIMULATOR_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
221
222 // Resolution
223 for (uint8_t i = 0; i < Resolutions.size(); i++)
224 {
225 std::ostringstream ss;
226 if (Resolutions[i].first > 0)
227 ss << Resolutions[i].first << " x " << Resolutions[i].second;
228 else
229 ss << "Custom";
230 ResolutionSP[i].fill(ss.str().c_str(), ss.str().c_str(), i == 0 ? ISS_ON : ISS_OFF);
231 }
232 ResolutionSP.fill(getDeviceName(), "CCD_RESOLUTION", "Resolution", SIMULATOR_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
233
235
236#ifdef USE_EQUATORIAL_PE
237 IDSnoopDevice(mount, "EQUATORIAL_PE");
238#else
239 IDSnoopDevice(mount, "EQUATORIAL_EOD_COORD");
240#endif
241
242 IDSnoopDevice(focuser, "FWHM");
243
244 uint32_t cap = 0;
245
246 cap |= CCD_CAN_ABORT;
247 cap |= CCD_CAN_BIN;
248 cap |= CCD_CAN_SUBFRAME;
249 cap |= CCD_HAS_COOLER;
250 cap |= CCD_HAS_GUIDE_HEAD;
251 cap |= CCD_HAS_SHUTTER;
252 cap |= CCD_HAS_ST4_PORT;
253 cap |= CCD_HAS_STREAMING;
254 cap |= CCD_HAS_DSP;
255
256 SetCCDCapability(cap);
257
258 // This should be called after the initial SetCCDCapability (above)
259 // as it modifies the capabilities.
261
263
264 FilterSlotNP[0].setMin(1);
265 FilterSlotNP[0].setMax(8);
266
267 // ===================== Planet Simulation =====================
268
269 // Planet mode toggle
270 PlanetModeSP[PLANET_ENABLED].fill("PLANET_ENABLED", "Enabled", ISS_OFF);
271 PlanetModeSP[PLANET_DISABLED].fill("PLANET_DISABLED", "Disabled", ISS_ON);
272 PlanetModeSP.fill(getDeviceName(), "PLANET_MODE", "Planet Mode", SIMULATOR_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
273
274 // Planet selection
275 PlanetSelectSP[PLANET_MERCURY].fill("PLANET_MERCURY", "Mercury", ISS_OFF);
276 PlanetSelectSP[PLANET_VENUS].fill("PLANET_VENUS", "Venus", ISS_OFF);
277 PlanetSelectSP[PLANET_MARS].fill("PLANET_MARS", "Mars", ISS_OFF);
278 PlanetSelectSP[PLANET_JUPITER].fill("PLANET_JUPITER", "Jupiter", ISS_ON);
279 PlanetSelectSP[PLANET_SATURN].fill("PLANET_SATURN", "Saturn", ISS_OFF);
280 PlanetSelectSP[PLANET_URANUS].fill("PLANET_URANUS", "Uranus", ISS_OFF);
281 PlanetSelectSP[PLANET_NEPTUNE].fill("PLANET_NEPTUNE", "Neptune", ISS_OFF);
282 PlanetSelectSP[PLANET_MOON].fill("PLANET_MOON", "Moon", ISS_OFF);
283 PlanetSelectSP[PLANET_SUN].fill("PLANET_SUN", "Sun", ISS_OFF);
284 PlanetSelectSP.fill(getDeviceName(), "PLANET_SELECT", "Planet", SIMULATOR_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
285
286 // Planet settings: refresh interval (minutes) and size override (arcsec)
287 PlanetSettingsNP[PLANET_REFRESH].fill("PLANET_REFRESH", "Refresh (min)", "%4.0f", 1, 1440, 10, 60);
288 PlanetSettingsNP[PLANET_SIZE].fill("PLANET_SIZE", "Size (arcsec, 0=auto)", "%4.1f", 0, 3600, 10, 0);
289 PlanetSettingsNP.fill(getDeviceName(), "PLANET_SETTINGS", "Planet Settings", SIMULATOR_TAB, IP_RW, 60, IPS_IDLE);
291
292 // xplanet executable and config paths
293 PlanetPathsTP[PLANET_XPLANET_PATH].fill("XPLANET_PATH", "xplanet binary", "xplanet");
294 PlanetPathsTP[PLANET_XPLANET_CONFIG].fill("XPLANET_CONFIG", "Config file", "/usr/share/xplanet/config/default");
295 PlanetPathsTP.fill(getDeviceName(), "PLANET_PATHS", "xplanet Paths", SIMULATOR_TAB, IP_RW, 60, IPS_IDLE);
297
299
301
302 return true;
303}
304
306{
307 if (onOff)
308 {
310 BayerTP[CFA_OFFSET_X].setText("0");
311 BayerTP[CFA_OFFSET_Y].setText("0");
312 BayerTP[CFA_TYPE].setText("RGGB");
313 }
314 else
315 {
317 }
318}
319
336
338{
340
341 if (isConnected())
342 {
343 if (HasCooler())
345
348
352
354
355 if (HasGuideHead())
356 {
357 SetGuiderParams(500, 290, 16, 9.8, 12.6);
359 }
360
361 // Define the Filter Slot and name properties
363 }
364 else
365 {
366 if (HasCooler())
368
374
376 }
377
378 return true;
379}
380
382{
384 if (std::abs(temperature - TemperatureNP[0].getValue()) < 0.1)
385 {
386 TemperatureNP[0].setValue(temperature);
387 return 1;
388 }
389
390 auto isCooling = temperature < TemperatureNP[0].getValue();
393 CoolerSP.setState(isCooling ? IPS_BUSY : IPS_IDLE);
394 CoolerSP.apply();
395 return 0;
396}
397
398bool CCDSim::StartExposure(float duration)
399{
400 // for the simulator, we can just draw the frame now
401 // and it will get returned at the right time
402 // by the timer routines
403 AbortPrimaryFrame = false;
404 ExposureRequest = duration;
405
407 gettimeofday(&ExpStart, nullptr);
408
409 // Planet mode: use cached xplanet rendering
411 {
412 time_t now;
413 time(&now);
414 double elapsedMinutes = difftime(now, m_LastPlanetRenderTime) / 60.0;
415 if (m_CachedPlanetBuffer.empty() || elapsedMinutes >= m_PlanetRefreshMinutes)
416 {
417 if (renderPlanet() == false)
418 {
419 // Fallback to blank frame on failure
421 }
422 }
423 else
424 {
425 // Ensure buffer is sized correctly (NAXIS=3 already set by SetCaptureFormat)
426 uint32_t rgbBufSize = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * 3 * 2;
427 if (PrimaryCCD.getFrameBufferSize() != static_cast<int>(rgbBufSize))
428 PrimaryCCD.setFrameBufferSize(rgbBufSize);
429 // Use cached planet buffer
430 size_t copySize = std::min(m_CachedPlanetBuffer.size(),
431 static_cast<size_t>(PrimaryCCD.getFrameBufferSize()));
432 memcpy(PrimaryCCD.getFrameBuffer(), m_CachedPlanetBuffer.data(), copySize);
433 LOGF_DEBUG("Using cached planet render (%.1f minutes since last render)", elapsedMinutes);
434 }
435 }
436 // When the directory is enabled, load every frame type (LIGHT, DARK, BIAS, FLAT)
437 // from the watched folder instead of generating a synthetic frame.
438 else if (DirectorySP[INDI_ENABLED].getState() == ISS_ON)
439 {
440 if (loadNextImage() == false)
441 return false;
442 }
443 else
444 {
445 // Reset to mono (NAXIS=2) if coming from planet mode
446 if (PrimaryCCD.getNAxis() == 3)
447 {
449 uint32_t nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * PrimaryCCD.getBPP() / 8;
451 // Re-enable Bayer if previously enabled (not applicable during planet mode)
453 }
455 }
456
457 // Now compress the actual wait time
458 ExposureRequest = duration * m_TimeFactor;
459 InExposure = true;
460
461 return true;
462}
463
465{
467 AbortGuideFrame = false;
470 gettimeofday(&GuideExpStart, nullptr);
471 InGuideExposure = true;
472 return true;
473}
474
476{
477 if (!InExposure)
478 return true;
479
480 AbortPrimaryFrame = true;
481
482 return true;
483}
484
486{
487 //IDLog("Enter AbortGuideExposure\n");
488 if (!InGuideExposure)
489 return true; // no need to abort if we aren't doing one
490 AbortGuideFrame = true;
491 return true;
492}
493
495{
496 uint32_t nextTimer = getCurrentPollingPeriod();
497
498 // No need to reset timer if we are not connected anymore
499 if (!isConnected())
500 return;
501
503 {
505 {
506 InExposure = false;
507 AbortPrimaryFrame = false;
508 }
509 else
510 {
511 float timeleft;
513
514 //IDLog("CCD Exposure left: %g - Request: %g\n", timeleft, ExposureRequest);
515 if (timeleft < 0)
516 timeleft = 0;
517
518 PrimaryCCD.setExposureLeft(timeleft);
519
520 if (timeleft < 1.0)
521 {
522 if (timeleft <= 0.001)
523 {
524 InExposure = false;
525 // Skip binning for planet mode (RGB data) and directory mode; only bin for generated star fields
526 if (!m_PlanetMode && DirectorySP[INDI_DISABLED].getState() == ISS_ON)
529 }
530 else
531 {
532 // set a shorter timer
533 nextTimer = timeleft * 1000;
534 }
535 }
536 }
537 }
538
539 if (InGuideExposure)
540 {
542 if (timeleft < 0)
543 timeleft = 0;
544
545 GuideCCD.setExposureLeft(timeleft);
546
547 if (timeleft < 1.0)
548 {
549 if (timeleft <= 0.001)
550 {
551 InGuideExposure = false;
552 if (!AbortGuideFrame)
553 {
556 if (InGuideExposure)
557 {
558 // the call to complete triggered another exposure
560 if (timeleft < 1.0)
561 {
562 nextTimer = timeleft * 1000;
563 }
564 }
565 }
566 else
567 {
568 //IDLog("Not sending guide frame cuz of abort\n");
569 }
570 AbortGuideFrame = false;
571 }
572 else
573 {
574 nextTimer = timeleft * 1000; // set a shorter timer
575 }
576 }
577 }
578
579 // Simulate realistic TEC cooler with ambient temperature of 25°C.
580 // Minimum achievable temperature: 40°C below ambient (-15°C).
581 // Warming up beyond ambient is not possible (TEC cannot heat).
582 constexpr double AMBIENT_TEMPERATURE = 25.0;
583 constexpr double MIN_TEC_TEMPERATURE = -15.0; // 40°C below ambient
584
585 const double currentTemp = TemperatureNP[0].getValue();
586 // Clamp target to achievable range
587 const double targetTemp = std::clamp(TemperatureRequest, MIN_TEC_TEMPERATURE, AMBIENT_TEMPERATURE);
588
589 // The base class declares the target as reached (IPS_OK) as soon as the temperature is within
590 // RAMP_THRESHOLD of the requested target. That is adequate for real hardware which keeps
591 // settling onto its setpoint, so keep simulating while the sensor has not settled onto the
592 // requested setpoint yet, even if the state is no longer BUSY. Otherwise the reported
593 // temperature would freeze at the threshold boundary (e.g. stop at -2.5°C for a -2°C request
594 // with a 0.5°C threshold) and never reach the actual setpoint.
595 if (TemperatureNP.getState() == IPS_BUSY || std::abs(targetTemp - currentTemp) > 0.01)
596 {
597 double newTemp = currentTemp;
598
599 if (targetTemp < newTemp)
600 {
601 // Cooling: approach target at -0.5°C per TimerHit cycle
602 newTemp = std::max(targetTemp, newTemp - 0.5);
603 }
604 else if (targetTemp > newTemp)
605 {
606 // Warming: approach target naturally (ambient), slowing as we get closer
607 // Simulate exponential decay toward ambient
608 double deltaT = AMBIENT_TEMPERATURE - newTemp;
609 // When below ambient, warm up by ~20% of remaining gap per cycle
610 // (roughly 0.4°C at 10°C delta, slowing to ~0.02°C near ambient)
611 double warmingStep = deltaT * 0.2;
612 if (warmingStep < 0.02)
613 warmingStep = 0.02;
614 if (warmingStep > 0.5)
615 warmingStep = 0.5;
616 newTemp = std::min(targetTemp, newTemp + warmingStep);
617 }
618
619 TemperatureNP[0].setValue(newTemp);
620
621 if (std::abs(newTemp - m_LastTemperature) > 0.1)
622 {
623 m_LastTemperature = newTemp;
625 }
626
627 // Cooler is effectively off when at or near ambient
628 if (newTemp >= AMBIENT_TEMPERATURE - 0.1 && CoolerSP.getState() != IPS_IDLE)
629 {
633 CoolerSP.apply();
634 }
635 }
636
637
638 SetTimer(nextTimer);
639}
640
642{
643 float exposure_time;
644
645 uint16_t * ptr = reinterpret_cast<uint16_t *>(targetChip->getFrameBuffer());
646
647 if (targetChip->getXRes() == 500)
648 exposure_time = GuideExposureRequest * 4;
649 else if (Streamer->isStreaming())
650 exposure_time = (ExposureRequest < 1) ? (ExposureRequest * 100) : ExposureRequest * 2;
651 else
652 exposure_time = ExposureRequest;
653
654 exposure_time *= (1 + sqrt(GainNP[0].getValue()));
655
656 auto targetFocalLength = ScopeInfoNP[FOCAL_LENGTH].getValue() > 0 ? ScopeInfoNP[FOCAL_LENGTH].getValue() :
658
659 if (ShowStarField)
660 {
661 float PEOffset {0};
662
663 if (m_PEPeriod > 0)
664 {
665 time_t now;
666 time(&now);
667 double timesince = difftime(now, RunStart);
668 double PESpot = timesince / m_PEPeriod * 2.0 * 3.14159;
669 PEOffset = m_PEMax * std::sin(PESpot) / 3600.0f;
670 }
671
672 // Camera rotation: manual offset + snooped rotator + pier flip
674 double theta = m_RotationOffset;
675 if (!std::isnan(RotatorAngle))
676 theta += RotatorAngle;
677 if (pierSide == 1)
678 theta -= 180;
679 theta = range360(theta);
680 LOGF_DEBUG("Rotator Angle: %f, Camera Rotation: %f", RotatorAngle, theta);
681
682#ifdef USE_EQUATORIAL_PE
683 if (usePE)
684 {
687 }
688 else
689 {
690 static bool s_warnedNoSnoop = false;
691 if (!s_warnedNoSnoop)
692 {
693 LOG_WARN("EQUATORIAL_PE not yet snooped -- rendering at raw EQUATORIAL_EOD_COORD (mount errors not active)");
694 s_warnedNoSnoop = true;
695 }
696#endif
697 currentRA = RA;
698 currentDE = Dec;
699
700 if (std::isnan(currentRA))
701 {
702 currentRA = 0;
703 currentDE = 0;
704 }
705
706 INDI::IEquatorialCoordinates epochPos { currentRA, currentDE }, J2000Pos { 0, 0 };
707 INDI::ObservedToJ2000(&epochPos, ln_get_julian_from_sys(), &J2000Pos);
708 currentRA = J2000Pos.rightascension;
709 currentDE = J2000Pos.declination;
710
713#ifdef USE_EQUATORIAL_PE
714 }
715#endif
716
717 // Field center: RA in degrees (with PE), Dec with OAG offset and polar drift
718 double ra_deg = currentRA * 15.0 + PEOffset;
719 double cameradec = currentDE + m_OAGOffset / 60.0;
720 double decr = cameradec * 0.0174532925;
721 double decDrift = (m_PolarDrift * m_PolarError * cos(decr)) / 3.81;
722 double dec_deg = cameradec + decDrift / 3600.0;
723
724 INDI::CCDChip::CCD_FRAME ftype = targetChip->getFrameType();
725 bool const isLight = (ftype == INDI::CCDChip::LIGHT_FRAME);
726 bool const isFlat = (ftype == INDI::CCDChip::FLAT_FRAME);
727
728 RenderConfig cfg;
729 cfg.maxVal = m_MaxVal;
732 cfg.seeing = m_Seeing;
733 cfg.tiltLR = TiltSimulationNP[SIM_TILT_LR].getValue();
734 cfg.tiltTB = TiltSimulationNP[SIM_TILT_TB].getValue();
735 // Sky glow: 30% boost for realistic light-frame backgrounds; flat frames use diffuser daylight level
736 cfg.skyGlow = isLight ? m_SkyGlow * 1.3f : m_SkyGlow / 10.0f;
738
739 std::unique_lock<std::mutex> guard(ccdBufferLock);
740
741 if (isLight || isFlat)
742 {
743 int drawn = m_Renderer.renderFrame(targetChip, ra_deg, dec_deg,
744 targetFocalLength, theta, exposure_time, isLight);
745 if (isLight && drawn < 0)
746 LOG_ERROR("Error launching gsc, is it installed with appropriate environment variables set?");
747 else if (isLight && drawn == 0)
748 LOG_WARN("No stars found in field -- check gsc catalog coverage for this region.");
749 }
750 else
751 {
752 memset(targetChip->getFrameBuffer(), 0, targetChip->getFrameBufferSize());
753 }
754
756 }
757 else
758 {
759 testvalue++;
760 if (testvalue > 255)
761 testvalue = 0;
762 uint16_t val = testvalue;
763
764 int nbuf = targetChip->getSubW() * targetChip->getSubH();
765
766 for (int x = 0; x < nbuf; x++)
767 {
768 *ptr = val++;
769 ptr++;
770 }
771 }
772 return 0;
773}
774
775float CCDSim::CalcTimeLeft(timeval start, float req)
776{
777 struct timeval now
778 {
779 0, 0
780 };
781 gettimeofday(&now, nullptr);
782 double const timesince = (now.tv_sec * 1000.0 + now.tv_usec / 1000.0)
783 - (start.tv_sec * 1000.0 + start.tv_usec / 1000.0);
784 return static_cast<float>(req - timesince / 1000.0);
785}
786
788{
789 guideNSOffset += v / 1000.0 * GuideRate / 3600;
790 return IPS_OK;
791}
792
794{
795 guideNSOffset += v / -1000.0 * GuideRate / 3600;
796 return IPS_OK;
797}
798
800{
801 float c = v / 1000.0 * GuideRate;
802 c = c / 3600.0 / 15.0;
803 c = c / (cos(currentDE * 0.0174532925));
804
805 guideWEOffset += c;
806
807 return IPS_OK;
808}
809
811{
812 float c = v / -1000.0 * GuideRate;
813 c = c / 3600.0 / 15.0;
814 c = c / (cos(currentDE * 0.0174532925));
815
816 guideWEOffset += c;
817
818 return IPS_OK;
819}
820
821bool CCDSim::ISNewText(const char * dev, const char * name, char * texts[], char * names[], int n)
822{
823 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
824 {
825 // This is for our device
826 // Now lets see if it's something we process here
827 if (INDI::FilterInterface::processText(dev, name, texts, names, n))
828 return true;
829
830 if (DirectoryTP.isNameMatch(name))
831 {
832 DirectoryTP.update(texts, names, n);
833 if (DirectorySP[INDI_ENABLED].getState() == ISS_OFF || (DirectorySP[INDI_ENABLED].getState() == ISS_ON && watchDirectory()))
835 else
839 return true;
840 }
841
842 if (PlanetPathsTP.isNameMatch(name))
843 {
844 PlanetPathsTP.update(texts, names, n);
847 // Invalidate cache to force re-render with new paths
852 return true;
853 }
854 }
855
856 return INDI::CCD::ISNewText(dev, name, texts, names, n);
857}
858
859bool CCDSim::ISNewNumber(const char * dev, const char * name, double values[], char * names[], int n)
860{
861 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
862 {
863 if (INDI::FilterInterface::processNumber(dev, name, values, names, n))
864 return true;
865
866 if (GainNP.isNameMatch(name))
867 {
868 GainNP.update(values, names, n);
870 GainNP.apply();
871 return true;
872 }
873 if (OffsetNP.isNameMatch(name))
874 {
875 OffsetNP.update(values, names, n);
877 OffsetNP.apply();
878 m_Bias = OffsetNP[0].getValue();
879 return true;
880 }
881 else if (SimulatorSettingsNP.isNameMatch(name))
882 {
883 SimulatorSettingsNP.update(values, names, n);
885
886 // Reset our parameters now
888 // Invalidate planet cache since FOV/pixel scale may have changed
892 return true;
893 }
894 // Record PE EQ to simulate different position in the sky than actual mount coordinate
895 // This can be useful to simulate Periodic Error or cone error or any arbitrary error.
896 else if (EqPENP.isNameMatch(name))
897 {
898 EqPENP.update(values, names, n);
900
901 INDI::IEquatorialCoordinates epochPos { 0, 0 }, J2000Pos { 0, 0 };
902 epochPos.rightascension = EqPENP[AXIS_RA].getValue();
903 epochPos.declination = EqPENP[AXIS_DE].getValue();
904
905 RA = EqPENP[AXIS_RA].getValue();
906 Dec = EqPENP[AXIS_DE].getValue();
907
908 INDI::ObservedToJ2000(&epochPos, ln_get_julian_from_sys(), &J2000Pos);
909 currentRA = J2000Pos.rightascension;
910 currentDE = J2000Pos.declination;
911 usePE = true;
912
913 EqPENP.apply();
914 return true;
915 }
916 else if (FocusSimulationNP.isNameMatch(name))
917 {
918 // update focus simulation parameters
919 FocusSimulationNP.update(values, names, n);
922 }
923 else if (TiltSimulationNP.isNameMatch(name))
924 {
925 TiltSimulationNP.update(values, names, n);
928 }
929 else if (PlanetSettingsNP.isNameMatch(name))
930 {
931 PlanetSettingsNP.update(values, names, n);
934 // Invalidate cache so new settings take effect next exposure
939 return true;
940 }
941 }
942
943 return INDI::CCD::ISNewNumber(dev, name, values, names, n);
944}
945
946bool CCDSim::ISNewSwitch(const char * dev, const char * name, ISState * states, char * names[], int n)
947{
948 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
949 {
950 // Sim Tests
951 if (SimTestsSP.isNameMatch(name))
952 {
953 SimTestsSP.update(states, names, n);
954
955 // Bayer
956 if (SimTestsSP[SIM_TESTS_BAYER].getState() == ISS_ON)
957 {
958 m_SimulateBayer = true;
960 }
961 else
962 {
963 m_SimulateBayer = false;
965 }
967
968 // Crash
969 if (SimTestsSP[SIM_TESTS_CRASH].getState() == ISS_ON)
970 {
971 abort();
972 }
973
974 // Timeout
975 if (SimTestsSP[SIM_TESTS_TIMEOUT].getState() == ISS_ON)
976 {
977 m_SimulateTimeout = true;
979 }
980 else
981 {
982 m_SimulateTimeout = false;
984 }
985
988 return true;
989 }
990 else if (CoolerSP.isNameMatch(name))
991 {
992 CoolerSP.update(states, names, n);
993
994 if (CoolerSP[INDI_ENABLED].getState() == ISS_ON)
996 else
997 {
999 // When cooler is turned off, warm up to ambient (25°C)
1000 constexpr double AMBIENT_TEMPERATURE = 25.0;
1001 TemperatureRequest = AMBIENT_TEMPERATURE;
1002 m_TargetTemperature = AMBIENT_TEMPERATURE;
1006 }
1007
1008 CoolerSP.apply();
1009
1010 return true;
1011 }
1012 else if (DirectorySP.isNameMatch(name))
1013 {
1014 DirectorySP.update(states, names, n);
1015 m_AllFiles.clear();
1016 m_RemainingFiles.clear();
1017 if (DirectorySP[INDI_ENABLED].getState() == ISS_ON)
1018 {
1019 if (watchDirectory() == false)
1020 {
1024 }
1025 }
1026 else
1027 {
1028 m_RemainingFiles.clear();
1031 LOG_INFO("Directory-based images are disabled.");
1032 }
1033 DirectorySP.apply(nullptr);
1034 return true;
1035 }
1036 else if (ResolutionSP.isNameMatch(name))
1037 {
1038 ResolutionSP.update(states, names, n);
1041
1042 int index = ResolutionSP.findOnSwitchIndex();
1043 if (index >= 0 && index < static_cast<int>(Resolutions.size() - 1))
1044 {
1045 SimulatorSettingsNP[SIM_XRES].setValue(Resolutions[index].first);
1046 SimulatorSettingsNP[SIM_YRES].setValue(Resolutions[index].second);
1048 SimulatorSettingsNP[SIM_YRES].getValue(),
1049 16,
1050 SimulatorSettingsNP[SIM_XSIZE].getValue(),
1051 SimulatorSettingsNP[SIM_YSIZE].getValue());
1053 uint32_t nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * PrimaryCCD.getBPP() / 8;
1055
1057
1058 // Invalidate planet cache since resolution changed
1060 if (m_PlanetMode)
1061 {
1062 SetCaptureFormat(1); // Re-apply RGB format with new buffer size
1063 }
1064 }
1065 return true;
1066 }
1067 // Planet mode toggle
1068 else if (PlanetModeSP.isNameMatch(name))
1069 {
1070 PlanetModeSP.update(states, names, n);
1072 if (m_PlanetMode)
1073 {
1074 // Switch to RGB capture format
1075 int rgbIndex = -1;
1076 for (size_t i = 0; i < m_CaptureFormats.size(); i++)
1077 {
1078 if (m_CaptureFormats[i].name == "INDI_RGB")
1079 {
1080 rgbIndex = static_cast<int>(i);
1081 break;
1082 }
1083 }
1084 if (rgbIndex >= 0 && CaptureFormatSP[rgbIndex].getState() != ISS_ON)
1085 {
1087 CaptureFormatSP[rgbIndex].setState(ISS_ON);
1089 SetCaptureFormat(rgbIndex);
1090 }
1091 // Disable directory mode when planet mode is active
1095 m_AllFiles.clear();
1096 m_RemainingFiles.clear();
1097 // Invalidate cached planet so it re-renders on next exposure
1099 LOGF_INFO("Planet simulation enabled for %s", m_SelectedPlanet.c_str());
1100 }
1101 else
1102 {
1103 // Switch back to Mono capture format
1104 int monoIndex = -1;
1105 for (size_t i = 0; i < m_CaptureFormats.size(); i++)
1106 {
1107 if (m_CaptureFormats[i].name == "INDI_MONO")
1108 {
1109 monoIndex = static_cast<int>(i);
1110 break;
1111 }
1112 }
1113 if (monoIndex >= 0 && CaptureFormatSP[monoIndex].getState() != ISS_ON)
1114 {
1116 CaptureFormatSP[monoIndex].setState(ISS_ON);
1118 SetCaptureFormat(monoIndex);
1119 }
1120 // Restore star field settings: pixel format, buffer, Bayer, streamer
1123 LOG_INFO("Planet simulation disabled. Returning to star field mode.");
1124 }
1126 PlanetModeSP.apply(nullptr);
1127 return true;
1128 }
1129 // Planet selection
1130 else if (PlanetSelectSP.isNameMatch(name))
1131 {
1132 PlanetSelectSP.update(states, names, n);
1133 int index = PlanetSelectSP.findOnSwitchIndex();
1134 static const char* planetNames[] =
1135 {
1136 "mercury", "venus", "mars", "jupiter", "saturn",
1137 "uranus", "neptune", "moon", "sun"
1138 };
1139 if (index >= 0 && index < 9)
1140 {
1141 m_SelectedPlanet = planetNames[index];
1142 // Invalidate cache to re-render with new planet
1144 LOGF_INFO("Selected planet: %s", m_SelectedPlanet.c_str());
1145 }
1147 PlanetSelectSP.apply(nullptr);
1148 return true;
1149 }
1150 }
1151
1152 // Nobody has claimed this, so, ignore it
1153 return INDI::CCD::ISNewSwitch(dev, name, states, names, n);
1154}
1155
1157{
1158 DIR* dirp = opendir(DirectoryTP[0].getText());
1159 if (dirp == nullptr)
1160 {
1161 LOGF_ERROR("Cannot monitor invalid directory %s", DirectoryTP[0].getText());
1162 return false;
1163 }
1164
1165 struct dirent * dp;
1166 std::string d_dir = std::string(DirectoryTP[0].getText());
1167 auto directory = DirectoryTP[0].getText();
1168 if (directory[std::string(directory).length() - 1] != '/')
1169 d_dir += "/";
1170
1171 // Accept FITS (.fit/.fits) and, when built with LibXISF support, XISF (.xisf) files.
1172 static const std::vector<std::string> supportedExtensions =
1173 {
1174 ".fits", ".fit",
1175#ifdef HAVE_XISF
1176 ".xisf",
1177#endif
1178 };
1179
1180 auto hasSupportedExtension = [](const std::string &name)
1181 {
1182 for (const auto &extension : supportedExtensions)
1183 {
1184 if (name.size() < extension.size())
1185 continue;
1186 if (strcasecmp(name.c_str() + name.size() - extension.size(), extension.c_str()) == 0)
1187 return true;
1188 }
1189 return false;
1190 };
1191
1192 while ((dp = readdir(dirp)) != NULL)
1193 {
1194 if (hasSupportedExtension(dp->d_name))
1195 m_AllFiles.push_back(d_dir + dp->d_name);
1196 }
1197 closedir(dirp);
1198
1199 if (m_AllFiles.empty())
1200 {
1201 LOGF_ERROR("No supported image files (.fit, .fits%s) found in directory %s",
1202#ifdef HAVE_XISF
1203 ", .xisf"
1204#else
1205 ""
1206#endif
1207 , directory);
1208 return false;
1209 }
1210 else
1211 {
1212 std::sort(m_AllFiles.begin(), m_AllFiles.end());
1214 LOGF_INFO("Directory-based images are enabled. Subsequent exposures will be loaded from directory %s",
1215 directory);
1216 }
1217
1218 return true;
1219}
1220
1222{
1223#ifdef USE_EQUATORIAL_PE
1225#endif
1226 IDSnoopDevice(ActiveDeviceTP[ACTIVE_TELESCOPE].getText(), "EQUATORIAL_EOD_COORD");
1228
1230}
1231
1233{
1234 if (IUSnoopNumber(root, &FWHMNP) == 0)
1235 {
1236 // we calculate the FWHM and do not snoop it from the focus simulator
1237 // m_Seeing = FWHMNP.np[0].value;
1238 return true;
1239 }
1240
1241 XMLEle * ep = nullptr;
1242 const char * propName = findXMLAttValu(root, "name");
1243
1244 if (!strcmp(propName, "ABS_FOCUS_POSITION"))
1245 {
1246 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
1247 {
1248 const char * name = findXMLAttValu(ep, "name");
1249
1250 if (!strcmp(name, "FOCUS_ABSOLUTE_POSITION"))
1251 {
1252 FocuserPos = atol(pcdataXMLEle(ep));
1253 LOGF_DEBUG("Snooped FocuserPosition %g", FocuserPos);
1254 // calculate FWHM
1255 double focus = FocusSimulationNP[SIM_FOCUS_POSITION].getValue();
1256 double max = FocusSimulationNP[SIM_FOCUS_MAX].getValue();
1257 double optimalFWHM = FocusSimulationNP[SIM_SEEING].getValue();
1258
1259 // limit to +/- 10
1260 double ticks = 20 * (FocuserPos - focus) / max;
1261
1262 m_Seeing = 0.5625 * ticks * ticks + optimalFWHM;
1263 return true;
1264 }
1265 }
1266 }
1267 else if (!strcmp(propName, "ABS_ROTATOR_ANGLE"))
1268 {
1269 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
1270 {
1271 const char * name = findXMLAttValu(ep, "name");
1272
1273 if (!strcmp(name, "ANGLE"))
1274 {
1275 RotatorAngle = atof(pcdataXMLEle(ep));
1276 LOGF_DEBUG("Snooped RotatorAngle %f", RotatorAngle);
1277 return true;
1278 }
1279 }
1280 }
1281 // We try to snoop EQUATORIAL_PE first (true pointing with mount errors injected);
1282 // if not found we fall through to the regular EQUATORIAL_EOD_COORD snoop below.
1283#ifdef USE_EQUATORIAL_PE
1284 if (!strcmp(propName, "EQUATORIAL_PE"))
1285 {
1286 int rc_ra = -1, rc_de = -1;
1287 double newra = 0, newdec = 0;
1288
1289 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
1290 {
1291 const char * elemName = findXMLAttValu(ep, "name");
1292
1293 if (!strcmp(elemName, "RA_PE"))
1294 rc_ra = f_scansexa(pcdataXMLEle(ep), &newra);
1295 else if (!strcmp(elemName, "DEC_PE"))
1296 rc_de = f_scansexa(pcdataXMLEle(ep), &newdec);
1297 }
1298
1299 if (rc_ra == 0 && rc_de == 0)
1300 {
1301 INDI::IEquatorialCoordinates epochPos { newra, newdec }, J2000Pos { 0, 0 };
1302 INDI::ObservedToJ2000(&epochPos, ln_get_julian_from_sys(), &J2000Pos);
1303 raPE = J2000Pos.rightascension;
1304 decPE = J2000Pos.declination;
1305
1306 // Only activate PE rendering once the telescope reports a valid (Ok) state.
1307 // The initial defNumberVector arrives with state Idle and values 0/0.
1308 const char * state = findXMLAttValu(root, "state");
1309 if (!usePE && strcmp(state, "Ok") == 0)
1310 {
1311 LOGF_INFO("EQUATORIAL_PE snoop active: JNow RA %.4f Dec %.4f -> J2000 RA %.4f Dec %.4f", newra, newdec, raPE, decPE);
1312 usePE = true;
1313 }
1314
1315 EqPENP[AXIS_RA].setValue(newra);
1316 EqPENP[AXIS_DE].setValue(newdec);
1317 EqPENP.apply();
1318
1319 LOGF_DEBUG("Snooped EQUATORIAL_PE JNow RA %g Dec %g -> J2000 RA %g Dec %g", newra, newdec, raPE, decPE);
1320 return true;
1321 }
1322 }
1323#endif
1324
1325 return INDI::CCD::ISSnoopDevice(root);
1326}
1327
1329{
1330 // Save CCD Config
1332
1333 // Save Filter Wheel Config
1335
1336 // Save CCD Simulator Config
1338
1339 // Gain
1340 GainNP.save(fp);
1341 OffsetNP.save(fp);
1342
1343 // Directory
1344 DirectoryTP.save(fp);
1345
1346 // Resolution
1347 ResolutionSP.save(fp);
1348
1349 // Focus simulation
1351
1352 // Tilt simulation
1354
1355 // Planet simulation
1357 PlanetPathsTP.save(fp);
1358
1359 return true;
1360}
1361
1363{
1364 // Sleep randomly between 1500 and 2000ms to simulate filter selection
1365 std::random_device rd;
1366 std::mt19937 gen(rd());
1367 std::uniform_int_distribution<> dist(1500, 2000);
1368 std::this_thread::sleep_for(std::chrono::milliseconds(dist(gen)));
1369
1370 CurrentFilter = f;
1372 return true;
1373}
1374
1376{
1377 return CurrentFilter;
1378}
1379
1381{
1382 ExposureRequest = 1.0 / Streamer->getTargetExposure();
1383
1384 // Planet mode: stream pre-compressed JPEG directly (INDI_JPG format, like gphoto_ccd)
1385 if (m_PlanetMode)
1386 {
1387 Streamer->setPixelFormat(INDI_JPG, 8);
1389 }
1390 else
1391 {
1392 Streamer->setPixelFormat(INDI_MONO, 16);
1394 }
1395
1396 pthread_mutex_lock(&condMutex);
1397 streamPredicate = 1;
1398 pthread_mutex_unlock(&condMutex);
1399 pthread_cond_signal(&cv);
1400
1401 return true;
1402}
1403
1405{
1406 pthread_mutex_lock(&condMutex);
1407 streamPredicate = 0;
1408 pthread_mutex_unlock(&condMutex);
1409 pthread_cond_signal(&cv);
1410
1411 return true;
1412}
1413
1414bool CCDSim::UpdateCCDFrame(int x, int y, int w, int h)
1415{
1416 long bin_width = w / PrimaryCCD.getBinX();
1417 long bin_height = h / PrimaryCCD.getBinY();
1418
1419 bin_width = bin_width - (bin_width % 2);
1420 bin_height = bin_height - (bin_height % 2);
1421
1422 Streamer->setSize(bin_width, bin_height);
1423
1424 return INDI::CCD::UpdateCCDFrame(x, y, w, h);
1425}
1426
1427bool CCDSim::UpdateCCDBin(int hor, int ver)
1428{
1429 // Binning is not supported in planet mode (RGB data)
1430 if (m_PlanetMode && (hor != 1 || ver != 1))
1431 {
1432 LOG_ERROR("Binning is not supported in planet mode.");
1433 return false;
1434 }
1435
1436 if (PrimaryCCD.getSubW() % hor != 0 || PrimaryCCD.getSubH() % ver != 0)
1437 {
1438 LOGF_ERROR("%dx%d binning is not supported.", hor, ver);
1439 return false;
1440 }
1441
1442 uint32_t bin_width = PrimaryCCD.getSubW() / hor;
1443 uint32_t bin_height = PrimaryCCD.getSubH() / ver;
1444 Streamer->setSize(bin_width, bin_height);
1445
1446 return INDI::CCD::UpdateCCDBin(hor, ver);
1447}
1448
1449bool CCDSim::UpdateGuiderBin(int hor, int ver)
1450{
1451 if (GuideCCD.getSubW() % hor != 0 || GuideCCD.getSubH() % ver != 0)
1452 {
1453 LOGF_ERROR("%dx%d binning is not supported.", hor, ver);
1454 return false;
1455 }
1456
1457 return INDI::CCD::UpdateGuiderBin(hor, ver);
1458}
1459
1460void * CCDSim::streamVideoHelper(void * context)
1461{
1462 return static_cast<CCDSim *>(context)->streamVideo();
1463}
1464
1466{
1467 auto start = std::chrono::high_resolution_clock::now();
1468
1469 while (true)
1470 {
1471 pthread_mutex_lock(&condMutex);
1472
1473 while (streamPredicate == 0)
1474 {
1475 pthread_cond_wait(&cv, &condMutex);
1476 ExposureRequest = Streamer->getTargetExposure();
1477 }
1478
1479 if (terminateThread)
1480 break;
1481
1482 // release condMutex
1483 pthread_mutex_unlock(&condMutex);
1484
1485
1486 // Planet mode: stream cached PNG directly (no encoding, like gphoto_ccd JPEG)
1487 if (m_PlanetMode)
1488 {
1489 renderPlanet(); // updates both caches if needed
1490 if (!m_CachedStreamBuffer.empty())
1491 {
1492 auto finish = std::chrono::high_resolution_clock::now();
1493 std::chrono::duration<double> elapsed = finish - start;
1494 if (elapsed.count() < ExposureRequest)
1495 usleep(fabs(ExposureRequest - elapsed.count()) * 1e6);
1496 Streamer->newFrame(m_CachedStreamBuffer.data(), m_CachedStreamBuffer.size());
1497 }
1498 }
1499 else
1500 {
1502 // Software binning for mono star field mode only
1503 if (DirectorySP[INDI_DISABLED].getState() == ISS_ON)
1505
1506 auto finish = std::chrono::high_resolution_clock::now();
1507 std::chrono::duration<double> elapsed = finish - start;
1508 if (elapsed.count() < ExposureRequest)
1509 usleep(fabs(ExposureRequest - elapsed.count()) * 1e6);
1510
1511 uint32_t size = PrimaryCCD.getFrameBufferSize();
1512 Streamer->newFrame(PrimaryCCD.getFrameBuffer(), size);
1513 }
1514
1515 start = std::chrono::high_resolution_clock::now();
1516 }
1517
1518 pthread_mutex_unlock(&condMutex);
1519 return nullptr;
1520}
1521
1523{
1524 // Get current CCD dimensions
1525 uint32_t width = PrimaryCCD.getXRes();
1526 uint32_t height = PrimaryCCD.getYRes();
1527
1528 // Compute pixel scale: arcsec per pixel = 206.265 * pixel_size_um / focal_length_mm
1529 float pixelSizeX = SimulatorSettingsNP[SIM_XSIZE].getValue();
1530 float pixelSizeY = SimulatorSettingsNP[SIM_YSIZE].getValue();
1531 float focalLength = ScopeInfoNP[FOCAL_LENGTH].getValue() > 0 ?
1533 if (focalLength <= 0)
1534 focalLength = 1000.0f; // default fallback
1535 float arcsecPerPixelX = 206.265f * pixelSizeX / focalLength;
1536 float arcsecPerPixelY = 206.265f * pixelSizeY / focalLength;
1537
1538 // Determine planet angular diameter
1539 float planetDiameterArcsec = 0;
1540 if (m_PlanetSizeOverride > 0)
1541 {
1542 planetDiameterArcsec = m_PlanetSizeOverride;
1543 }
1544 else
1545 {
1547 if (it != PlanetAngularDiameter.end())
1548 planetDiameterArcsec = it->second;
1549 else
1550 planetDiameterArcsec = 42.0f; // fallback
1551 }
1552
1553 // How many sensor pixels the planet spans (use average of X and Y pixel scales)
1554 float planetPixelsX = planetDiameterArcsec / arcsecPerPixelX;
1555 float planetPixelsY = planetDiameterArcsec / arcsecPerPixelY;
1556 float avgPlanetPixels = (planetPixelsX + planetPixelsY) / 2.0f;
1557
1558 // Render xplanet at high resolution (1024x1024) with visible black margin.
1559 // Use -range (camera distance in degrees) - NOT -radius which is angular FOV
1560 // and causes planet to fill entire frame under orthographic projection.
1561 // Range of 0.05° gives ~40% black border for Jupiter (~0.0117° angular diam).
1562 const int renderSize = 1024;
1563 const float planetRange = 0.05f; // degrees
1564
1565 // Compute planet disk pixel radius from angular size and range
1566 float planetDiamDeg = planetDiameterArcsec / 3600.0f;
1567 float planetFracOfFrame = planetDiamDeg / planetRange;
1568 float srcPlanetRadius = planetFracOfFrame * renderSize / 2.0f;
1569 if (srcPlanetRadius < 10.0f) srcPlanetRadius = 10.0f;
1570
1571 // Build xplanet command. We output PNG — same file used for both FITS rendering
1572 // and direct streaming (cached as-is, like gphoto_ccd sends JPEG).
1573 std::string searchDir;
1574 size_t configPos = m_XPlanetConfig.rfind("/config/");
1575 if (configPos != std::string::npos)
1576 searchDir = m_XPlanetConfig.substr(0, configPos);
1577
1578 const char *pngPath = "/tmp/indi_planet.png";
1579 char cmd[2048];
1580 const bool useCustomConfig = (m_XPlanetConfig != "/usr/share/xplanet/config/default");
1581
1582 if (!searchDir.empty() && useCustomConfig)
1583 {
1584 snprintf(cmd, sizeof(cmd),
1585 "%s -body %s -geometry %dx%d -range %.4f "
1586 "-num_times 1 -projection orthographic "
1587 "-config %s -searchdir %s "
1588 "-output %s 2>/dev/null",
1589 m_XPlanetBinary.c_str(), m_SelectedPlanet.c_str(),
1590 renderSize, renderSize, static_cast<double>(planetRange),
1591 m_XPlanetConfig.c_str(), searchDir.c_str(), pngPath);
1592 }
1593 else if (!searchDir.empty())
1594 {
1595 snprintf(cmd, sizeof(cmd),
1596 "%s -body %s -geometry %dx%d -range %.4f "
1597 "-num_times 1 -projection orthographic "
1598 "-searchdir %s "
1599 "-output %s 2>/dev/null",
1600 m_XPlanetBinary.c_str(), m_SelectedPlanet.c_str(),
1601 renderSize, renderSize, static_cast<double>(planetRange),
1602 searchDir.c_str(), pngPath);
1603 }
1604 else
1605 {
1606 snprintf(cmd, sizeof(cmd),
1607 "%s -body %s -geometry %dx%d -range %.4f "
1608 "-num_times 1 -projection orthographic "
1609 "-output %s 2>/dev/null",
1610 m_XPlanetBinary.c_str(), m_SelectedPlanet.c_str(),
1611 renderSize, renderSize, static_cast<double>(planetRange), pngPath);
1612 }
1613
1614 // Ensure buffer is sized for 3-channel 16-bit planar RGB
1615 uint32_t rgbBufSize = width * height * 3 * 2;
1616 if (PrimaryCCD.getFrameBufferSize() != static_cast<int>(rgbBufSize))
1617 PrimaryCCD.setFrameBufferSize(rgbBufSize);
1618
1619 // Check if cache needs invalidation before spawning any external processes
1623 m_CachedFocalLength != focalLength ||
1624 m_CachedPixelSizeX != pixelSizeX ||
1625 m_CachedWidth != width))
1626 {
1627 LOG_DEBUG("Cache params changed, re-rendering");
1628 m_CachedPlanetBuffer.clear();
1630 }
1631
1632 // Only spawn xplanet if cache is empty (fresh render needed)
1633 if (m_CachedPlanetBuffer.empty())
1634 {
1636 LOGF_DEBUG("Running: %s", cmd);
1637 int ret = system(cmd);
1638 if (ret != 0)
1639 {
1640 LOGF_ERROR("xplanet failed with exit code %d. Is xplanet installed?", ret);
1641 return false;
1642 }
1643
1644 // Generate CCD-resolution JPEG for streaming: resize planet to correct pixel size,
1645 // center on black background matching CCD dimensions
1646 {
1647 char jpgCmd[2048];
1648 int planetRenderPixels = static_cast<int>(avgPlanetPixels);
1649 if (planetRenderPixels < 1) planetRenderPixels = 1;
1650 snprintf(jpgCmd, sizeof(jpgCmd),
1651 "convert %s -resize %dx%d -background black "
1652 "-gravity center -extent %dx%d -quality 90 /tmp/indi_planet.jpg 2>/dev/null",
1653 pngPath, planetRenderPixels, planetRenderPixels,
1654 static_cast<int>(width), static_cast<int>(height));
1655 ret = system(jpgCmd);
1656 if (ret != 0)
1657 {
1658 LOG_ERROR("Failed to generate CCD-resolution JPEG. Is ImageMagick installed?");
1659 return false;
1660 }
1661 }
1662
1663 // Cache the CCD-resolution JPEG for direct streaming
1664 FILE *jpgFile = fopen("/tmp/indi_planet.jpg", "rb");
1665 if (jpgFile)
1666 {
1667 fseek(jpgFile, 0, SEEK_END);
1668 long jpgSize = ftell(jpgFile);
1669 fseek(jpgFile, 0, SEEK_SET);
1670 m_CachedStreamBuffer.resize(jpgSize);
1671 size_t bytesRead = fread(m_CachedStreamBuffer.data(), 1, jpgSize, jpgFile);
1672 INDI_UNUSED(bytesRead);
1673 fclose(jpgFile);
1674 }
1675
1676 ret = system("convert /tmp/indi_planet.png /tmp/indi_planet.ppm 2>/dev/null");
1677 if (ret != 0)
1678 {
1679 LOG_ERROR("Failed to convert PNG to PPM. Is ImageMagick installed?");
1680 return false;
1681 }
1682
1683 // Parse the PPM file
1684 FILE *fp = fopen("/tmp/indi_planet.ppm", "rb");
1685 if (!fp)
1686 {
1687 LOG_ERROR("Failed to open /tmp/indi_planet.ppm");
1688 return false;
1689 }
1690
1691 char magic[3];
1692 if (fscanf(fp, "%2s\n", magic) != 1 || strcmp(magic, "P6") != 0)
1693 {
1694 LOG_ERROR("PPM file is not P6 format (binary RGB)");
1695 fclose(fp);
1696 return false;
1697 }
1698
1699 int ppmWidth, ppmHeight, maxVal;
1700 // Skip comments
1701 int c = fgetc(fp);
1702 while (c == '#')
1703 {
1704 while (fgetc(fp) != '\n');
1705 c = fgetc(fp);
1706 }
1707 ungetc(c, fp);
1708 if (fscanf(fp, "%d %d\n%d\n", &ppmWidth, &ppmHeight, &maxVal) != 3)
1709 {
1710 LOG_ERROR("Failed to parse PPM header");
1711 fclose(fp);
1712 return false;
1713 }
1714
1715 std::vector<uint8_t> rgbData(ppmWidth * ppmHeight * 3);
1716 size_t bytesRead = fread(rgbData.data(), 1, rgbData.size(), fp);
1717 fclose(fp);
1718
1719 if (bytesRead != rgbData.size())
1720 {
1721 LOGF_ERROR("PPM read error: expected %zu bytes, got %zu", rgbData.size(), bytesRead);
1722 return false;
1723 }
1724
1725 // Allocate cached planet buffer at CCD resolution (16-bit RGB, 3-channel PLANAR layout for FITS)
1726 m_CachedPlanetBuffer.resize(rgbBufSize);
1727 m_CachedPlanetWidth = width;
1728 m_CachedPlanetHeight = height;
1729 uint16_t *buffer = reinterpret_cast<uint16_t *>(m_CachedPlanetBuffer.data());
1730
1731 // Clear to black
1732 memset(buffer, 0, rgbBufSize);
1733
1734 // Plane offsets within buffer (FITS NAXIS=3 planar: R_plane | G_plane | B_plane)
1735 size_t planeSize = static_cast<size_t>(width) * static_cast<size_t>(height);
1736 uint16_t *rPlane = buffer;
1737 uint16_t *gPlane = buffer + planeSize;
1738 uint16_t *bPlane = buffer + planeSize * 2;
1739
1740 // Center the planet in the CCD frame
1741 float centerX = width / 2.0f;
1742 float centerY = height / 2.0f;
1743
1744 // Scale factor from xplanet render to planet pixel size
1745 float dstPlanetRadius = avgPlanetPixels / 2.0f;
1746 if (dstPlanetRadius < 1.0f)
1747 dstPlanetRadius = 1.0f;
1748 float scale = srcPlanetRadius / dstPlanetRadius;
1749
1750 // Map each CCD pixel to the source PPM, writing planar 16-bit data.
1751 // Use simple linear scaling (8-bit × 256 → 16-bit) so the planet's
1752 // 8-bit perceptual brightness maps to the upper 16-bit range, making
1753 // auto-stretch work naturally without crushing mid-tones.
1754 for (uint32_t y = 0; y < height; y++)
1755 {
1756 for (uint32_t x = 0; x < width; x++)
1757 {
1758 float sx = (x - centerX) * scale + ppmWidth / 2.0f;
1759 float sy = (y - centerY) * scale + ppmHeight / 2.0f;
1760
1761 float dx = (sx - ppmWidth / 2.0f);
1762 float dy = (sy - ppmHeight / 2.0f);
1763 if (sqrtf(dx * dx + dy * dy) > srcPlanetRadius)
1764 continue;
1765
1766 int ix = static_cast<int>(sx);
1767 int iy = static_cast<int>(sy);
1768 if (ix < 0 || ix >= ppmWidth || iy < 0 || iy >= ppmHeight)
1769 continue;
1770
1771 int srcIdx = (iy * ppmWidth + ix) * 3;
1772 uint8_t r8 = rgbData[srcIdx];
1773 uint8_t g8 = rgbData[srcIdx + 1];
1774 uint8_t b8 = rgbData[srcIdx + 2];
1775
1776 // Linear 8→16-bit scaling: perceptual brightness preserved
1777 uint16_t r16 = static_cast<uint16_t>(r8) * 256;
1778 uint16_t g16 = static_cast<uint16_t>(g8) * 256;
1779 uint16_t b16 = static_cast<uint16_t>(b8) * 256;
1780
1781 if (r16 > static_cast<uint32_t>(m_MaxVal)) r16 = m_MaxVal;
1782 if (g16 > static_cast<uint32_t>(m_MaxVal)) g16 = m_MaxVal;
1783 if (b16 > static_cast<uint32_t>(m_MaxVal)) b16 = m_MaxVal;
1784
1785 size_t px = y * width + x;
1786 rPlane[px] = r16;
1787 gPlane[px] = g16;
1788 bPlane[px] = b16;
1789 }
1790 }
1791
1792 // Store FOV params so we can detect changes on cache check
1794 m_CachedFocalLength = focalLength;
1795 m_CachedPixelSizeX = pixelSizeX;
1796 m_CachedWidth = width;
1797 }
1798 else
1799 {
1800 // Cache hit: PPM already parsed and scaled, just copy to frame buffer
1801 // (NAXIS and buffer size already set by SetCaptureFormat when planet mode enabled)
1802 }
1803
1804 // Copy the cached buffer into PrimaryCCD's frame buffer
1805 size_t copySize = std::min(m_CachedPlanetBuffer.size(), static_cast<size_t>(rgbBufSize));
1806 memcpy(PrimaryCCD.getFrameBuffer(), m_CachedPlanetBuffer.data(), copySize);
1807
1809 // Only log on fresh render, not cache hits
1811 {
1812 LOGF_INFO("Rendered planet %s: diameter=%.1f arcsec, pixels=%.1f, scale=%.1f arcsec/px",
1813 m_SelectedPlanet.c_str(), planetDiameterArcsec, planetPixelsX, arcsecPerPixelX);
1814 }
1815
1816 return true;
1817}
1818
1819void CCDSim::addFITSKeywords(INDI::CCDChip *targetChip, std::vector<INDI::FITSRecord> &fitsKeyword)
1820{
1821 INDI::CCD::addFITSKeywords(targetChip, fitsKeyword);
1822
1823 fitsKeyword.push_back({"GAIN", GainNP[0].getValue(), 3, "Gain"});
1824
1825 if (m_PlanetMode)
1826 {
1827 fitsKeyword.emplace_back("OBJECT", m_SelectedPlanet.c_str(), "Target planet");
1828 fitsKeyword.emplace_back("XPLANET", static_cast<int64_t>(1), "Generated by xplanet");
1829 fitsKeyword.emplace_back("COLORSPC", "RGB", "Color space");
1830 }
1831}
1832
1834{
1835 if (m_RemainingFiles.empty())
1837 const std::string filename = m_RemainingFiles[0];
1838 m_RemainingFiles.pop_front();
1839
1840#ifdef HAVE_XISF
1841 if (filename.size() >= 5 && strcasecmp(filename.c_str() + filename.size() - 5, ".xisf") == 0)
1842 return loadNextXISFImage(filename);
1843#endif
1844
1845 return loadNextFITSImage(filename);
1846}
1847
1848bool CCDSim::loadNextFITSImage(const std::string &filename)
1849{
1850 char comment[512] = {0}, bayer_pattern[16] = {0};
1851 int status = 0, anynull = 0;
1852 double pixel_size = 5.2;
1853 int ndim {2}, bitpix {8};
1854 long naxes[3];
1855 fitsfile *fptr = nullptr;
1856
1857 if (fits_open_diskfile(&fptr, filename.c_str(), READONLY, &status))
1858 {
1859 char error_status[512] = {0};
1860 fits_get_errstatus(status, error_status);
1861 LOGF_WARN("Error opening file %s due to error %s", filename.c_str(), error_status);
1862 return false;
1863 }
1864
1865 if (fits_get_img_param(fptr, 3, &bitpix, &ndim, naxes, &status))
1866 {
1867 char error_status[512] = {0};
1868 fits_get_errstatus(status, error_status);
1869 LOGF_WARN("Error reading image parameters from %s due to error %s", filename.c_str(), error_status);
1870 fits_close_file(fptr, &status);
1871 return false;
1872 }
1873
1874 if (ndim < 3)
1875 naxes[2] = 1;
1876 else
1878 int samples_per_channel = naxes[0] * naxes[1];
1879 int channels = naxes[2];
1880 int elements = samples_per_channel * channels;
1881 int size = elements * bitpix / 8;
1883
1884 if (fits_read_img(fptr, bitpix == 8 ? TBYTE : TUSHORT, 1, elements, 0, PrimaryCCD.getFrameBuffer(), &anynull, &status))
1885 {
1886 char error_status[512] = {0};
1887 fits_get_errstatus(status, error_status);
1888 LOGF_WARN("Error reading pixel data from %s due to error %s", filename.c_str(), error_status);
1889 fits_close_file(fptr, &status);
1890 return false;
1891 }
1892
1893 // Pixel size is informational only. Different capture applications use different keywords
1894 // for it (e.g. INDI writes PIXSIZE1 while others like N.I.N.A/SharpCap write XPIXSZ), and some
1895 // omit it entirely, so fall back gracefully instead of rejecting the whole image over it.
1896 status = 0;
1897 if (fits_read_key_dbl(fptr, "PIXSIZE1", &pixel_size, comment, &status))
1898 {
1899 status = 0;
1900 if (fits_read_key_dbl(fptr, "XPIXSZ", &pixel_size, comment, &status))
1901 {
1902 LOGF_DEBUG("No PIXSIZE1 or XPIXSZ keyword found in %s, defaulting pixel size to %.2f microns",
1903 filename.c_str(), pixel_size);
1904 }
1905 }
1906
1907 status = 0;
1908 if (fits_read_key_str(fptr, "BAYERPAT", bayer_pattern, comment, &status))
1909 {
1910 char error_status[512] = {0};
1911 fits_get_errstatus(status, error_status);
1912 LOGF_DEBUG("No BAYERPAT keyword found in %s (%s)", filename.c_str(), error_status);
1913 }
1914 SetCCDParams(naxes[0], naxes[1], bitpix, pixel_size, pixel_size);
1915
1916 // Check if MONO or Bayer
1917 if (channels == 1 && strlen(bayer_pattern) == 4)
1918 {
1920 BayerTP[CFA_OFFSET_X].setText("0");
1921 BayerTP[CFA_OFFSET_Y].setText("0");
1922 BayerTP[CFA_TYPE].setText(bayer_pattern);
1923 }
1924 else
1925 {
1927 }
1928
1929 status = 0;
1930 fits_close_file(fptr, &status);
1931 return true;
1932}
1933
1934#ifdef HAVE_XISF
1935bool CCDSim::loadNextXISFImage(const std::string &filename)
1936{
1937 try
1938 {
1939 LibXISF::XISFReader reader;
1940 reader.open(filename);
1941
1942 if (reader.imagesCount() < 1)
1943 {
1944 LOGF_WARN("No images found in XISF file %s", filename.c_str());
1945 return false;
1946 }
1947
1948 // Work on a local copy so we can normalize pixel storage before copying it out.
1949 LibXISF::Image image = reader.getImage(0);
1950
1951 const auto sampleFormat = image.sampleFormat();
1952 const bool isFloat = (sampleFormat == LibXISF::Image::Float32 || sampleFormat == LibXISF::Image::Float64);
1953 int bitpix = 0;
1954 switch (sampleFormat)
1955 {
1956 case LibXISF::Image::UInt8:
1957 bitpix = 8;
1958 break;
1959 case LibXISF::Image::UInt16:
1960 bitpix = 16;
1961 break;
1962 case LibXISF::Image::Float32:
1963 case LibXISF::Image::Float64:
1964 // Processed/master XISF files (e.g. exported from PixInsight) are commonly stored as
1965 // normalized [0,1] floating point samples per the XISF spec. Scale them to 16-bit so
1966 // they can flow through the same integer pipeline as camera-native captures.
1967 bitpix = 16;
1968 break;
1969 default:
1970 LOGF_WARN("Unsupported XISF sample format '%s' in %s. Only 8/16-bit integer and 32/64-bit "
1971 "normalized float samples are supported.",
1972 LibXISF::Image::sampleFormatString(sampleFormat).c_str(), filename.c_str());
1973 return false;
1974 }
1975
1976 int channels = static_cast<int>(image.channelCount());
1977 PrimaryCCD.setNAxis(channels >= 3 ? 3 : 2);
1978
1979 // The CCD frame buffer expects channel-planar data (as FITS image cubes store it),
1980 // matching how this driver (and INDI in general) writes XISF images.
1981 if (image.pixelStorage() != LibXISF::Image::Planar)
1982 image.convertPixelStorageTo(LibXISF::Image::Planar);
1983
1984 const size_t elementCount = image.width() * image.height() * channels;
1985 if (isFloat)
1986 {
1987 PrimaryCCD.setFrameBufferSize(elementCount * 2);
1988 auto * dest = reinterpret_cast<uint16_t *>(PrimaryCCD.getFrameBuffer());
1989 auto scaleClamp = [](double value) -> uint16_t
1990 {
1991 if (value <= 0.0)
1992 return 0;
1993 if (value >= 1.0)
1994 return 65535;
1995 return static_cast<uint16_t>(std::lround(value * 65535.0));
1996 };
1997
1998 if (sampleFormat == LibXISF::Image::Float32)
1999 {
2000 const float * src = image.imageData<float>();
2001 for (size_t i = 0; i < elementCount; i++)
2002 dest[i] = scaleClamp(src[i]);
2003 }
2004 else
2005 {
2006 const double * src = image.imageData<double>();
2007 for (size_t i = 0; i < elementCount; i++)
2008 dest[i] = scaleClamp(src[i]);
2009 }
2010 }
2011 else
2012 {
2013 PrimaryCCD.setFrameBufferSize(image.imageDataSize());
2014 memcpy(PrimaryCCD.getFrameBuffer(), image.imageData(), image.imageDataSize());
2015 }
2016
2017 // Pixel size is informational only; fall back to a default if not present, same as the FITS path.
2018 double pixel_size = 5.2;
2019 bool foundPixelSize = false;
2020 for (const auto &keyword : image.fitsKeywords())
2021 {
2022 if (keyword.name == "PIXSIZE1" || keyword.name == "XPIXSZ")
2023 {
2024 pixel_size = std::stod(keyword.value);
2025 foundPixelSize = true;
2026 break;
2027 }
2028 }
2029 if (!foundPixelSize)
2030 LOGF_DEBUG("No PIXSIZE1 or XPIXSZ keyword found in %s, defaulting pixel size to %.2f microns",
2031 filename.c_str(), pixel_size);
2032
2033 SetCCDParams(image.width(), image.height(), bitpix, pixel_size, pixel_size);
2034
2035 // Check if MONO or Bayer
2036 const auto cfa = image.colorFilterArray();
2037 if (channels == 1 && cfa.pattern.size() == 4)
2038 {
2040 BayerTP[CFA_OFFSET_X].setText("0");
2041 BayerTP[CFA_OFFSET_Y].setText("0");
2042 BayerTP[CFA_TYPE].setText(cfa.pattern.c_str());
2043 }
2044 else
2045 {
2047 }
2048 }
2049 catch (LibXISF::Error &error)
2050 {
2051 LOGF_WARN("Error reading XISF file %s due to error: %s", filename.c_str(), error.what());
2052 return false;
2053 }
2054
2055 return true;
2056}
2057#endif
2058
2059bool CCDSim::SetCaptureFormat(uint8_t index)
2060{
2061 // Get the selected format
2062 if (index >= m_CaptureFormats.size())
2063 return false;
2064
2065 const auto &format = m_CaptureFormats[index];
2066 bool isRGB = (format.name == "INDI_RGB");
2067
2068 // Set NAXIS and buffer size for RGB vs Mono
2069 // The base CCD class handles CaptureFormatSP state, we just need to configure the chip
2070 uint32_t nbuf;
2071 if (isRGB)
2072 {
2074 nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * 3 * (PrimaryCCD.getBPP() / 8);
2076 }
2077 else
2078 {
2080 nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * (PrimaryCCD.getBPP() / 8);
2082 }
2084 Streamer->setPixelFormat(isRGB ? INDI_RGB : INDI_MONO, 16);
2085 m_IsRGBFormat = isRGB;
2086 return true;
2087}
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
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
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
@ FOCAL_LENGTH
Definition indiccd.h:723
@ CFA_OFFSET_Y
Definition indiccd.h:688
@ CFA_OFFSET_X
Definition indiccd.h:687
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
@ 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
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
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
@ ACTIVE_TELESCOPE
Definition indiccd.h:659
@ ACTIVE_FOCUSER
Definition indiccd.h:661
CCDChip GuideCCD
Definition indiccd.h:622
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
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 * 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