1# INDI Alignment Subsystem
2
3## Introduction
4The INDI alignment subsystem is a collection of classes that together provide support for telescope alignment using a database of stored sync points. Support is also provided for "Math Plugin Modules". One of these runtime loadable modules is active at any one time. The currently loaded module uses the sync point database to provide conversion functions to and from coordinates in the celestial reference frame and the telescope mount's local reference frame.
5
6During observing runs the sync point database is held in memory within the INDI device driver. It can also be loaded and saved to and from a file on the system the driver is running. The database can be edited via INDI properties (for details of the properties see the class MapPropertiesToInMemoryDatabase), by an API class for use in INDI drivers(InMemoryDatabase), or an API class for use in INDI clients(ClientAPIForAlignmentDatabase).
7
8The current math plugin module can be selected and initialised via INDI properties (for details of the properties see the class MathPluginManagement), by and API class for use in INDI drivers(MathPluginManagement), or by an API class for use in INDI clients(ClientAPIForMathPluginManagement).
9
10## Math Plugins
11The following math plugins are included in the first release.
12
13### Built in math plugin
14This is the default plugin which is used if no other plugin has been loaded. The plugin is normally initialised or re-initialised when the database has been loaded or when a new sync point has been added. The initialisation process scans the current database and builds a number of transformation matrices depending on how many sync points are present. Before the matrices are computed all celestial reference frame Right Ascension and Declination coordinates are transformed to horizontal Altitude Azimuth coordinates using the julian date stored in the sync point entry. This means that all transformations using the computed transformation matrices will be to and from a zenith aligned celestial reference frame and the telescope mounts local reference frame. This has the advantage of incorporating in the transformation any systematic alignment errors which occur due to the direction the mount is pointing relative to the zenith. Examples of such errors include those due to atmospheric refraction, and those due the effect of gravity on the telescope and mount. All transformation matrices are computed using the simple method proposed by [Toshimi Taki](http://www.geocities.jp/toshimi_taki/matrix/matrix_method_rev_e.pdf). This is quick and dirty but can result in matrices that are not true transforms. These will be reported as errors and an identity matrix will be substituted.
15
16#### No sync points present
17
18No action is taken.
19
20#### One sync point present
21
22A transformation matrix is computed using a hint to mounts approximate alignment supplied by the driver, this can either be ZENITH, NORTH_CELESTIAL_POLE or SOUTH_CELESTIAL_POLE. The hint is used to make a dummy second sync point entry. A dummy third entry is computed from the cross product of the first two. A single transformation matrix and its inverse is computed from these three points.
23
24#### Two sync points present
25
26A transformation matrix is computed using the two sync points and a dummy third sync point computed from the cross product of the first two. A single transformation matrix and its inverse is computed from these three points.
27
28#### Three sync points present
29
30A single transformation matrix and its inverse is computed from the three sync points.
31
32#### Four or more sync points present
33
34Two convex hulls are computed. One from the zenith aligned celestial reference frame sync point coordinates plus a dummy nadir, and the other from the mounts local reference frame sync point coordinates plus a dummy nadir. These convex hulls are made up of triangular facets. Forward and inverse transformation matrices are then computed for each corresponding pair of facets and stored alongside the facet in the relevant convex hull.
35
36#### Coordinate conversion
37
38If when the plugin is asked to translate a coordinate it only has a single conversion matrix (the one, two and three sync points case) this will be used. Otherwise (the four or more sync points case) a ray will shot from the origin of the requested source reference frame in the requested direction into the relevant convex hull and the transformation matrix from the facet it intersects will be used for the conversion.
39
40### SVD math plugin
41This plugin works in an identical manner to the built in math plugin. The only difference being that [Markley's Singular Value Decomposition algorithm](http://www.control.auc.dk/~tb/best/aug23-Bak-svdalg.pdf) is used to calculate the transformation matrices. This is a highly robust method and forms the basis of the pointing system used in many professional telescope installations.
42
43## Using the Alignment Subsystem from KStars
44The easiest way to use a telescope driver that supports the Alignment Subsystem is via an INDI aware client such as KStars. The following example uses the indi_SkywatcherAltAzMount driver and a Synscan 114GT mount. If you are using a different driver then the name of that driver will appear in KStars not "skywatcherAPIMount".
45
461. Firstly connect the mount to the computer that is to run the driver. I use a readily available PL2303 chip based serial to USB converter cable.
472. From the handset utility menu select PC direct mode. As it is the computer that will be driving the mount not the handset, you can enter whatever values you want to get through the handset initialisation process.
483. Start indiserver and the indi_SkyWatcherAPIMount driver. Using the following command in a terminal:
49
50        indiserver indi_skywatcherAPIMount
51
524. Start KStars and from the tools menu select "Devices" and then "Device Manager".
53![Tools menu](toolsmenu.png)
54
555. In the device manager window select the "Client" tab, and in the client tab select the host that indiserver is running on. Click on connect.
56![Device Manager](devicemanager.png)
57
586. An INDI Control Panel window should open with a skywatcherAPIMount tab. Select the "Options" subtab (I think I have invented this word!). Ensure that the port property is set to the correct serial device. My PL2303 usb cable always appears as /dev/ttyUSB0.
59![INDI Control Panel](controlpanel1.png)
60
617. Select the "Main Control" tab and click on connect.
62![INDI Control Panel](controlpanel2.png)
63
648. After a few seconds pause (whilst the driver determines what type of motor board is in use) a number of extra tabs should appear. One of these should be the "Site Management" tab. Select this and ensure that you have correct values entered for "Scope Location", you can safely ignore elevation at this time.
65![INDI Control Panel](controlpanel3.png)
66
679. At this point it is probably wise to save the configuration. Return to the "Options" tab and click on "Configuration" "Save".
68![INDI Control Panel](controlpanel4.png)
69
7010. Check that the "Alignment" tab is present and select it. Using the controls on this tab you can view and manipulate the entries in the alignment database, and select which math plugin you want to use. It probably best to ignore this tab for the time being and use KStars to create sync points to align your mount.
71![INDI Control Panel](controlpanel5.png)
72
7311. To create a sync point using KStars. First ensure your target star is visible in the KStars display. I usually do this using the "Pointing...Find Object" tool.
74![Find Object Tool](findobject.png)
75
7612. Once you have the target in the KStars window right click on it and then hover your mouse over the "Sync" option in the "skywatcherAltAzMount" sub-menu. Do not left click on the "Sync" option yet. N.B. The "Centre and Track" item in the main popup menu is nothing to do with your mount. It merely tells KStars to keep this object centered in the display window.
77![Object popup menu](objectpopup.png)
78
7913. Go back to your scope and centre the target in the eyepiece. Quickly get back to your computer and left click the mouse (be careful not to move it off the Sync menu item or you will have to right click to bring it up again). If you have been successful you should see the KStars telescope crosshairs displayed around the target.
80![Crosshair Display](crosshair.png).
81
8214. The Alignment Subsystem is now in "one star" alignment mode. You can try this out by right clicking on your target star or a nearby star and selecting "Track" from the "skywatcherAltAzMount" sub-menu. The further away the object you track is from the sync point star the less accurate the initial slew will be and the more quickly the tracked star will drift off centre. To correct this you need to add more sync points.
83
8415. To add another sync point you can select a new target star in KStars and use the slew command from the "skywatcherAltAzMount" sub-menu to approximately slew your scope onto the target. The procedure for adding the sync point is the same as before. With the default math plugin one achieves maximum accuracy for a particular triangular patch of sky when it is surrounded by three sync points. If more than three sync points are defined then more triangular patches will be added to the mesh.
85
8616. If would be very useful if you could collect information on how well the alignment mechanism holds a star centred, measured in degrees of drift per second. Please share these on the indi-devel list.
87
88## Adding Alignment Subsystem support to an INDI driver
89The Alignment Subsystem provides two API classes and a support function class for use in drivers. These are MapPropertiesToInMemoryDatabase, MathPluginManagement, and TelescopeDirectionVectorSupportFunctions. Driver developers can use these classes individually, however, the easiest way to use them is via the AlignmentSubsystemForDrivers class. To use this class simply ensure that is a parent of your driver class.
90
91    class ScopeSim : public INDI::Telescope, public INDI::GuiderInterface, public INDI::AlignmentSubsystem::AlignmentSubsystemForDrivers
92
93Somewhere in your drivers initProperties function add a call to AlignmentSubsystemForDrivers::InitAlignmentProperties.
94
95    bool ScopeSim::initProperties()
96    {
97        /* Make sure to init parent properties first */
98        INDI::Telescope::initProperties();
99
100        ...
101
102        /* Add debug controls so we may debug driver if necessary */
103        addDebugControl();
104
105        // Add alignment properties
106        InitAlignmentProperties(this);
107
108        return true;
109    }
110
111Hook the alignment subsystem into your drivers processing of properties by putting calls to AlignmentSubsystemForDrivers::ProcessNumberProperties,
112AlignmentSubsystemForDrivers::ProcessSwitchProperties, AlignmentSubsystemForDrivers::ProcessBLOBProperties AlignmentSubsystemForDrivers::ProcessTextProperties, in the relevant routines.
113
114    bool ScopeSim::ISNewNumber (const char *dev, const char *name, double values[], char *names[], int n)
115    {
116        //  first check if it's for our device
117
118        if(strcmp(dev,getDeviceName())==0)
119        {
120            ...
121
122            // Process alignment properties
123            ProcessNumberProperties(this, name, values, names, n);
124
125        }
126
127        //  if we didn't process it, continue up the chain, let somebody else
128        //  give it a shot
129        return INDI::Telescope::ISNewNumber(dev,name,values,names,n);
130    }
131
132    bool ScopeSim::ISNewSwitch (const char *dev, const char *name, ISState *states, char *names[], int n)
133    {
134        if(strcmp(dev,getDeviceName())==0)
135        {
136            ...
137            // Process alignment properties
138            ProcessSwitchProperties(this, name, states, names, n);
139        }
140
141        //  Nobody has claimed this, so, ignore it
142        return INDI::Telescope::ISNewSwitch(dev,name,states,names,n);
143    }
144
145    bool ScopeSim::ISNewBLOB (const char *dev, const char *name, int sizes[], int blobsizes[], char *blobs[], char *formats[], char *names[], int n)
146    {
147        if(strcmp(dev,getDeviceName())==0)
148        {
149            // Process alignment properties
150            ProcessBlobProperties(this, name, sizes, blobsizes, blobs, formats, names, n);
151        }
152        // Pass it up the chain
153        return INDI::Telescope::ISNewBLOB(dev, name, sizes, blobsizes, blobs, formats, names, n);
154    }
155
156    bool ScopeSim::ISNewText (const char *dev, const char *name, char *texts[], char *names[], int n)
157    {
158        if(strcmp(dev,getDeviceName())==0)
159        {
160            // Process alignment properties
161            ProcessTextProperties(this, name, texts, names, n);
162        }
163        // Pass it up the chain
164        return INDI::Telescope::ISNewText(dev, name, texts, names, n);
165    }
166
167Then make sure you override the INDI::Telescope::updateLocation function and call the AligmmentSubsystemForDrivers::UpdateLocation function.
168
169    bool ScopeSim::updateLocation(double latitude, double longitude, double elevation)
170    {
171        UpdateLocation(latitude, longitude, elevation);
172        return true;
173    }
174
175
176The next step is to add the handling of sync points into your drivers Sync function.
177
178    bool ScopeSim::Sync(double ra, double dec)
179    {
180        struct ln_hrz_posn AltAz;
181        AltAz.alt = double(CurrentEncoderMicrostepsDEC) / MICROSTEPS_PER_DEGREE;
182        AltAz.az = double(CurrentEncoderMicrostepsRA) / MICROSTEPS_PER_DEGREE;
183
184        AlignmentDatabaseEntry NewEntry;
185        NewEntry.ObservationJulianDate = ln_get_julian_from_sys();
186        NewEntry.RightAscension = ra;
187        NewEntry.Declination = dec;
188        NewEntry.TelescopeDirection = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz);
189        NewEntry.PrivateDataSize = 0;
190
191        if (!CheckForDuplicateSyncPoint(NewEntry))
192        {
193
194            GetAlignmentDatabase().push_back(NewEntry);
195
196            // Tell the client about size change
197            UpdateSize();
198
199            // Tell the math plugin to reinitialise
200            Initialise(this);
201
202            return true;
203        }
204        return false;
205    }
206
207The final step is to add coordinate conversion to ReadScopeStatus, TimerHit (for tracking), and Goto.
208
209    bool ScopeSim::ReadScopeStatus()
210    {
211        struct ln_hrz_posn AltAz;
212        AltAz.alt = double(CurrentEncoderMicrostepsDEC) / MICROSTEPS_PER_DEGREE;
213        AltAz.az = double(CurrentEncoderMicrostepsRA) / MICROSTEPS_PER_DEGREE;
214        TelescopeDirectionVector TDV = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz);
215
216        double RightAscension, Declination;
217        if (!TransformTelescopeToCelestial( TDV, RightAscension, Declination))
218        {
219            if (TraceThisTick)
220                DEBUG(DBG_SIMULATOR, "ReadScopeStatus - TransformTelescopeToCelestial failed");
221
222            bool HavePosition = false;
223            ln_lnlat_posn Position;
224            if ((NULL != IUFindNumber(&LocationNP, "LAT")) && ( 0 != IUFindNumber(&LocationNP, "LAT")->value)
225                && (NULL != IUFindNumber(&LocationNP, "LONG")) && ( 0 != IUFindNumber(&LocationNP, "LONG")->value))
226            {
227                // I assume that being on the equator and exactly on the prime meridian is unlikely
228                Position.lat = IUFindNumber(&LocationNP, "LAT")->value;
229                Position.lng = IUFindNumber(&LocationNP, "LONG")->value;
230                HavePosition = true;
231            }
232            struct ln_equ_posn EquatorialCoordinates;
233            if (HavePosition)
234            {
235                if (TraceThisTick)
236                    DEBUG(DBG_SIMULATOR, "ReadScopeStatus - HavePosition true");
237                TelescopeDirectionVector RotatedTDV(TDV);
238                switch (GetApproximateMountAlignment())
239                {
240                    case ZENITH:
241                        if (TraceThisTick)
242                            DEBUG(DBG_SIMULATOR, "ReadScopeStatus - ApproximateMountAlignment ZENITH");
243                        break;
244
245                    case NORTH_CELESTIAL_POLE:
246                        if (TraceThisTick)
247                            DEBUG(DBG_SIMULATOR, "ReadScopeStatus - ApproximateMountAlignment NORTH_CELESTIAL_POLE");
248                        // Rotate the TDV coordinate system anticlockwise (positive) around the y axis by 90 minus
249                        // the (positive)observatory latitude. The vector itself is rotated clockwise
250                        RotatedTDV.RotateAroundY(90.0 - Position.lat);
251                        AltitudeAzimuthFromTelescopeDirectionVector(RotatedTDV, AltAz);
252                        break;
253
254                    case SOUTH_CELESTIAL_POLE:
255                        if (TraceThisTick)
256                            DEBUG(DBG_SIMULATOR, "ReadScopeStatus - ApproximateMountAlignment SOUTH_CELESTIAL_POLE");
257                        // Rotate the TDV coordinate system clockwise (negative) around the y axis by 90 plus
258                        // the (negative)observatory latitude. The vector itself is rotated anticlockwise
259                        RotatedTDV.RotateAroundY(-90.0 - Position.lat);
260                        AltitudeAzimuthFromTelescopeDirectionVector(RotatedTDV, AltAz);
261                        break;
262                }
263                ln_get_equ_from_hrz(&AltAz, &Position, ln_get_julian_from_sys(), &EquatorialCoordinates);
264            }
265            else
266            {
267                if (TraceThisTick)
268                    DEBUG(DBG_SIMULATOR, "ReadScopeStatus - HavePosition false");
269
270                // The best I can do is just do a direct conversion to RA/DEC
271                EquatorialCoordinatesFromTelescopeDirectionVector(TDV, EquatorialCoordinates);
272            }
273            // libnova works in decimal degrees
274            RightAscension = EquatorialCoordinates.ra * 24.0 / 360.0;
275            Declination = EquatorialCoordinates.dec;
276        }
277
278        if (TraceThisTick)
279            DEBUGF(DBG_SIMULATOR, "ReadScopeStatus - RA %lf hours DEC %lf degrees", RightAscension, Declination);
280
281        NewRaDec(RightAscension, Declination);
282
283        return true;
284    }
285
286    bool ScopeSim::Sync(double ra, double dec)
287    {
288        struct ln_hrz_posn AltAz;
289        AltAz.alt = double(CurrentEncoderMicrostepsDEC) / MICROSTEPS_PER_DEGREE;
290        AltAz.az = double(CurrentEncoderMicrostepsRA) / MICROSTEPS_PER_DEGREE;
291
292        AlignmentDatabaseEntry NewEntry;
293        NewEntry.ObservationJulianDate = ln_get_julian_from_sys();
294        NewEntry.RightAscension = ra;
295        NewEntry.Declination = dec;
296        NewEntry.TelescopeDirection = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz);
297        NewEntry.PrivateDataSize = 0;
298
299        if (!CheckForDuplicateSyncPoint(NewEntry))
300        {
301
302            GetAlignmentDatabase().push_back(NewEntry);
303
304            // Tell the client about size change
305            UpdateSize();
306
307            // Tell the math plugin to reinitialise
308            Initialise(this);
309
310            return true;
311        }
312        return false;
313    }
314
315    void ScopeSim::TimerHit()
316    {
317        // Simulate mount movement
318
319        ...
320
321        INDI::Telescope::TimerHit(); // This will call ReadScopeStatus
322
323        // OK I have updated the celestial reference frame RA/DEC in ReadScopeStatus
324        // Now handle the tracking state
325        switch(TrackState)
326        {
327            case SCOPE_SLEWING:
328                if ((STOPPED == AxisStatusRA) && (STOPPED == AxisStatusDEC))
329                {
330                    if (ISS_ON == IUFindSwitch(&CoordSP,"TRACK")->s)
331                    {
332                        // Goto has finished start tracking
333                        DEBUG(DBG_SIMULATOR, "TimerHit - Goto finished start tracking");
334                        TrackState = SCOPE_TRACKING;
335                        // Fall through to tracking case
336                    }
337                    else
338                    {
339                        TrackState = SCOPE_IDLE;
340                        break;
341                    }
342                }
343                else
344                    break;
345
346            case SCOPE_TRACKING:
347            {
348                // Continue or start tracking
349                // Calculate where the mount needs to be in POLLMS time
350                // POLLMS is hardcoded to be one second
351                double JulianOffset = 1.0 / (24.0 * 60 * 60); // TODO may need to make this longer to get a meaningful result
352                TelescopeDirectionVector TDV;
353                ln_hrz_posn AltAz;
354                if (TransformCelestialToTelescope(CurrentTrackingTarget.ra, CurrentTrackingTarget.dec,
355                                                    JulianOffset, TDV))
356                    AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz);
357                else
358                {
359                    // Try a conversion with the stored observatory position if any
360                    bool HavePosition = false;
361                    ln_lnlat_posn Position;
362                    if ((NULL != IUFindNumber(&LocationNP, "LAT")) && ( 0 != IUFindNumber(&LocationNP, "LAT")->value)
363                        && (NULL != IUFindNumber(&LocationNP, "LONG")) && ( 0 != IUFindNumber(&LocationNP, "LONG")->value))
364                    {
365                        // I assume that being on the equator and exactly on the prime meridian is unlikely
366                        Position.lat = IUFindNumber(&LocationNP, "LAT")->value;
367                        Position.lng = IUFindNumber(&LocationNP, "LONG")->value;
368                        HavePosition = true;
369                    }
370                    struct ln_equ_posn EquatorialCoordinates;
371                    // libnova works in decimal degrees
372                    EquatorialCoordinates.ra = CurrentTrackingTarget.ra * 360.0 / 24.0;
373                    EquatorialCoordinates.dec = CurrentTrackingTarget.dec;
374                    if (HavePosition)
375                        ln_get_hrz_from_equ(&EquatorialCoordinates, &Position,
376                                                ln_get_julian_from_sys() + JulianOffset, &AltAz);
377                    else
378                    {
379                        // No sense in tracking in this case
380                        TrackState = SCOPE_IDLE;
381                        break;
382                    }
383                }
384
385                // My altitude encoder runs -90 to +90
386                if ((AltAz.alt > 90.0) || (AltAz.alt < -90.0))
387                {
388                    DEBUG(DBG_SIMULATOR, "TimerHit tracking - Altitude out of range");
389                    // This should not happen
390                    return;
391                }
392
393                // My polar encoder runs 0 to +360
394                if ((AltAz.az > 360.0) || (AltAz.az < -360.0))
395                {
396                    DEBUG(DBG_SIMULATOR, "TimerHit tracking - Azimuth out of range");
397                    // This should not happen
398                    return;
399                }
400
401                if (AltAz.az < 0.0)
402                {
403                    DEBUG(DBG_SIMULATOR, "TimerHit tracking - Azimuth negative");
404                    AltAz.az = 360.0 + AltAz.az;
405                }
406
407                long AltitudeOffsetMicrosteps = int(AltAz.alt * MICROSTEPS_PER_DEGREE - CurrentEncoderMicrostepsDEC);
408                long AzimuthOffsetMicrosteps = int(AltAz.az * MICROSTEPS_PER_DEGREE - CurrentEncoderMicrostepsRA);
409
410                DEBUGF(DBG_SIMULATOR, "TimerHit - Tracking AltitudeOffsetMicrosteps %d AzimuthOffsetMicrosteps %d",
411                        AltitudeOffsetMicrosteps, AzimuthOffsetMicrosteps);
412
413                if (0 != AzimuthOffsetMicrosteps)
414                {
415                    // Calculate the slewing rates needed to reach that position
416                    // at the correct time. This is simple as interval is one second
417                    if (AzimuthOffsetMicrosteps > 0)
418                    {
419                        if (AzimuthOffsetMicrosteps < MICROSTEPS_PER_REVOLUTION / 2.0)
420                        {
421                            // Foward
422                            AxisDirectionRA = FORWARD;
423                            AxisSlewRateRA = AzimuthOffsetMicrosteps;
424                        }
425                        else
426                        {
427                            // Reverse
428                            AxisDirectionRA = REVERSE;
429                            AxisSlewRateRA = MICROSTEPS_PER_REVOLUTION - AzimuthOffsetMicrosteps;
430                        }
431                    }
432                    else
433                    {
434                        AzimuthOffsetMicrosteps = abs(AzimuthOffsetMicrosteps);
435                        if (AzimuthOffsetMicrosteps < MICROSTEPS_PER_REVOLUTION / 2.0)
436                        {
437                            // Foward
438                            AxisDirectionRA = REVERSE;
439                            AxisSlewRateRA = AzimuthOffsetMicrosteps;
440                        }
441                        else
442                        {
443                            // Reverse
444                            AxisDirectionRA = FORWARD;
445                            AxisSlewRateRA = MICROSTEPS_PER_REVOLUTION - AzimuthOffsetMicrosteps;
446                        }
447                    }
448                    AxisSlewRateRA = abs(AzimuthOffsetMicrosteps);
449                    AxisDirectionRA = AzimuthOffsetMicrosteps > 0 ? FORWARD : REVERSE;  // !!!! BEWARE INERTIA FREE MOUNT
450                    AxisStatusRA = SLEWING;
451                    DEBUGF(DBG_SIMULATOR, "TimerHit - Tracking AxisSlewRateRA %lf AxisDirectionRA %d",
452                        AxisSlewRateRA, AxisDirectionRA);
453                }
454                else
455                {
456                    // Nothing to do - stop the axis
457                    AxisStatusRA = STOPPED; // !!!! BEWARE INERTIA FREE MOUNT
458                    DEBUG(DBG_SIMULATOR, "TimerHit - Tracking nothing to do stopping RA axis");
459                }
460
461                if (0 != AltitudeOffsetMicrosteps)
462                {
463                     // Calculate the slewing rates needed to reach that position
464                    // at the correct time.
465                    AxisSlewRateDEC = abs(AltitudeOffsetMicrosteps);
466                    AxisDirectionDEC = AltitudeOffsetMicrosteps > 0 ? FORWARD : REVERSE;  // !!!! BEWARE INERTIA FREE MOUNT
467                    AxisStatusDEC = SLEWING;
468                    DEBUGF(DBG_SIMULATOR, "TimerHit - Tracking AxisSlewRateDEC %lf AxisDirectionDEC %d",
469                        AxisSlewRateDEC, AxisDirectionDEC);
470                }
471                else
472                {
473                    // Nothing to do - stop the axis
474                    AxisStatusDEC = STOPPED;  // !!!! BEWARE INERTIA FREE MOUNT
475                    DEBUG(DBG_SIMULATOR, "TimerHit - Tracking nothing to do stopping DEC axis");
476                }
477
478                break;
479            }
480
481            default:
482                break;
483        }
484
485    bool ScopeSim::Goto(double ra,double dec)
486    {
487
488        DEBUGF(DBG_SIMULATOR, "Goto - Celestial reference frame target right ascension %lf(%lf) declination %lf", ra * 360.0 / 24.0, ra, dec);
489
490        if (ISS_ON == IUFindSwitch(&CoordSP,"TRACK")->s)
491        {
492            char RAStr[32], DecStr[32];
493            fs_sexa(RAStr, ra, 2, 3600);
494            fs_sexa(DecStr, dec, 2, 3600);
495            CurrentTrackingTarget.ra = ra;
496            CurrentTrackingTarget.dec = dec;
497            DEBUG(DBG_SIMULATOR, "Goto - tracking requested");
498        }
499
500        // Call the alignment subsystem to translate the celestial reference frame coordinate
501        // into a telescope reference frame coordinate
502        TelescopeDirectionVector TDV;
503        ln_hrz_posn AltAz;
504        if (TransformCelestialToTelescope(ra, dec, 0.0, TDV))
505        {
506            // The alignment subsystem has successfully transformed my coordinate
507            AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz);
508        }
509        else
510        {
511            // The alignment subsystem cannot transform the coordinate.
512            // Try some simple rotations using the stored observatory position if any
513            bool HavePosition = false;
514            ln_lnlat_posn Position;
515            if ((NULL != IUFindNumber(&LocationNP, "LAT")) && ( 0 != IUFindNumber(&LocationNP, "LAT")->value)
516                && (NULL != IUFindNumber(&LocationNP, "LONG")) && ( 0 != IUFindNumber(&LocationNP, "LONG")->value))
517            {
518                // I assume that being on the equator and exactly on the prime meridian is unlikely
519                Position.lat = IUFindNumber(&LocationNP, "LAT")->value;
520                Position.lng = IUFindNumber(&LocationNP, "LONG")->value;
521                HavePosition = true;
522            }
523            struct ln_equ_posn EquatorialCoordinates;
524            // libnova works in decimal degrees
525            EquatorialCoordinates.ra = ra * 360.0 / 24.0;
526            EquatorialCoordinates.dec = dec;
527            if (HavePosition)
528            {
529                ln_get_hrz_from_equ(&EquatorialCoordinates, &Position, ln_get_julian_from_sys(), &AltAz);
530                TDV = TelescopeDirectionVectorFromAltitudeAzimuth(AltAz);
531                switch (GetApproximateMountAlignment())
532                {
533                    case ZENITH:
534                        break;
535
536                    case NORTH_CELESTIAL_POLE:
537                        // Rotate the TDV coordinate system clockwise (negative) around the y axis by 90 minus
538                        // the (positive)observatory latitude. The vector itself is rotated anticlockwise
539                        TDV.RotateAroundY(Position.lat - 90.0);
540                        break;
541
542                    case SOUTH_CELESTIAL_POLE:
543                        // Rotate the TDV coordinate system anticlockwise (positive) around the y axis by 90 plus
544                        // the (negative)observatory latitude. The vector itself is rotated clockwise
545                        TDV.RotateAroundY(Position.lat + 90.0);
546                        break;
547                }
548                AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz);
549            }
550            else
551            {
552                // The best I can do is just do a direct conversion to Alt/Az
553                TDV = TelescopeDirectionVectorFromEquatorialCoordinates(EquatorialCoordinates);
554                AltitudeAzimuthFromTelescopeDirectionVector(TDV, AltAz);
555            }
556        }
557
558        // My altitude encoder runs -90 to +90
559        if ((AltAz.alt > 90.0) || (AltAz.alt < -90.0))
560        {
561            DEBUG(DBG_SIMULATOR, "Goto - Altitude out of range");
562            // This should not happen
563            return false;
564        }
565
566        // My polar encoder runs 0 to +360
567        if ((AltAz.az > 360.0) || (AltAz.az < -360.0))
568        {
569            DEBUG(DBG_SIMULATOR, "Goto - Azimuth out of range");
570            // This should not happen
571            return false;
572        }
573
574        if (AltAz.az < 0.0)
575        {
576            DEBUG(DBG_SIMULATOR, "Goto - Azimuth negative");
577            AltAz.az = 360.0 + AltAz.az;
578        }
579
580        DEBUGF(DBG_SIMULATOR, "Goto - Scope reference frame target altitude %lf azimuth %lf", AltAz.alt, AltAz.az);
581
582        GotoTargetMicrostepsDEC = int(AltAz.alt * MICROSTEPS_PER_DEGREE);
583        if (GotoTargetMicrostepsDEC == CurrentEncoderMicrostepsDEC)
584            AxisStatusDEC = STOPPED;
585        else
586        {
587            if (GotoTargetMicrostepsDEC > CurrentEncoderMicrostepsDEC)
588                AxisDirectionDEC = FORWARD;
589            else
590                AxisDirectionDEC = REVERSE;
591            AxisStatusDEC = SLEWING_TO;
592        }
593        GotoTargetMicrostepsRA = int(AltAz.az * MICROSTEPS_PER_DEGREE);
594        if (GotoTargetMicrostepsRA == CurrentEncoderMicrostepsRA)
595            AxisStatusRA = STOPPED;
596        else
597        {
598            if (GotoTargetMicrostepsRA > CurrentEncoderMicrostepsRA)
599                AxisDirectionRA = (GotoTargetMicrostepsRA - CurrentEncoderMicrostepsRA) < MICROSTEPS_PER_REVOLUTION / 2.0 ? FORWARD : REVERSE;
600            else
601                AxisDirectionRA = (CurrentEncoderMicrostepsRA - GotoTargetMicrostepsRA) < MICROSTEPS_PER_REVOLUTION / 2.0 ? REVERSE : FORWARD;
602            AxisStatusRA = SLEWING_TO;
603        }
604
605        TrackState = SCOPE_SLEWING;
606
607        EqNP.s    = IPS_BUSY;
608
609        return true;
610    }
611
612
613## Developing Alignment Subsystem clients
614The Alignment Subsystem provides two API classes for use in clients. These are ClientAPIForAlignmentDatabase and ClientAPIForMathPluginManagement. Client developers can use these classes individually, however, the easiest way to use them is via the AlignmentSubsystemForClients class. To use this class simply ensure that is a parent of your client class.
615
616	class LoaderClient : public INDI::BaseClient, INDI::AlignmentSubsystem::AlignmentSubsystemForClients
617
618Somewhere in the initialisation of your client make a call to the Initalise method of the AlignmentSubsystemForClients class for example:
619
620    void LoaderClient::Initialise(int argc, char* argv[])
621    {
622        std::string HostName("localhost");
623        int Port = 7624;
624
625        if (argc > 1)
626            DeviceName = argv[1];
627        if (argc > 2)
628            HostName = argv[2];
629        if (argc > 3)
630        {
631            std::istringstream Parameter(argv[3]);
632            Parameter >> Port;
633        }
634
635        AlignmentSubsystemForClients::Initialise(DeviceName.c_str(), this);
636
637        setServer(HostName.c_str(), Port);
638
639        watchDevice(DeviceName.c_str());
640
641        connectServer();
642
643        setBLOBMode(B_ALSO, DeviceName.c_str(), NULL);
644    }
645
646To hook the Alignment Subsystem into the clients property handling you must ensure that the following virtual functions are overriden.
647
648    virtual void newBLOB(IBLOB *bp);
649    virtual void newDevice(INDI::BaseDevice *dp);
650    virtual void newNumber(INumberVectorProperty *nvp);
651    virtual void newProperty(INDI::Property *property);
652    virtual void newSwitch(ISwitchVectorProperty *svp);
653
654A call to the Alignment Subsystems property handling functions must then be placed in the body of these functions.
655
656    void LoaderClient::newBLOB(IBLOB *bp)
657    {
658        ProcessNewBLOB(bp);
659    }
660
661    void LoaderClient::newDevice(INDI::BaseDevice *dp)
662    {
663        ProcessNewDevice(dp);
664    }
665
666    void LoaderClient::newNumber(INumberVectorProperty *nvp)
667    {
668        ProcessNewNumber(nvp);
669    }
670
671    void LoaderClient::newProperty(INDI::Property *property)
672    {
673        ProcessNewProperty(property);
674    }
675
676    void LoaderClient::newSwitch(ISwitchVectorProperty *svp)
677    {
678        ProcessNewSwitch(svp);
679    }
680
681See the documentation for the ClientAPIForAlignmentDatabase and ClientAPIForMathPluginManagement to see what other functionality is available.
682