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
259
260#ifdef USE_SIM_TAB
261 const char *pacDevice = PACDeviceTP[0].getText();
262 if (pacDevice && strlen(pacDevice) > 0)
263 IDSnoopDevice(pacDevice, "PAC_MANUAL_ADJUSTMENT");
264#endif
265
266 defineProperty(EqPENP);
268 }
269 else
270 {
271 deleteProperty(EqPENP);
272 deleteProperty(GuideRateNP);
273 }
274
276
277 return true;
278}
279
281{
282 LOG_INFO("Telescope simulator is online.");
284
285 return true;
286}
287
289{
290 LOG_INFO("Telescope simulator is offline.");
291 return true;
292}
293
294
295void ScopeSim::updateCurrentCoordsFromAxes()
296{
297 Angle instHA, instDec;
298 alignment.mountToInstrumentHaDec(axisPrimary.position, axisSecondary.position, &instHA, &instDec);
299 m_currentRA = (alignment.lst() - instHA).Hours();
300 m_currentDEC = instDec.Degrees();
301
302 if (GetAlignmentDatabase().size() >= 1)
303 {
304 double corrRA, corrDec;
305 bool corrected = false;
306 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
307 {
308 corrected = TelescopeAltAzToSky(axisSecondary.position.Degrees(),
309 axisPrimary.position.Degrees(), corrRA, corrDec);
310 }
311 else
312 {
313 // Encode the current encoder RA (= LST − HA), not HA, so the TDV
314 // fed into TransformTelescopeToCelestial uses the same time-stable
315 // coordinate that Sync() now stores in the alignment database.
316 double encoderRA = (alignment.lst() - instHA).Hours();
317 INDI::IEquatorialCoordinates raCoords{ encoderRA, instDec.Degrees() };
320 corrected = TransformTelescopeToCelestialJD(tdv, corrRA, corrDec, ln_get_julian_from_sys());
321 }
322 if (corrected)
323 {
324 m_currentRA = corrRA;
325 m_currentDEC = corrDec;
326 }
327 }
328}
329
330void ScopeSim::setTargetFromAxisPosition(Angle primary, Angle secondary)
331{
332 Angle instHA, instDec;
333 alignment.mountToInstrumentHaDec(primary, secondary, &instHA, &instDec);
334 m_targetRA = (alignment.lst() - instHA).Hours();
335 m_targetDEC = instDec.Degrees();
336}
337
339{
340 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ && TrackState == SCOPE_TRACKING &&
342 {
343 double dt = getCurrentPollingPeriod() / 1000.0;
344 double JDnow = ln_get_julian_from_sys();
345 double JDoffset = dt / 86400.0;
346
347 // RA drift rate relative to sidereal for the selected track mode, in hours/sec.
348 // Nudging RA at each parabola sample by the non-sidereal offset makes the
349 // derived Az/Alt rates correct for the track mode without changing the meaning
350 // of m_targetRA/Dec. Mirrors EQ behaviour: RA is corrected, Dec is not.
351 int trackMode = TrackModeSP.findOnSwitchIndex();
352 double raDriftHrsPerSec = 0;
353 if (trackMode == TRACK_SOLAR)
354 raDriftHrsPerSec = (TRACKRATE_SIDEREAL - TRACKRATE_SOLAR) / 3600.0 / 15.0;
355 else if (trackMode == TRACK_LUNAR)
356 raDriftHrsPerSec = (TRACKRATE_SIDEREAL - TRACKRATE_LUNAR) / 3600.0 / 15.0;
357
358 auto getAltAz = [&](double JD, INDI::IHorizontalCoordinates & coords)
359 {
360 double dtFromNow = (JD - JDnow) * 86400.0;
361 INDI::IEquatorialCoordinates eq { m_targetRA + raDriftHrsPerSec * dtFromNow, m_targetDEC };
363 };
364
365 bool targetChanged = std::abs(m_LastTrackingRA - m_targetRA) > 1e-6 ||
366 std::abs(m_LastTrackingDec - m_targetDEC) > 1e-6;
367
368 if (!m_IsPipelinePrimed || targetChanged)
369 {
370 getAltAz(JDnow - JDoffset, m_TrackingWindowCoords[0]);
371 getAltAz(JDnow, m_TrackingWindowCoords[1]);
372 getAltAz(JDnow + JDoffset, m_TrackingWindowCoords[2]);
373 m_IsPipelinePrimed = true;
374 m_LastTrackingRA = m_targetRA;
375 m_LastTrackingDec = m_targetDEC;
376 }
377 else
378 {
379 m_TrackingWindowCoords[0] = m_TrackingWindowCoords[1];
380 m_TrackingWindowCoords[1] = m_TrackingWindowCoords[2];
381 getAltAz(JDnow + JDoffset, m_TrackingWindowCoords[2]);
382 }
383
384 // Unwrap azimuth around the center point to avoid 360/0 discontinuity
385 // Angle(x).Degrees() normalizes x to (-180, +180], same as the private Angle::range()
386 auto range180 = [](double x)
387 {
388 return Angle(x).Degrees();
389 };
390 double pAz[3] = { m_TrackingWindowCoords[0].azimuth,
391 m_TrackingWindowCoords[1].azimuth,
392 m_TrackingWindowCoords[2].azimuth
393 };
394 pAz[0] = pAz[1] + range180(pAz[0] - pAz[1]);
395 pAz[2] = pAz[1] + range180(pAz[2] - pAz[1]);
396
397 // Parabolic coefficients (t in normalized units; points at t = -1, 0, +1)
398 double az_a = (pAz[2] + pAz[0] - 2.0 * pAz[1]) / 2.0;
399 double az_b = (pAz[2] - pAz[0]) / 2.0;
400 double alt_a = (m_TrackingWindowCoords[2].altitude + m_TrackingWindowCoords[0].altitude
401 - 2.0 * m_TrackingWindowCoords[1].altitude) / 2.0;
402 double alt_b = (m_TrackingWindowCoords[2].altitude - m_TrackingWindowCoords[0].altitude) / 2.0;
403
404 // Predicted position at next tick (t = +1 normalized = +dt seconds from now)
405 double targetAzNext = az_a + az_b + pAz[1];
406 double targetAltNext = alt_a + alt_b + m_TrackingWindowCoords[1].altitude;
407
408 // Rate to close the gap in exactly dt seconds, converted to arcsec/s for SetTrackRate
409 double azRate = range180(targetAzNext - m_currentAz) / dt * 3600.0;
410 double altRate = (targetAltNext - m_currentAlt) / dt * 3600.0;
411
412 SetTrackRate(azRate, altRate);
413 }
414
415 // new mechanical angle calculation
416 axisPrimary.update();
417 axisSecondary.update();
418
419 // True pointing: apply Wallace error injection to get where the scope physically points.
420 // Published as EQUATORIAL_PE so the CCD simulator generates star fields at this position.
421 Angle trueRa, trueDec;
422 alignment.mountToApparentRaDec(axisPrimary.position, axisSecondary.position, &trueRa, &trueDec);
423 EqPENP[PE_RA].setValue(trueRa.Hours());
424 EqPENP[PE_DEC].setValue(trueDec.Degrees());
425 EqPENP.setState(IPS_OK);
426 EqPENP.apply();
427
428 // Raw encoder position: what the mount "reports" without knowing its own errors.
429 // This is what EQUATORIAL_EOD_COORD carries and what the INDI alignment module uses.
430 // Apply INDI alignment correction if sync points exist.
431 updateCurrentCoordsFromAxes();
432
433 // Az/Alt for the ALTAZ tracking servo. The servo (top of ReadScopeStatus) drives the
434 // axis motors; it must compare axis-space positions against axis-space targets. Using
435 // Wallace-corrected (true) Az/Alt here would inject the Wallace error as a permanent
436 // position offset, causing the servo to oscillate every tick. Use raw axis positions.
437 // axisPrimary stores INDI Az (0=North through East), same convention as m_currentAz.
438 m_currentAz = axisPrimary.position.Degrees();
439 m_currentAlt = axisSecondary.position.Degrees();
440
441 // update properties from the axis
443 {
444 setPierSide(fabs(axisSecondary.position.Degrees()) > 90 ? PIER_WEST : PIER_EAST);
445 }
446
447 bool slewing = axisPrimary.isSlewing || axisSecondary.isSlewing;
448 switch (TrackState)
449 {
450 case SCOPE_PARKING:
451 if (!slewing)
452 {
453 SetParked(true);
455 LOG_INFO("Telescope slew is complete. Parked");
456 }
457 break;
458 case SCOPE_SLEWING:
459 if (!slewing)
460 {
461 // It seems to be required that tracking is enabled when a slew finishes but is it correct?
462 // if the mount was not tracking before the slew should it remain not tracking?
464 SetTrackEnabled(true);
466
467 if (HomeSP.getState() == IPS_BUSY)
468 {
469 HomeSP.reset();
471 HomeSP.apply();
472 LOG_INFO("Home position reached.");
473 }
474 else
475 LOG_INFO("Telescope slew is complete. Tracking...");
476
477 // Check slew accuracy in axis space so the result is frame-independent:
478 // m_targetRA/Dec are instrument-frame; m_currentRA/Dec may be alignment-corrected.
479 {
480 Angle targetHa = alignment.lst() - Angle(m_targetRA * 15.0);
481 Angle tPrimary, tSecondary;
482 alignment.instrumentHaDecToMount(targetHa, Angle(m_targetDEC), &tPrimary, &tSecondary);
483 double dPri = (axisPrimary.position - tPrimary).Degrees();
484 double dSec = (axisSecondary.position - tSecondary).Degrees();
485 LOGF_DEBUG("slew accuracy pri %f arcsec, sec %f arcsec", dPri * 3600, dSec * 3600);
486 }
487
488 }
489 break;
490 case SCOPE_PARKED:
491 return true;
492 default:
493 break;
494 }
495
496 if (guidingEW && !axisPrimary.IsGuiding())
497 {
498 GuideWENP[0].setValue(0);
499 GuideWENP[1].setValue(0);
501 guidingEW = false;
502 }
503
504 if (guidingNS && !axisSecondary.IsGuiding())
505 {
506 GuideNSNP[0].setValue(0);
507 GuideNSNP[1].setValue(0);
509 guidingNS = false;
510 }
511
512#ifdef USE_SIM_TAB
513 double PrimaryAngle = axisPrimary.position.Degrees();
514 double SecondaryAngle = axisSecondary.position.Degrees();
515 // No need to spam log until we have some actual changes.
516 if (std::fabs(mountAxisNP[PRIMARY].getValue() - PrimaryAngle) > 0.0001 ||
517 std::fabs(mountAxisNP[SECONDARY].getValue() - SecondaryAngle) > 0.0001)
518 {
519 mountAxisNP[PRIMARY].setValue(PrimaryAngle);
520 mountAxisNP[SECONDARY].setValue(SecondaryAngle);
521
522 LOGF_EXTRA1("new %s: %f, ra %f", axisPrimary.axisName, PrimaryAngle, trueRa.Hours());
523 LOGF_EXTRA1("new %s: %f, dec %f", axisSecondary.axisName, SecondaryAngle, trueDec.Degrees());
524
525 mountAxisNP.apply();
526 }
527#endif
528
529 char RAStr[64], DecStr[64];
530 fs_sexa(RAStr, m_currentRA, 2, 3600);
531 fs_sexa(DecStr, m_currentDEC, 2, 3600);
532 DEBUGF(DBG_SCOPE, "Current RA: %s Current DEC: %s", RAStr, DecStr);
533
534 NewRaDec(m_currentRA, m_currentDEC);
535
536 return true;
537}
538
539bool ScopeSim::Goto(double r, double d)
540{
541 // When the INDI alignment subsystem has learned sync points, transform the celestial
542 // target through the alignment database so the mount ends up at the right sky position
543 // despite the injected mount errors.
544 //
545 // ALTAZ mounts: the math plugin uses Az/Alt encoding. Decode via SkyToTelescopeAltAz
546 // and set axis slew targets directly in Az/Alt space.
547 //
548 // EQ mounts: the math plugin uses RA/Dec encoding. Decode via
549 // EquatorialCoordinatesFromTelescopeDirectionVector to obtain encoder RA directly,
550 // then pass it to StartSlew which converts it to HA for the axis motor.
551 if (GetAlignmentDatabase().size() >= 1 && m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
552 {
553 double instrumentAlt, instrumentAz_INDI;
554 if (SkyToTelescopeAltAz(r, d, instrumentAlt, instrumentAz_INDI))
555 {
556 axisPrimary.StartSlew(Angle(instrumentAz_INDI));
557 axisSecondary.StartSlew(Angle(instrumentAlt));
558
559 // m_targetRA/Dec must be the instrument position (not celestial) so the ALTAZ
560 // tracking servo's getAltAz() computes instrument Az/Alt rates, not celestial.
561 setTargetFromAxisPosition(Angle(instrumentAz_INDI), Angle(instrumentAlt));
562
565 LOGF_INFO("Slewing to RA: %.4f Dec: %.4f", m_targetRA, m_targetDEC);
566 return true;
567 }
568 }
569
570 double targetRA = r, targetDec = d;
571 if (GetAlignmentDatabase().size() >= 1)
572 {
574 if (TransformCelestialToTelescopeJD(r, d, ln_get_julian_from_sys(), tdv))
575 {
576 // The TDV now encodes encoder RA (time-stable), so decode with
577 // EquatorialCoordinatesFromTelescopeDirectionVector — no LST conversion needed.
580 targetRA = raCoords.rightascension;
581 targetDec = raCoords.declination;
582 LOGF_INFO("EQ Goto: alignment corrected RA %.4fh Dec %.4f -> RA %.4fh Dec %.4f",
583 r, d, targetRA, targetDec);
584 }
585 else
586 {
587 LOG_WARN("EQ Goto: TransformCelestialToTelescope FAILED, using raw coordinates");
588 }
589 }
590
591 StartSlew(targetRA, targetDec, SCOPE_SLEWING);
592 return true;
593}
594
595bool ScopeSim::Sync(double ra, double dec)
596{
597 // The INDI alignment math plugins encode telescope directions as:
598 // - ALTAZ (ZENITH) mounts: Az/Alt via TelescopeDirectionVectorFromAltitudeAzimuth
599 // - EQ mounts: RA/Dec via TelescopeDirectionVectorFromEquatorialCoordinates
600 // Using encoder RA (= LST − HA, time-stable during sidereal tracking) ensures the
601 // stored offset sky_RA − encoder_RA remains constant across iterations, so successive
602 // sync+goto cycles converge rather than diverging.
603
605 entry.ObservationJulianDate = ln_get_julian_from_sys();
606 entry.RightAscension = ra;
607 entry.Declination = dec;
608 entry.PrivateDataSize = 0;
609
610 const bool altAz = (m_MountType == Alignment::MOUNT_TYPE::ALTAZ);
611 const double indi_az = axisPrimary.position.Degrees();
612 const double indi_alt = axisSecondary.position.Degrees();
613 if (altAz)
614 {
615 INDI::IHorizontalCoordinates altAzCoords { indi_az, indi_alt };
617 }
618 else
619 {
620 // EQ mounts: encode the raw encoder RA/Dec as the telescope direction.
621 // Use RA (= LST − HA), not HA, so the stored offset is time-stable.
622 // HA changes at the sidereal rate even during tracking; encoder RA does not.
623 // Storing HA causes the correction to drift by ~15 arcsec/s, accumulating
624 // error over successive alignment iterations.
625 Angle instHA, instDec;
626 alignment.mountToInstrumentHaDec(axisPrimary.position, axisSecondary.position,
627 &instHA, &instDec);
628
629 double encoderRA = (alignment.lst() - instHA).Hours();
630 INDI::IEquatorialCoordinates raCoords{ encoderRA, instDec.Degrees() };
632 }
633
634 // A new sync supersedes older sync points at (nearly) the same place: the latest plate solve is the
635 // better measurement. Previously the new point was silently dropped as a duplicate while this function
636 // still reported success, so re-syncing near an existing point had no effect on the pointing model.
637 // ALTAZ entries need the geographic reference position (see AddAlignmentEntryAltAz); without it
638 // nothing is stored and the existing points are kept.
639 INDI::IGeographicCoordinates referencePosition;
640 if (altAz && !GetDatabaseReferencePosition(referencePosition))
641 {
642 LOG_WARN("Sync not stored in the alignment database: geographic location is not known yet.");
643 }
644 else
645 {
646 const size_t before = GetAlignmentDatabase().size();
647 RemoveSyncPoint(entry);
648 const int replaced = static_cast<int>(before - GetAlignmentDatabase().size());
649
650 if (altAz)
651 AddAlignmentEntryAltAz(ra, dec, indi_alt, indi_az); // handles DB + Initialise
652 else
653 {
654 GetAlignmentDatabase().push_back(entry);
655 UpdateSize();
656 Initialise(this);
657 }
658
659 if (replaced > 0)
660 LOGF_INFO("Sync replaced %d older alignment point(s) at the same position.", replaced);
661 }
662
663 // Keep the ALTAZ tracking servo on the current instrument position after sync.
664 setTargetFromAxisPosition(axisPrimary.position, axisSecondary.position);
665
666 LOGF_DEBUG("Sync at RA %f Dec %f m_targetRA %f m_targetDEC %f",
667 ra, dec, m_targetRA, m_targetDEC);
668
669 LOG_INFO("Sync is successful.");
670
672 NewRaDec(m_currentRA, m_currentDEC);
673
674 return true;
675}
676
678{
679 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
680 {
681 // For ALTAZ, park position is stored as INDI Az (degrees) / Alt (degrees).
682 // Convert directly to axis positions and slew; do not go through StartSlew's
683 // HA/Dec path which treats Axis1 as hours.
684 Angle indi_az = Angle(GetAxis1Park());
685 Angle alt = Angle(GetAxis2Park());
686 axisPrimary.StartSlew(indi_az);
687 axisSecondary.StartSlew(alt);
688 setTargetFromAxisPosition(indi_az, alt);
691 LOG_INFO("Parking...");
692 }
693 else
694 {
695 double ra = (alignment.lst() - Angle(GetAxis1Park() * 15.)).Degrees() / 15.;
696 StartSlew(ra, GetAxis2Park(), SCOPE_PARKING);
697 }
698 return true;
699}
700
701// common code for GoTo and park
702void ScopeSim::StartSlew(double ra, double dec, TelescopeStatus status)
703{
704 // Naive axis conversion: no Wallace corrections. The mount slews to where its
705 // encoders say, unaware of its own IH/ID/etc. errors — just like a real mount.
706 Angle ha = alignment.lst() - Angle(ra * 15.0);
707 Angle primary, secondary;
708 alignment.instrumentHaDecToMount(ha, Angle(dec), &primary, &secondary);
709
710 axisPrimary.StartSlew(primary);
711 axisSecondary.StartSlew(secondary);
712
713 // Store instrument-frame RA/Dec so the ALTAZ tracking servo always works
714 // in a consistent coordinate frame, regardless of how StartSlew was called.
715 setTargetFromAxisPosition(primary, secondary);
716 char RAStr[64], DecStr[64];
717
718 fs_sexa(RAStr, m_targetRA, 2, 3600);
719 fs_sexa(DecStr, m_targetDEC, 2, 3600);
720
721 const char * statusStr;
722 switch (status)
723 {
724 case SCOPE_PARKING:
725 statusStr = "Parking";
726 break;
727 case SCOPE_SLEWING:
728 statusStr = "Slewing";
729 break;
730 default:
731 statusStr = "unknown";
732 }
733 TrackState = status;
734
735 LOGF_INFO("%s to RA: %s - DEC: %s", statusStr, RAStr, DecStr);
736}
737
739{
740 SetParked(false);
741 return true;
742}
743
744bool ScopeSim::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
745{
746 // Check guider interface
747 if (GI::processNumber(dev, name, values, names, n))
748 return true;
749
750 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
751 {
752 if (strcmp(name, "GUIDE_RATE") == 0)
753 {
754 GuideRateNP.update(values, names, n);
755 GuideRateNP.setState(IPS_OK);
756 GuideRateNP.apply();
757 return true;
758 }
759
760#ifdef USE_SIM_TAB
761 if (mountModelNP.isNameMatch(name))
762 {
763 if (mountModelNP.isUpdated(values, names, n))
764 {
765 mountModelNP.update(values, names, n);
766 alignment.setCorrections(mountModelNP[MM_IH].getValue() / 60.0, mountModelNP[MM_ID].getValue() / 60.0,
767 mountModelNP[MM_CH].getValue() / 60.0, mountModelNP[MM_NP].getValue() / 60.0,
768 mountModelNP[MM_MA].getValue() / 60.0, mountModelNP[MM_ME].getValue() / 60.0);
769 saveConfig(mountModelNP);
770 }
771 mountModelNP.setState(IPS_OK);
772 mountModelNP.apply();
773 return true;
774 }
775
776 if (flipHourAngleNP.isNameMatch(name))
777 {
778 flipHourAngleNP.update(values, names, n);
779 flipHourAngleNP.setState(IPS_OK);
780 flipHourAngleNP.apply();
781 alignment.setFlipHourAngle(flipHourAngleNP[0].getValue());
782 return true;
783 }
784 if (decBacklashNP.isNameMatch(name))
785 {
786 decBacklashNP.update(values, names, n);
787 decBacklashNP.setState(IPS_OK);
788 decBacklashNP.apply();
789 m_DecGuideBacklashMs = decBacklashNP[0].getValue();
790 return true;
791 }
792#endif
793 }
794
795 ProcessAlignmentNumberProperties(this, name, values, names, n);
796
797 // if we didn't process it, continue up the chain, let somebody else
798 // give it a shot
799 return INDI::Telescope::ISNewNumber(dev, name, values, names, n);
800}
801
802bool ScopeSim::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
803{
804 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
805 {
806#ifdef USE_SIM_TAB
807 if (MountTypeSP.isNameMatch(name))
808 {
809 if (!MountTypeSP.update(states, names, n))
810 return false;
811
814 updateMountAndPierSide();
816 return true;
817 }
818 if (simPierSideSP.isNameMatch(name))
819 {
820 if (!simPierSideSP.update(states, names, n))
821 return false;
822
823 simPierSideSP.setState(IPS_OK);
824 simPierSideSP.apply();
825 updateMountAndPierSide();
826 return true;
827 }
828#endif \
829 // Slew mode
830 if (SlewRateSP.isNameMatch(name))
831 {
832 if (SlewRateSP.update( states, names, n) == false)
833 return false;
834
837 return true;
838 }
839 }
840
841 ProcessAlignmentSwitchProperties(this, name, states, names, n);
842
843 // Persist alignment plugin and active state immediately when changed.
844 // Use a full saveConfig() — the selective form (saveConfig(true, name)) only
845 // patches the named property in the existing XML and skips CURRENT_MATH_PLUGIN.
846 if (strcmp(name, "ALIGNMENT_SUBSYSTEM_MATH_PLUGINS") == 0 ||
847 strcmp(name, "ALIGNMENT_SUBSYSTEM_ACTIVE") == 0)
848 saveConfig();
849
850 // Nobody has claimed this, so pass it over
851 return INDI::Telescope::ISNewSwitch(dev, name, states, names, n);
852}
853
854bool ScopeSim::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
855{
856#ifdef USE_SIM_TAB
857 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
858 {
859 if (PACDeviceTP.isNameMatch(name))
860 {
861 PACDeviceTP.setState(IPS_OK);
862 PACDeviceTP.update(texts, names, n);
863 PACDeviceTP.apply();
864
865 const char *pacDevice = PACDeviceTP[0].getText();
866 if (pacDevice && strlen(pacDevice) > 0)
867 IDSnoopDevice(pacDevice, "PAC_MANUAL_ADJUSTMENT");
868
869 saveConfig(PACDeviceTP);
870 return true;
871 }
872 }
873#endif
874 ProcessAlignmentTextProperties(this, name, texts, names, n);
875 return INDI::Telescope::ISNewText(dev, name, texts, names, n);
876}
877
879{
880#ifdef USE_SIM_TAB
881 const char *propName = findXMLAttValu(root, "name");
882 const char *deviceName = findXMLAttValu(root, "device");
883 const char *pacDevice = PACDeviceTP[0].getText();
884
885 if (pacDevice && strlen(pacDevice) > 0 && deviceName && strcmp(deviceName, pacDevice) == 0)
886 {
887 if (!strcmp(propName, "PAC_MANUAL_ADJUSTMENT"))
888 {
889 // Capture the step values (AZ and ALT) whenever the property is updated.
890 // PAC_MANUAL_ADJUSTMENT[MANUAL_AZ_STEP] > 0 → East, < 0 → West
891 // PAC_MANUAL_ADJUSTMENT[MANUAL_ALT_STEP] > 0 → North, < 0 → South
892 XMLEle *ep = nullptr;
893 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
894 {
895 const char *elemName = findXMLAttValu(ep, "name");
896 if (!strcmp(elemName, "MANUAL_AZ_STEP"))
897 m_snoopedAzError = atof(pcdataXMLEle(ep));
898 else if (!strcmp(elemName, "MANUAL_ALT_STEP"))
899 m_snoopedAltError = atof(pcdataXMLEle(ep));
900 }
901
902 // Only update the mount model once the physical move has completed (state = Ok).
903 const char *stateStr = findXMLAttValu(root, "state");
904 if (!strcmp(stateStr, "Ok"))
905 {
906 // The PAA's azStep and MM_MA use opposite sign conventions:
907 // azStep > 0 (correction East) ↔ MM_MA decreases in the model
908 // Adding m_snoopedAzError to the current MM_MA correctly simulates
909 // the physical delta correction applied to the mount.
910 double oldMA = mountModelNP[MM_MA].getValue();
911 double oldME = mountModelNP[MM_ME].getValue();
912 double newMA = oldMA + m_snoopedAzError * 60.0;
913 double newME = oldME + m_snoopedAltError * 60.0;
914
915 mountModelNP[MM_MA].setValue(newMA);
916 mountModelNP[MM_ME].setValue(newME);
917 alignment.setCorrections(mountModelNP[MM_IH].getValue() / 60.0, mountModelNP[MM_ID].getValue() / 60.0,
918 mountModelNP[MM_CH].getValue() / 60.0, mountModelNP[MM_NP].getValue() / 60.0,
919 mountModelNP[MM_MA].getValue() / 60.0, mountModelNP[MM_ME].getValue() / 60.0);
920
921 mountModelNP.setState(IPS_OK);
922 mountModelNP.apply();
923
924 LOGF_INFO("Applied PAC correction: MA %.4f -> %.4f, ME %.4f -> %.4f (arcmin)",
925 oldMA, newMA, oldME, newME);
926
927 // Force an immediate coordinate update so that the next CCD capture
928 // uses the corrected pointing rather than waiting up to one full
929 // polling cycle (default 500 ms) for ReadScopeStatus to run.
930 updateCurrentCoordsFromAxes();
931 NewRaDec(m_currentRA, m_currentDEC);
932 }
933 return true;
934 }
935 }
936#endif
938}
939
941{
942 axisPrimary.Abort();
943 axisSecondary.Abort();
946 return true;
947}
948
950{
952 {
953 LOG_ERROR("Please unpark the mount before issuing any motion commands.");
954 return false;
955 }
956 mcRate = static_cast<int>(SlewRateSP.findOnSwitchIndex()) + 1;
957 mcRate = std::max(1, std::min(4, mcRate));
958
959 int rate = (dir == INDI_DIR_NS::DIRECTION_NORTH) ? mcRate : -mcRate;
960 if (HasPierSide() && (currentPierSide == PIER_WEST)) // see scopesim_helper.cpp: alignment
961 rate = -rate;
962 LOGF_DEBUG("MoveNS dir %s, motion %s, rate %d", dir == DIRECTION_NORTH ? "N" : "S", command == 0 ? "start" : "stop", rate);
963
964 axisSecondary.mcRate = command == MOTION_START ? rate : 0;
965
966 return true;
967}
968
970{
972 {
973 LOG_ERROR("Please unpark the mount before issuing any motion commands.");
974 return false;
975 }
976
977 mcRate = static_cast<int>(SlewRateSP.findOnSwitchIndex()) + 1;
978 mcRate = std::max(1, std::min(4, mcRate));
979
980 int rate = (dir == INDI_DIR_WE::DIRECTION_EAST) ? -mcRate : mcRate;
981 LOGF_DEBUG("MoveWE dir %d, motion %s, rate %d", dir == DIRECTION_EAST ? "E" : "W", command == 0 ? "start" : "stop", rate);
982
983 axisPrimary.mcRate = command == MOTION_START ? rate : 0;
984 return true;
985}
986
987uint32_t ScopeSim::backlashComputation(uint32_t ms)
988{
989 if (m_DecGuideRemainingBacklash <= 0)
990 {
991 m_DecGuideRemainingBacklash = 0;
992 }
993 else if (m_DecGuideRemainingBacklash >= ms)
994 {
995 m_DecGuideRemainingBacklash -= ms;
996 ms = 0;
997 }
998 else
999 {
1000 ms -= m_DecGuideRemainingBacklash;
1001 m_DecGuideRemainingBacklash = 0;
1002 }
1003 return ms;
1004}
1005
1006uint32_t ScopeSim::applyDecBacklash(double rate, uint32_t ms)
1007{
1008 const bool changingDirections = (rate < 0 && m_GuideDecLastNorth) ||
1009 (rate > 0 && !m_GuideDecLastNorth);
1010 m_GuideDecLastNorth = rate >= 0;
1011
1012 if (!changingDirections)
1013 {
1014 ms = backlashComputation(ms);
1015 return ms;
1016 }
1017 else
1018 {
1019 if (m_DecGuideRemainingBacklash <= 0)
1020 m_DecGuideRemainingBacklash = m_DecGuideBacklashMs;
1021 else if (m_DecGuideRemainingBacklash >= m_DecGuideBacklashMs)
1022 m_DecGuideRemainingBacklash = 0;
1023 else
1024 m_DecGuideRemainingBacklash = m_DecGuideBacklashMs - m_DecGuideRemainingBacklash;
1025
1026 ms = backlashComputation(ms);
1027 return ms;
1028 }
1029}
1030
1031void ScopeSim::guideAltAzDecomposed(double dNS, double dEW, uint32_t ms)
1032{
1033 // Decompose a celestial N/S/E/W guide pulse into Az/Alt axis components using
1034 // the parallactic angle q. sinQ = sin(q)/cos(alt), cosQ = cos(q)/cos(alt).
1035 // The azimuth rate for celestial North is -sin(q)/cos(alt); since sinQ already
1036 // carries one 1/cos(alt), dAz_N = -sinQ/cosAlt gives -sin(q)/cos^2(alt), which
1037 // is the correct field rotation formula.
1038 //
1039 // Celestial North in Az/Alt: dAlt = cos(q), dAz = -sin(q)/cos(alt)
1040 // Celestial East in Az/Alt: dAlt = sin(q), dAz = cos(q)/cos(alt)
1041
1042 double H = (alignment.lst() - Angle(m_currentRA * 15.0)).radians();
1043 double dec = Angle(m_currentDEC).radians();
1044 double lat = alignment.latitude.radians();
1045 double alt = m_currentAlt * M_PI / 180.0;
1046 double cosAlt = std::cos(alt);
1047
1048 if (std::abs(cosAlt) > 1e-4)
1049 {
1050 double sinQ = std::sin(H) * std::cos(lat) / cosAlt;
1051 double cosQ = (std::sin(lat) - std::sin(dec) * std::sin(alt))
1052 / (std::cos(dec) * cosAlt);
1053
1054 double dAlt_N = cosQ;
1055 double dAz_N = -sinQ / cosAlt;
1056 double dAlt_E = sinQ;
1057 double dAz_E = cosQ / cosAlt;
1058
1059 axisPrimary.StartGuide(dNS * dAz_N + dEW * dAz_E, ms);
1060 axisSecondary.StartGuide(dNS * dAlt_N + dEW * dAlt_E, ms);
1061 }
1062 else
1063 {
1064 // Near zenith: Az is degenerate; fall back to direct axis motion.
1065 axisPrimary.StartGuide(dEW, ms);
1066 axisSecondary.StartGuide(dNS, ms);
1067 }
1068}
1069
1071{
1072 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
1073 {
1074 guideAltAzDecomposed(GuideRateNP[DEC_AXIS].getValue(), 0.0, ms);
1075 }
1076 else
1077 {
1078 double rate = GuideRateNP[DEC_AXIS].getValue();
1079 if (HasPierSide() && (currentPierSide == PIER_WEST)) // see scopsim_helper.cpp: alignment
1080 rate = -rate;
1081 ms = applyDecBacklash(rate, ms);
1082 axisSecondary.StartGuide(rate, ms);
1083 }
1084 guidingNS = true;
1085 return IPS_BUSY;
1086}
1087
1089{
1090 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
1091 {
1092 guideAltAzDecomposed(-GuideRateNP[DEC_AXIS].getValue(), 0.0, ms);
1093 }
1094 else
1095 {
1096 double rate = GuideRateNP[DEC_AXIS].getValue();
1097 if (HasPierSide() && (currentPierSide == PIER_WEST)) // see scopsim_helper.cpp: alignment
1098 rate = -rate;
1099 ms = applyDecBacklash(-rate, ms);
1100 axisSecondary.StartGuide(-rate, ms);
1101 }
1102 guidingNS = true;
1103 return IPS_BUSY;
1104}
1105
1107{
1108 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
1109 guideAltAzDecomposed(0.0, -GuideRateNP[RA_AXIS].getValue(), ms);
1110 else
1111 axisPrimary.StartGuide(-GuideRateNP[RA_AXIS].getValue(), ms);
1112 guidingEW = true;
1113 return IPS_BUSY;
1114}
1115
1117{
1118 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
1119 guideAltAzDecomposed(0.0, GuideRateNP[RA_AXIS].getValue(), ms);
1120 else
1121 axisPrimary.StartGuide(GuideRateNP[RA_AXIS].getValue(), ms);
1122 guidingEW = true;
1123 return IPS_BUSY;
1124}
1125
1127{
1128 if (m_MountType == Alignment::MOUNT_TYPE::ALTAZ)
1129 {
1130 // Store INDI Az (0=North) and Alt in degrees.
1131 SetAxis1Park(m_currentAz);
1132 SetAxis2Park(m_currentAlt);
1133 }
1134 else
1135 {
1136 double ha = (alignment.lst() - Angle(m_currentRA, Angle::ANGLE_UNITS::HOURS)).Hours();
1137 SetAxis1Park(ha);
1138 SetAxis2Park(m_currentDEC);
1139 }
1140 return true;
1141}
1142
1144{
1145 // mount points to East (counterweights down) at the horizon
1146 // works for both hemispheres
1147 SetAxis1Park(-6.);
1148 SetAxis2Park(0.);
1149
1150 return true;
1151}
1152
1153bool ScopeSim::SetTrackMode(uint8_t mode)
1154{
1155 switch (static_cast<TelescopeTrackMode>(mode))
1156 {
1157 case TRACK_SIDEREAL:
1158 axisPrimary.TrackRate(Axis::SIDEREAL);
1159 if (m_MountType != Alignment::MOUNT_TYPE::ALTAZ)
1160 axisSecondary.TrackRate(Axis::OFF);
1161 return true;
1162 case TRACK_SOLAR:
1163 axisPrimary.TrackRate(Axis::SOLAR);
1164 if (m_MountType != Alignment::MOUNT_TYPE::ALTAZ)
1165 axisSecondary.TrackRate(Axis::OFF);
1166 return true;
1167 case TRACK_LUNAR:
1168 axisPrimary.TrackRate(Axis::LUNAR);
1169 if (m_MountType != Alignment::MOUNT_TYPE::ALTAZ)
1170 axisSecondary.TrackRate(Axis::OFF);
1171 return true;
1172 case TRACK_CUSTOM:
1173 SetTrackRate(TrackRateNP[AXIS_RA].getValue(), TrackRateNP[AXIS_DE].getValue());
1174 return true;
1175 }
1176 return false;
1177}
1178
1180{
1181 if (enabled)
1182 m_IsPipelinePrimed = false;
1183 axisPrimary.Tracking(enabled);
1184 axisSecondary.Tracking(enabled);
1185 return true;
1186}
1187
1188bool ScopeSim::SetTrackRate(double raRate, double deRate)
1189{
1190 axisPrimary.TrackingRateDegSec = Angle(raRate / 3600.0);
1191 axisSecondary.TrackingRateDegSec = Angle(deRate / 3600.0);
1192 return true;
1193}
1194
1196{
1198
1200
1201#ifdef USE_SIM_TAB
1202 GuideRateNP.save(fp);
1203 MountTypeSP.save(fp);
1204 simPierSideSP.save(fp);
1205 mountModelNP.save(fp);
1206 flipHourAngleNP.save(fp);
1207 decBacklashNP.save(fp);
1208 PACDeviceTP.save(fp);
1209
1210#endif
1211 return true;
1212}
1213
1214bool ScopeSim::updateLocation(double latitude, double longitude, double elevation)
1215{
1216 LOGF_DEBUG("Update location %8.3f, %8.3f, %4.0f", latitude, longitude, elevation);
1217
1218 alignment.latitude = Angle(latitude);
1219 alignment.longitude = Angle(longitude);
1220
1221 UpdateLocation(latitude, longitude, elevation);
1222 return true;
1223}
1224
1225bool ScopeSim::updateMountAndPierSide()
1226{
1227#ifdef USE_SIM_TAB
1228 int mountType = MountTypeSP.findOnSwitchIndex();
1229 int pierSide = simPierSideSP.findOnSwitchIndex();
1230 if (mountType < 0 || pierSide < 0)
1231 return false;
1232
1233 // If nothing changed, return
1234 if (mountType == m_MountType && pierSide == m_PierSide)
1235 return true;
1236
1237 const int previousMountType = m_MountType;
1238 m_MountType = mountType;
1239 m_PierSide = pierSide;
1240
1241 LOGF_INFO("update mount and pier side: Pier Side %s, mount type %d", pierSide == 0 ? "Off" : "On", mountType);
1242#else
1243 int mountType = static_cast<int>(Alignment::MOUNT_TYPE::EQ_GEM);
1244 int pierSide = PS_ON;
1245#endif
1246 if ( mountType == Alignment::MOUNT_TYPE::ALTAZ)
1247 {
1248 LOG_INFO("AltAz mount type experimental");
1249 // return false;
1250 }
1251
1252 alignment.mountType = static_cast<Alignment::MOUNT_TYPE>(mountType);
1253
1254 // Tell the INDI alignment subsystem whether this is an equatorial or ALTAZ mount.
1255 // Without this, the math plugin always fits an ALTAZ model regardless of mount type.
1259 : INDI::AlignmentSubsystem::MathPluginManagement::EQUATORIAL);
1260
1261#ifdef USE_SIM_TAB
1262 // Sync points store the telescope direction in the encoding of the mount geometry that was active
1263 // when they were taken: Az/Alt for ALTAZ, encoder RA/Dec for the equatorial types (see Sync()).
1264 // After switching between ALTAZ and an equatorial type, those entries are decoded with the wrong
1265 // encoding and every GoTo lands at an unrelated sky position. They cannot be re-encoded reliably,
1266 // so drop them. EQ_GEM <-> EQ_FORK share the RA/Dec encoding and keep their sync points.
1267 // previousMountType is -1 during initial config load, so a restored database is never discarded.
1268 const bool wasAltAz = (previousMountType == Alignment::MOUNT_TYPE::ALTAZ);
1269 const bool isAltAz = (mountType == Alignment::MOUNT_TYPE::ALTAZ);
1270 if (previousMountType >= 0 && wasAltAz != isAltAz && !GetAlignmentDatabase().empty())
1271 {
1272 LOGF_WARN("Mount type changed between ALTAZ and equatorial: clearing %d alignment sync point(s) "
1273 "recorded for the previous mount geometry.", static_cast<int>(GetAlignmentDatabase().size()));
1274 GetAlignmentDatabase().clear();
1275 UpdateSize();
1276 Initialise(this);
1277 }
1278#endif
1279
1280 // Set the park data type appropriate for the mount geometry.
1281 if (mountType == static_cast<int>(Alignment::MOUNT_TYPE::ALTAZ))
1283 else
1285
1286 // Reinitialize the secondary axis track rate for the selected mount type.
1287 // ALTAZ: both axes need sidereal rate (overridden each tick by parabolic tracking).
1288 // EQ: primary tracks sidereal, secondary (Dec) is driven only when slewing.
1289 if (mountType == Alignment::MOUNT_TYPE::ALTAZ)
1290 axisSecondary.TrackRate(Axis::SIDEREAL);
1291 else
1292 axisSecondary.TrackRate(Axis::OFF);
1293
1294 // update the pier side capability depending on the mount type
1295 uint32_t cap = GetTelescopeCapability();
1296 if (pierSide == PS_ON && mountType == static_cast<int>(Alignment::MOUNT_TYPE::EQ_GEM))
1297 {
1299 }
1300 else
1301 {
1302 cap &= ~static_cast<uint32_t>(TELESCOPE_HAS_PIER_SIDE);
1303 }
1304 SetTelescopeCapability(cap, 4);
1305
1306 return true;
1307}
1308
1309
1311{
1312 switch (action)
1313 {
1314 case HOME_FIND:
1315 LOG_INFO("Finding home position...");
1316 m_Home[AXIS_RA] = alignment.lst().Hours();
1317 m_Home[AXIS_DE] = LocationNP[LOCATION_LATITUDE].getValue() > 0 ? 90 : -90;
1318 Goto(m_Home[AXIS_RA], m_Home[AXIS_DE]);
1319 return IPS_BUSY;
1320
1321 case HOME_SET:
1322 m_Home[AXIS_RA] = (alignment.lst() - Angle(EqNP[AXIS_RA].getValue(), Angle::HOURS)).HoursHa();
1323 m_Home[AXIS_DE] = EqNP[AXIS_DE].getValue();
1324 LOG_INFO("Setting home position to current position.");
1325 return IPS_OK;
1326
1327 case HOME_GO:
1328 {
1329 LOG_INFO("Going to home position.");
1330 if (m_Home[AXIS_RA] == 0)
1331 {
1332 m_Home[AXIS_RA] = 0;
1333 m_Home[AXIS_DE] = LocationNP[LOCATION_LATITUDE].getValue() > 0 ? 90 : -90;
1334 }
1335
1336 // Home HA to home RA
1337 auto ra = (alignment.lst() - Angle(m_Home[AXIS_RA], Angle::HOURS)).Hours();
1338 Goto(ra, m_Home[AXIS_DE]);
1339 return IPS_BUSY;
1340 }
1341
1342 default:
1343 return IPS_ALERT;
1344 }
1345 return IPS_ALERT;
1346}
1347
void setCorrections(double ih, double id, double ch, double np, double ma, double me)
setCorrections set the values of the six corrections
void instrumentHaDecToMount(Angle instrumentHa, Angle instrumentDec, Angle *primary, Angle *secondary)
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 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.
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.