Instrument Neutral Distributed Interface INDI 2.2.5
telescope_simulator.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2015 Jasem Mutlaq. All rights reserved.
3
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 "telescope_simulator.h"
20#include "scopesim_helper.h"
21
22#include "indicom.h"
23#include "lilxml.h"
24
25#include <libnova/julian_day.h>
26
27using namespace INDI::AlignmentSubsystem;
28
29#include <cmath>
30#include <cstring>
31#include <memory>
32
33// We declare an auto pointer to ScopeSim.
34static std::unique_ptr<ScopeSim> telescope_sim(new ScopeSim());
35
36#define RA_AXIS 0
37#define DEC_AXIS 1
38#define GUIDE_NORTH 0
39#define GUIDE_SOUTH 1
40#define GUIDE_WEST 0
41#define GUIDE_EAST 1
42
44{
45 DBG_SCOPE = static_cast<uint32_t>(INDI::Logger::getInstance().addDebugLevel("Scope Verbose", "SCOPE"));
46
50 4
51 );
52
53 /* initialize random seed: */
54 srand(static_cast<uint32_t>(time(nullptr)));
55
56 // Initialize axes to a safe default (EQ pointing at NCP, counterweight down).
57 // updateMountAndPierSide() sets mount-type-dependent rates when config is loaded.
58 axisPrimary.setDegrees(0.0);
59 axisPrimary.TrackRate(Axis::SIDEREAL);
60 axisSecondary.setDegrees(90.0);
61 axisSecondary.TrackRate(Axis::OFF);
62}
63
65{
66 return "Telescope Simulator";
67}
68
70{
71 /* Make sure to init parent properties first */
73
74 // Override the mount type property to make it writable in the simulator
75 MountTypeSP.fill(getDeviceName(), "TELESCOPE_MOUNT_TYPE", "Mount Type", MOTION_TAB, IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
76
77#ifdef USE_SIM_TAB
78 // alignment properties, these are in the Simulation tab
79
80 simPierSideSP[PS_OFF].fill("PS_OFF", "Off", ISS_OFF);
81 simPierSideSP[PS_ON].fill("PS_ON", "On", ISS_ON);
82 simPierSideSP.fill(getDeviceName(), "SIM_PIER_SIDE", "Sim Pier Side",
83 "Simulation", IP_WO, ISR_1OFMANY, 60, IPS_IDLE);
84
85 mountModelNP[MM_IH].fill("MM_IH", "Ha Zero (IH) '", "%.1f", -10800, 10800, 0.1, 0);
86 mountModelNP[MM_ID].fill("MM_ID", "Dec Zero (ID) '", "%.1f", -10800, 10800, 0.1, 0);
87 mountModelNP[MM_CH].fill("MM_CH", "Cone (CH) '", "%.1f", -300, 300, 0.1, 0);
88 mountModelNP[MM_NP].fill("MM_NP", "Ha/Dec (NP) '", "%.1f", -300, 300, 0.1, 0);
89 mountModelNP[MM_MA].fill("MM_MA", "Pole Azm (MA) '", "%.1f", -300, 300, 0.1, 0);
90 mountModelNP[MM_ME].fill("MM_ME", "Pole elev (ME) '", "%.1f", -300, 300, 0.1, 0);
91 mountModelNP.fill(getDeviceName(), "MOUNT_MODEL", "Mount Model",
92 "Simulation", IP_RW, 0, IPS_IDLE);
93
94 flipHourAngleNP[0].fill("FLIP_HA", "Hour Angle (deg)", "%g", -20, 20, 0.1, 0);
95 flipHourAngleNP.fill(getDeviceName(), "FLIP_HA", "Flip Posn.",
96 "Simulation", IP_WO, 0, IPS_IDLE);
97
98 mountAxisNP[PRIMARY].fill("PRIMARY", "Primary (Ha)", "%g", -180, 180, 0.01, 0);
99 mountAxisNP[SECONDARY].fill("SECONDARY", "Secondary (Dec)", "%g", -180, 180, 0.01, 0);
100 mountAxisNP.fill(getDeviceName(), "MOUNT_AXES", "Mount Axes",
101 "Simulation", IP_RO, 0, IPS_IDLE);
102
103 decBacklashNP[0].fill("DEC_BACKLASH", "DEC Backlash (ms)", "%.0f", 0, 5000, 0, 0);
104 decBacklashNP.fill(getDeviceName(), "DEC_BACKLASH", "DEC Backlash",
105 "Simulation", IP_RW, 0, IPS_IDLE);
106
107 PACDeviceTP[0].fill("PAC_DEVICE", "Alignment Corrector", "Alignment Correction Simulator");
108 PACDeviceTP.fill(getDeviceName(), "ACTIVE_PAC", "Polar Alignment Corrector", "Simulation", IP_RW, 60, IPS_IDLE);
109 PACDeviceTP.load();
110
111 const char *pacDevice = PACDeviceTP[0].getText();
112 if (pacDevice && strlen(pacDevice) > 0)
113 IDSnoopDevice(pacDevice, "PAC_MANUAL_ADJUSTMENT");
114
115#endif
116
117 /* How fast do we guide compared to sidereal rate */
118 GuideRateNP[RA_AXIS].fill("GUIDE_RATE_WE", "W/E Rate", "%g", 0, 1, 0.1, 0.5);
119 GuideRateNP[DEC_AXIS].fill("GUIDE_RATE_NS", "N/S Rate", "%g", 0, 1, 0.1, 0.5);
120 GuideRateNP.fill(getDeviceName(), "GUIDE_RATE", "Guiding Rate", MOTION_TAB, IP_RW, 0,
121 IPS_IDLE);
122
123 SlewRateSP[SLEW_GUIDE].fill("SLEW_GUIDE", "Guide", ISS_OFF);
124 SlewRateSP[SLEW_CENTERING].fill("SLEW_CENTERING", "Centering", ISS_OFF);
125 SlewRateSP[SLEW_FIND].fill("SLEW_FIND", "Find", ISS_OFF);
126 SlewRateSP[SLEW_MAX].fill("SLEW_MAX", "Max", ISS_ON);
127 SlewRateSP.fill(getDeviceName(), "TELESCOPE_SLEW_RATE", "Slew Rate", MOTION_TAB,
129
130 // Add Tracking Modes, the order must match the order of the TelescopeTrackMode enum
131 AddTrackMode("TRACK_SIDEREAL", "Sidereal", true);
132 AddTrackMode("TRACK_SOLAR", "Solar");
133 AddTrackMode("TRACK_LUNAR", "Lunar");
134 AddTrackMode("TRACK_CUSTOM", "Custom");
135
136 // Park data type is set in updateMountAndPierSide() based on the mount type.
137
139
140 /* Add debug controls so we may debug driver if necessary */
142
144
146
148
149 EqPENP[PE_RA].fill("RA_PE", "RA (hh:mm:ss)", "%010.6m", 0, 24, 0, 0);
150 EqPENP[PE_DEC].fill("DEC_PE", "DEC (dd:mm:ss)", "%010.6m", -90, 90, 0, 0);
151 EqPENP.fill(getDeviceName(), "EQUATORIAL_PE", "True Pointing", MAIN_CONTROL_TAB, IP_RO, 0, IPS_IDLE);
152
153 return true;
154}
155
156void ScopeSim::ISGetProperties(const char *dev)
157{
158 /* First we let our parent populate */
160
161#ifdef USE_SIM_TAB
162 // Load mount type settings and apply immediately so axis track rates and
163 // alignment.mountType are correct before the first ReadScopeStatus tick.
166 defineProperty(simPierSideSP);
167 simPierSideSP.load();
168 updateMountAndPierSide();
169 defineProperty(mountModelNP);
170 mountModelNP.load();
171 alignment.setCorrections(mountModelNP[MM_IH].getValue() / 60.0, mountModelNP[MM_ID].getValue() / 60.0,
172 mountModelNP[MM_CH].getValue() / 60.0, mountModelNP[MM_NP].getValue() / 60.0,
173 mountModelNP[MM_MA].getValue() / 60.0, mountModelNP[MM_ME].getValue() / 60.0);
174 defineProperty(mountAxisNP);
175 defineProperty(flipHourAngleNP);
176 flipHourAngleNP.load();
177 defineProperty(decBacklashNP);
178 decBacklashNP.load();
179 defineProperty(PACDeviceTP);
180 PACDeviceTP.load();
181#endif
182 m_currentAz = axisPrimary.position.Degrees();
183 m_currentAlt = axisSecondary.position.Degrees();
184}
185
187{
188 updateMountAndPierSide();
189
191
192 if (isConnected())
193 {
194 defineProperty(GuideRateNP);
195 GuideRateNP.load();
196 // Always set lat/lon before axis init so instrumentHaDecToMount uses the right location.
197 // updateLocation() is called later by the framework; this ensures it's set now from config.
198 alignment.latitude = Angle(LocationNP[LOCATION_LATITUDE].getValue());
199 alignment.longitude = Angle(LocationNP[LOCATION_LONGITUDE].getValue());
200
201 if (InitPark())
202 {
203 if (isParked())
204 {
205 // at this point there is a valid ParkData.xml available.
206 // For ALTAZ, Axis1=Az (degrees), Axis2=Alt (degrees).
207 // For EQ, Axis1=HA (hours), Axis2=Dec (degrees).
208 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
209 {
210 // Convert stored INDI Az/Alt park position to instrument RA/Dec.
211 Angle instHA, instDec;
212 alignment.mountToInstrumentHaDec(
213 Angle(ParkPositionNP[AXIS_RA].getValue()),
214 Angle(ParkPositionNP[AXIS_DE].getValue()),
215 &instHA, &instDec);
216 m_currentRA = (alignment.lst() - instHA).Hours();
217 m_currentDEC = instDec.Degrees();
218 }
219 else
220 {
221 m_currentRA = (alignment.lst() - Angle(ParkPositionNP[AXIS_RA].getValue(), Angle::ANGLE_UNITS::HOURS)).Hours();
222 m_currentDEC = ParkPositionNP[AXIS_DE].getValue();
223 }
224 }
225 // If loading parking data is successful, we just set the default parking values.
226 // ALTAZ: Az=0 (North horizon), Alt=0. EQ: HA=-6h (counterweight down), Dec=0.
227 double defaultAxis1 = (m_MountType == Alignment::MOUNT_TYPE::ALTAZ) ? 0. : -6.;
228 SetAxis1ParkDefault(defaultAxis1);
230 }
231 else
232 {
233 // Otherwise, we set all parking data to default in case no parking data is found.
234 double defaultAxis1 = (m_MountType == Alignment::MOUNT_TYPE::ALTAZ) ? 0. : -6.;
235 SetAxis1Park(defaultAxis1);
236 SetAxis2Park(0.);
237 SetAxis1ParkDefault(defaultAxis1);
239 }
240
241 // Initialize axis positions from the current RA/Dec (park position if parked,
242 // last known position otherwise) using naive (no Wallace) conversion.
243 // Do NOT call Sync() here — it would add a spurious identity entry to the INDI
244 // alignment database that conflicts with real sync points.
245 // Also seed m_targetRA/Dec so the ALTAZ tracking servo has a valid target from the start.
246 {
247 Angle ha = alignment.lst() - Angle(m_currentRA * 15.0);
248 Angle primary, secondary;
249 alignment.instrumentHaDecToMount(ha, Angle(m_currentDEC), &primary, &secondary);
250 axisPrimary.position = primary;
251 axisSecondary.position = secondary;
252 m_currentAz = axisPrimary.position.Degrees();
253 m_currentAlt = axisSecondary.position.Degrees();
254 m_targetRA = m_currentRA;
255 m_targetDEC = m_currentDEC;
256 }
257
258 // A new connection starts from the system clock until the client sets TIME_UTC
259 alignment.clockOffsetDays = 0;
261
262#ifdef USE_SIM_TAB
263 const char *pacDevice = PACDeviceTP[0].getText();
264 if (pacDevice && strlen(pacDevice) > 0)
265 IDSnoopDevice(pacDevice, "PAC_MANUAL_ADJUSTMENT");
266#endif
267
268 defineProperty(EqPENP);
270 }
271 else
272 {
273 deleteProperty(EqPENP);
274 deleteProperty(GuideRateNP);
275 }
276
278
279 return true;
280}
281
283{
284 LOG_INFO("Telescope simulator is online.");
286
287 return true;
288}
289
291{
292 LOG_INFO("Telescope simulator is offline.");
293 return true;
294}
295
296
297void ScopeSim::updateCurrentCoordsFromAxes()
298{
299 Angle instHA, instDec;
300 alignment.mountToInstrumentHaDec(axisPrimary.position, axisSecondary.position, &instHA, &instDec);
301 m_currentRA = (alignment.lst() - instHA).Hours();
302 m_currentDEC = instDec.Degrees();
303
304 if (GetAlignmentDatabase().size() >= 1)
305 {
306 double corrRA, corrDec;
307 bool corrected = false;
308 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
309 {
310 corrected = TelescopeAltAzToSky(axisSecondary.position.Degrees(),
311 axisPrimary.position.Degrees(), corrRA, corrDec);
312 }
313 else
314 {
315 // Encode the current encoder RA (= LST − HA), not HA, so the TDV
316 // fed into TransformTelescopeToCelestial uses the same time-stable
317 // coordinate that Sync() now stores in the alignment database.
318 double encoderRA = (alignment.lst() - instHA).Hours();
319 INDI::IEquatorialCoordinates raCoords{ encoderRA, instDec.Degrees() };
322 corrected = TransformTelescopeToCelestialJD(tdv, corrRA, corrDec, alignment.julianDate());
323 }
324 if (corrected)
325 {
326 m_currentRA = corrRA;
327 m_currentDEC = corrDec;
328 }
329 }
330}
331
332void ScopeSim::setTargetFromAxisPosition(Angle primary, Angle secondary)
333{
334 Angle instHA, instDec;
335 alignment.mountToInstrumentHaDec(primary, secondary, &instHA, &instDec);
336 m_targetRA = (alignment.lst() - instHA).Hours();
337 m_targetDEC = instDec.Degrees();
338}
339
341{
342 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ && TrackState == SCOPE_TRACKING &&
344 {
345 double dt = getCurrentPollingPeriod() / 1000.0;
346 double JDnow = alignment.julianDate();
347 double JDoffset = dt / 86400.0;
348
349 // RA drift rate relative to sidereal for the selected track mode, in hours/sec.
350 // Nudging RA at each parabola sample by the non-sidereal offset makes the
351 // derived Az/Alt rates correct for the track mode without changing the meaning
352 // of m_targetRA/Dec. Mirrors EQ behaviour: RA is corrected, Dec is not.
353 int trackMode = TrackModeSP.findOnSwitchIndex();
354 double raDriftHrsPerSec = 0;
355 if (trackMode == TRACK_SOLAR)
356 raDriftHrsPerSec = (TRACKRATE_SIDEREAL - TRACKRATE_SOLAR) / 3600.0 / 15.0;
357 else if (trackMode == TRACK_LUNAR)
358 raDriftHrsPerSec = (TRACKRATE_SIDEREAL - TRACKRATE_LUNAR) / 3600.0 / 15.0;
359
360 auto getAltAz = [&](double JD, INDI::IHorizontalCoordinates & coords)
361 {
362 double dtFromNow = (JD - JDnow) * 86400.0;
363 INDI::IEquatorialCoordinates eq { m_targetRA + raDriftHrsPerSec * dtFromNow, m_targetDEC };
365 };
366
367 bool targetChanged = std::abs(m_LastTrackingRA - m_targetRA) > 1e-6 ||
368 std::abs(m_LastTrackingDec - m_targetDEC) > 1e-6;
369
370 if (!m_IsPipelinePrimed || targetChanged)
371 {
372 getAltAz(JDnow - JDoffset, m_TrackingWindowCoords[0]);
373 getAltAz(JDnow, m_TrackingWindowCoords[1]);
374 getAltAz(JDnow + JDoffset, m_TrackingWindowCoords[2]);
375 m_IsPipelinePrimed = true;
376 m_LastTrackingRA = m_targetRA;
377 m_LastTrackingDec = m_targetDEC;
378 }
379 else
380 {
381 m_TrackingWindowCoords[0] = m_TrackingWindowCoords[1];
382 m_TrackingWindowCoords[1] = m_TrackingWindowCoords[2];
383 getAltAz(JDnow + JDoffset, m_TrackingWindowCoords[2]);
384 }
385
386 // Unwrap azimuth around the center point to avoid 360/0 discontinuity
387 // Angle(x).Degrees() normalizes x to (-180, +180], same as the private Angle::range()
388 auto range180 = [](double x)
389 {
390 return Angle(x).Degrees();
391 };
392 double pAz[3] = { m_TrackingWindowCoords[0].azimuth,
393 m_TrackingWindowCoords[1].azimuth,
394 m_TrackingWindowCoords[2].azimuth
395 };
396 pAz[0] = pAz[1] + range180(pAz[0] - pAz[1]);
397 pAz[2] = pAz[1] + range180(pAz[2] - pAz[1]);
398
399 // Parabolic coefficients (t in normalized units; points at t = -1, 0, +1)
400 double az_a = (pAz[2] + pAz[0] - 2.0 * pAz[1]) / 2.0;
401 double az_b = (pAz[2] - pAz[0]) / 2.0;
402 double alt_a = (m_TrackingWindowCoords[2].altitude + m_TrackingWindowCoords[0].altitude
403 - 2.0 * m_TrackingWindowCoords[1].altitude) / 2.0;
404 double alt_b = (m_TrackingWindowCoords[2].altitude - m_TrackingWindowCoords[0].altitude) / 2.0;
405
406 // Predicted position at next tick (t = +1 normalized = +dt seconds from now)
407 double targetAzNext = az_a + az_b + pAz[1];
408 double targetAltNext = alt_a + alt_b + m_TrackingWindowCoords[1].altitude;
409
410 // Rate to close the gap in exactly dt seconds, converted to arcsec/s for SetTrackRate
411 double azRate = range180(targetAzNext - m_currentAz) / dt * 3600.0;
412 double altRate = (targetAltNext - m_currentAlt) / dt * 3600.0;
413
414 SetTrackRate(azRate, altRate);
415 }
416
417 // new mechanical angle calculation
418 axisPrimary.update();
419 axisSecondary.update();
420
421 // True pointing: apply Wallace error injection to get where the scope physically points.
422 // Published as EQUATORIAL_PE so the CCD simulator generates star fields at this position.
423 Angle trueRa, trueDec;
424 alignment.mountToApparentRaDec(axisPrimary.position, axisSecondary.position, &trueRa, &trueDec);
425 EqPENP[PE_RA].setValue(trueRa.Hours());
426 EqPENP[PE_DEC].setValue(trueDec.Degrees());
427 EqPENP.setState(IPS_OK);
428 EqPENP.apply();
429
430 // Raw encoder position: what the mount "reports" without knowing its own errors.
431 // This is what EQUATORIAL_EOD_COORD carries and what the INDI alignment module uses.
432 // Apply INDI alignment correction if sync points exist.
433 updateCurrentCoordsFromAxes();
434
435 // Az/Alt for the ALTAZ tracking servo. The servo (top of ReadScopeStatus) drives the
436 // axis motors; it must compare axis-space positions against axis-space targets. Using
437 // Wallace-corrected (true) Az/Alt here would inject the Wallace error as a permanent
438 // position offset, causing the servo to oscillate every tick. Use raw axis positions.
439 // axisPrimary stores INDI Az (0=North through East), same convention as m_currentAz.
440 m_currentAz = axisPrimary.position.Degrees();
441 m_currentAlt = axisSecondary.position.Degrees();
442
443 // update properties from the axis
445 {
446 setPierSide(fabs(axisSecondary.position.Degrees()) > 90 ? PIER_WEST : PIER_EAST);
447 }
448
449 bool slewing = axisPrimary.isSlewing || axisSecondary.isSlewing;
450 switch (TrackState)
451 {
452 case SCOPE_PARKING:
453 if (!slewing)
454 {
455 SetParked(true);
457 LOG_INFO("Telescope slew is complete. Parked");
458 }
459 break;
460 case SCOPE_SLEWING:
461 if (!slewing)
462 {
463 // It seems to be required that tracking is enabled when a slew finishes but is it correct?
464 // if the mount was not tracking before the slew should it remain not tracking?
466 SetTrackEnabled(true);
468
469 if (HomeSP.getState() == IPS_BUSY)
470 {
471 HomeSP.reset();
473 HomeSP.apply();
474 LOG_INFO("Home position reached.");
475 }
476 else
477 LOG_INFO("Telescope slew is complete. Tracking...");
478
479 // Check slew accuracy in axis space so the result is frame-independent:
480 // m_targetRA/Dec are instrument-frame; m_currentRA/Dec may be alignment-corrected.
481 {
482 Angle targetHa = alignment.lst() - Angle(m_targetRA * 15.0);
483 Angle tPrimary, tSecondary;
484 alignment.instrumentHaDecToMount(targetHa, Angle(m_targetDEC), &tPrimary, &tSecondary);
485 double dPri = (axisPrimary.position - tPrimary).Degrees();
486 double dSec = (axisSecondary.position - tSecondary).Degrees();
487 LOGF_DEBUG("slew accuracy pri %f arcsec, sec %f arcsec", dPri * 3600, dSec * 3600);
488 }
489
490 }
491 break;
492 case SCOPE_PARKED:
493 return true;
494 default:
495 break;
496 }
497
498 if (guidingEW && !axisPrimary.IsGuiding())
499 {
500 GuideWENP[0].setValue(0);
501 GuideWENP[1].setValue(0);
503 guidingEW = false;
504 }
505
506 if (guidingNS && !axisSecondary.IsGuiding())
507 {
508 GuideNSNP[0].setValue(0);
509 GuideNSNP[1].setValue(0);
511 guidingNS = false;
512 }
513
514#ifdef USE_SIM_TAB
515 double PrimaryAngle = axisPrimary.position.Degrees();
516 double SecondaryAngle = axisSecondary.position.Degrees();
517 // No need to spam log until we have some actual changes.
518 if (std::fabs(mountAxisNP[PRIMARY].getValue() - PrimaryAngle) > 0.0001 ||
519 std::fabs(mountAxisNP[SECONDARY].getValue() - SecondaryAngle) > 0.0001)
520 {
521 mountAxisNP[PRIMARY].setValue(PrimaryAngle);
522 mountAxisNP[SECONDARY].setValue(SecondaryAngle);
523
524 LOGF_EXTRA1("new %s: %f, ra %f", axisPrimary.axisName, PrimaryAngle, trueRa.Hours());
525 LOGF_EXTRA1("new %s: %f, dec %f", axisSecondary.axisName, SecondaryAngle, trueDec.Degrees());
526
527 mountAxisNP.apply();
528 }
529#endif
530
531 char RAStr[64], DecStr[64];
532 fs_sexa(RAStr, m_currentRA, 2, 3600);
533 fs_sexa(DecStr, m_currentDEC, 2, 3600);
534 DEBUGF(DBG_SCOPE, "Current RA: %s Current DEC: %s", RAStr, DecStr);
535
536 NewRaDec(m_currentRA, m_currentDEC);
537
538 return true;
539}
540
541bool ScopeSim::Goto(double r, double d)
542{
543 // When the INDI alignment subsystem has learned sync points, transform the celestial
544 // target through the alignment database so the mount ends up at the right sky position
545 // despite the injected mount errors.
546 //
547 // ALTAZ mounts: the math plugin uses Az/Alt encoding. Decode via SkyToTelescopeAltAz
548 // and set axis slew targets directly in Az/Alt space.
549 //
550 // EQ mounts: the math plugin uses RA/Dec encoding. Decode via
551 // EquatorialCoordinatesFromTelescopeDirectionVector to obtain encoder RA directly,
552 // then pass it to StartSlew which converts it to HA for the axis motor.
553 if (GetAlignmentDatabase().size() >= 1 && m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
554 {
555 double instrumentAlt, instrumentAz_INDI;
556 if (SkyToTelescopeAltAz(r, d, instrumentAlt, instrumentAz_INDI))
557 {
558 axisPrimary.StartSlew(Angle(instrumentAz_INDI));
559 axisSecondary.StartSlew(Angle(instrumentAlt));
560
561 // m_targetRA/Dec must be the instrument position (not celestial) so the ALTAZ
562 // tracking servo's getAltAz() computes instrument Az/Alt rates, not celestial.
563 setTargetFromAxisPosition(Angle(instrumentAz_INDI), Angle(instrumentAlt));
564
567 LOGF_INFO("Slewing to RA: %.4f Dec: %.4f", m_targetRA, m_targetDEC);
568 return true;
569 }
570 }
571
572 double targetRA = r, targetDec = d;
573 if (GetAlignmentDatabase().size() >= 1)
574 {
576 if (TransformCelestialToTelescopeJD(r, d, alignment.julianDate(), tdv))
577 {
578 // The TDV now encodes encoder RA (time-stable), so decode with
579 // EquatorialCoordinatesFromTelescopeDirectionVector — no LST conversion needed.
582 targetRA = raCoords.rightascension;
583 targetDec = raCoords.declination;
584 LOGF_INFO("EQ Goto: alignment corrected RA %.4fh Dec %.4f -> RA %.4fh Dec %.4f",
585 r, d, targetRA, targetDec);
586 }
587 else
588 {
589 LOG_WARN("EQ Goto: TransformCelestialToTelescope FAILED, using raw coordinates");
590 }
591 }
592
593 StartSlew(targetRA, targetDec, SCOPE_SLEWING);
594 return true;
595}
596
597bool ScopeSim::Sync(double ra, double dec)
598{
599 // The INDI alignment math plugins encode telescope directions as:
600 // - ALTAZ (ZENITH) mounts: Az/Alt via TelescopeDirectionVectorFromAltitudeAzimuth
601 // - EQ mounts: RA/Dec via TelescopeDirectionVectorFromEquatorialCoordinates
602 // Using encoder RA (= LST − HA, time-stable during sidereal tracking) ensures the
603 // stored offset sky_RA − encoder_RA remains constant across iterations, so successive
604 // sync+goto cycles converge rather than diverging.
605
607 entry.ObservationJulianDate = alignment.julianDate();
608 entry.RightAscension = ra;
609 entry.Declination = dec;
610 entry.PrivateDataSize = 0;
611
612 const bool altAz = (m_MountType == Alignment::MOUNT_TYPE::ALTAZ);
613 const double indi_az = axisPrimary.position.Degrees();
614 const double indi_alt = axisSecondary.position.Degrees();
615 if (altAz)
616 {
617 INDI::IHorizontalCoordinates altAzCoords { indi_az, indi_alt };
619 }
620 else
621 {
622 // EQ mounts: encode the raw encoder RA/Dec as the telescope direction.
623 // Use RA (= LST − HA), not HA, so the stored offset is time-stable.
624 // HA changes at the sidereal rate even during tracking; encoder RA does not.
625 // Storing HA causes the correction to drift by ~15 arcsec/s, accumulating
626 // error over successive alignment iterations.
627 Angle instHA, instDec;
628 alignment.mountToInstrumentHaDec(axisPrimary.position, axisSecondary.position,
629 &instHA, &instDec);
630
631 double encoderRA = (alignment.lst() - instHA).Hours();
632 INDI::IEquatorialCoordinates raCoords{ encoderRA, instDec.Degrees() };
634 }
635
636 // A new sync supersedes older sync points at (nearly) the same place: the latest plate solve is the
637 // better measurement. Previously the new point was silently dropped as a duplicate while this function
638 // still reported success, so re-syncing near an existing point had no effect on the pointing model.
639 // ALTAZ entries need the geographic reference position (see AddAlignmentEntryAltAz); without it
640 // nothing is stored and the existing points are kept.
641 INDI::IGeographicCoordinates referencePosition;
642 if (altAz && !GetDatabaseReferencePosition(referencePosition))
643 {
644 LOG_WARN("Sync not stored in the alignment database: geographic location is not known yet.");
645 }
646 else
647 {
648 const size_t before = GetAlignmentDatabase().size();
649 RemoveSyncPoint(entry);
650 const int replaced = static_cast<int>(before - GetAlignmentDatabase().size());
651
652 if (altAz)
653 AddAlignmentEntryAltAz(ra, dec, indi_alt, indi_az); // handles DB + Initialise
654 else
655 {
656 GetAlignmentDatabase().push_back(entry);
657 UpdateSize();
658 Initialise(this);
659 }
660
661 if (replaced > 0)
662 LOGF_INFO("Sync replaced %d older alignment point(s) at the same position.", replaced);
663 }
664
665 // Keep the ALTAZ tracking servo on the current instrument position after sync.
666 setTargetFromAxisPosition(axisPrimary.position, axisSecondary.position);
667
668 LOGF_DEBUG("Sync at RA %f Dec %f m_targetRA %f m_targetDEC %f",
669 ra, dec, m_targetRA, m_targetDEC);
670
671 LOG_INFO("Sync is successful.");
672
674 NewRaDec(m_currentRA, m_currentDEC);
675
676 return true;
677}
678
680{
681 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
682 {
683 // For ALTAZ, park position is stored as INDI Az (degrees) / Alt (degrees).
684 // Convert directly to axis positions and slew; do not go through StartSlew's
685 // HA/Dec path which treats Axis1 as hours.
686 Angle indi_az = Angle(GetAxis1Park());
687 Angle alt = Angle(GetAxis2Park());
688 axisPrimary.StartSlew(indi_az);
689 axisSecondary.StartSlew(alt);
690 setTargetFromAxisPosition(indi_az, alt);
693 LOG_INFO("Parking...");
694 }
695 else
696 {
697 double ra = (alignment.lst() - Angle(GetAxis1Park() * 15.)).Degrees() / 15.;
698 StartSlew(ra, GetAxis2Park(), SCOPE_PARKING);
699 }
700 return true;
701}
702
703// common code for GoTo and park
704void ScopeSim::StartSlew(double ra, double dec, TelescopeStatus status)
705{
706 // Naive axis conversion: no Wallace corrections. The mount slews to where its
707 // encoders say, unaware of its own IH/ID/etc. errors — just like a real mount.
708 Angle ha = alignment.lst() - Angle(ra * 15.0);
709 Angle primary, secondary;
710 alignment.instrumentHaDecToMount(ha, Angle(dec), &primary, &secondary);
711
712 axisPrimary.StartSlew(primary);
713 axisSecondary.StartSlew(secondary);
714
715 // Store instrument-frame RA/Dec so the ALTAZ tracking servo always works
716 // in a consistent coordinate frame, regardless of how StartSlew was called.
717 setTargetFromAxisPosition(primary, secondary);
718 char RAStr[64], DecStr[64];
719
720 fs_sexa(RAStr, m_targetRA, 2, 3600);
721 fs_sexa(DecStr, m_targetDEC, 2, 3600);
722
723 const char * statusStr;
724 switch (status)
725 {
726 case SCOPE_PARKING:
727 statusStr = "Parking";
728 break;
729 case SCOPE_SLEWING:
730 statusStr = "Slewing";
731 break;
732 default:
733 statusStr = "unknown";
734 }
735 TrackState = status;
736
737 LOGF_INFO("%s to RA: %s - DEC: %s", statusStr, RAStr, DecStr);
738}
739
741{
742 SetParked(false);
743 return true;
744}
745
746bool ScopeSim::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
747{
748 // Check guider interface
749 if (GI::processNumber(dev, name, values, names, n))
750 return true;
751
752 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
753 {
754 if (strcmp(name, "GUIDE_RATE") == 0)
755 {
756 GuideRateNP.update(values, names, n);
757 GuideRateNP.setState(IPS_OK);
758 GuideRateNP.apply();
759 return true;
760 }
761
762#ifdef USE_SIM_TAB
763 if (mountModelNP.isNameMatch(name))
764 {
765 if (mountModelNP.isUpdated(values, names, n))
766 {
767 mountModelNP.update(values, names, n);
768 alignment.setCorrections(mountModelNP[MM_IH].getValue() / 60.0, mountModelNP[MM_ID].getValue() / 60.0,
769 mountModelNP[MM_CH].getValue() / 60.0, mountModelNP[MM_NP].getValue() / 60.0,
770 mountModelNP[MM_MA].getValue() / 60.0, mountModelNP[MM_ME].getValue() / 60.0);
771 saveConfig(mountModelNP);
772 }
773 mountModelNP.setState(IPS_OK);
774 mountModelNP.apply();
775 return true;
776 }
777
778 if (flipHourAngleNP.isNameMatch(name))
779 {
780 flipHourAngleNP.update(values, names, n);
781 flipHourAngleNP.setState(IPS_OK);
782 flipHourAngleNP.apply();
783 alignment.setFlipHourAngle(flipHourAngleNP[0].getValue());
784 return true;
785 }
786 if (decBacklashNP.isNameMatch(name))
787 {
788 decBacklashNP.update(values, names, n);
789 decBacklashNP.setState(IPS_OK);
790 decBacklashNP.apply();
791 m_DecGuideBacklashMs = decBacklashNP[0].getValue();
792 return true;
793 }
794#endif
795 }
796
797 ProcessAlignmentNumberProperties(this, name, values, names, n);
798
799 // if we didn't process it, continue up the chain, let somebody else
800 // give it a shot
801 return INDI::Telescope::ISNewNumber(dev, name, values, names, n);
802}
803
804bool ScopeSim::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
805{
806 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
807 {
808#ifdef USE_SIM_TAB
809 if (MountTypeSP.isNameMatch(name))
810 {
811 if (!MountTypeSP.update(states, names, n))
812 return false;
813
816 updateMountAndPierSide();
818 return true;
819 }
820 if (simPierSideSP.isNameMatch(name))
821 {
822 if (!simPierSideSP.update(states, names, n))
823 return false;
824
825 simPierSideSP.setState(IPS_OK);
826 simPierSideSP.apply();
827 updateMountAndPierSide();
828 return true;
829 }
830#endif \
831 // Slew mode
832 if (SlewRateSP.isNameMatch(name))
833 {
834 if (SlewRateSP.update( states, names, n) == false)
835 return false;
836
839 return true;
840 }
841 }
842
843 ProcessAlignmentSwitchProperties(this, name, states, names, n);
844
845 // Persist alignment plugin and active state immediately when changed.
846 // Use a full saveConfig() — the selective form (saveConfig(true, name)) only
847 // patches the named property in the existing XML and skips CURRENT_MATH_PLUGIN.
848 if (strcmp(name, "ALIGNMENT_SUBSYSTEM_MATH_PLUGINS") == 0 ||
849 strcmp(name, "ALIGNMENT_SUBSYSTEM_ACTIVE") == 0)
850 saveConfig();
851
852 // Nobody has claimed this, so pass it over
853 return INDI::Telescope::ISNewSwitch(dev, name, states, names, n);
854}
855
856bool ScopeSim::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
857{
858#ifdef USE_SIM_TAB
859 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
860 {
861 if (PACDeviceTP.isNameMatch(name))
862 {
863 PACDeviceTP.setState(IPS_OK);
864 PACDeviceTP.update(texts, names, n);
865 PACDeviceTP.apply();
866
867 const char *pacDevice = PACDeviceTP[0].getText();
868 if (pacDevice && strlen(pacDevice) > 0)
869 IDSnoopDevice(pacDevice, "PAC_MANUAL_ADJUSTMENT");
870
871 saveConfig(PACDeviceTP);
872 return true;
873 }
874 }
875#endif
876 ProcessAlignmentTextProperties(this, name, texts, names, n);
877 return INDI::Telescope::ISNewText(dev, name, texts, names, n);
878}
879
881{
882#ifdef USE_SIM_TAB
883 const char *propName = findXMLAttValu(root, "name");
884 const char *deviceName = findXMLAttValu(root, "device");
885 const char *pacDevice = PACDeviceTP[0].getText();
886
887 if (pacDevice && strlen(pacDevice) > 0 && deviceName && strcmp(deviceName, pacDevice) == 0)
888 {
889 if (!strcmp(propName, "PAC_MANUAL_ADJUSTMENT"))
890 {
891 // Capture the step values (AZ and ALT) whenever the property is updated.
892 // PAC_MANUAL_ADJUSTMENT[MANUAL_AZ_STEP] > 0 → East, < 0 → West
893 // PAC_MANUAL_ADJUSTMENT[MANUAL_ALT_STEP] > 0 → North, < 0 → South
894 XMLEle *ep = nullptr;
895 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
896 {
897 const char *elemName = findXMLAttValu(ep, "name");
898 if (!strcmp(elemName, "MANUAL_AZ_STEP"))
899 m_snoopedAzError = atof(pcdataXMLEle(ep));
900 else if (!strcmp(elemName, "MANUAL_ALT_STEP"))
901 m_snoopedAltError = atof(pcdataXMLEle(ep));
902 }
903
904 // Only update the mount model once the physical move has completed (state = Ok).
905 const char *stateStr = findXMLAttValu(root, "state");
906 if (!strcmp(stateStr, "Ok"))
907 {
908 // The PAA's azStep and MM_MA use opposite sign conventions:
909 // azStep > 0 (correction East) ↔ MM_MA decreases in the model
910 // Adding m_snoopedAzError to the current MM_MA correctly simulates
911 // the physical delta correction applied to the mount.
912 double oldMA = mountModelNP[MM_MA].getValue();
913 double oldME = mountModelNP[MM_ME].getValue();
914 double newMA = oldMA + m_snoopedAzError * 60.0;
915 double newME = oldME + m_snoopedAltError * 60.0;
916
917 mountModelNP[MM_MA].setValue(newMA);
918 mountModelNP[MM_ME].setValue(newME);
919 alignment.setCorrections(mountModelNP[MM_IH].getValue() / 60.0, mountModelNP[MM_ID].getValue() / 60.0,
920 mountModelNP[MM_CH].getValue() / 60.0, mountModelNP[MM_NP].getValue() / 60.0,
921 mountModelNP[MM_MA].getValue() / 60.0, mountModelNP[MM_ME].getValue() / 60.0);
922
923 mountModelNP.setState(IPS_OK);
924 mountModelNP.apply();
925
926 LOGF_INFO("Applied PAC correction: MA %.4f -> %.4f, ME %.4f -> %.4f (arcmin)",
927 oldMA, newMA, oldME, newME);
928
929 // Force an immediate coordinate update so that the next CCD capture
930 // uses the corrected pointing rather than waiting up to one full
931 // polling cycle (default 500 ms) for ReadScopeStatus to run.
932 updateCurrentCoordsFromAxes();
933 NewRaDec(m_currentRA, m_currentDEC);
934 }
935 return true;
936 }
937 }
938#endif
940}
941
943{
944 axisPrimary.Abort();
945 axisSecondary.Abort();
948 return true;
949}
950
952{
954 {
955 LOG_ERROR("Please unpark the mount before issuing any motion commands.");
956 return false;
957 }
958 mcRate = static_cast<int>(SlewRateSP.findOnSwitchIndex()) + 1;
959 mcRate = std::max(1, std::min(4, mcRate));
960
961 int rate = (dir == INDI_DIR_NS::DIRECTION_NORTH) ? mcRate : -mcRate;
962 if (HasPierSide() && (currentPierSide == PIER_WEST)) // see scopesim_helper.cpp: alignment
963 rate = -rate;
964 LOGF_DEBUG("MoveNS dir %s, motion %s, rate %d", dir == DIRECTION_NORTH ? "N" : "S", command == 0 ? "start" : "stop", rate);
965
966 axisSecondary.mcRate = command == MOTION_START ? rate : 0;
967
968 return true;
969}
970
972{
974 {
975 LOG_ERROR("Please unpark the mount before issuing any motion commands.");
976 return false;
977 }
978
979 mcRate = static_cast<int>(SlewRateSP.findOnSwitchIndex()) + 1;
980 mcRate = std::max(1, std::min(4, mcRate));
981
982 int rate = (dir == INDI_DIR_WE::DIRECTION_EAST) ? -mcRate : mcRate;
983 LOGF_DEBUG("MoveWE dir %d, motion %s, rate %d", dir == DIRECTION_EAST ? "E" : "W", command == 0 ? "start" : "stop", rate);
984
985 axisPrimary.mcRate = command == MOTION_START ? rate : 0;
986 return true;
987}
988
989uint32_t ScopeSim::backlashComputation(uint32_t ms)
990{
991 if (m_DecGuideRemainingBacklash <= 0)
992 {
993 m_DecGuideRemainingBacklash = 0;
994 }
995 else if (m_DecGuideRemainingBacklash >= ms)
996 {
997 m_DecGuideRemainingBacklash -= ms;
998 ms = 0;
999 }
1000 else
1001 {
1002 ms -= m_DecGuideRemainingBacklash;
1003 m_DecGuideRemainingBacklash = 0;
1004 }
1005 return ms;
1006}
1007
1008uint32_t ScopeSim::applyDecBacklash(double rate, uint32_t ms)
1009{
1010 const bool changingDirections = (rate < 0 && m_GuideDecLastNorth) ||
1011 (rate > 0 && !m_GuideDecLastNorth);
1012 m_GuideDecLastNorth = rate >= 0;
1013
1014 if (!changingDirections)
1015 {
1016 ms = backlashComputation(ms);
1017 return ms;
1018 }
1019 else
1020 {
1021 if (m_DecGuideRemainingBacklash <= 0)
1022 m_DecGuideRemainingBacklash = m_DecGuideBacklashMs;
1023 else if (m_DecGuideRemainingBacklash >= m_DecGuideBacklashMs)
1024 m_DecGuideRemainingBacklash = 0;
1025 else
1026 m_DecGuideRemainingBacklash = m_DecGuideBacklashMs - m_DecGuideRemainingBacklash;
1027
1028 ms = backlashComputation(ms);
1029 return ms;
1030 }
1031}
1032
1033void ScopeSim::guideAltAzDecomposed(double dNS, double dEW, uint32_t ms)
1034{
1035 // Decompose a celestial N/S/E/W guide pulse into Az/Alt axis components using
1036 // the parallactic angle q. sinQ = sin(q)/cos(alt), cosQ = cos(q)/cos(alt).
1037 // The azimuth rate for celestial North is -sin(q)/cos(alt); since sinQ already
1038 // carries one 1/cos(alt), dAz_N = -sinQ/cosAlt gives -sin(q)/cos^2(alt), which
1039 // is the correct field rotation formula.
1040 //
1041 // Celestial North in Az/Alt: dAlt = cos(q), dAz = -sin(q)/cos(alt)
1042 // Celestial East in Az/Alt: dAlt = sin(q), dAz = cos(q)/cos(alt)
1043
1044 double H = (alignment.lst() - Angle(m_currentRA * 15.0)).radians();
1045 double dec = Angle(m_currentDEC).radians();
1046 double lat = alignment.latitude.radians();
1047 double alt = m_currentAlt * M_PI / 180.0;
1048 double cosAlt = std::cos(alt);
1049
1050 if (std::abs(cosAlt) > 1e-4)
1051 {
1052 double sinQ = std::sin(H) * std::cos(lat) / cosAlt;
1053 double cosQ = (std::sin(lat) - std::sin(dec) * std::sin(alt))
1054 / (std::cos(dec) * cosAlt);
1055
1056 double dAlt_N = cosQ;
1057 double dAz_N = -sinQ / cosAlt;
1058 double dAlt_E = sinQ;
1059 double dAz_E = cosQ / cosAlt;
1060
1061 axisPrimary.StartGuide(dNS * dAz_N + dEW * dAz_E, ms);
1062 axisSecondary.StartGuide(dNS * dAlt_N + dEW * dAlt_E, ms);
1063 }
1064 else
1065 {
1066 // Near zenith: Az is degenerate; fall back to direct axis motion.
1067 axisPrimary.StartGuide(dEW, ms);
1068 axisSecondary.StartGuide(dNS, ms);
1069 }
1070}
1071
1073{
1074 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
1075 {
1076 guideAltAzDecomposed(GuideRateNP[DEC_AXIS].getValue(), 0.0, ms);
1077 }
1078 else
1079 {
1080 double rate = GuideRateNP[DEC_AXIS].getValue();
1081 if (HasPierSide() && (currentPierSide == PIER_WEST)) // see scopsim_helper.cpp: alignment
1082 rate = -rate;
1083 ms = applyDecBacklash(rate, ms);
1084 axisSecondary.StartGuide(rate, ms);
1085 }
1086 guidingNS = true;
1087 return IPS_BUSY;
1088}
1089
1091{
1092 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
1093 {
1094 guideAltAzDecomposed(-GuideRateNP[DEC_AXIS].getValue(), 0.0, ms);
1095 }
1096 else
1097 {
1098 double rate = GuideRateNP[DEC_AXIS].getValue();
1099 if (HasPierSide() && (currentPierSide == PIER_WEST)) // see scopsim_helper.cpp: alignment
1100 rate = -rate;
1101 ms = applyDecBacklash(-rate, ms);
1102 axisSecondary.StartGuide(-rate, ms);
1103 }
1104 guidingNS = true;
1105 return IPS_BUSY;
1106}
1107
1109{
1110 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
1111 guideAltAzDecomposed(0.0, -GuideRateNP[RA_AXIS].getValue(), ms);
1112 else
1113 axisPrimary.StartGuide(-GuideRateNP[RA_AXIS].getValue(), ms);
1114 guidingEW = true;
1115 return IPS_BUSY;
1116}
1117
1119{
1120 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
1121 guideAltAzDecomposed(0.0, GuideRateNP[RA_AXIS].getValue(), ms);
1122 else
1123 axisPrimary.StartGuide(GuideRateNP[RA_AXIS].getValue(), ms);
1124 guidingEW = true;
1125 return IPS_BUSY;
1126}
1127
1129{
1130 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
1131 {
1132 // Store INDI Az (0=North) and Alt in degrees.
1133 SetAxis1Park(m_currentAz);
1134 SetAxis2Park(m_currentAlt);
1135 }
1136 else
1137 {
1138 double ha = (alignment.lst() - Angle(m_currentRA, Angle::ANGLE_UNITS::HOURS)).Hours();
1139 SetAxis1Park(ha);
1140 SetAxis2Park(m_currentDEC);
1141 }
1142 return true;
1143}
1144
1146{
1147 // mount points to East (counterweights down) at the horizon
1148 // works for both hemispheres
1149 SetAxis1Park(-6.);
1150 SetAxis2Park(0.);
1151
1152 return true;
1153}
1154
1155bool ScopeSim::SetTrackMode(uint8_t mode)
1156{
1157 switch (static_cast<TelescopeTrackMode>(mode))
1158 {
1159 case TRACK_SIDEREAL:
1160 axisPrimary.TrackRate(Axis::SIDEREAL);
1161 if (m_MountType != Alignment::MOUNT_TYPE::ALTAZ)
1162 axisSecondary.TrackRate(Axis::OFF);
1163 return true;
1164 case TRACK_SOLAR:
1165 axisPrimary.TrackRate(Axis::SOLAR);
1166 if (m_MountType != Alignment::MOUNT_TYPE::ALTAZ)
1167 axisSecondary.TrackRate(Axis::OFF);
1168 return true;
1169 case TRACK_LUNAR:
1170 axisPrimary.TrackRate(Axis::LUNAR);
1171 if (m_MountType != Alignment::MOUNT_TYPE::ALTAZ)
1172 axisSecondary.TrackRate(Axis::OFF);
1173 return true;
1174 case TRACK_CUSTOM:
1175 SetTrackRate(TrackRateNP[AXIS_RA].getValue(), TrackRateNP[AXIS_DE].getValue());
1176 return true;
1177 }
1178 return false;
1179}
1180
1182{
1183 if (enabled)
1184 m_IsPipelinePrimed = false;
1185 axisPrimary.Tracking(enabled);
1186 axisSecondary.Tracking(enabled);
1187 return true;
1188}
1189
1190bool ScopeSim::SetTrackRate(double raRate, double deRate)
1191{
1192 axisPrimary.TrackingRateDegSec = Angle(raRate / 3600.0);
1193 axisSecondary.TrackingRateDegSec = Angle(deRate / 3600.0);
1194 return true;
1195}
1196
1198{
1200
1202
1203#ifdef USE_SIM_TAB
1204 GuideRateNP.save(fp);
1205 MountTypeSP.save(fp);
1206 simPierSideSP.save(fp);
1207 mountModelNP.save(fp);
1208 flipHourAngleNP.save(fp);
1209 decBacklashNP.save(fp);
1210 PACDeviceTP.save(fp);
1211
1212#endif
1213 return true;
1214}
1215
1216bool ScopeSim::updateLocation(double latitude, double longitude, double elevation)
1217{
1218 LOGF_DEBUG("Update location %8.3f, %8.3f, %4.0f", latitude, longitude, elevation);
1219
1220 alignment.latitude = Angle(latitude);
1221 alignment.longitude = Angle(longitude);
1222
1223 UpdateLocation(latitude, longitude, elevation);
1224 return true;
1225}
1226
1227bool ScopeSim::updateTime(ln_date *utc, double utc_offset)
1228{
1229 INDI_UNUSED(utc_offset);
1230
1231 // Follow the client's clock: the sky (LST, hour angle, pier side) moves on from the time the client set,
1232 // so it agrees with the client instead of the system clock
1233 alignment.clockOffsetDays = ln_get_julian_day(utc) - ln_get_julian_from_sys();
1234 LOGF_DEBUG("Simulated clock is %.3f s from the system clock", alignment.clockOffsetDays * 86400);
1235 return true;
1236}
1237
1238bool ScopeSim::updateMountAndPierSide()
1239{
1240#ifdef USE_SIM_TAB
1241 int mountType = MountTypeSP.findOnSwitchIndex();
1242 int pierSide = simPierSideSP.findOnSwitchIndex();
1243 if (mountType < 0 || pierSide < 0)
1244 return false;
1245
1246 // If nothing changed, return
1247 if (mountType == m_MountType && pierSide == m_PierSide)
1248 return true;
1249
1250 const int previousMountType = m_MountType;
1251 m_MountType = mountType;
1252 m_PierSide = pierSide;
1253
1254 LOGF_INFO("update mount and pier side: Pier Side %s, mount type %d", pierSide == 0 ? "Off" : "On", mountType);
1255#else
1256 int mountType = static_cast<int>(Alignment::MOUNT_TYPE::EQ_GEM);
1257 int pierSide = PS_ON;
1258#endif
1259 if ( mountType == Alignment::MOUNT_TYPE::ALTAZ)
1260 {
1261 LOG_INFO("AltAz mount type experimental");
1262 // return false;
1263 }
1264
1265 alignment.mountType = static_cast<Alignment::MOUNT_TYPE>(mountType);
1266
1267 // Tell the INDI alignment subsystem whether this is an equatorial or ALTAZ mount.
1268 // Without this, the math plugin always fits an ALTAZ model regardless of mount type.
1272 : INDI::AlignmentSubsystem::MathPluginManagement::EQUATORIAL);
1273
1274#ifdef USE_SIM_TAB
1275 // Sync points store the telescope direction in the encoding of the mount geometry that was active
1276 // when they were taken: Az/Alt for ALTAZ, encoder RA/Dec for the equatorial types (see Sync()).
1277 // After switching between ALTAZ and an equatorial type, those entries are decoded with the wrong
1278 // encoding and every GoTo lands at an unrelated sky position. They cannot be re-encoded reliably,
1279 // so drop them. EQ_GEM <-> EQ_FORK share the RA/Dec encoding and keep their sync points.
1280 // previousMountType is -1 during initial config load, so a restored database is never discarded.
1281 const bool wasAltAz = (previousMountType == Alignment::MOUNT_TYPE::ALTAZ);
1282 const bool isAltAz = (mountType == Alignment::MOUNT_TYPE::ALTAZ);
1283 if (previousMountType >= 0 && wasAltAz != isAltAz && !GetAlignmentDatabase().empty())
1284 {
1285 LOGF_WARN("Mount type changed between ALTAZ and equatorial: clearing %d alignment sync point(s) "
1286 "recorded for the previous mount geometry.", static_cast<int>(GetAlignmentDatabase().size()));
1287 GetAlignmentDatabase().clear();
1288 UpdateSize();
1289 Initialise(this);
1290 }
1291#endif
1292
1293 // Set the park data type appropriate for the mount geometry.
1294 if (mountType == static_cast<int>(Alignment::MOUNT_TYPE::ALTAZ))
1296 else
1298
1299 // Reinitialize the secondary axis track rate for the selected mount type.
1300 // ALTAZ: both axes need sidereal rate (overridden each tick by parabolic tracking).
1301 // EQ: primary tracks sidereal, secondary (Dec) is driven only when slewing.
1302 if (mountType == Alignment::MOUNT_TYPE::ALTAZ)
1303 axisSecondary.TrackRate(Axis::SIDEREAL);
1304 else
1305 axisSecondary.TrackRate(Axis::OFF);
1306
1307 // update the pier side capability depending on the mount type
1308 uint32_t cap = GetTelescopeCapability();
1309 if (pierSide == PS_ON && mountType == static_cast<int>(Alignment::MOUNT_TYPE::EQ_GEM))
1310 {
1312 }
1313 else
1314 {
1315 cap &= ~static_cast<uint32_t>(TELESCOPE_HAS_PIER_SIDE);
1316 }
1317 SetTelescopeCapability(cap, 4);
1318
1319 return true;
1320}
1321
1322
1324{
1325 switch (action)
1326 {
1327 case HOME_FIND:
1328 LOG_INFO("Finding home position...");
1329 m_Home[AXIS_RA] = alignment.lst().Hours();
1330 m_Home[AXIS_DE] = LocationNP[LOCATION_LATITUDE].getValue() > 0 ? 90 : -90;
1331 Goto(m_Home[AXIS_RA], m_Home[AXIS_DE]);
1332 return IPS_BUSY;
1333
1334 case HOME_SET:
1335 m_Home[AXIS_RA] = (alignment.lst() - Angle(EqNP[AXIS_RA].getValue(), Angle::HOURS)).HoursHa();
1336 m_Home[AXIS_DE] = EqNP[AXIS_DE].getValue();
1337 LOG_INFO("Setting home position to current position.");
1338 return IPS_OK;
1339
1340 case HOME_GO:
1341 {
1342 LOG_INFO("Going to home position.");
1343 if (m_Home[AXIS_RA] == 0)
1344 {
1345 m_Home[AXIS_RA] = 0;
1346 m_Home[AXIS_DE] = LocationNP[LOCATION_LATITUDE].getValue() > 0 ? 90 : -90;
1347 }
1348
1349 // Home HA to home RA
1350 auto ra = (alignment.lst() - Angle(m_Home[AXIS_RA], Angle::HOURS)).Hours();
1351 Goto(ra, m_Home[AXIS_DE]);
1352 return IPS_BUSY;
1353 }
1354
1355 default:
1356 return IPS_ALERT;
1357 }
1358 return IPS_ALERT;
1359}
1360
void setCorrections(double ih, double id, double ch, double np, double ma, double me)
setCorrections set the values of the six corrections
double julianDate()
void instrumentHaDecToMount(Angle instrumentHa, Angle instrumentDec, Angle *primary, Angle *secondary)
double clockOffsetDays
void mountToApparentRaDec(Angle primary, Angle secondary, Angle *apparentRa, Angle *apparentDec)
mountToApparentRaDec: convert mount position to apparent Ra, Dec
void setFlipHourAngle(double deg)
MOUNT_TYPE mountType
void mountToInstrumentHaDec(Angle primary, Angle secondary, Angle *instrumentHa, Angle *instrumentDec)
mountToInstrumentHaDec: convert mount axis positions to instrument (encoder) HA/Dec without applying ...
The Angle class This class implements an angle type. This holds an angle that is always in the range ...
double radians()
radians
double Hours()
Hours.
double Degrees()
Degrees.
Angle TrackingRateDegSec
TrackingRateDegSec.
bool isSlewing
const char * axisName
void setDegrees(double degrees)
void StartGuide(double rate, uint32_t durationMs)
StartGuide start guiding.
void Abort()
void Tracking(bool enabled)
void StartSlew(Angle angle)
void update()
Angle position
bool IsGuiding()
void TrackRate(AXIS_TRACK_RATE rate)
TrackRate set the track rate to one of the standard rates.
void ProcessAlignmentNumberProperties(Telescope *pTelescope, const char *name, double values[], char *names[], int n)
Call this function from within the ISNewNumber processing path. The function will handle any alignmen...
bool SkyToTelescopeAltAz(double actualRA, double actualDec, double &mountAlt, double &mountAz, double JD=ln_get_julian_from_sys())
Converts an actual sky location to coordinates to send to the mount, usually called in Goto.
void ProcessAlignmentSwitchProperties(Telescope *pTelescope, const char *name, ISState *states, char *names[], int n)
Call this function from within the ISNewSwitch processing path. The function will handle any alignmen...
void InitAlignmentProperties(Telescope *pTelescope)
Initialize alignment subsystem properties. It is recommended to call this function within initPropert...
bool TelescopeAltAzToSky(double mountAlt, double mountAz, double &actualRA, double &actualDec, double JD=ln_get_julian_from_sys())
Converts a mount location to actual sky coordinates, usually called in ReadScopeStatus.
bool AddAlignmentEntryAltAz(double actualRA, double actualDec, double mountAlt, double mountAz, double JD=ln_get_julian_from_sys())
Adds an alignment point to the model database, usually called from Sync.
void SaveAlignmentConfigProperties(FILE *fp)
Call this function to save persistent alignment related properties. This function should be called fr...
void ProcessAlignmentTextProperties(Telescope *pTelescope, const char *name, char *texts[], char *names[], int n)
Call this function from within the ISNewText processing path. The function will handle any alignment ...
void RemoveSyncPoint(const AlignmentDatabaseEntry &CandidateEntry, double Tolerance=0.1)
Remove a sync point that falls within the tolerance of a candidate point.
AlignmentDatabaseType & GetAlignmentDatabase()
Get a reference to the in memory database.
bool GetDatabaseReferencePosition(IGeographicCoordinates &Position)
Get the database reference position.
void UpdateLocation(double latitude, double longitude, double elevation)
Call this function from within the updateLocation processing path.
void UpdateSize()
Call this function when the number of entries in the database changes.
The following INDI properties are used to manage math plugins.
bool TransformTelescopeToCelestialJD(const TelescopeDirectionVector &ApparentTelescopeDirectionVector, double &RightAscension, double &Declination, double JulianDate)
TransformTelescopeToCelestialJD Transforms Mount Coords to Celestial (Sky) Coordinates.
bool TransformCelestialToTelescopeJD(double RightAscension, double Declination, double JulianDate, TelescopeDirectionVector &ApparentTelescopeDirectionVector)
TransformCelestialToTelescopeJD Transforms Celestial (Sky) Coords to Mount Coordinates.
bool Initialise(InMemoryDatabase *pInMemoryDatabase)
Initialise or re-initialise the math plugin. Re-reading the in memory database as necessary.
void SetAlignmentSubsystemActive(bool enable)
SetAlignmentSubsystemActive Enable or Disable alignment subsystem.
void SetApproximateMountAlignmentFromMountType(MountType_t Type)
Call this function to set the ApproximateMountAlignment property of the current Math Plugin....
const TelescopeDirectionVector TelescopeDirectionVectorFromAltitudeAzimuth(INDI::IHorizontalCoordinates HorizontalCoordinates)
Calculates a normalised direction vector from the supplied altitude and azimuth.
void EquatorialCoordinatesFromTelescopeDirectionVector(const TelescopeDirectionVector TelescopeDirectionVector, INDI::IEquatorialCoordinates &EquatorialCoordinates)
Calculates equatorial coordinates from the supplied telescope direction vector and declination.
const TelescopeDirectionVector TelescopeDirectionVectorFromEquatorialCoordinates(INDI::IEquatorialCoordinates EquatorialCoordinates)
Calculates a telescope direction vector from the supplied equatorial coordinates.
bool isConnected() const
virtual bool saveConfig(bool silent=false, const char *property=nullptr)
Save the current properties in a configuration file.
void setDefaultPollingPeriod(uint32_t msec)
setDefaultPollingPeriod Change the default polling period to call TimerHit() function in the driver.
void setDriverInterface(uint32_t value)
setInterface Set driver interface. By default the driver interface is set to GENERAL_DEVICE....
virtual bool deleteProperty(const char *propertyName)
Delete a property and unregister it. It will also be deleted from all clients.
void defineProperty(INumberVectorProperty *property)
uint32_t getCurrentPollingPeriod() const
getCurrentPollingPeriod Return the current polling period.
uint32_t getDriverInterface() const
int SetTimer(uint32_t ms)
Set a timer to call the function TimerHit after ms milliseconds.
void addDebugControl()
Add Debug control to the driver.
INDI::PropertyNumber GuideNSNP
virtual void GuideComplete(INDI_EQ_AXIS axis)
Call GuideComplete once the guiding pulse is complete.
bool updateProperties()
updateProperties Define or Delete Rotator properties based on the connection status of the base devic...
INDI::PropertyNumber GuideWENP
void initProperties(const char *groupName)
Initialize guider properties. It is recommended to call this function within initProperties() of your...
bool processNumber(const char *dev, const char *name, double values[], char *names[], int n)
Call this function whenever client updates GuideNSNP or GuideWSP properties in the primary device....
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
bool load()
load Attempt to load property values from configuration file.
void save(FILE *f) const
save Save property to configuration file.
bool isNameMatch(const char *otherName) const
bool isUpdated(const double values[], const char *const names[], int n) const
bool update(const double values[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
bool update(const ISState states[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, ISRule rule, double timeout, IPState state)
bool update(const char *const texts[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
INDI::PropertyViewText * getText() const
TelescopeStatus TrackState
void SetAxis1Park(double value)
SetRAPark Set current RA/AZ parking position. The data park file (stored in ~/.indi/ParkData....
void SetAxis1ParkDefault(double steps)
SetRAPark Set default RA/AZ parking position.
void SetTelescopeCapability(uint32_t cap, uint8_t slewRateCount)
SetTelescopeCapability sets the Telescope capabilities. All capabilities must be initialized.
virtual bool initProperties() override
Called to initialize basic properties required all the time.
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
double GetAxis1Park() const
virtual void ISGetProperties(const char *dev) override
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
double GetAxis2Park() const
bool isParked()
isParked is mount currently parked?
virtual int AddTrackMode(const char *name, const char *label, bool isDefault=false)
AddTrackMode.
virtual bool updateProperties() override
Called when connected state changes, to add/remove properties.
virtual void SetParked(bool isparked)
SetParked Change the mount parking status. The data park file (stored in ~/.indi/ParkData....
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
virtual bool ISSnoopDevice(XMLEle *root) override
Process a snoop event from INDI server. This function is called when a snooped property is updated in...
IGeographicCoordinates m_Location
uint32_t GetTelescopeCapability() const
GetTelescopeCapability returns the capability of the Telescope.
void NewRaDec(double ra, double dec)
The child class calls this function when it has updates.
void setPierSide(TelescopePierSide side)
bool InitPark()
InitPark Loads parking data (stored in ~/.indi/ParkData.xml) that contains parking status and parking...
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
void SetAxis2Park(double steps)
SetDEPark Set current DEC/ALT parking position. The data park file (stored in ~/.indi/ParkData....
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
void SetParkDataType(TelescopeParkData type)
setParkDataType Sets the type of parking data stored in the park data file and presented to the user.
void SetAxis2ParkDefault(double steps)
SetDEParkDefault Set default DEC/ALT parking position.
The ScopeSim class provides a simple mount simulator of an equatorial mount.
virtual bool ReadScopeStatus() override
Read telescope status.
virtual bool SetTrackMode(uint8_t mode) override
SetTrackMode Set active tracking mode. Do not change track state.
virtual bool Abort() override
Abort any telescope motion including tracking if possible.
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
virtual IPState ExecuteHomeAction(TelescopeHomeAction action) override
Execute a homing action.
virtual const char * getDefaultName() override
virtual bool Disconnect() override
Disconnect from device.
virtual bool Sync(double ra, double dec) override
Set the telescope current RA and DEC coordinates to the supplied RA and DEC coordinates.
virtual bool updateTime(ln_date *utc, double utc_offset) override
Update telescope time, date, and UTC offset.
virtual bool SetCurrentPark() override
SetCurrentPark Set current coordinates/encoders value as the desired parking position.
virtual IPState GuideSouth(uint32_t ms) override
Guide south for ms milliseconds. South is defined as DEC-.
virtual bool Goto(double, double) override
Move the scope to the supplied RA and DEC coordinates.
virtual void ISGetProperties(const char *dev) override
define the driver's properties to the client. Usually, only a minimum set of properties are defined t...
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
virtual bool Park() override
Park the telescope to its home position.
virtual bool SetDefaultPark() override
SetDefaultPark Set default coordinates/encoders value as the desired parking position.
virtual bool updateProperties() override
Called when connected state changes, to add/remove properties.
virtual bool Connect() override
Connect to the device. INDI::DefaultDevice implementation connects to appropriate connection interfac...
virtual IPState GuideWest(uint32_t ms) override
Guide west for ms milliseconds. West is defined as RA-.
virtual IPState GuideNorth(uint32_t ms) override
Guide north for ms milliseconds. North is defined as DEC+.
virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override
Move the telescope in the direction dir.
virtual bool SetTrackEnabled(bool enabled) override
SetTrackEnabled Engages or disengages mount tracking. If there are no tracking modes available,...
virtual bool updateLocation(double latitude, double longitude, double elevation) override
Update telescope location settings.
virtual bool SetTrackRate(double raRate, double deRate) override
SetTrackRate Set custom tracking rates.
virtual bool ISSnoopDevice(XMLEle *root) override
Process a snoop event from INDI server. This function is called when a snooped property is updated in...
virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override
Start or Stop the telescope motion in the direction dir.
virtual bool initProperties() override
Called to initialize basic properties required all the time.
virtual bool UnPark() override
Unpark the telescope if already parked.
virtual IPState GuideEast(uint32_t ms) override
Guide east for ms milliseconds. East is defined as RA+.
const char * MAIN_CONTROL_TAB
MAIN_CONTROL_TAB Where all the primary controls for the device are located.
const char * MOTION_TAB
MOTION_TAB Where all the motion control properties of the device are located.
#define M_PI
Definition dsp.h:52
TelescopePierSide currentPierSide
INDI::PropertyNumber TrackRateNP
INDI::PropertyNumber EqNP
INDI::PropertySwitch HomeSP
INDI::PropertySwitch SlewRateSP
INDI::PropertySwitch TrackModeSP
INDI::PropertySwitch MountTypeSP
INDI::PropertyNumber LocationNP
INDI::PropertyNumber ParkPositionNP
double ra
double dec
#define RA_AXIS
#define DEC_AXIS
ISState
Switch state.
Definition indiapi.h:148
@ ISS_OFF
Definition indiapi.h:149
@ ISS_ON
Definition indiapi.h:150
@ IP_RW
Definition indiapi.h:184
@ IP_RO
Definition indiapi.h:182
@ IP_WO
Definition indiapi.h:183
IPState
Property state.
Definition indiapi.h:158
@ IPS_BUSY
Definition indiapi.h:161
@ IPS_ALERT
Definition indiapi.h:162
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
@ ISR_1OFMANY
Definition indiapi.h:171
@ AXIS_DE
@ AXIS_RA
INDI_DIR_WE
@ DIRECTION_EAST
INDI_DIR_NS
@ DIRECTION_NORTH
double range180(double r)
range180 Limits an angle to be between -180 to +180 degrees
Definition indicom.c:1331
int fs_sexa(char *out, double a, int w, int fracbase)
Converts a sexagesimal number to a string. sprint the variable a in sexagesimal format into out[].
Definition indicom.c:130
Implementations for common driver routines.
#define INDI_UNUSED(x)
Definition indidevapi.h:132
void IDSnoopDevice(const char *snooped_device, const char *snooped_property)
Function a Driver calls to snoop on another Device. Snooped messages will then arrive via ISSnoopDevi...
Definition indidriver.c:143
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOGF_WARN(fmt,...)
Definition indilogger.h:81
#define LOG_WARN(txt)
Definition indilogger.h:73
#define LOGF_DEBUG(fmt,...)
Definition indilogger.h:83
#define LOG_ERROR(txt)
Shorter logging macros. In order to use these macros, the function (or method) "getDeviceName()" must...
Definition indilogger.h:72
#define LOG_INFO(txt)
Definition indilogger.h:74
#define LOGF_EXTRA1(fmt,...)
Definition indilogger.h:84
#define DEBUGF(priority, msg,...)
Definition indilogger.h:57
#define TRACKRATE_SOLAR
Definition indimacros.h:45
#define TRACKRATE_SIDEREAL
Definition indimacros.h:39
#define TRACKRATE_LUNAR
Definition indimacros.h:48
#define getDeviceName
INumber eq[]
const char * findXMLAttValu(XMLEle *ep, const char *name)
Find an XML element's attribute value.
Definition lilxml.cpp:644
XMLEle * nextXMLEle(XMLEle *ep, int init)
Iterate an XML element for a list of nesetd XML elements.
Definition lilxml.cpp:555
char * pcdataXMLEle(XMLEle *ep)
Return the pcdata of an XML element.
Definition lilxml.cpp:606
A little DOM-style library to handle parsing and processing an XML file.
Namespace to encapsulate the INDI Alignment Subsystem classes. For more information see "INDI Alignme...
Namespace to encapsulate INDI client, drivers, and mediator classes.
void EquatorialToHorizontal(IEquatorialCoordinates *object, IGeographicCoordinates *observer, double JD, IHorizontalCoordinates *position)
EquatorialToHorizontal Calculate horizontal coordinates from equatorial coordinates.
Definition libastro.cpp:141
Entry in the in memory alignment database.
Definition Common.h:152
double RightAscension
Right ascension in decimal hours. N.B. libnova works in decimal degrees so conversion is always neede...
Definition Common.h:190
TelescopeDirectionVector TelescopeDirection
Normalised vector giving telescope pointing direction. This is referred to elsewhere as the "apparent...
Definition Common.h:197
double Declination
Declination in decimal degrees.
Definition Common.h:193
int PrivateDataSize
This size in bytes of any private data.
Definition Common.h:203
Holds a normalised direction vector (direction cosines)
Definition Common.h:69
static Logger & getInstance()
Method to get a reference to the object (i.e., Singleton) It is a static method.
int addDebugLevel(const char *debugLevelName, const char *LoggingLevelName)
Adds a new debugging level to the driver.