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