1 /*
2  *  ascom.cpp
3  *  PHD Guiding
4  *
5  *  Created by Craig Stark.
6  *  Copyright (c) 2006-2010 Craig Stark.
7  *  All rights reserved.
8  *
9  *  Modified by Bret McKee
10  *  Copyright (c) 2012-2013 Bret McKee
11  *  All rights reserved.
12  *
13  *  This source code is distributed under the following "BSD" license
14  *  Redistribution and use in source and binary forms, with or without
15  *  modification, are permitted provided that the following conditions are met:
16  *    Redistributions of source code must retain the above copyright notice,
17  *     this list of conditions and the following disclaimer.
18  *    Redistributions in binary form must reproduce the above copyright notice,
19  *     this list of conditions and the following disclaimer in the
20  *     documentation and/or other materials provided with the distribution.
21  *    Neither the name of Craig Stark, Stark Labs nor the names of its
22  *     contributors may be used to endorse or promote products derived from
23  *     this software without specific prior written permission.
24  *
25  *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
26  *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27  *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
28  *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
29  *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
30  *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
31  *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
32  *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
33  *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
34  *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
35  *  POSSIBILITY OF SUCH DAMAGE.
36  *
37  */
38 
39 #include "phd.h"
40 
41 #ifdef GUIDE_ASCOM
42 
43 #include "comdispatch.h"
44 
45 #include <wx/msw/ole/oleutils.h>
46 #include <comdef.h>
47 #include <objbase.h>
48 #include <ole2ver.h>
49 #include <initguid.h>
50 #include <wx/textfile.h>
51 #include <wx/stdpaths.h>
52 #include <wx/stopwatch.h>
53 
ScopeASCOM(const wxString & choice)54 ScopeASCOM::ScopeASCOM(const wxString& choice)
55 {
56     m_choice = choice;
57     m_canPulseGuide = false;                           // will get updated in Connect()
58 
59     dispid_connected = DISPID_UNKNOWN;
60     dispid_ispulseguiding = DISPID_UNKNOWN;
61     dispid_isslewing = DISPID_UNKNOWN;
62     dispid_pulseguide = DISPID_UNKNOWN;
63     dispid_declination = DISPID_UNKNOWN;
64     dispid_rightascension = DISPID_UNKNOWN;
65     dispid_siderealtime = DISPID_UNKNOWN;
66     dispid_sitelatitude = DISPID_UNKNOWN;
67     dispid_sitelongitude = DISPID_UNKNOWN;
68     dispid_slewtocoordinates = DISPID_UNKNOWN;
69     dispid_raguiderate = DISPID_UNKNOWN;
70     dispid_decguiderate = DISPID_UNKNOWN;
71     dispid_sideofpier = DISPID_UNKNOWN;
72     dispid_abortslew = DISPID_UNKNOWN;
73 }
74 
~ScopeASCOM()75 ScopeASCOM::~ScopeASCOM()
76 {
77 }
78 
displayName(const wxString & ascomName)79 static wxString displayName(const wxString& ascomName)
80 {
81     if (ascomName.Find(_T("ASCOM")) != wxNOT_FOUND)
82         return ascomName;
83     return ascomName + _T(" (ASCOM)");
84 }
85 
86 // map descriptive name to progid
87 static std::map<wxString, wxString> s_progid;
88 
EnumAscomScopes()89 wxArrayString ScopeASCOM::EnumAscomScopes()
90 {
91     wxArrayString list;
92 
93     try
94     {
95         DispatchObj profile;
96         if (!profile.Create(L"ASCOM.Utilities.Profile"))
97             throw ERROR_INFO("ASCOM Scope: could not instantiate ASCOM profile class ASCOM.Utilities.Profile. Is ASCOM installed?");
98 
99         Variant res;
100         if (!profile.InvokeMethod(&res, L"RegisteredDevices", L"Telescope"))
101             throw ERROR_INFO("ASCOM Scope: could not query registered telescope devices: " + ExcepMsg(profile.Excep()));
102 
103         DispatchClass ilist_class;
104         DispatchObj ilist(res.pdispVal, &ilist_class);
105 
106         Variant vcnt;
107         if (!ilist.GetProp(&vcnt, L"Count"))
108             throw ERROR_INFO("ASCOM Scope: could not query registered telescopes: " + ExcepMsg(ilist.Excep()));
109 
110         unsigned int const count = vcnt.intVal;
111         DispatchClass kvpair_class;
112 
113         for (unsigned int i = 0; i < count; i++)
114         {
115             Variant kvpres;
116             if (ilist.GetProp(&kvpres, L"Item", i))
117             {
118                 DispatchObj kvpair(kvpres.pdispVal, &kvpair_class);
119                 Variant vkey, vval;
120                 if (kvpair.GetProp(&vkey, L"Key") && kvpair.GetProp(&vval, L"Value"))
121                 {
122                     wxString ascomName = vval.bstrVal;
123                     wxString displName = displayName(ascomName);
124                     wxString progid = vkey.bstrVal;
125                     s_progid[displName] = progid;
126                     list.Add(displName);
127                 }
128             }
129         }
130     }
131     catch (const wxString& msg)
132     {
133         POSSIBLY_UNUSED(msg);
134     }
135 
136     return list;
137 }
138 
Create(DispatchObj & obj)139 bool ScopeASCOM::Create(DispatchObj& obj)
140 {
141     try
142     {
143         // is there already an instance registered in the global interface table?
144         IDispatch *idisp = m_gitEntry.Get();
145         if (idisp)
146         {
147             obj.Attach(idisp, NULL);
148             return true;
149         }
150 
151         Debug.Write(wxString::Format("Create ASCOM Scope: choice '%s' progid %s\n", m_choice, s_progid[m_choice]));
152 
153         wxBasicString progid(s_progid[m_choice]);
154 
155         if (!obj.Create(progid))
156         {
157             throw ERROR_INFO("Could not establish instance of " + wxString(progid));
158         }
159 
160         Debug.Write(wxString::Format("pScopeDriver = 0x%p\n", obj.IDisp()));
161 
162         // store the driver interface in the global table for access by other threads
163         m_gitEntry.Register(obj);
164     }
165     catch (const wxString& msg)
166     {
167         Debug.Write(msg + "\n");
168         return false;
169     }
170 
171     return true;
172 }
173 
HasSetupDialog() const174 bool ScopeASCOM::HasSetupDialog() const
175 {
176     return true;
177 }
178 
SetupDialog()179 void ScopeASCOM::SetupDialog()
180 {
181     DispatchObj scope;
182     if (Create(scope))
183     {
184         Variant res;
185         if (!scope.InvokeMethod(&res, L"SetupDialog"))
186         {
187             wxString msg(scope.Excep().bstrSource);
188             if (scope.Excep().bstrDescription)
189                 msg += ":\n" + wxString(scope.Excep().bstrDescription);
190             wxMessageBox(msg, _("Error"), wxOK | wxICON_ERROR);
191         }
192     }
193     // destroy the COM object now as this reduces the likelhood of getting into a
194     // state where the user has killed the ASCOM local server instance and PHD2 is
195     // holding a reference to the defunct driver instance in the global interface
196     // table
197     m_gitEntry.Unregister();
198 }
199 
Connect()200 bool ScopeASCOM::Connect()
201 {
202     bool bError = false;
203 
204     try
205     {
206         Debug.Write("ASCOM Scope: Connecting\n");
207 
208         if (IsConnected())
209         {
210             wxMessageBox("Scope already connected",_("Error"));
211             throw ERROR_INFO("ASCOM Scope: Connected - Already Connected");
212         }
213 
214         DispatchObj pScopeDriver;
215 
216         if (!Create(pScopeDriver))
217         {
218             wxMessageBox(_T("Could not establish instance of ") + m_choice, _("Error"), wxOK | wxICON_ERROR);
219             throw ERROR_INFO("ASCOM Scope: Could not establish ASCOM Scope instance");
220         }
221 
222         // --- get the dispatch IDs we need ...
223 
224         // ... get the dispatch ID for the Connected property ...
225         if (!pScopeDriver.GetDispatchId(&dispid_connected, L"Connected"))
226         {
227             wxMessageBox(_T("ASCOM driver problem -- cannot connect"),_("Error"), wxOK | wxICON_ERROR);
228             throw ERROR_INFO("ASCOM Scope: Could not get the dispatch id for the Connected property");
229         }
230 
231         // ... get the dispatch ID for the "IsPulseGuiding" property ....
232         m_canCheckPulseGuiding = true;
233         if (!pScopeDriver.GetDispatchId(&dispid_ispulseguiding, L"IsPulseGuiding"))
234         {
235             m_canCheckPulseGuiding = false;
236             Debug.Write("cannot get dispid_ispulseguiding\n");
237             // don't fail if we can't get the status on this - can live without it as it's really a safety net for us
238         }
239 
240         // ... get the dispatch ID for the "Slewing" property ....
241         if (!pScopeDriver.GetDispatchId(&dispid_isslewing, L"Slewing"))
242         {
243             wxMessageBox(_T("ASCOM driver missing the Slewing property"),_("Error"), wxOK | wxICON_ERROR);
244             throw ERROR_INFO("ASCOM Scope: Could not get the dispatch id for the Slewing property");
245         }
246 
247         // ... get the dispatch ID for the "PulseGuide" property ....
248         if (!pScopeDriver.GetDispatchId(&dispid_pulseguide, L"PulseGuide"))
249         {
250             wxMessageBox(_T("ASCOM driver missing the PulseGuide property"),_("Error"), wxOK | wxICON_ERROR);
251             throw ERROR_INFO("ASCOM Scope: Could not get the dispatch id for the PulseGuide property");
252         }
253 
254         // ... get the dispatch ID for the "Declination" property ....
255         m_canGetCoordinates = true;
256         if (!pScopeDriver.GetDispatchId(&dispid_declination, L"Declination"))
257         {
258             m_canGetCoordinates = false;
259             Debug.Write("cannot get dispid_declination\n");
260         }
261         else if (!pScopeDriver.GetDispatchId(&dispid_rightascension, L"RightAscension"))
262         {
263             Debug.Write("cannot get dispid_rightascension\n");
264             m_canGetCoordinates = false;
265         }
266         else if (!pScopeDriver.GetDispatchId(&dispid_siderealtime, L"SiderealTime"))
267         {
268             Debug.Write("cannot get dispid_siderealtime\n");
269             m_canGetCoordinates = false;
270         }
271 
272         if (!pScopeDriver.GetDispatchId(&dispid_sitelatitude, L"SiteLatitude"))
273         {
274             Debug.Write("cannot get dispid_sitelatitude\n");
275         }
276         if (!pScopeDriver.GetDispatchId(&dispid_sitelongitude, L"SiteLongitude"))
277         {
278             Debug.Write("cannot get dispid_sitelongitude\n");
279         }
280 
281         m_canSlew = true;
282         if (!pScopeDriver.GetDispatchId(&dispid_slewtocoordinates, L"SlewToCoordinates"))
283         {
284             m_canSlew = false;
285             Debug.Write("cannot get dispid_slewtocoordinates\n");
286         }
287 
288         // ... get the dispatch IDs for the two guide rate properties - if we can't get them, no sweat, doesn't matter for actual guiding
289         // Used for things like calibration sanity checking, backlash clearing, etc.
290         m_canGetGuideRates = true;         // Likely case, required for any ASCOM driver at V2 or later
291         if (!pScopeDriver.GetDispatchId(&dispid_decguiderate, L"GuideRateDeclination"))
292         {
293             Debug.Write("cannot get dispid_decguiderate\n");
294             m_canGetGuideRates = false;
295             // don't throw if we can't get this one
296         }
297         else if (!pScopeDriver.GetDispatchId(&dispid_raguiderate, L"GuideRateRightAscension"))
298         {
299             Debug.Write("cannot get dispid_raguiderate\n");
300             m_canGetGuideRates = false;
301             // don't throw if we can't get this one
302         }
303 
304         if (!pScopeDriver.GetDispatchId(&dispid_sideofpier, L"SideOfPier"))
305         {
306             Debug.Write("cannot get dispid_sideofpier\n");
307             dispid_sideofpier = DISPID_UNKNOWN;
308         }
309 
310         if (!pScopeDriver.GetDispatchId(&dispid_abortslew, L"AbortSlew"))
311         {
312             Debug.Write("cannot get dispid_abortslew\n");
313             dispid_abortslew = DISPID_UNKNOWN;
314         }
315 
316         struct ConnectInBg : public ConnectMountInBg
317         {
318             ScopeASCOM *sa;
319             ConnectInBg(ScopeASCOM *sa_) : sa(sa_) { }
320             bool Entry()
321             {
322                 GITObjRef scope(sa->m_gitEntry);
323                 // ... set the Connected property to true....
324                 if (!scope.PutProp(sa->dispid_connected, true))
325                 {
326                     SetErrorMsg(ExcepMsg(scope.Excep()));
327                     return true;
328                 }
329                 return false;
330             }
331         };
332         ConnectInBg bg(this);
333 
334         // set the Connected property to true in a background thread
335         if (bg.Run())
336         {
337             wxMessageBox(_T("ASCOM driver problem during connection: ") + bg.GetErrorMsg(),
338                 _("Error"), wxOK | wxICON_ERROR);
339             throw ERROR_INFO("ASCOM Scope: Could not set Connected property to true");
340         }
341 
342         // get the scope name
343         Variant vRes;
344         if (!pScopeDriver.GetProp(&vRes, L"Name"))
345         {
346             wxMessageBox(_T("ASCOM driver problem getting Name property"), _("Error"), wxOK | wxICON_ERROR);
347             throw ERROR_INFO("ASCOM Scope: Could not get the scope name: " + ExcepMsg(pScopeDriver.Excep()));
348         }
349 
350         m_Name = displayName(vRes.bstrVal);
351 
352         Debug.Write(wxString::Format("Scope reports its name as %s\n", m_Name));
353 
354         m_abortSlewWhenGuidingStuck = false;
355 
356         if (m_Name.Find(_T("Gemini Telescope .NET")) != wxNOT_FOUND)
357         {
358             // Gemini2 firmware (2013 Oct 13 version, perhaps others) has been found to contain a
359             // bug where a pulse guide command can fail to complete, with the Guiding property
360             // returning true forever. The firmware developer suggests that PHD2 should issue an
361             // AbortSlew when this condition is detected.
362             Debug.Write("ASCOM scope: enabling stuck guide pulse workaround\n");
363             m_abortSlewWhenGuidingStuck = true;
364         }
365 
366         m_checkForSyncPulseGuide = false;
367 
368         if (m_Name.Find(_T("AstroPhysicsV2")) != wxNOT_FOUND)
369         {
370             // The Astro-Physics ASCOM driver can hang intermittently if its
371             // synchronous pulseguide option is enabled.  We will attempt to
372             // detect synchronous guide pulses and display an alert if sync
373             // pulses are enabled.
374             Debug.Write("ASCOM scope: enabling sync pulse guide check\n");
375             m_checkForSyncPulseGuide = true;
376         }
377 
378         // see if we can pulse guide
379         m_canPulseGuide = true;
380         if (!pScopeDriver.GetProp(&vRes, L"CanPulseGuide") || vRes.boolVal != VARIANT_TRUE)
381         {
382             Debug.Write("Connecting to ASCOM scope that does not support PulseGuide\n");
383             m_canPulseGuide = false;
384         }
385 
386         // see if scope can slew
387         m_canSlewAsync = false;
388         if (m_canSlew)
389         {
390             if (!pScopeDriver.GetProp(&vRes, L"CanSlew"))
391             {
392                 Debug.Write(wxString::Format("ASCOM scope got error invoking CanSlew: %s\n", ExcepMsg(pScopeDriver.Excep())));
393                 m_canSlew = false;
394             }
395             else if (vRes.boolVal != VARIANT_TRUE)
396             {
397                 Debug.Write("ASCOM scope reports CanSlew = false\n");
398                 m_canSlew = false;
399             }
400 
401             m_canSlewAsync = pScopeDriver.GetProp(&vRes, L"CanSlewAsync") && vRes.boolVal == VARIANT_TRUE;
402             Debug.Write(wxString::Format("ASCOM scope CanSlewAsync is %s\n", m_canSlewAsync ? "true" : "false"));
403         }
404 
405         Debug.Write(wxString::Format("%s connected\n", Name()));
406 
407         Scope::Connect();
408 
409         Debug.Write("ASCOM Scope: Connect success\n");
410     }
411     catch (const wxString& Msg)
412     {
413         POSSIBLY_UNUSED(Msg);
414         bError = true;
415     }
416 
417     return bError;
418 }
419 
Disconnect()420 bool ScopeASCOM::Disconnect()
421 {
422     bool bError = false;
423 
424     try
425     {
426         Debug.Write("ASCOM Scope: Disconnecting\n");
427 
428         if (!IsConnected())
429         {
430             throw ERROR_INFO("ASCOM Scope: attempt to disconnect when not connected");
431         }
432 
433         // Setting the Connected property to false will cause the scope to be disconnected for all
434         // ASCOM clients that are connected to the scope, and we do not want this!
435         bool const disconnectAscomDriver = false;
436         if (disconnectAscomDriver)
437         {
438             GITObjRef scope(m_gitEntry);
439 
440             // Set the Connected property to false
441             if (!scope.PutProp(dispid_connected, false))
442             {
443                 pFrame->Alert(_("ASCOM driver problem during disconnect, check the debug log for more information"));
444                 throw ERROR_INFO("ASCOM Scope: Could not set Connected property to false: " + ExcepMsg(scope.Excep()));
445             }
446         }
447 
448         m_gitEntry.Unregister();
449 
450         Debug.Write("ASCOM Scope: Disconnected Successfully\n");
451     }
452     catch (const wxString& Msg)
453     {
454         POSSIBLY_UNUSED(Msg);
455         bError = true;
456     }
457 
458     Scope::Disconnect();
459 
460     return bError;
461 }
462 
463 #define CheckSlewing(dispobj, result) \
464     do { \
465         if (IsStopGuidingWhenSlewingEnabled() && IsSlewing(dispobj)) \
466         { \
467             *(result) = MOVE_ERROR_SLEWING; \
468             throw ERROR_INFO("attempt to guide while slewing"); \
469         } \
470     } while (0)
471 
SlewWarningEnabledKey()472 static wxString SlewWarningEnabledKey()
473 {
474     // we want the key to be under "/Confirm" so ConfirmDialog::ResetAllDontAskAgain() resets it, but we also want the setting to be per-profile
475     return wxString::Format("/Confirm/%d/SlewWarningEnabled", pConfig->GetCurrentProfileId());
476 }
477 
SuppressSlewAlert(long)478 static void SuppressSlewAlert(long)
479 {
480     //If the user doesn't want to see these, we shouldn't be checking for the condition
481     TheScope()->EnableStopGuidingWhenSlewing(false);
482 }
483 
PulseGuideFailedAlertEnabledKey()484 static wxString PulseGuideFailedAlertEnabledKey()
485 {
486     // we want the key to be under "/Confirm" so ConfirmDialog::ResetAllDontAskAgain() resets it, but we also want the setting to be per-profile
487     return wxString::Format("/Confirm/%d/PulseGuideFailedAlertEnabled", pConfig->GetCurrentProfileId());
488 }
489 
SuppressPulseGuideFailedAlert(long)490 static void SuppressPulseGuideFailedAlert(long)
491 {
492     pConfig->Global.SetBoolean(PulseGuideFailedAlertEnabledKey(), false);
493 }
494 
SyncPulseGuideAlertEnabledKey()495 static wxString SyncPulseGuideAlertEnabledKey()
496 {
497     // we want the key to be under "/Confirm" so
498     // ConfirmDialog::ResetAllDontAskAgain() resets it, but we also want the
499     // setting to be per-profile
500     return wxString::Format("/Confirm/%d/SyncPulseGuideAlertEnabled", pConfig->GetCurrentProfileId());
501 }
502 
SuppressSyncPulseGuideAlert(long)503 static void SuppressSyncPulseGuideAlert(long)
504 {
505     pConfig->Global.SetBoolean(SyncPulseGuideAlertEnabledKey(), false);
506 }
507 
Guide(GUIDE_DIRECTION direction,int duration)508 Mount::MOVE_RESULT ScopeASCOM::Guide(GUIDE_DIRECTION direction, int duration)
509 {
510     MOVE_RESULT result = MOVE_OK;
511 
512     try
513     {
514         Debug.Write(wxString::Format("Guiding  Dir = %d, Dur = %d\n", direction, duration));
515 
516         if (!IsConnected())
517         {
518             throw ERROR_INFO("ASCOM Scope: attempt to guide when not connected");
519         }
520 
521         if (!m_canPulseGuide)
522         {
523             // Could happen if move command is issued on the Aux mount or CanPulseGuide property got changed on the fly
524             pFrame->Alert(_("ASCOM driver does not support PulseGuide. Check your ASCOM driver settings."));
525             throw ERROR_INFO("ASCOM scope: guide command issued but PulseGuide not supported");
526         }
527 
528         GITObjRef scope(m_gitEntry);
529 
530         // First, check to see if already moving
531 
532         CheckSlewing(&scope, &result);
533 
534         if (IsGuiding(&scope))
535         {
536             Debug.Write("Entered PulseGuideScope while moving\n");
537             int i;
538             for (i = 0; i < 20; i++)
539             {
540                 wxMilliSleep(50);
541 
542                 CheckSlewing(&scope, &result);
543 
544                 if (!IsGuiding(&scope))
545                     break;
546 
547                 Debug.Write("Still moving\n");
548             }
549             if (i == 20)
550             {
551                 Debug.Write("Still moving after 1s - aborting\n");
552                 throw ERROR_INFO("ASCOM Scope: scope is still moving after 1 second");
553             }
554             else
555             {
556                 Debug.Write("Movement stopped - continuing\n");
557             }
558         }
559 
560         // Do the move
561 
562         VARIANTARG rgvarg[2];
563         rgvarg[1].vt = VT_I2;
564         rgvarg[1].iVal = direction;
565         rgvarg[0].vt = VT_I4;
566         rgvarg[0].lVal = (long) duration;
567 
568         DISPPARAMS dispParms;
569         dispParms.cArgs = 2;
570         dispParms.rgvarg = rgvarg;
571         dispParms.cNamedArgs = 0;
572         dispParms.rgdispidNamedArgs = NULL;
573 
574         wxStopWatch swatch;
575 
576         HRESULT hr;
577         ExcepInfo excep;
578         Variant vRes;
579 
580         if (FAILED(hr = scope.IDisp()->Invoke(dispid_pulseguide, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD,
581             &dispParms, &vRes, &excep, NULL)))
582         {
583             Debug.Write(wxString::Format("pulseguide: [%x] %s\n", hr, _com_error(hr).ErrorMessage()));
584 
585             // Make sure nothing got by us and the mount can really handle pulse guide - HIGHLY unlikely
586             if (scope.GetProp(&vRes, L"CanPulseGuide") && vRes.boolVal != VARIANT_TRUE)
587             {
588                 Debug.Write("Tried to guide mount that has no PulseGuide support\n");
589                 // This will trigger a nice alert the next time through Guide
590                 m_canPulseGuide = false;
591             }
592             throw ERROR_INFO("ASCOM Scope: pulseguide command failed: " + ExcepMsg(excep));
593         }
594 
595         long elapsed = swatch.Time();
596 
597         if (m_checkForSyncPulseGuide)
598         {
599             // check for a long pulse and an elapsed time close to or longer
600             // than the pulse duration
601             if (duration >= 250 && elapsed >= duration - 30)
602             {
603                 Debug.Write(wxString::Format("SyncPulseGuide checking: sync pulse detected. "
604                                              "Duration = %d Elapsed = %ld\n", duration, elapsed));
605 
606                 pFrame->SuppressableAlert(SyncPulseGuideAlertEnabledKey(),
607                     _("Please disable the Synchronous PulseGuide option in the mount's ASCOM driver "
608                       "settings. Enabling the setting can cause unpredictable results."),
609                     SuppressSyncPulseGuideAlert, 0);
610 
611                 // only show the Alert once
612                 m_checkForSyncPulseGuide = false;
613             }
614         }
615 
616         if (elapsed < (long)duration)
617         {
618             unsigned long rem = (unsigned long)((long)duration - elapsed);
619 
620             Debug.Write(wxString::Format("PulseGuide returned control before completion, sleep %lu\n", rem + 10));
621 
622             if (WorkerThread::MilliSleep(rem + 10))
623                 throw ERROR_INFO("ASCOM Scope: thread terminate requested");
624         }
625 
626         if (IsGuiding(&scope))
627         {
628             Debug.Write("scope still moving after pulse duration time elapsed\n");
629 
630             // try waiting a little longer. If scope does not stop moving after 1 second, try doing AbortSlew
631             // if it still does not stop after 2 seconds, bail out with an error
632 
633             enum { GRACE_PERIOD_MS = 1000,
634                    TIMEOUT_MS = GRACE_PERIOD_MS + 1000, };
635 
636             bool timeoutExceeded = false;
637             bool didAbortSlew = false;
638 
639             while (true)
640             {
641                 ::wxMilliSleep(20);
642 
643                 if (WorkerThread::InterruptRequested())
644                     throw ERROR_INFO("ASCOM Scope: thread interrupt requested");
645 
646                 CheckSlewing(&scope, &result);
647 
648                 if (!IsGuiding(&scope))
649                 {
650                     Debug.Write(wxString::Format("scope move finished after %ld + %ld ms\n", (long)duration, swatch.Time() - (long)duration));
651                     break;
652                 }
653 
654                 long now = swatch.Time();
655 
656                 if (!didAbortSlew && now > duration + GRACE_PERIOD_MS && m_abortSlewWhenGuidingStuck)
657                 {
658                     Debug.Write(wxString::Format("scope still moving after %ld + %ld ms, try aborting slew\n", (long)duration, now - (long)duration));
659                     AbortSlew(&scope);
660                     didAbortSlew = true;
661                     continue;
662                 }
663 
664                 if (now > duration + TIMEOUT_MS)
665                 {
666                     timeoutExceeded = true;
667                     break;
668                 }
669             }
670 
671             if (timeoutExceeded && IsGuiding(&scope))
672             {
673                 throw ERROR_INFO("timeout exceeded waiting for guiding pulse to complete");
674             }
675         }
676     }
677     catch (const wxString& msg)
678     {
679         POSSIBLY_UNUSED(msg);
680 
681         if (result == MOVE_OK)
682         {
683             result = MOVE_ERROR;
684 
685             if (!WorkerThread::InterruptRequested())
686             {
687                 pFrame->SuppressableAlert(PulseGuideFailedAlertEnabledKey(), _("PulseGuide command to mount has failed - guiding is likely to be ineffective."),
688                     SuppressPulseGuideFailedAlert, 0);
689             }
690         }
691     }
692 
693     if (result == MOVE_ERROR_SLEWING)
694     {
695         pFrame->SuppressableAlert(SlewWarningEnabledKey(), _("Guiding stopped: the scope started slewing."),
696             SuppressSlewAlert, 0);
697     }
698 
699     return result;
700 }
701 
IsGuiding(DispatchObj * scope)702 bool ScopeASCOM::IsGuiding(DispatchObj *scope)
703 {
704     bool bReturn = true;
705 
706     try
707     {
708         if (!m_canCheckPulseGuiding)
709         {
710             // Assume all is good - best we can do as this is really a fail-safe check.  If we can't call this property (lame driver) guides will have to
711             // enforce the wait.  But, enough don't support this that we can't throw an error.
712             throw ERROR_INFO("ASCOM Scope: IsGuiding - !m_canCheckPulseGuiding");
713         }
714 
715         // First, check to see if already moving
716         Variant vRes;
717         if (!scope->GetProp(&vRes, dispid_ispulseguiding))
718         {
719             pFrame->Alert(_("ASCOM driver failed checking IsPulseGuiding. See the debug log for more information."));
720             throw ERROR_INFO("ASCOM Scope: IsGuiding - IsPulseGuiding failed: " + ExcepMsg(scope->Excep()));
721         }
722 
723         bReturn = vRes.boolVal == VARIANT_TRUE;
724     }
725     catch (const wxString& Msg)
726     {
727         POSSIBLY_UNUSED(Msg);
728         bReturn = false;
729     }
730 
731     Debug.Write(wxString::Format("IsGuiding returns %d\n", bReturn));
732 
733     return bReturn;
734 }
735 
IsSlewing(DispatchObj * scope)736 bool ScopeASCOM::IsSlewing(DispatchObj *scope)
737 {
738     Variant vRes;
739     if (!scope->GetProp(&vRes, dispid_isslewing))
740     {
741         Debug.Write(wxString::Format("ScopeASCOM::IsSlewing failed: %s\n", ExcepMsg(scope->Excep())));
742         pFrame->Alert(_("ASCOM driver failed checking for slewing, see the debug log for more information."));
743         return false;
744     }
745 
746     bool result = vRes.boolVal == VARIANT_TRUE;
747 
748     Debug.Write(wxString::Format("IsSlewing returns %d\n", result));
749 
750     return result;
751 }
752 
AbortSlew(DispatchObj * scope)753 void ScopeASCOM::AbortSlew(DispatchObj *scope)
754 {
755     Debug.Write("ScopeASCOM: AbortSlew\n");
756     Variant vRes;
757     if (!scope->InvokeMethod(&vRes, dispid_abortslew))
758     {
759         pFrame->Alert(_("ASCOM driver failed calling AbortSlew, see the debug log for more information."));
760     }
761 }
762 
CanCheckSlewing()763 bool ScopeASCOM::CanCheckSlewing()
764 {
765     return true;
766 }
767 
Slewing()768 bool ScopeASCOM::Slewing()
769 {
770     bool bReturn = true;
771 
772     try
773     {
774         if (!IsConnected())
775         {
776             throw ERROR_INFO("ASCOM Scope: Cannot check Slewing when not connected to mount");
777         }
778 
779         GITObjRef scope(m_gitEntry);
780         bReturn = IsSlewing(&scope);
781     }
782     catch (const wxString& Msg)
783     {
784         POSSIBLY_UNUSED(Msg);
785         bReturn = false;
786     }
787 
788     return bReturn;
789 }
790 
HasNonGuiMove()791 bool ScopeASCOM::HasNonGuiMove()
792 {
793     return true;
794 }
795 
796 // return the declination in radians, or UNKNOWN_DECLINATION
GetDeclination()797 double ScopeASCOM::GetDeclination()
798 {
799     double dReturn = UNKNOWN_DECLINATION;
800 
801     try
802     {
803         if (!IsConnected())
804         {
805             throw ERROR_INFO("ASCOM Scope: cannot get Declination when not connected to mount");
806         }
807 
808         if (!m_canGetCoordinates)
809         {
810             throw THROW_INFO("!m_canGetCoordinates");
811         }
812 
813         GITObjRef scope(m_gitEntry);
814 
815         Variant vRes;
816         if (!scope.GetProp(&vRes, dispid_declination))
817         {
818             throw ERROR_INFO("GetDeclination() fails: " + ExcepMsg(scope.Excep()));
819         }
820 
821         dReturn = radians(vRes.dblVal);
822     }
823     catch (const wxString& Msg)
824     {
825         POSSIBLY_UNUSED(Msg);
826         m_canGetCoordinates = false;
827     }
828 
829     Debug.Write(wxString::Format("ScopeASCOM::GetDeclination() returns %s\n", DeclinationStr(dReturn)));
830 
831     return dReturn;
832 }
833 
834 // Return RA and Dec guide rates in native ASCOM units, degrees/sec.
835 // Convention is to return true on an error
GetGuideRates(double * pRAGuideRate,double * pDecGuideRate)836 bool ScopeASCOM::GetGuideRates(double *pRAGuideRate, double *pDecGuideRate)
837 {
838     bool bError = false;
839 
840     try
841     {
842         if (!IsConnected())
843         {
844             throw ERROR_INFO("ASCOM Scope: cannot get guide rates when not connected");
845         }
846 
847         if (!m_canGetGuideRates)
848         {
849             throw THROW_INFO("ASCOM Scope: not capable of getting guide rates");
850         }
851 
852         GITObjRef scope(m_gitEntry);
853 
854         Variant vRes;
855 
856         if (!scope.GetProp(&vRes, dispid_decguiderate))
857         {
858             throw ERROR_INFO("ASCOM Scope: GuideRateDec() failed: " + ExcepMsg(scope.Excep()));
859         }
860 
861         *pDecGuideRate = vRes.dblVal;
862 
863         if (!scope.GetProp(&vRes, dispid_raguiderate))
864         {
865             throw ERROR_INFO("ASCOM Scope: GuideRateRA() failed: " + ExcepMsg(scope.Excep()));
866         }
867 
868         *pRAGuideRate = vRes.dblVal;
869     }
870     catch (const wxString& Msg)
871     {
872         bError = true;
873         POSSIBLY_UNUSED(Msg);
874     }
875 
876     Debug.Write(wxString::Format("ScopeASCOM::GetGuideRates returns %u %.3f %.3f a-s/sec\n", bError,
877         bError ? 0.0 : *pDecGuideRate * 3600., bError ? 0.0 : *pRAGuideRate * 3600.));
878 
879     return bError;
880 }
881 
GetCoordinates(double * ra,double * dec,double * siderealTime)882 bool ScopeASCOM::GetCoordinates(double *ra, double *dec, double *siderealTime)
883 {
884     bool bError = false;
885 
886     try
887     {
888         if (!IsConnected())
889         {
890             throw ERROR_INFO("ASCOM Scope: cannot get coordinates when not connected");
891         }
892 
893         if (!m_canGetCoordinates)
894         {
895             throw THROW_INFO("ASCOM Scope: not capable of getting coordinates");
896         }
897 
898         GITObjRef scope(m_gitEntry);
899 
900         Variant vRA;
901 
902         if (!scope.GetProp(&vRA, dispid_rightascension))
903         {
904             throw ERROR_INFO("ASCOM Scope: get right ascension failed: " + ExcepMsg(scope.Excep()));
905         }
906 
907         Variant vDec;
908 
909         if (!scope.GetProp(&vDec, dispid_declination))
910         {
911             throw ERROR_INFO("ASCOM Scope: get declination failed: " + ExcepMsg(scope.Excep()));
912         }
913 
914         Variant vST;
915 
916         if (!scope.GetProp(&vST, dispid_siderealtime))
917         {
918             throw ERROR_INFO("ASCOM Scope: get sidereal time failed: " + ExcepMsg(scope.Excep()));
919         }
920 
921         *ra = vRA.dblVal;
922         *dec = vDec.dblVal;
923         *siderealTime = vST.dblVal;
924     }
925     catch (const wxString& Msg)
926     {
927         bError = true;
928         POSSIBLY_UNUSED(Msg);
929     }
930 
931     return bError;
932 }
933 
GetSiteLatLong(double * latitude,double * longitude)934 bool ScopeASCOM::GetSiteLatLong(double *latitude, double *longitude)
935 {
936     if (dispid_sitelatitude == DISPID_UNKNOWN || dispid_sitelongitude == DISPID_UNKNOWN)
937         return true;
938 
939     bool bError = false;
940 
941     try
942     {
943         if (!IsConnected())
944         {
945             throw ERROR_INFO("ASCOM Scope: cannot get site latitude/longitude when not connected");
946         }
947 
948         GITObjRef scope(m_gitEntry);
949 
950         Variant vLat;
951 
952         if (!scope.GetProp(&vLat, dispid_sitelatitude))
953         {
954             throw ERROR_INFO("ASCOM Scope: get site latitude failed: " + ExcepMsg(scope.Excep()));
955         }
956 
957         Variant vLong;
958 
959         if (!scope.GetProp(&vLong, dispid_sitelongitude))
960         {
961             throw ERROR_INFO("ASCOM Scope: get site longitude failed: " + ExcepMsg(scope.Excep()));
962         }
963 
964         *latitude = vLat.dblVal;
965         *longitude = vLong.dblVal;
966     }
967     catch (const wxString& Msg)
968     {
969         bError = true;
970         POSSIBLY_UNUSED(Msg);
971     }
972 
973     return bError;
974 }
975 
CanSlew()976 bool ScopeASCOM::CanSlew()
977 {
978     try
979     {
980         if (!IsConnected())
981         {
982             throw ERROR_INFO("ASCOM Scope: cannot get CanSlew property when not connected to mount");
983         }
984 
985         return m_canSlew;
986     }
987     catch (const wxString& Msg)
988     {
989         POSSIBLY_UNUSED(Msg);
990         return false;
991     }
992 }
993 
CanSlewAsync()994 bool ScopeASCOM::CanSlewAsync()
995 {
996     try
997     {
998         if (!IsConnected())
999         {
1000             throw ERROR_INFO("ASCOM Scope: cannot get CanSlewAsync property when not connected to mount");
1001         }
1002 
1003         return m_canSlewAsync;
1004     }
1005     catch (const wxString& Msg)
1006     {
1007         POSSIBLY_UNUSED(Msg);
1008         return false;
1009     }
1010 }
1011 
CanReportPosition()1012 bool ScopeASCOM::CanReportPosition()
1013 {
1014     return true;
1015 }
1016 
CanPulseGuide()1017 bool ScopeASCOM::CanPulseGuide()
1018 {
1019     return m_canPulseGuide;
1020 }
1021 
SlewToCoordinates(double ra,double dec)1022 bool ScopeASCOM::SlewToCoordinates(double ra, double dec)
1023 {
1024     bool bError = false;
1025 
1026     try
1027     {
1028         if (!IsConnected())
1029         {
1030             throw ERROR_INFO("ASCOM Scope: cannot slew when not connected");
1031         }
1032 
1033         if (!m_canSlew)
1034         {
1035             throw THROW_INFO("ASCOM Scope: not capable of slewing");
1036         }
1037 
1038         GITObjRef scope(m_gitEntry);
1039 
1040         Variant vRes;
1041 
1042         if (!scope.InvokeMethod(&vRes, dispid_slewtocoordinates, ra, dec))
1043         {
1044             throw ERROR_INFO("ASCOM Scope: slew to coordinates failed");
1045         }
1046     }
1047     catch (const wxString& Msg)
1048     {
1049         POSSIBLY_UNUSED(Msg);
1050         bError = true;
1051     }
1052 
1053     return bError;
1054 }
1055 
SlewToCoordinatesAsync(double ra,double dec)1056 bool ScopeASCOM::SlewToCoordinatesAsync(double ra, double dec)
1057 {
1058     bool bError = false;
1059 
1060     try
1061     {
1062         if (!IsConnected())
1063         {
1064             throw ERROR_INFO("ASCOM Scope: cannot slew when not connected");
1065         }
1066 
1067         if (!m_canSlewAsync)
1068         {
1069             throw THROW_INFO("ASCOM Scope: not capable of async slewing");
1070         }
1071 
1072         GITObjRef scope(m_gitEntry);
1073 
1074         Variant vRes;
1075 
1076         if (!scope.InvokeMethod(&vRes, L"SlewToCoordinatesAsync", ra, dec))
1077         {
1078             throw ERROR_INFO("ASCOM Scope: async slew to coordinates failed");
1079         }
1080     }
1081     catch (const wxString& Msg)
1082     {
1083         POSSIBLY_UNUSED(Msg);
1084         bError = true;
1085     }
1086 
1087     return bError;
1088 }
1089 
AbortSlew()1090 void ScopeASCOM::AbortSlew()
1091 {
1092     GITObjRef scope(m_gitEntry);
1093     Variant vRes;
1094     scope.InvokeMethod(&vRes, L"AbortSlew");
1095 }
1096 
SideOfPier()1097 PierSide ScopeASCOM::SideOfPier()
1098 {
1099     PierSide pierSide = PIER_SIDE_UNKNOWN;
1100 
1101     try
1102     {
1103         if (!IsConnected())
1104         {
1105             throw ERROR_INFO("ASCOM Scope: cannot get side of pier when not connected");
1106         }
1107 
1108         if (dispid_sideofpier == DISPID_UNKNOWN)
1109         {
1110             throw THROW_INFO("ASCOM Scope: not capable of getting side of pier");
1111         }
1112 
1113         GITObjRef scope(m_gitEntry);
1114 
1115         Variant vRes;
1116 
1117         if (!scope.GetProp(&vRes, dispid_sideofpier))
1118         {
1119             throw ERROR_INFO("ASCOM Scope: SideOfPier failed: " + ExcepMsg(scope.Excep()));
1120         }
1121 
1122         switch (vRes.intVal) {
1123         case 0: pierSide = PIER_SIDE_EAST; break;
1124         case 1: pierSide = PIER_SIDE_WEST; break;
1125         }
1126     }
1127     catch (const wxString& Msg)
1128     {
1129         POSSIBLY_UNUSED(Msg);
1130     }
1131 
1132     Debug.Write(wxString::Format("ScopeASCOM::SideOfPier() returns %d\n", pierSide));
1133 
1134     return pierSide;
1135 }
1136 
1137 #endif /* GUIDE_ASCOM */
1138