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"
21 #include "stream/streammanager.h"
22
23 #include "locale_compat.h"
24
25 #include <libnova/julian_day.h>
26 #include <libastro.h>
27
28 #include <cmath>
29 #include <unistd.h>
30 #include <sys/types.h>
31 #include <dirent.h>
32 #include <sys/stat.h>
33 #include <algorithm>
34
35 static pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
36 static pthread_mutex_t condMutex = PTHREAD_MUTEX_INITIALIZER;
37
38 static std::unique_ptr<CCDSim> ccdsim(new CCDSim());
39
CCDSim()40 CCDSim::CCDSim() : INDI::FilterInterface(this)
41 {
42 currentRA = RA;
43 currentDE = Dec;
44
45 terminateThread = false;
46
47 // focal length of the telescope in millimeters
48 primaryFocalLength = 900;
49 guiderFocalLength = 300;
50
51 time(&RunStart);
52
53 // Filter stuff
54 FilterSlotN[0].min = 1;
55 FilterSlotN[0].max = 8;
56 }
57
setupParameters()58 bool CCDSim::setupParameters()
59 {
60 SetCCDParams(SimulatorSettingsN[SIM_XRES].value,
61 SimulatorSettingsN[SIM_YRES].value,
62 16,
63 SimulatorSettingsN[SIM_XSIZE].value,
64 SimulatorSettingsN[SIM_YSIZE].value);
65
66 m_MaxNoise = SimulatorSettingsN[SIM_NOISE].value;
67 m_SkyGlow = SimulatorSettingsN[SIM_SKYGLOW].value;
68 m_MaxVal = SimulatorSettingsN[SIM_MAXVAL].value;
69 m_Bias = OffsetN[0].value;
70 m_LimitingMag = SimulatorSettingsN[SIM_LIMITINGMAG].value;
71 m_SaturationMag = SimulatorSettingsN[SIM_SATURATION].value;
72 // An oag is offset this much from center of scope position (arcminutes);
73 m_OAGOffset = SimulatorSettingsN[SIM_OAGOFFSET].value;
74 m_PolarError = SimulatorSettingsN[SIM_POLAR].value;
75 m_PolarDrift = SimulatorSettingsN[SIM_POLARDRIFT].value;
76 m_PEPeriod = SimulatorSettingsN[SIM_PE_PERIOD].value;
77 m_PEMax = SimulatorSettingsN[SIM_PE_MAX].value;
78 m_TimeFactor = SimulatorSettingsN[SIM_TIME_FACTOR].value;
79 RotatorAngle = SimulatorSettingsN[SIM_ROTATION].value;
80
81 uint32_t nbuf = PrimaryCCD.getXRes() * PrimaryCCD.getYRes() * PrimaryCCD.getBPP() / 8;
82 PrimaryCCD.setFrameBufferSize(nbuf);
83
84 Streamer->setPixelFormat(INDI_MONO, 16);
85 Streamer->setSize(PrimaryCCD.getXRes(), PrimaryCCD.getYRes());
86
87 return true;
88 }
89
Connect()90 bool CCDSim::Connect()
91 {
92 streamPredicate = 0;
93 terminateThread = false;
94 pthread_create(&primary_thread, nullptr, &streamVideoHelper, this);
95 SetTimer(getCurrentPollingPeriod());
96 return true;
97 }
98
Disconnect()99 bool CCDSim::Disconnect()
100 {
101 pthread_mutex_lock(&condMutex);
102 streamPredicate = 1;
103 terminateThread = true;
104 pthread_cond_signal(&cv);
105 pthread_mutex_unlock(&condMutex);
106
107 return true;
108 }
109
getDefaultName()110 const char * CCDSim::getDefaultName()
111 {
112 return "CCD Simulator";
113 }
114
initProperties()115 bool CCDSim::initProperties()
116 {
117 INDI::CCD::initProperties();
118
119 IUFillNumber(&SimulatorSettingsN[SIM_XRES], "SIM_XRES", "CCD X resolution", "%4.0f", 512, 8192, 512, 1280);
120 IUFillNumber(&SimulatorSettingsN[SIM_YRES], "SIM_YRES", "CCD Y resolution", "%4.0f", 512, 8192, 512, 1024);
121 IUFillNumber(&SimulatorSettingsN[SIM_XSIZE], "SIM_XSIZE", "CCD X Pixel Size", "%4.2f", 1, 30, 5, 5.2);
122 IUFillNumber(&SimulatorSettingsN[SIM_YSIZE], "SIM_YSIZE", "CCD Y Pixel Size", "%4.2f", 1, 30, 5, 5.2);
123 IUFillNumber(&SimulatorSettingsN[SIM_MAXVAL], "SIM_MAXVAL", "CCD Maximum ADU", "%4.0f", 255, 65000, 1000, 65000);
124 IUFillNumber(&SimulatorSettingsN[SIM_SATURATION], "SIM_SATURATION", "Saturation Mag", "%4.1f", 0, 20, 1, 1.0);
125 IUFillNumber(&SimulatorSettingsN[SIM_LIMITINGMAG], "SIM_LIMITINGMAG", "Limiting Mag", "%4.1f", 0, 20, 1, 17.0);
126 IUFillNumber(&SimulatorSettingsN[SIM_NOISE], "SIM_NOISE", "CCD Noise", "%4.0f", 0, 6000, 500, 10);
127 IUFillNumber(&SimulatorSettingsN[SIM_SKYGLOW], "SIM_SKYGLOW", "Sky Glow (magnitudes)", "%4.1f", 0, 6000, 500, 19.5);
128 IUFillNumber(&SimulatorSettingsN[SIM_OAGOFFSET], "SIM_OAGOFFSET", "Oag Offset (arcminutes)", "%4.1f", 0, 6000, 500, 0);
129 IUFillNumber(&SimulatorSettingsN[SIM_POLAR], "SIM_POLAR", "PAE (arcminutes)", "%4.1f", -600, 600, 100, 0);
130 IUFillNumber(&SimulatorSettingsN[SIM_POLARDRIFT], "SIM_POLARDRIFT", "PAE Drift (minutes)", "%4.1f", 0, 60, 5, 0);
131 IUFillNumber(&SimulatorSettingsN[SIM_PE_PERIOD], "SIM_PEPERIOD", "PE Period (minutes)", "%4.1f", 0, 60, 5, 0);
132 IUFillNumber(&SimulatorSettingsN[SIM_PE_MAX], "SIM_PEMAX", "PE Max (arcsec)", "%4.1f", 0, 6000, 500, 0);
133 IUFillNumber(&SimulatorSettingsN[SIM_TIME_FACTOR], "SIM_TIME_FACTOR", "Time Factor (x)", "%.2f", 0.01, 100, 10, 1);
134 IUFillNumber(&SimulatorSettingsN[SIM_ROTATION], "SIM_ROTATION", "CCD Rotation", "%.2f", 0, 360, 10, 0);
135
136 IUFillNumberVector(&SimulatorSettingsNP, SimulatorSettingsN, SIM_N, getDeviceName(), "SIMULATOR_SETTINGS",
137 "Settings", SIMULATOR_TAB, IP_RW, 60, IPS_IDLE);
138
139 // RGB Simulation
140 IUFillSwitch(&SimulateBayerS[INDI_ENABLED], "INDI_ENABLED", "Enabled", ISS_OFF);
141 IUFillSwitch(&SimulateBayerS[INDI_DISABLED], "INDI_DISABLED", "Disabled", ISS_ON);
142 IUFillSwitchVector(&SimulateBayerSP, SimulateBayerS, 2, getDeviceName(), "SIMULATE_BAYER", "Bayer", SIMULATOR_TAB, IP_RW,
143 ISR_1OFMANY, 60, IPS_IDLE);
144
145 // Simulate focusing
146 IUFillNumber(&FocusSimulationN[0], "SIM_FOCUS_POSITION", "Focus", "%.f", 0.0, 100000.0, 1.0, 36700.0);
147 IUFillNumber(&FocusSimulationN[1], "SIM_FOCUS_MAX", "Max. Position", "%.f", 0.0, 100000.0, 1.0, 100000.0);
148 IUFillNumber(&FocusSimulationN[2], "SIM_SEEING", "Seeing (arcsec)", "%4.2f", 0, 60, 0, 3.5);
149 IUFillNumberVector(&FocusSimulationNP, FocusSimulationN, 3, getDeviceName(), "SIM_FOCUSING", "Focus Simulation",
150 SIMULATOR_TAB, IP_RW, 60, IPS_IDLE);
151
152 // Simulate Crash
153 IUFillSwitch(&CrashS[0], "CRASH", "Crash driver", ISS_OFF);
154 IUFillSwitchVector(&CrashSP, CrashS, 1, getDeviceName(), "CCD_SIMULATE_CRASH", "Crash", SIMULATOR_TAB, IP_WO,
155 ISR_ATMOST1, 0, IPS_IDLE);
156
157 // Periodic Error
158 IUFillNumber(&EqPEN[AXIS_RA], "RA_PE", "RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 0);
159 IUFillNumber(&EqPEN[AXIS_DE], "DEC_PE", "DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0);
160 IUFillNumberVector(&EqPENP, EqPEN, 2, getDeviceName(), "EQUATORIAL_PE", "EQ PE", SIMULATOR_TAB, IP_RW, 60,
161 IPS_IDLE);
162
163 // FWHM
164 IUFillNumber(&FWHMN[0], "SIM_FWHM", "FWHM (arcseconds)", "%4.2f", 0, 60, 0, 7.5);
165 IUFillNumberVector(&FWHMNP, FWHMN, 1, ActiveDeviceT[ACTIVE_FOCUSER].text, "FWHM", "FWHM", OPTIONS_TAB, IP_RO, 60, IPS_IDLE);
166
167 // Cooler
168 IUFillSwitch(&CoolerS[INDI_ENABLED], "COOLER_ON", "ON", ISS_OFF);
169 IUFillSwitch(&CoolerS[INDI_DISABLED], "COOLER_OFF", "OFF", ISS_ON);
170 IUFillSwitchVector(&CoolerSP, CoolerS, 2, getDeviceName(), "CCD_COOLER", "Cooler", MAIN_CONTROL_TAB, IP_WO,
171 ISR_1OFMANY, 0, IPS_IDLE);
172
173 // Gain
174 IUFillNumber(&GainN[0], "GAIN", "value", "%.f", 0, 100, 10, 50);
175 IUFillNumberVector(&GainNP, GainN, 1, getDeviceName(), "CCD_GAIN", "Gain", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
176
177 // Offset
178 IUFillNumber(&OffsetN[0], "OFFSET", "value", "%.f", 0, 6000, 500, 0);
179 IUFillNumberVector(&OffsetNP, OffsetN, 1, getDeviceName(), "CCD_OFFSET", "Offset", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
180
181 // Directory to read images from. This is useful to test real images captured by camera
182 // For each capture, one file is read (sorted by name) and is sent to client.
183 IUFillText(&DirectoryT[0], "LOCATION", "Location", getenv("HOME"));
184 IUFillTextVector(&DirectoryTP, DirectoryT, 1, getDeviceName(), "CCD_DIRECTORY_LOCATION", "Directory", SIMULATOR_TAB, IP_RW,
185 60, IPS_IDLE);
186
187 // Toggle Directory Reading. If enabled. The simulator will just read images from the directory and not generate them.
188 IUFillSwitch(&DirectoryS[INDI_ENABLED], "INDI_ENABLED", "Enabled", ISS_OFF);
189 IUFillSwitch(&DirectoryS[INDI_DISABLED], "INDI_DISABLED", "Disabled", ISS_ON);
190 IUFillSwitchVector(&DirectorySP, DirectoryS, 2, getDeviceName(), "CCD_DIRECTORY_TOGGLE", "Use Dir.", SIMULATOR_TAB, IP_RW,
191 ISR_1OFMANY, 60, IPS_IDLE);
192
193 #ifdef USE_EQUATORIAL_PE
194 IDSnoopDevice(ActiveDeviceT[0].text, "EQUATORIAL_PE");
195 #else
196 IDSnoopDevice(ActiveDeviceT[ACTIVE_TELESCOPE].text, "EQUATORIAL_EOD_COORD");
197 #endif
198
199
200 IDSnoopDevice(ActiveDeviceT[ACTIVE_FOCUSER].text, "FWHM");
201
202 uint32_t cap = 0;
203
204 cap |= CCD_CAN_ABORT;
205 cap |= CCD_CAN_BIN;
206 cap |= CCD_CAN_SUBFRAME;
207 cap |= CCD_HAS_COOLER;
208 cap |= CCD_HAS_GUIDE_HEAD;
209 cap |= CCD_HAS_SHUTTER;
210 cap |= CCD_HAS_ST4_PORT;
211 cap |= CCD_HAS_STREAMING;
212 cap |= CCD_HAS_DSP;
213
214 #ifdef HAVE_WEBSOCKET
215 cap |= CCD_HAS_WEB_SOCKET;
216 #endif
217
218 SetCCDCapability(cap);
219
220 // This should be called after the initial SetCCDCapability (above)
221 // as it modifies the capabilities.
222 setBayerEnabled(m_SimulateBayer);
223
224 INDI::FilterInterface::initProperties(FILTER_TAB);
225
226 FilterSlotN[0].min = 1;
227 FilterSlotN[0].max = 8;
228
229 addDebugControl();
230
231 setDriverInterface(getDriverInterface() | FILTER_INTERFACE);
232
233 // Make Guide Scope ON by default
234 TelescopeTypeS[TELESCOPE_PRIMARY].s = ISS_OFF;
235 TelescopeTypeS[TELESCOPE_GUIDE].s = ISS_ON;
236
237 return true;
238 }
239
setBayerEnabled(bool onOff)240 void CCDSim::setBayerEnabled(bool onOff)
241 {
242 if (onOff)
243 {
244 SetCCDCapability(GetCCDCapability() | CCD_HAS_BAYER);
245 IUSaveText(&BayerT[0], "0");
246 IUSaveText(&BayerT[1], "0");
247 IUSaveText(&BayerT[2], "RGGB");
248 }
249 else
250 {
251 SetCCDCapability(GetCCDCapability() & ~CCD_HAS_BAYER);
252 }
253 }
254
ISGetProperties(const char * dev)255 void CCDSim::ISGetProperties(const char * dev)
256 {
257 INDI::CCD::ISGetProperties(dev);
258
259 defineProperty(&SimulatorSettingsNP);
260 defineProperty(&EqPENP);
261 defineProperty(&FocusSimulationNP);
262 defineProperty(&SimulateBayerSP);
263 defineProperty(&CrashSP);
264 }
265
updateProperties()266 bool CCDSim::updateProperties()
267 {
268 INDI::CCD::updateProperties();
269
270 if (isConnected())
271 {
272 if (HasCooler())
273 defineProperty(&CoolerSP);
274
275 defineProperty(&GainNP);
276 defineProperty(&OffsetNP);
277
278 defineProperty(&DirectoryTP);
279 defineProperty(&DirectorySP);
280
281 setupParameters();
282
283 if (HasGuideHead())
284 {
285 SetGuiderParams(500, 290, 16, 9.8, 12.6);
286 GuideCCD.setFrameBufferSize(GuideCCD.getXRes() * GuideCCD.getYRes() * 2);
287 }
288
289 // Define the Filter Slot and name properties
290 INDI::FilterInterface::updateProperties();
291 }
292 else
293 {
294 if (HasCooler())
295 deleteProperty(CoolerSP.name);
296
297 deleteProperty(GainNP.name);
298 deleteProperty(OffsetNP.name);
299 deleteProperty(DirectoryTP.name);
300 deleteProperty(DirectorySP.name);
301
302 INDI::FilterInterface::updateProperties();
303 }
304
305 return true;
306 }
307
SetTemperature(double temperature)308 int CCDSim::SetTemperature(double temperature)
309 {
310 TemperatureRequest = temperature;
311 if (fabs(temperature - TemperatureN[0].value) < 0.1)
312 {
313 TemperatureN[0].value = temperature;
314 return 1;
315 }
316
317 CoolerS[0].s = ISS_ON;
318 CoolerS[1].s = ISS_OFF;
319 CoolerSP.s = IPS_BUSY;
320 IDSetSwitch(&CoolerSP, nullptr);
321 return 0;
322 }
323
StartExposure(float duration)324 bool CCDSim::StartExposure(float duration)
325 {
326 if (std::isnan(RA) && std::isnan(Dec))
327 {
328 LOG_ERROR("Telescope coordinates missing. Make sure telescope is connected and its name is set in CCD Options.");
329 return false;
330 }
331
332 // for the simulator, we can just draw the frame now
333 // and it will get returned at the right time
334 // by the timer routines
335 AbortPrimaryFrame = false;
336 ExposureRequest = duration;
337
338 PrimaryCCD.setExposureDuration(duration);
339 gettimeofday(&ExpStart, nullptr);
340 // Leave the proper time showing for the draw routines
341 if (DirectoryS[INDI_ENABLED].s == ISS_ON)
342 {
343 if (loadNextImage() == false)
344 return false;
345 }
346 else
347 DrawCcdFrame(&PrimaryCCD);
348 // Now compress the actual wait time
349 ExposureRequest = duration * m_TimeFactor;
350 InExposure = true;
351
352 return true;
353 }
354
StartGuideExposure(float n)355 bool CCDSim::StartGuideExposure(float n)
356 {
357 GuideExposureRequest = n;
358 AbortGuideFrame = false;
359 GuideCCD.setExposureDuration(n);
360 DrawCcdFrame(&GuideCCD);
361 gettimeofday(&GuideExpStart, nullptr);
362 InGuideExposure = true;
363 return true;
364 }
365
AbortExposure()366 bool CCDSim::AbortExposure()
367 {
368 if (!InExposure)
369 return true;
370
371 AbortPrimaryFrame = true;
372
373 return true;
374 }
375
AbortGuideExposure()376 bool CCDSim::AbortGuideExposure()
377 {
378 //IDLog("Enter AbortGuideExposure\n");
379 if (!InGuideExposure)
380 return true; // no need to abort if we aren't doing one
381 AbortGuideFrame = true;
382 return true;
383 }
384
CalcTimeLeft(timeval start,float req)385 float CCDSim::CalcTimeLeft(timeval start, float req)
386 {
387 double timesince;
388 double timeleft;
389 struct timeval now
390 {
391 0, 0
392 };
393 gettimeofday(&now, nullptr);
394
395 timesince =
396 (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) - (double)(start.tv_sec * 1000.0 + start.tv_usec / 1000);
397 timesince = timesince / 1000;
398 timeleft = req - timesince;
399 return timeleft;
400 }
401
TimerHit()402 void CCDSim::TimerHit()
403 {
404 uint32_t nextTimer = getCurrentPollingPeriod();
405
406 // No need to reset timer if we are not connected anymore
407 if (!isConnected())
408 return;
409
410 if (InExposure)
411 {
412 if (AbortPrimaryFrame)
413 {
414 InExposure = false;
415 AbortPrimaryFrame = false;
416 }
417 else
418 {
419 float timeleft;
420 timeleft = CalcTimeLeft(ExpStart, ExposureRequest);
421
422 //IDLog("CCD Exposure left: %g - Requset: %g\n", timeleft, ExposureRequest);
423 if (timeleft < 0)
424 timeleft = 0;
425
426 PrimaryCCD.setExposureLeft(timeleft);
427
428 if (timeleft < 1.0)
429 {
430 if (timeleft <= 0.001)
431 {
432 InExposure = false;
433 // We don't bin for raw images.
434 if (DirectoryS[INDI_DISABLED].s == ISS_ON)
435 PrimaryCCD.binFrame();
436 ExposureComplete(&PrimaryCCD);
437 }
438 else
439 {
440 // set a shorter timer
441 nextTimer = timeleft * 1000;
442 }
443 }
444 }
445 }
446
447 if (InGuideExposure)
448 {
449 double timeleft = CalcTimeLeft(GuideExpStart, GuideExposureRequest);
450 if (timeleft < 0)
451 timeleft = 0;
452
453 GuideCCD.setExposureLeft(timeleft);
454
455 if (timeleft < 1.0)
456 {
457 if (timeleft <= 0.001)
458 {
459 InGuideExposure = false;
460 if (!AbortGuideFrame)
461 {
462 GuideCCD.binFrame();
463 ExposureComplete(&GuideCCD);
464 if (InGuideExposure)
465 {
466 // the call to complete triggered another exposure
467 timeleft = CalcTimeLeft(GuideExpStart, GuideExposureRequest);
468 if (timeleft < 1.0)
469 {
470 nextTimer = timeleft * 1000;
471 }
472 }
473 }
474 else
475 {
476 //IDLog("Not sending guide frame cuz of abort\n");
477 }
478 AbortGuideFrame = false;
479 }
480 else
481 {
482 nextTimer = timeleft * 1000; // set a shorter timer
483 }
484 }
485 }
486
487 if (TemperatureNP.s == IPS_BUSY)
488 {
489 if (TemperatureRequest < TemperatureN[0].value)
490 TemperatureN[0].value = std::max(TemperatureRequest, TemperatureN[0].value - 0.5);
491 else
492 TemperatureN[0].value = std::min(TemperatureRequest, TemperatureN[0].value + 0.5);
493
494 IDSetNumber(&TemperatureNP, nullptr);
495
496 // Above 20, cooler is off
497 if (TemperatureN[0].value >= 20)
498 {
499 CoolerS[0].s = ISS_OFF;
500 CoolerS[1].s = ISS_ON;
501 CoolerSP.s = IPS_IDLE;
502 IDSetSwitch(&CoolerSP, nullptr);
503 }
504 }
505
506
507 SetTimer(nextTimer);
508 }
509
flux(double mag) const510 double CCDSim::flux(double mag) const
511 {
512 // The limiting magnitude provides zero ADU whatever the exposure
513 // The saturation magnitude provides max ADU in one second
514 double const z = m_LimitingMag;
515 double const k = 2.5 * log10(m_MaxVal) / (m_LimitingMag - m_SaturationMag);
516 return pow(10, (z - mag) * k / 2.5);
517 }
518
DrawCcdFrame(INDI::CCDChip * targetChip)519 int CCDSim::DrawCcdFrame(INDI::CCDChip * targetChip)
520 {
521 // CCD frame is 16 bit data
522 float exposure_time;
523 float targetFocalLength;
524
525 uint16_t * ptr = reinterpret_cast<uint16_t *>(targetChip->getFrameBuffer());
526
527 if (targetChip->getXRes() == 500)
528 exposure_time = GuideExposureRequest * 4;
529 else if (Streamer->isStreaming())
530 exposure_time = (ExposureRequest < 1) ? (ExposureRequest * 100) : ExposureRequest * 2;
531 else
532 exposure_time = ExposureRequest;
533
534 if (GainN[0].value > 50)
535 exposure_time *= sqrt(GainN[0].value - 50);
536 else if (GainN[0].value < 50)
537 exposure_time /= sqrt(50 - GainN[0].value);
538
539 if (TelescopeTypeS[TELESCOPE_PRIMARY].s == ISS_ON)
540 targetFocalLength = primaryFocalLength;
541 else
542 targetFocalLength = guiderFocalLength;
543
544 if (ShowStarField)
545 {
546 float PEOffset {0};
547 float decDrift {0};
548 // telescope ra in degrees
549 double rad {0};
550 // telescope ra in radians
551 double rar {0};
552 // telescope dec in radians;
553 double decr {0};
554 int nwidth = 0, nheight = 0;
555
556 if (m_PEPeriod > 0)
557 {
558 double timesince;
559 time_t now;
560 time(&now);
561
562 // Lets figure out where we are on the pe curve
563 timesince = difftime(now, RunStart);
564 // This is our spot in the curve
565 double PESpot = timesince / m_PEPeriod;
566 // Now convert to radians
567 PESpot = PESpot * 2.0 * 3.14159;
568
569 PEOffset = m_PEMax * std::sin(PESpot);
570 // convert to degrees
571 PEOffset = PEOffset / 3600;
572 }
573
574 // Spin up a set of plate constants that will relate
575 // ra/dec of stars, to our fictitious ccd layout
576
577 // to account for various rotations etc
578 // we should spin up some plate constants here
579 // then we can use these constants to rotate and offset
580 // the standard co-ordinates on each star for drawing
581 // a ccd frame;
582 double pa, pb, pc, pd, pe, pf;
583 // Pixels per radian
584 double pprx, ppry;
585 // Scale in arcsecs per pixel
586 double Scalex;
587 double Scaley;
588 // CCD width in pixels
589 double ccdW = targetChip->getXRes();
590
591 // Pixels per radian
592 pprx = targetFocalLength / targetChip->getPixelSizeX() * 1000;
593 ppry = targetFocalLength / targetChip->getPixelSizeY() * 1000;
594
595 // we do a simple scale for x and y locations
596 // based on the focal length and pixel size
597 // focal length in mm, pixels in microns
598 // JM: 2015-03-17: Using a simpler formula, Scalex and Scaley are in arcsecs/pixel
599 Scalex = (targetChip->getPixelSizeX() / targetFocalLength) * 206.3;
600 Scaley = (targetChip->getPixelSizeY() / targetFocalLength) * 206.3;
601
602 #if 0
603 DEBUGF(
604 INDI::Logger::DBG_DEBUG,
605 "pprx: %g pixels per radian ppry: %g pixels per radian ScaleX: %g arcsecs/pixel ScaleY: %g arcsecs/pixel",
606 pprx, ppry, Scalex, Scaley);
607 #endif
608
609 double theta = 270;
610 if (!std::isnan(RotatorAngle))
611 theta += RotatorAngle;
612 if (pierSide == 1)
613 theta -= 180; // rotate 180 if on East
614 theta = range360(theta);
615
616 // JM: 2015-03-17: Next we do a rotation assuming CW for angle theta
617 pa = pprx * cos(theta * M_PI / 180.0);
618 pb = ppry * sin(theta * M_PI / 180.0);
619
620 pd = pprx * -sin(theta * M_PI / 180.0);
621 pe = ppry * cos(theta * M_PI / 180.0);
622
623 nwidth = targetChip->getXRes();
624 pc = nwidth / 2;
625
626 nheight = targetChip->getYRes();
627 pf = nheight / 2;
628
629 ImageScalex = Scalex;
630 ImageScaley = Scaley;
631
632 #ifdef USE_EQUATORIAL_PE
633 if (!usePE)
634 {
635 #endif
636 currentRA = RA;
637 currentDE = Dec;
638
639 INDI::IEquatorialCoordinates epochPos { 0, 0 }, J2000Pos { 0, 0 };
640
641 double jd = ln_get_julian_from_sys();
642
643 epochPos.rightascension = currentRA;
644 epochPos.declination = currentDE;
645
646 // Convert from JNow to J2000
647 INDI::ObservedToJ2000(&epochPos, jd, &J2000Pos);
648
649 currentRA = J2000Pos.rightascension;
650 currentDE = J2000Pos.declination;
651
652 //LOGF_DEBUG("DrawCcdFrame JNow %f, %f J2000 %f, %f", epochPos.ra, epochPos.dec, J2000Pos.ra, J2000Pos.dec);
653 //INDI::IEquatorialCoordinates jnpos;
654 //INDI::J2000toObserved(&J2000Pos, jd, &jnpos);
655 //LOGF_DEBUG("J2000toObserved JNow %f, %f J2000 %f, %f", jnpos.ra, jnpos.dec, J2000Pos.ra, J2000Pos.dec);
656
657 currentDE += guideNSOffset;
658 currentRA += guideWEOffset;
659 #ifdef USE_EQUATORIAL_PE
660 }
661 #endif
662
663 // calc this now, we will use it a lot later
664 rad = currentRA * 15.0;
665 rar = rad * 0.0174532925;
666 // offsetting the dec by the guide head offset
667 float cameradec;
668 cameradec = currentDE + m_OAGOffset / 60;
669 decr = cameradec * 0.0174532925;
670
671 decDrift = (m_PolarDrift * m_PolarError * cos(decr)) / 3.81;
672
673 // Add declination drift, if any.
674 decr += decDrift / 3600.0 * 0.0174532925;
675
676 // now lets calculate the radius we need to fetch
677 double radius = sqrt((Scalex * Scalex * targetChip->getXRes() / 2.0 * targetChip->getXRes() / 2.0) +
678 (Scaley * Scaley * targetChip->getYRes() / 2.0 * targetChip->getYRes() / 2.0));
679 // we have radius in arcseconds now
680 // convert to arcminutes
681 radius = radius / 60;
682 #if 0
683 LOGF_DEBUG("Lookup radius %4.2f", radius);
684 #endif
685
686 // A saturationmag star saturates in one second
687 // and a limitingmag produces a one adu level in one second
688 // solve for zero point and system gain
689
690 //k = (m_SaturationMag - m_LimitingMag) / ((-2.5 * log(m_MaxVal)) - (-2.5 * log(1.0 / 2.0)));
691 //z = m_SaturationMag - k * (-2.5 * log(m_MaxVal));
692
693 // Should probably do some math here to figure out the dimmest
694 // star we can see on this exposure
695 // and only fetch to that magnitude
696 // for now, just use the limiting mag number with some room to spare
697 double lookuplimit = m_LimitingMag;
698
699 if (radius > 60)
700 lookuplimit = 11;
701
702 // if this is a light frame, we need a star field drawn
703 INDI::CCDChip::CCD_FRAME ftype = targetChip->getFrameType();
704
705 std::unique_lock<std::mutex> guard(ccdBufferLock);
706
707 // Start by clearing the frame buffer
708 memset(targetChip->getFrameBuffer(), 0, targetChip->getFrameBufferSize());
709
710 if (ftype == INDI::CCDChip::LIGHT_FRAME)
711 {
712 AutoCNumeric locale;
713 char gsccmd[250];
714 FILE * pp;
715 int drawn = 0;
716
717 sprintf(gsccmd, "gsc -c %8.6f %+8.6f -r %4.1f -m 0 %4.2f -n 3000",
718 range360(rad + PEOffset),
719 rangeDec(cameradec),
720 radius,
721 lookuplimit);
722
723 pp = popen(gsccmd, "r");
724 if (pp != nullptr)
725 {
726 char line[256];
727 int stars = 0;
728 int lines = 0;
729
730 while (fgets(line, 256, pp) != nullptr)
731 {
732 // ok, lets parse this line for specifcs we want
733 char id[20];
734 char plate[6];
735 char ob[6];
736 float mag;
737 float mage;
738 float ra;
739 float dec;
740 float pose;
741 int band;
742 float dist;
743 int dir;
744 int c;
745
746 int rc = sscanf(line, "%10s %f %f %f %f %f %d %d %4s %2s %f %d", id, &ra, &dec, &pose, &mag, &mage,
747 &band, &c, plate, ob, &dist, &dir);
748 if (rc == 12)
749 {
750 lines++;
751 stars++;
752
753 // Convert the ra/dec to standard co-ordinates
754 double sx; // standard co-ords
755 double sy; //
756 double srar; // star ra in radians
757 double sdecr; // star dec in radians;
758 double ccdx;
759 double ccdy;
760
761 srar = ra * 0.0174532925;
762 sdecr = dec * 0.0174532925;
763
764 // Handbook of astronomical image processing
765 // page 253
766 // equations 9.1 and 9.2
767 // convert ra/dec to standard co-ordinates
768
769 sx = cos(sdecr) * sin(srar - rar) /
770 (cos(decr) * cos(sdecr) * cos(srar - rar) + sin(decr) * sin(sdecr));
771 sy = (sin(decr) * cos(sdecr) * cos(srar - rar) - cos(decr) * sin(sdecr)) /
772 (cos(decr) * cos(sdecr) * cos(srar - rar) + sin(decr) * sin(sdecr));
773
774 // now convert to pixels
775 ccdx = pa * sx + pb * sy + pc;
776 ccdy = pd * sx + pe * sy + pf;
777
778 // Invert horizontally
779 ccdx = ccdW - ccdx;
780
781 rc = DrawImageStar(targetChip, mag, ccdx, ccdy, exposure_time);
782 drawn += rc;
783 #ifdef __DEV__
784 if (rc == 1)
785 {
786 LOGF_DEBUG("star %s scope %6.4f %6.4f star %6.4f %6.4f ccd %6.2f %6.2f", id, rad, decPE, ra, dec, ccdx, ccdy);
787 LOGF_DEBUG("star %s ccd %6.2f %6.2f", id, ccdx, ccdy);
788 }
789 #endif
790 }
791 }
792 pclose(pp);
793 }
794 else
795 {
796 LOG_ERROR("Error looking up stars, is gsc installed with appropriate environment variables set ??");
797 }
798 if (drawn == 0)
799 {
800 LOG_ERROR("Got no stars, is gsc installed with appropriate environment variables set ??");
801 }
802 }
803
804 // now we need to add background sky glow, with vignetting
805 // this is essentially the same math as drawing a dim star with
806 // fwhm equivalent to the full field of view
807
808 if (ftype == INDI::CCDChip::LIGHT_FRAME || ftype == INDI::CCDChip::FLAT_FRAME)
809 {
810 // calculate flux from our zero point and gain values
811 float glow = m_SkyGlow;
812
813 if (ftype == INDI::CCDChip::FLAT_FRAME)
814 {
815 // Assume flats are done with a diffuser
816 // in broad daylight, so, the sky magnitude
817 // is much brighter than at night
818 glow = m_SkyGlow / 10;
819 }
820
821 //fprintf(stderr,"Using glow %4.2f\n",glow);
822
823 // Flux represents one second, scale up linearly for exposure time
824 float const skyflux = flux(glow) * exposure_time;
825
826 uint16_t * pt = reinterpret_cast<uint16_t *>(targetChip->getFrameBuffer());
827
828 nheight = targetChip->getSubH();
829 nwidth = targetChip->getSubW();
830
831 for (int y = 0; y < nheight; y++)
832 {
833 float const sy = nheight / 2 - y;
834
835 for (int x = 0; x < nwidth; x++)
836 {
837 float const sx = nwidth / 2 - x;
838
839 // Vignetting parameter in arcsec
840 float const vig = std::min(nwidth, nheight) * ImageScalex;
841
842 // Squared distance to center in arcsec (need to make this account for actual pixel size)
843 float const dc2 = sx * sx * ImageScalex * ImageScalex + sy * sy * ImageScaley * ImageScaley;
844
845 // Gaussian falloff to the edges of the frame
846 float const fa = exp(-2.0 * 0.7 * dc2 / (vig * vig));
847
848 // Get the current value of the pixel, add the sky glow and scale for vignetting
849 float fp = (pt[0] + skyflux) * fa;
850
851 // Clamp to limits, store minmax
852 if (fp > m_MaxVal) fp = m_MaxVal;
853 if (fp < pt[0]) fp = pt[0];
854 if (fp > maxpix) maxpix = fp;
855 if (fp < minpix) minpix = fp;
856
857 // And put it back
858 pt[0] = fp;
859 pt++;
860 }
861 }
862 }
863
864 // Now we add some bias and read noise
865 int subX = targetChip->getSubX();
866 int subY = targetChip->getSubY();
867 int subW = targetChip->getSubW() + subX;
868 int subH = targetChip->getSubH() + subY;
869
870 if (m_MaxNoise > 0)
871 {
872 for (int x = subX; x < subW; x++)
873 {
874 for (int y = subY; y < subH; y++)
875 {
876 int noise;
877
878 noise = random();
879 noise = noise % m_MaxNoise; //
880
881 AddToPixel(targetChip, x, y, m_Bias + noise);
882 }
883 }
884 }
885 }
886 else
887 {
888 testvalue++;
889 if (testvalue > 255)
890 testvalue = 0;
891 uint16_t val = testvalue;
892
893 int nbuf = targetChip->getSubW() * targetChip->getSubH();
894
895 for (int x = 0; x < nbuf; x++)
896 {
897 *ptr = val++;
898 ptr++;
899 }
900 }
901 return 0;
902 }
903
DrawImageStar(INDI::CCDChip * targetChip,float mag,float x,float y,float exposure_time)904 int CCDSim::DrawImageStar(INDI::CCDChip * targetChip, float mag, float x, float y, float exposure_time)
905 {
906 int sx, sy;
907 int drew = 0;
908 //int boxsizex = 5;
909 int boxsizey = 5;
910 float flux;
911
912 int subX = targetChip->getSubX();
913 int subY = targetChip->getSubY();
914 int subW = targetChip->getSubW() + subX;
915 int subH = targetChip->getSubH() + subY;
916
917 if ((x < subX) || (x > subW || (y < subY) || (y > subH)))
918 {
919 // this star is not on the ccd frame anyways
920 return 0;
921 }
922
923 // calculate flux from our zero point and gain values
924 flux = this->flux(mag);
925
926 // ok, flux represents one second now
927 // scale up linearly for exposure time
928 flux = flux * exposure_time;
929
930 float qx;
931 // we need a box size that gives a radius at least 3 times fwhm
932 qx = seeing / ImageScalex;
933 qx = qx * 3;
934 //boxsizex = (int)qx;
935 //boxsizex++;
936 qx = seeing / ImageScaley;
937 qx = qx * 3;
938 boxsizey = static_cast<int>(qx);
939 boxsizey++;
940
941 //IDLog("BoxSize %d %d\n",boxsizex,boxsizey);
942
943 for (sy = -boxsizey; sy <= boxsizey; sy++)
944 {
945 for (sx = -boxsizey; sx <= boxsizey; sx++)
946 {
947 int rc;
948
949 // Squared distance to center in arcsec (need to make this account for actual pixel size)
950 float const dc2 = sx * sx * ImageScalex * ImageScalex + sy * sy * ImageScaley * ImageScaley;
951
952 // Use a gaussian of unitary integral, scale it with the source flux
953 // f(x) = 1/(sqrt(2*pi)*sigma) * exp( -x² / (2*sigma²) )
954 // FWHM = 2*sqrt(2*log(2))*sigma => sigma = seeing/(2*sqrt(2*log(2)))
955 float const sigma = seeing / ( 2 * sqrt(2 * log(2)));
956 float const fa = 1 / (sigma * sqrt(2 * 3.1416)) * exp( -dc2 / (2 * sigma * sigma));
957
958 // The source contribution is the gaussian value, stretched by seeing/FWHM
959 float fp = fa * flux;
960
961 if (fp < 0)
962 fp = 0;
963
964 rc = AddToPixel(targetChip, x + sx, y + sy, fp);
965 if (rc != 0)
966 drew = 1;
967 }
968 }
969 return drew;
970 }
971
AddToPixel(INDI::CCDChip * targetChip,int x,int y,int val)972 int CCDSim::AddToPixel(INDI::CCDChip * targetChip, int x, int y, int val)
973 {
974 int nwidth = targetChip->getSubW();
975 int nheight = targetChip->getSubH();
976
977 x -= targetChip->getSubX();
978 y -= targetChip->getSubY();
979
980 int drew = 0;
981 if (x >= 0)
982 {
983 if (x < nwidth)
984 {
985 if (y >= 0)
986 {
987 if (y < nheight)
988 {
989 unsigned short * pt;
990 int newval;
991 drew++;
992
993 pt = reinterpret_cast<uint16_t *>(targetChip->getFrameBuffer());
994
995 pt += (y * nwidth);
996 pt += x;
997 newval = pt[0];
998 newval += val;
999 if (newval > m_MaxVal)
1000 newval = m_MaxVal;
1001 if (newval > maxpix)
1002 maxpix = newval;
1003 if (newval < minpix)
1004 minpix = newval;
1005 pt[0] = newval;
1006 }
1007 }
1008 }
1009 }
1010 return drew;
1011 }
1012
GuideNorth(uint32_t v)1013 IPState CCDSim::GuideNorth(uint32_t v)
1014 {
1015 guideNSOffset += v / 1000.0 * GuideRate / 3600;
1016 return IPS_OK;
1017 }
1018
GuideSouth(uint32_t v)1019 IPState CCDSim::GuideSouth(uint32_t v)
1020 {
1021 guideNSOffset += v / -1000.0 * GuideRate / 3600;
1022 return IPS_OK;
1023 }
1024
GuideEast(uint32_t v)1025 IPState CCDSim::GuideEast(uint32_t v)
1026 {
1027 float c = v / 1000.0 * GuideRate;
1028 c = c / 3600.0 / 15.0;
1029 c = c / (cos(currentDE * 0.0174532925));
1030
1031 guideWEOffset += c;
1032
1033 return IPS_OK;
1034 }
1035
GuideWest(uint32_t v)1036 IPState CCDSim::GuideWest(uint32_t v)
1037 {
1038 float c = v / -1000.0 * GuideRate;
1039 c = c / 3600.0 / 15.0;
1040 c = c / (cos(currentDE * 0.0174532925));
1041
1042 guideWEOffset += c;
1043
1044 return IPS_OK;
1045 }
1046
ISNewText(const char * dev,const char * name,char * texts[],char * names[],int n)1047 bool CCDSim::ISNewText(const char * dev, const char * name, char * texts[], char * names[], int n)
1048 {
1049 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
1050 {
1051 // This is for our device
1052 // Now lets see if it's something we process here
1053 if (strcmp(name, FilterNameTP->name) == 0)
1054 {
1055 INDI::FilterInterface::processText(dev, name, texts, names, n);
1056 return true;
1057 }
1058 else if (!strcmp(DirectoryTP.name, name))
1059 {
1060 IUUpdateText(&DirectoryTP, texts, names, n);
1061 DirectoryTP.s = IPS_OK;
1062 IDSetText(&DirectoryTP, nullptr);
1063 return true;
1064 }
1065
1066 }
1067
1068 return INDI::CCD::ISNewText(dev, name, texts, names, n);
1069 }
1070
ISNewNumber(const char * dev,const char * name,double values[],char * names[],int n)1071 bool CCDSim::ISNewNumber(const char * dev, const char * name, double values[], char * names[], int n)
1072 {
1073 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
1074 {
1075
1076 if (!strcmp(name, GainNP.name))
1077 {
1078 IUUpdateNumber(&GainNP, values, names, n);
1079 GainNP.s = IPS_OK;
1080 IDSetNumber(&GainNP, nullptr);
1081 return true;
1082 }
1083 if (!strcmp(name, OffsetNP.name))
1084 {
1085 IUUpdateNumber(&OffsetNP, values, names, n);
1086 OffsetNP.s = IPS_OK;
1087 IDSetNumber(&OffsetNP, nullptr);
1088 m_Bias = OffsetN[0].value;
1089 return true;
1090 }
1091 else if (!strcmp(name, SimulatorSettingsNP.name))
1092 {
1093 IUUpdateNumber(&SimulatorSettingsNP, values, names, n);
1094 SimulatorSettingsNP.s = IPS_OK;
1095
1096 // Reset our parameters now
1097 setupParameters();
1098 IDSetNumber(&SimulatorSettingsNP, nullptr);
1099 return true;
1100 }
1101 // Record PE EQ to simulate different position in the sky than actual mount coordinate
1102 // This can be useful to simulate Periodic Error or cone error or any arbitrary error.
1103 else if (!strcmp(name, EqPENP.name))
1104 {
1105 IUUpdateNumber(&EqPENP, values, names, n);
1106 EqPENP.s = IPS_OK;
1107
1108 INDI::IEquatorialCoordinates epochPos { 0, 0 }, J2000Pos { 0, 0 };
1109 epochPos.rightascension = EqPEN[AXIS_RA].value;
1110 epochPos.declination = EqPEN[AXIS_DE].value;
1111
1112 RA = EqPEN[AXIS_RA].value;
1113 Dec = EqPEN[AXIS_DE].value;
1114
1115 INDI::ObservedToJ2000(&epochPos, ln_get_julian_from_sys(), &J2000Pos);
1116 currentRA = J2000Pos.rightascension;
1117 currentDE = J2000Pos.declination;
1118 usePE = true;
1119
1120 IDSetNumber(&EqPENP, nullptr);
1121 return true;
1122 }
1123 else if (!strcmp(name, FilterSlotNP.name))
1124 {
1125 INDI::FilterInterface::processNumber(dev, name, values, names, n);
1126 return true;
1127 }
1128 else if (!strcmp(name, FocusSimulationNP.name))
1129 {
1130 // update focus simulation parameters
1131 IUUpdateNumber(&FocusSimulationNP, values, names, n);
1132 FocusSimulationNP.s = IPS_OK;
1133 IDSetNumber(&FocusSimulationNP, nullptr);
1134 }
1135 }
1136
1137 return INDI::CCD::ISNewNumber(dev, name, values, names, n);
1138 }
1139
ISNewSwitch(const char * dev,const char * name,ISState * states,char * names[],int n)1140 bool CCDSim::ISNewSwitch(const char * dev, const char * name, ISState * states, char * names[], int n)
1141 {
1142 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
1143 {
1144 // Simulate RGB
1145 if (!strcmp(name, SimulateBayerSP.name))
1146 {
1147 IUUpdateSwitch(&SimulateBayerSP, states, names, n);
1148 int index = IUFindOnSwitchIndex(&SimulateBayerSP);
1149 if (index == -1)
1150 {
1151 SimulateBayerSP.s = IPS_ALERT;
1152 LOG_INFO("Cannot determine whether RGB simulation should be switched on or off.");
1153 IDSetSwitch(&SimulateBayerSP, nullptr);
1154 return false;
1155 }
1156
1157 m_SimulateBayer = index == 0;
1158 setBayerEnabled(m_SimulateBayer);
1159
1160 SimulateBayerS[INDI_ENABLED].s = m_SimulateBayer ? ISS_ON : ISS_OFF;
1161 SimulateBayerS[INDI_DISABLED].s = m_SimulateBayer ? ISS_OFF : ISS_ON;
1162 SimulateBayerSP.s = IPS_OK;
1163 IDSetSwitch(&SimulateBayerSP, nullptr);
1164
1165 return true;
1166 }
1167 else if (strcmp(name, CoolerSP.name) == 0)
1168 {
1169 IUUpdateSwitch(&CoolerSP, states, names, n);
1170
1171 if (CoolerS[0].s == ISS_ON)
1172 CoolerSP.s = IPS_BUSY;
1173 else
1174 {
1175 CoolerSP.s = IPS_IDLE;
1176 m_TargetTemperature = 20;
1177 TemperatureNP.s = IPS_BUSY;
1178 m_TemperatureCheckTimer.start();
1179 m_TemperatureElapsedTimer.start();
1180 }
1181
1182 IDSetSwitch(&CoolerSP, nullptr);
1183
1184 return true;
1185 }
1186 else if (!strcmp(DirectorySP.name, name))
1187 {
1188 IUUpdateSwitch(&DirectorySP, states, names, n);
1189 m_AllFiles.clear();
1190 m_RemainingFiles.clear();
1191 if (DirectoryS[INDI_ENABLED].s == ISS_ON)
1192 {
1193 DIR* dirp = opendir(DirectoryT[0].text);
1194 struct dirent * dp;
1195 std::string d_dir = std::string(DirectoryT[0].text);
1196 if (DirectoryT[0].text[strlen(DirectoryT[0].text) - 1] != '/')
1197 d_dir += "/";
1198 while ((dp = readdir(dirp)) != NULL)
1199 {
1200
1201 // For now, just FITS.
1202 if (strstr(dp->d_name, ".fits"))
1203 m_AllFiles.push_back(d_dir + dp->d_name);
1204 }
1205 closedir(dirp);
1206
1207 if (m_AllFiles.empty())
1208 {
1209 IUResetSwitch(&DirectorySP);
1210 DirectoryS[INDI_DISABLED].s = ISS_ON;
1211 DirectorySP.s = IPS_ALERT;
1212 LOGF_ERROR("No FITS files found in directory %s", DirectoryT[0].text);
1213 IDSetSwitch(&DirectorySP, nullptr);
1214 }
1215 else
1216 {
1217 DirectorySP.s = IPS_OK;
1218 std::sort(m_AllFiles.begin(), m_AllFiles.end());
1219 m_RemainingFiles = m_AllFiles;
1220 LOGF_INFO("Directory-based images are enabled. Subsequent exposures will be loaded from directory %s", DirectoryT[0].text);
1221 }
1222 }
1223 else
1224 {
1225 m_RemainingFiles.clear();
1226 DirectorySP.s = IPS_OK;
1227 setBayerEnabled(SimulateBayerS[INDI_ENABLED].s == ISS_ON);
1228 LOG_INFO("Directory-based images are disabled.");
1229 }
1230 IDSetSwitch(&DirectorySP, nullptr);
1231 return true;
1232 }
1233 else if (strcmp(name, CrashSP.name) == 0)
1234 {
1235 abort();
1236 }
1237 }
1238
1239 // Nobody has claimed this, so, ignore it
1240 return INDI::CCD::ISNewSwitch(dev, name, states, names, n);
1241 }
1242
activeDevicesUpdated()1243 void CCDSim::activeDevicesUpdated()
1244 {
1245 #ifdef USE_EQUATORIAL_PE
1246 IDSnoopDevice(ActiveDeviceT[0].text, "EQUATORIAL_PE");
1247 #else
1248 IDSnoopDevice(ActiveDeviceT[ACTIVE_TELESCOPE].text, "EQUATORIAL_EOD_COORD");
1249 #endif
1250 IDSnoopDevice(ActiveDeviceT[ACTIVE_FOCUSER].text, "FWHM");
1251
1252 strncpy(FWHMNP.device, ActiveDeviceT[ACTIVE_FOCUSER].text, MAXINDIDEVICE);
1253 }
1254
ISSnoopDevice(XMLEle * root)1255 bool CCDSim::ISSnoopDevice(XMLEle * root)
1256 {
1257 if (IUSnoopNumber(root, &FWHMNP) == 0)
1258 {
1259 // we calculate the FWHM and do not snoop it from the focus simulator
1260 // seeing = FWHMNP.np[0].value;
1261 return true;
1262 }
1263
1264 XMLEle * ep = nullptr;
1265 const char * propName = findXMLAttValu(root, "name");
1266
1267 if (!strcmp(propName, "ABS_FOCUS_POSITION"))
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, "FOCUS_ABSOLUTE_POSITION"))
1274 {
1275 FocuserPos = atol(pcdataXMLEle(ep));
1276
1277 // calculate FWHM
1278 double focus = FocusSimulationN[0].value;
1279 double max = FocusSimulationN[1].value;
1280 double optimalFWHM = FocusSimulationN[2].value;
1281
1282 // limit to +/- 10
1283 double ticks = 20 * (FocuserPos - focus) / max;
1284
1285 seeing = 0.5625 * ticks * ticks + optimalFWHM;
1286 return true;
1287 }
1288 }
1289 }
1290 // We try to snoop EQPEC first, if not found, we snoop regular EQNP
1291 #ifdef USE_EQUATORIAL_PE
1292 const char * propName = findXMLAttValu(root, "name");
1293 if (!strcmp(propName, EqPENP.name))
1294 {
1295 XMLEle * ep = nullptr;
1296 int rc_ra = -1, rc_de = -1;
1297 double newra = 0, newdec = 0;
1298
1299 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
1300 {
1301 const char * elemName = findXMLAttValu(ep, "name");
1302
1303 if (!strcmp(elemName, "RA_PE"))
1304 rc_ra = f_scansexa(pcdataXMLEle(ep), &newra);
1305 else if (!strcmp(elemName, "DEC_PE"))
1306 rc_de = f_scansexa(pcdataXMLEle(ep), &newdec);
1307 }
1308
1309 if (rc_ra == 0 && rc_de == 0 && ((newra != raPE) || (newdec != decPE)))
1310 {
1311 INDI::IEquatorialCoordinates epochPos { 0, 0 }, J2000Pos { 0, 0 };
1312 epochPos.ra = newra * 15.0;
1313 epochPos.dec = newdec;
1314 ln_get_equ_prec2(&epochPos, ln_get_julian_from_sys(), JD2000, &J2000Pos);
1315 raPE = J2000Pos.ra / 15.0;
1316 decPE = J2000Pos.dec;
1317 usePE = true;
1318
1319 EqPEN[AXIS_RA].value = newra;
1320 EqPEN[AXIS_DE].value = newdec;
1321 IDSetNumber(&EqPENP, nullptr);
1322
1323 LOGF_DEBUG("raPE %g decPE %g Snooped raPE %g decPE %g", raPE, decPE, newra, newdec);
1324
1325 return true;
1326 }
1327 }
1328 #endif
1329
1330 return INDI::CCD::ISSnoopDevice(root);
1331 }
1332
saveConfigItems(FILE * fp)1333 bool CCDSim::saveConfigItems(FILE * fp)
1334 {
1335 // Save CCD Config
1336 INDI::CCD::saveConfigItems(fp);
1337
1338 // Save Filter Wheel Config
1339 INDI::FilterInterface::saveConfigItems(fp);
1340
1341 // Save CCD Simulator Config
1342 IUSaveConfigNumber(fp, &SimulatorSettingsNP);
1343
1344 // Gain
1345 IUSaveConfigNumber(fp, &GainNP);
1346 IUSaveConfigNumber(fp, &OffsetNP);
1347
1348 // Directory
1349 IUSaveConfigText(fp, &DirectoryTP);
1350
1351 // Bayer
1352 IUSaveConfigSwitch(fp, &SimulateBayerSP);
1353
1354 // Focus simulation
1355 IUSaveConfigNumber(fp, &FocusSimulationNP);
1356
1357 return true;
1358 }
1359
SelectFilter(int f)1360 bool CCDSim::SelectFilter(int f)
1361 {
1362 CurrentFilter = f;
1363 SelectFilterDone(f);
1364 return true;
1365 }
1366
QueryFilter()1367 int CCDSim::QueryFilter()
1368 {
1369 return CurrentFilter;
1370 }
1371
StartStreaming()1372 bool CCDSim::StartStreaming()
1373 {
1374 ExposureRequest = 1.0 / Streamer->getTargetExposure();
1375 pthread_mutex_lock(&condMutex);
1376 streamPredicate = 1;
1377 pthread_mutex_unlock(&condMutex);
1378 pthread_cond_signal(&cv);
1379
1380 return true;
1381 }
1382
StopStreaming()1383 bool CCDSim::StopStreaming()
1384 {
1385 pthread_mutex_lock(&condMutex);
1386 streamPredicate = 0;
1387 pthread_mutex_unlock(&condMutex);
1388 pthread_cond_signal(&cv);
1389
1390 return true;
1391 }
1392
UpdateCCDFrame(int x,int y,int w,int h)1393 bool CCDSim::UpdateCCDFrame(int x, int y, int w, int h)
1394 {
1395 long bin_width = w / PrimaryCCD.getBinX();
1396 long bin_height = h / PrimaryCCD.getBinY();
1397
1398 bin_width = bin_width - (bin_width % 2);
1399 bin_height = bin_height - (bin_height % 2);
1400
1401 Streamer->setSize(bin_width, bin_height);
1402
1403 return INDI::CCD::UpdateCCDFrame(x, y, w, h);
1404 }
1405
UpdateCCDBin(int hor,int ver)1406 bool CCDSim::UpdateCCDBin(int hor, int ver)
1407 {
1408 if (PrimaryCCD.getSubW() % hor != 0 || PrimaryCCD.getSubH() % ver != 0)
1409 {
1410 LOGF_ERROR("%dx%d binning is not supported.", hor, ver);
1411 return false;
1412 }
1413
1414 uint32_t bin_width = PrimaryCCD.getSubW() / hor;
1415 uint32_t bin_height = PrimaryCCD.getSubH() / ver;
1416 Streamer->setSize(bin_width, bin_height);
1417
1418 return INDI::CCD::UpdateCCDBin(hor, ver);
1419 }
1420
UpdateGuiderBin(int hor,int ver)1421 bool CCDSim::UpdateGuiderBin(int hor, int ver)
1422 {
1423 if (GuideCCD.getSubW() % hor != 0 || GuideCCD.getSubH() % ver != 0)
1424 {
1425 LOGF_ERROR("%dx%d binning is not supported.", hor, ver);
1426 return false;
1427 }
1428
1429 return INDI::CCD::UpdateGuiderBin(hor, ver);
1430 }
1431
streamVideoHelper(void * context)1432 void * CCDSim::streamVideoHelper(void * context)
1433 {
1434 return static_cast<CCDSim *>(context)->streamVideo();
1435 }
1436
streamVideo()1437 void * CCDSim::streamVideo()
1438 {
1439 auto start = std::chrono::high_resolution_clock::now();
1440
1441 while (true)
1442 {
1443 pthread_mutex_lock(&condMutex);
1444
1445 while (streamPredicate == 0)
1446 {
1447 pthread_cond_wait(&cv, &condMutex);
1448 ExposureRequest = Streamer->getTargetExposure();
1449 }
1450
1451 if (terminateThread)
1452 break;
1453
1454 // release condMutex
1455 pthread_mutex_unlock(&condMutex);
1456
1457
1458 // 16 bit
1459 DrawCcdFrame(&PrimaryCCD);
1460
1461 PrimaryCCD.binFrame();
1462
1463 auto finish = std::chrono::high_resolution_clock::now();
1464 std::chrono::duration<double> elapsed = finish - start;
1465
1466 if (elapsed.count() < ExposureRequest)
1467 usleep(fabs(ExposureRequest - elapsed.count()) * 1e6);
1468
1469 uint32_t size = PrimaryCCD.getFrameBufferSize() / (PrimaryCCD.getBinX() * PrimaryCCD.getBinY());
1470 Streamer->newFrame(PrimaryCCD.getFrameBuffer(), size);
1471
1472 start = std::chrono::high_resolution_clock::now();
1473 }
1474
1475 pthread_mutex_unlock(&condMutex);
1476 return nullptr;
1477 }
1478
addFITSKeywords(fitsfile * fptr,INDI::CCDChip * targetChip)1479 void CCDSim::addFITSKeywords(fitsfile *fptr, INDI::CCDChip *targetChip)
1480 {
1481 INDI::CCD::addFITSKeywords(fptr, targetChip);
1482
1483 int status = 0;
1484 fits_update_key_dbl(fptr, "Gain", GainN[0].value, 3, "Gain", &status);
1485 }
1486
loadNextImage()1487 bool CCDSim::loadNextImage()
1488 {
1489 if (m_RemainingFiles.empty())
1490 m_RemainingFiles = m_AllFiles;
1491 const std::string filename = m_RemainingFiles[0];
1492 m_RemainingFiles.pop_front();
1493 char comment[512] = {0}, bayer_pattern[16] = {0};
1494 int status = 0, anynull = 0;
1495 double pixel_size = 5.2;
1496 int ndim {2}, bitpix {8};
1497 long naxes[3];
1498 fitsfile *fptr = nullptr;
1499
1500 if (fits_open_diskfile(&fptr, filename.c_str(), READONLY, &status))
1501 {
1502 char error_status[512] = {0};
1503 fits_get_errstatus(status, error_status);
1504 LOGF_WARN("Error opening file %s due to error %s", filename.c_str(), error_status);
1505 return false;
1506 }
1507
1508 fits_get_img_param(fptr, 3, &bitpix, &ndim, naxes, &status);
1509
1510 if (ndim < 3)
1511 naxes[2] = 1;
1512 else
1513 PrimaryCCD.setNAxis(3);
1514 int samples_per_channel = naxes[0] * naxes[1];
1515 int channels = naxes[2];
1516 int elements = samples_per_channel * channels;
1517 int size = elements * bitpix / 8;
1518 PrimaryCCD.setFrameBufferSize(size);
1519
1520 if (fits_read_img(fptr, bitpix == 8 ? TBYTE : TUSHORT, 1, elements, 0, PrimaryCCD.getFrameBuffer(), &anynull, &status))
1521 {
1522 char error_status[512] = {0};
1523 fits_get_errstatus(status, error_status);
1524 LOGF_WARN("Error reading file %s due to error %s", filename.c_str(), error_status);
1525 return false;
1526 }
1527
1528 if (fits_read_key_dbl(fptr, "PIXSIZE1", &pixel_size, comment, &status))
1529 {
1530 char error_status[512] = {0};
1531 fits_get_errstatus(status, error_status);
1532 LOGF_WARN("Error reading file %s due to error %s", filename.c_str(), error_status);
1533 return false;
1534 }
1535
1536 if (fits_read_key_str(fptr, "BAYERPAT", bayer_pattern, comment, &status))
1537 {
1538 char error_status[512] = {0};
1539 fits_get_errstatus(status, error_status);
1540 LOGF_DEBUG("No BAYERPAT keyword found in %s (%s)", filename.c_str(), error_status);
1541 }
1542 SetCCDParams(naxes[0], naxes[1], bitpix, pixel_size, pixel_size);
1543
1544 // Check if MONO or Bayer
1545 if (channels == 1 && strlen(bayer_pattern) == 4)
1546 {
1547 SetCCDCapability(GetCCDCapability() | CCD_HAS_BAYER);
1548 IUSaveText(&BayerT[0], "0");
1549 IUSaveText(&BayerT[1], "0");
1550 IUSaveText(&BayerT[2], bayer_pattern);
1551 }
1552 else
1553 {
1554 SetCCDCapability(GetCCDCapability() & ~CCD_HAS_BAYER);
1555 }
1556
1557 fits_close_file(fptr, &status);
1558 return true;
1559 }
1560