1 /*
2 *  backlash_comp.cpp
3 *  PHD Guiding
4 *
5 *  Created by Bruce Waddington
6 *  Copyright (c) 2015 Bruce Waddington and Andy Galasso
7 *  All rights reserved.
8 *
9 *  This source code is distributed under the following "BSD" license
10 *  Redistribution and use in source and binary forms, with or without
11 *  modification, are permitted provided that the following conditions are met:
12 *    Redistributions of source code must retain the above copyright notice,
13 *     this list of conditions and the following disclaimer.
14 *    Redistributions in binary form must reproduce the above copyright notice,
15 *     this list of conditions and the following disclaimer in the
16 *     documentation and/or other materials provided with the distribution.
17 *    Neither the name of Bret McKee, Dad Dog Development,
18 *     Craig Stark, Stark Labs nor the names of its
19 *     contributors may be used to endorse or promote products derived from
20 *     this software without specific prior written permission.
21 *
22 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
26 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 *  POSSIBILITY OF SUCH DAMAGE.
33 *
34 */
35 
36 #include "phd.h"
37 #include "backlash_comp.h"
38 
39 #include <algorithm>
40 
41 static const unsigned int MIN_COMP_AMOUNT = 20;               // min pulse in ms, must be small enough to effectively disable blc
42 static const unsigned int MAX_COMP_AMOUNT = 8000;             // max pulse in ms
43 
44 
45 class CorrectionTuple
46 {
47 public:
48     long timeSeconds;
49     double miss;
50 
CorrectionTuple(long TimeInSecs,double Amount)51     CorrectionTuple(long TimeInSecs, double Amount)
52     {
53         timeSeconds = TimeInSecs;
54         miss = Amount;
55     }
56 };
57 
58 class BLCEvent
59 {
60 public:
61     std::vector<CorrectionTuple> corrections;
62     bool initialOvershoot;
63     bool initialUndershoot;
64     bool stictionSeen;
65 
BLCEvent()66     BLCEvent() {};
67 
BLCEvent(long TimeSecs,double Amount)68     BLCEvent(long TimeSecs, double Amount)
69     {
70         corrections.push_back(CorrectionTuple(TimeSecs, Amount));
71         initialOvershoot = false;
72         initialUndershoot = false;
73         stictionSeen = false;
74     }
75 
InfoCount() const76     size_t InfoCount() const
77     {
78         return corrections.size();
79     }
80 
AddEventInfo(long TimeSecs,double Amount,double minMove)81     void AddEventInfo(long TimeSecs, double Amount, double minMove)
82     {
83         // Correction[0] is the deflection that triggered the BLC in the first place.  Correction[1] is the first delta after the pulse was issued,
84         // Correction[2] is the (optional) subsequent delta, needed to detect stiction
85         if (InfoCount() < 3)
86         {
87             corrections.push_back(CorrectionTuple(TimeSecs, Amount));               // Regardless of size relative to min-move
88             if (fabs(Amount) > minMove)
89             {
90                 if (InfoCount() == 2)
91                 {
92                     if (Amount > 0)
93                         initialUndershoot = true;
94                     else
95                         initialOvershoot = true;
96 
97                 }
98                 else
99                 {
100                     if (InfoCount() == 3)
101                     {
102                         stictionSeen = Amount < 0 && corrections[1].miss > 0;           // 2nd follow-on miss was an over-shoot
103                     }
104                 }
105             }
106         }
107     }
108 
109 };
110 
111 // Basic operation
112 // Keep a record of the last <HISTORY_DEPTH> BLC events.  Each event entry holds the initial BLC
113 // deflection and either one or two immediate follow-on deflections.  The deflections are raw Dec
114 // amounts, unfiltered by min-move or guiding algorithm decisions. Recording of follow-on
115 // deflections is determined by windowOpen.  The window is opened when the BLC is initially
116 // triggered and is then closed if a) A pulse-size adjustment is made based on the first follow-on
117 // deflection or b) 2 follow-on events have been recorded
118 // Algorithm behavior for adjusting BLC pulse size is in AdjustmentNeeded()
119 
120 class BLCHistory
121 {
122     std::vector<BLCEvent> blcEvents;
123     int blcIndex = 0;
124     const int ENTRY_CAPACITY = 3;
125     const unsigned int HISTORY_DEPTH = 10;
126     bool windowOpen;
127     long timeBase;
128     int lastIncrease;
129 
130 public:
131     struct RecentStats
132     {
133         int shortCount;
134         int longCount;
135         int stictionCount;
136         double avgInitialMiss;
137         double avgStictionAmount;
138 
RecentStatsBLCHistory::RecentStats139         RecentStats() : shortCount(0), longCount(0), stictionCount(0), avgInitialMiss(0), avgStictionAmount(0)
140         {
141         }
142     };
143 
WindowOpen() const144     bool WindowOpen() const
145     {
146         return windowOpen;
147     }
148 
BLCHistory()149     BLCHistory()
150     {
151         windowOpen = false;
152         lastIncrease = 0;
153         timeBase = wxGetCurrentTime();
154     }
155 
LogStatus(const wxString & Msg)156     static void LogStatus(const wxString& Msg)
157     {
158         Debug.Write(wxString::Format("BLC: %s\n", Msg));
159     }
160 
CloseWindow()161     void CloseWindow()
162     {
163         windowOpen = false;
164         Debug.Write("BLC: window closed\n");
165     }
166 
RecordNewBLCPulse(long when,double triggerDeflection)167     void RecordNewBLCPulse(long when, double triggerDeflection)
168     {
169         if (blcEvents.size() >= HISTORY_DEPTH)
170         {
171             blcEvents.erase(blcEvents.begin());
172             LogStatus("Oldest BLC event removed");
173         }
174         blcEvents.push_back(BLCEvent((when - timeBase), triggerDeflection));
175         blcIndex = blcEvents.size() - 1;
176         windowOpen = true;
177     }
178 
AddDeflection(long When,double Amt,double MinMove)179     bool AddDeflection(long When, double Amt, double MinMove)
180     {
181         bool added = false;
182         if (blcIndex >= 0 && blcEvents[blcIndex].InfoCount() < ENTRY_CAPACITY)
183         {
184             blcEvents[blcIndex].AddEventInfo(When-timeBase, Amt, MinMove);
185             added = true;
186             //LogStatus("Deflection entry added for event " + std::to_string(blcIndex));
187         }
188         else
189         {
190             CloseWindow();
191         }
192         return added;
193     }
194 
RemoveOldestOvershoots(int howMany)195     void RemoveOldestOvershoots(int howMany)
196     {
197         for (int ct = 1; ct <= howMany; ct++)
198         {
199             for (unsigned int inx = 0; inx < blcEvents.size() - 1; inx++)
200             {
201                 if (blcEvents[inx].initialOvershoot)
202                 {
203                     blcEvents.erase(blcEvents.begin() + inx);
204                     blcIndex = blcEvents.size() - 1;
205                     break;
206                 }
207             }
208         }
209     }
210 
RemoveOldestStictions(int howMany)211     void RemoveOldestStictions(int howMany)
212     {
213         for (int ct = 1; ct <= howMany; ct++)
214         {
215             for (unsigned int inx = 0; inx < blcEvents.size() - 1; inx++)
216             {
217                 if (blcEvents[inx].stictionSeen)
218                 {
219                     blcEvents.erase(blcEvents.begin() + inx);
220                     blcIndex = blcEvents.size() - 1;
221                     break;
222                 }
223             }
224         }
225     }
226 
ClearHistory()227     void ClearHistory()
228     {
229         blcEvents.clear();
230         CloseWindow();
231         LogStatus("History cleared");
232     }
233 
234     // Stats over some number of recent events, returns the average initial miss
GetStats(int numEvents,RecentStats * Results) const235     double GetStats(int numEvents, RecentStats* Results) const
236     {
237         int bottom = std::max(0, blcIndex - (numEvents - 1));
238         double sum = 0;
239         double stictionSum = 0;
240         int ct = 0;
241         for (int inx = blcIndex; inx >= bottom; inx--)
242         {
243             const BLCEvent& evt = blcEvents[inx];
244             if (evt.initialOvershoot)
245                 Results->longCount++;
246             else
247                 Results->shortCount++;
248             if (evt.stictionSeen)
249             {
250                 Results->stictionCount++;
251                 stictionSum += evt.corrections[2].miss;
252             }
253             // Average only the initial misses immediately following the blcs
254             if (evt.InfoCount() > 1)
255             {
256                 sum += evt.corrections[1].miss;
257                 ct++;
258             }
259         }
260         if (ct > 0)
261             Results->avgInitialMiss = sum / ct;
262         else
263             Results->avgInitialMiss = 0;
264         if (Results->stictionCount > 0)
265             Results->avgStictionAmount = stictionSum / Results->stictionCount;
266         else
267             Results->avgStictionAmount = 0;
268         return Results->avgInitialMiss;
269     }
270 
AdjustmentNeeded(double miss,double minMove,double yRate,double * correction)271     bool AdjustmentNeeded(double miss, double minMove, double yRate, double* correction)
272     {
273         bool adjust = false;
274         const BLCEvent *currEvent;
275         RecentStats stats;
276         *correction = 0;
277         double avgInitMiss = 0;
278         if (blcIndex >= 0)
279         {
280             avgInitMiss = GetStats(HISTORY_DEPTH, &stats);
281             currEvent = &blcEvents[blcIndex];
282             wxString deflections = " Deflections: 0=" + std::to_string(currEvent->corrections[0].miss) + ", 1:" +
283                 wxString(std::to_string(currEvent->corrections[1].miss));
284             if (currEvent->InfoCount() > 2)
285                 deflections += ", 2:" + std::to_string(currEvent->corrections[2].miss);
286             LogStatus(wxString::Format("History state: CurrMiss=%0.2f, AvgInitMiss=%0.2f, ShCount=%d, LgCount=%d, SticCount=%d, %s",
287                 miss, stats.avgInitialMiss, stats.shortCount, stats.longCount, stats.stictionCount, deflections));
288         }
289         else
290             return false;
291 
292         if (fabs(miss) >= minMove)                      // Most recent miss was big enough to look at
293         {
294             int corr;
295             corr = (int)(floor(abs(avgInitMiss) / yRate) + 0.5);                          // unsigned correction value
296             if (miss > 0)
297                 // UNDER-SHOOT-------------------------------
298             {
299                 if (avgInitMiss > 0)
300                 {
301                     // Might want to increase the blc value - but check for stiction and history of over-corrections
302                     // Don't make any changes before getting two follow-on displacements after last BLC
303                     if (currEvent->InfoCount() == ENTRY_CAPACITY)
304                     {
305                         // Check for stiction history
306                         if (stats.stictionCount > 2)
307                             LogStatus("Under-shoot, no adjustment because of stiction history");
308                         else
309                         {
310                             // Check for over-shoot history
311                             if (stats.longCount >= 2)             // 2 or more over-shoots in window
312                                 LogStatus("Under-shoot; no adjustment because of over-shoot history");
313                             else
314                             {
315                                 adjust = true;
316                                 *correction = corr;
317                                 lastIncrease = corr;
318                                 LogStatus("Under-shoot: nominal increase by " + std::to_string(corr));
319                             }
320                         }
321                     }
322                     else
323                         LogStatus("Under-shoot, no adjustment, waiting for more data");
324                 }
325                 else
326                 {
327                     LogStatus("Under-shoot, no adjustment, avgInitialMiss <= 0");
328                     CloseWindow();
329                 }
330             }
331             else
332                 // OVER-SHOOT, miss < 0--------------------------------------
333             {
334                 std::string msg = "";
335                 if (currEvent->stictionSeen)
336                 {
337                     if (stats.stictionCount > 1)          // Seeing and low min-move can look like stiction, don't over-react
338                     {
339                         msg = "Over-shoot, stiction seen, ";
340                         double stictionCorr = (int)(floor(abs(stats.avgStictionAmount) / yRate) + 0.5);
341                         *correction = -stictionCorr;
342                         RemoveOldestStictions(1);
343                         adjust = true;
344                         LogStatus(msg + "nominal decrease by " + std::to_string(*correction));
345                     }
346                     else
347                         LogStatus("Over-shoot, first stiction event, no adjustment");
348                 }
349                 else if (stats.longCount > stats.shortCount && blcIndex >= 4)
350                 {
351                     msg = "Recent history of over-shoots, ";
352                     *correction = -corr;
353                     RemoveOldestOvershoots(2);
354                     adjust = true;
355                     LogStatus(msg + "nominal decrease by " + std::to_string(*correction));
356                 }
357                 else if (avgInitMiss <= -0.1)
358                 {
359                     msg = "Average miss indicates over-shooting, ";
360                     *correction = -corr;
361                     adjust = true;
362                     LogStatus(msg + "nominal decrease by " + std::to_string(*correction));
363                 }
364                 else
365                 {
366                     correction = 0;
367                     std::string msg = "Over-shoot, no adjustment based on avgInitialMiss";
368                     LogStatus(msg);
369                     CloseWindow();
370                 }
371             }
372         }
373         else
374         {
375             LogStatus("No correction, Miss < min_move");
376         }
377 
378         if (adjust)
379             CloseWindow();
380         return adjust;
381     }
382 };
383 
BacklashComp(Scope * scope)384 BacklashComp::BacklashComp(Scope *scope)
385 {
386     m_pScope = scope;
387     m_pHistory = new BLCHistory();
388     int lastAmt = pConfig->Profile.GetInt("/" + m_pScope->GetMountClassName() + "/DecBacklashPulse", 0);
389     int lastFloor = pConfig->Profile.GetInt("/" + m_pScope->GetMountClassName() + "/DecBacklashFloor", 0);
390     int lastCeiling = pConfig->Profile.GetInt("/" + m_pScope->GetMountClassName() + "/DecBacklashCeiling", 0);
391     if (lastAmt > 0)
392         m_compActive = pConfig->Profile.GetBoolean("/" + m_pScope->GetMountClassName() + "/BacklashCompEnabled", false);
393     else
394         m_compActive = false;
395     SetCompValues(lastAmt, lastFloor, lastCeiling);
396     m_lastDirection = NONE;
397     if (m_compActive)
398         Debug.Write(wxString::Format("BLC: Enabled with correction = %d ms, Floor = %d, Ceiling = %d, %s\n",
399         m_pulseWidth, m_adjustmentFloor, m_adjustmentCeiling, m_fixedSize ? "Fixed" : "Adjustable"));
400     else
401         Debug.Write("BLC: Backlash compensation is disabled\n");
402 }
403 
~BacklashComp()404 BacklashComp::~BacklashComp()
405 {
406     delete m_pHistory;
407 }
408 
GetBacklashPulseMaxValue()409 int BacklashComp::GetBacklashPulseMaxValue()
410 {
411     return MAX_COMP_AMOUNT;
412 }
413 
GetBacklashPulseMinValue()414 int BacklashComp::GetBacklashPulseMinValue()
415 {
416     return MIN_COMP_AMOUNT;
417 }
418 
GetBacklashCompSettings(int * pulseWidth,int * floor,int * ceiling) const419 void BacklashComp::GetBacklashCompSettings(int* pulseWidth, int* floor, int* ceiling) const
420 {
421     *pulseWidth = m_pulseWidth;
422     *floor = m_adjustmentFloor;
423     *ceiling = m_adjustmentCeiling;
424 }
425 
426 // Private method to be sure all comp values are rational and comply with limits
427 // May change max-move value for Dec depending on the context
SetCompValues(int requestedSize,int floor,int ceiling)428 void BacklashComp::SetCompValues(int requestedSize, int floor, int ceiling)
429 {
430     m_pulseWidth = wxMax(0, wxMin(requestedSize, MAX_COMP_AMOUNT));
431     if (floor > m_pulseWidth || floor < MIN_COMP_AMOUNT)                        // Coming from GA or user input makes no sense
432         m_adjustmentFloor = MIN_COMP_AMOUNT;
433     else
434         m_adjustmentFloor = floor;
435     if (ceiling < m_pulseWidth)
436         m_adjustmentCeiling = wxMin(1.50 * m_pulseWidth, MAX_COMP_AMOUNT);
437     else
438         m_adjustmentCeiling = wxMin(ceiling, MAX_COMP_AMOUNT);
439     m_fixedSize = abs(m_adjustmentCeiling - m_adjustmentFloor) < MIN_COMP_AMOUNT;
440     if (m_pulseWidth > m_pScope->GetMaxDecDuration() && m_compActive)
441         m_pScope->SetMaxDecDuration(m_pulseWidth);
442 }
443 
444 // Public method to ask for a set of backlash comp settings.  Ceiling == 0 implies compute a default
SetBacklashPulseWidth(int ms,int floor,int ceiling)445 void BacklashComp::SetBacklashPulseWidth(int ms, int floor, int ceiling)
446 {
447     if (m_pulseWidth != ms || m_adjustmentFloor != floor || m_adjustmentCeiling != ceiling)
448     {
449         int oldBLC = m_pulseWidth;
450         SetCompValues(ms, floor, ceiling);
451         pFrame->NotifyGuidingParam("Backlash comp amount", m_pulseWidth);
452         Debug.Write(wxString::Format("BLC: Comp pulse set to %d ms, Floor = %d ms, Ceiling = %d ms, %s\n",
453             m_pulseWidth, m_adjustmentFloor, m_adjustmentCeiling, m_fixedSize ? "Fixed" : "Adjustable"));
454         if (abs(m_pulseWidth - oldBLC) > 100)
455         {
456             m_pHistory->ClearHistory();
457             m_pHistory->CloseWindow();
458         }
459     }
460 
461     pConfig->Profile.SetInt("/" + m_pScope->GetMountClassName() + "/DecBacklashPulse", m_pulseWidth);
462     pConfig->Profile.SetInt("/" + m_pScope->GetMountClassName() + "/DecBacklashFloor", m_adjustmentFloor);
463     pConfig->Profile.SetInt("/" + m_pScope->GetMountClassName() + "/DecBacklashCeiling", m_adjustmentCeiling);
464 }
465 
EnableBacklashComp(bool enable)466 void BacklashComp::EnableBacklashComp(bool enable)
467 {
468     if (m_compActive != enable)
469     {
470         pFrame->NotifyGuidingParam("Backlash comp enabled", enable);
471         if (enable)
472             ResetBLCState();
473     }
474 
475     m_compActive = enable;
476     pConfig->Profile.SetBoolean("/" + m_pScope->GetMountClassName() + "/BacklashCompEnabled", m_compActive);
477     Debug.Write(wxString::Format("BLC: Backlash comp %s, Comp pulse = %d ms\n", m_compActive ? "enabled" : "disabled", m_pulseWidth));
478 }
479 
ResetBLCState()480 void BacklashComp::ResetBLCState()
481 {
482     if (m_compActive)
483     {
484         m_lastDirection = NONE;
485         m_pHistory->CloseWindow();
486         Debug.Write("BLC: Last direction was reset\n");
487     }
488 }
489 
TrackBLCResults(unsigned int moveOptions,double yRawOffset)490 void BacklashComp::TrackBLCResults(unsigned int moveOptions, double yRawOffset)
491 {
492     if (!m_compActive)
493         return;
494 
495     if (!(moveOptions & MOVEOPT_USE_BLC))
496     {
497         // Calibration-type move that can move mount in Dec w/out notifying blc about direction
498         ResetBLCState();
499         return;
500     }
501 
502     // only track algorithm result moves, do not track "fast
503     // recovery after dither" moves or deduced moves or AO bump
504     // moves
505     bool isAlgoResultMove = (moveOptions & MOVEOPT_ALGO_RESULT) != 0;
506     if (!isAlgoResultMove)
507     {
508         // non-algo blc move occurred before follow-up data were acquired for previous blc
509         m_pHistory->CloseWindow();
510         return;
511     }
512 
513     if (!m_pHistory->WindowOpen() || m_fixedSize)
514         return;
515 
516     // An earlier BLC was applied and we're tracking follow-up results
517 
518     // Record the history even if residual error is zero. Sign convention has nothing to do with N or S direction - only whether we
519     // needed more correction (+) or less (-)
520     GUIDE_DIRECTION dir = yRawOffset > 0.0 ? DOWN : UP;
521     double yDistance = fabs(yRawOffset);
522     double miss;
523     if (dir == m_lastDirection)
524         miss = yDistance;                           // + => we needed more of the same, under-shoot
525     else
526         miss = -yDistance;                         // over-shoot
527 
528     double minMove = fmax(m_pScope->GetYGuideAlgorithm()->GetMinMove(), 0.); // Algo w/ no min-move returns -1
529 
530     m_pHistory->AddDeflection(wxGetCurrentTime(), miss, minMove);
531 
532     double adjustment;
533     if (!m_pHistory->AdjustmentNeeded(miss, minMove, m_pScope->MountCal().yRate, &adjustment))
534         return;
535 
536     int newBLC;
537     double nominalBLC = m_pulseWidth + adjustment;
538     if (nominalBLC > m_pulseWidth)
539     {
540         newBLC = ROUND(fmin(m_pulseWidth * 1.1, nominalBLC));
541         if (newBLC > m_adjustmentCeiling)
542         {
543             Debug.Write(wxString::Format("BLC: Pulse increase limited by ceiling of %d\n", m_adjustmentCeiling));
544             newBLC = m_adjustmentCeiling;
545         }
546     }
547     else
548     {
549         newBLC = ROUND(fmax(0.8 * m_pulseWidth, nominalBLC));
550         if (newBLC < m_adjustmentFloor)
551         {
552             Debug.Write(wxString::Format("BLC: Pulse decrease limited by floor of %d\n", m_adjustmentFloor));
553             newBLC = m_adjustmentFloor;
554         }
555     }
556     Debug.Write(wxString::Format("BLC: Pulse adjusted to %d\n", newBLC));
557     pConfig->Profile.SetInt("/" + m_pScope->GetMountClassName() + "/DecBacklashPulse", newBLC);
558     SetCompValues(newBLC, m_adjustmentFloor, m_adjustmentCeiling);
559 }
560 
ApplyBacklashComp(unsigned int moveOptions,double yGuideDistance,int * yAmount)561 void BacklashComp::ApplyBacklashComp(unsigned int moveOptions, double yGuideDistance, int *yAmount)
562 {
563     if (!(moveOptions & MOVEOPT_USE_BLC))
564         return;
565     if (!m_compActive || m_pulseWidth <= 0 || yGuideDistance == 0.0)
566         return;
567 
568     GUIDE_DIRECTION dir = yGuideDistance > 0.0 ? DOWN : UP;
569     bool isAlgoResultMove = (moveOptions & MOVEOPT_ALGO_RESULT) != 0;
570 
571     if (m_lastDirection != NONE && dir != m_lastDirection)
572     {
573         *yAmount += m_pulseWidth;
574 
575         if (isAlgoResultMove)
576         {
577             // Only track results or make adjustments for algorithm-controlled blc's
578             m_pHistory->RecordNewBLCPulse(wxGetCurrentTime(), yGuideDistance);
579         }
580         else
581         {
582             m_pHistory->CloseWindow();
583             Debug.Write("BLC: Compensation needed for non-algo type move\n");
584         }
585 
586         Debug.Write(wxString::Format("BLC: Dec direction reversal from %s to %s, backlash comp pulse of %d applied\n",
587             m_pScope->DirectionStr(m_lastDirection), m_pScope->DirectionStr(dir), m_pulseWidth));
588     }
589     else if (!isAlgoResultMove)
590     {
591         Debug.Write("BLC: non-algo type move will not reverse Dec direction, no blc applied\n");
592     }
593 
594     m_lastDirection = dir;
595 }
596 
597 // Class for implementing the backlash graph dialog
598 class BacklashGraph : public wxDialog
599 {
600 public:
601     BacklashGraph(wxDialog *parent, const std::vector<double> &northSteps, const std::vector<double> &southSteps, int PulseSize);
602     wxBitmap CreateGraph(int graphicWidth, int graphicHeight, const std::vector<double> &northSteps, const std::vector<double> &southSteps, int PulseSize);
603 };
604 
BacklashGraph(wxDialog * parent,const std::vector<double> & northSteps,const std::vector<double> & southSteps,int PulseSize)605 BacklashGraph::BacklashGraph(wxDialog *parent, const std::vector<double> &northSteps, const std::vector<double> &southSteps, int PulseSize)
606     : wxDialog(parent, wxID_ANY, wxGetTranslation(_("Backlash Results")), wxDefaultPosition, wxSize(500, 400))
607 {
608     // Just but a big button area for the graph with a button below it
609     wxBoxSizer *vSizer = new wxBoxSizer(wxVERTICAL);
610     // Use a bitmap button so we don't waste cycles in paint events
611     wxBitmap graph_bitmap = CreateGraph(450, 300, northSteps, southSteps, PulseSize);
612     wxStaticBitmap *graph = new wxStaticBitmap(this, wxID_ANY, graph_bitmap, wxDefaultPosition, wxDefaultSize, 0);
613     vSizer->Add(graph, 0, wxALIGN_CENTER_HORIZONTAL | wxALL | wxFIXED_MINSIZE, 5);
614 
615     // ok button because we're modal
616     vSizer->Add(
617         CreateButtonSizer(wxOK),
618         wxSizerFlags(0).Expand().Border(wxALL, 10));
619 
620     SetSizerAndFit(vSizer);
621 }
622 
CreateGraph(int bmpWidth,int bmpHeight,const std::vector<double> & northSteps,const std::vector<double> & southSteps,int PulseSize)623 wxBitmap BacklashGraph::CreateGraph(int bmpWidth, int bmpHeight, const std::vector<double> &northSteps, const std::vector<double> &southSteps, int PulseSize)
624 {
625     wxMemoryDC dc;
626     wxBitmap bmp(bmpWidth, bmpHeight, -1);
627     wxColour decColor = pFrame->pGraphLog->GetDecOrDyColor();
628     wxColour idealColor("WHITE");
629     wxPen axisPen("GREY", 3, wxPENSTYLE_CROSS_HATCH);
630     wxPen decPen(decColor, 3, wxPENSTYLE_SOLID);
631     wxPen idealPen(idealColor, 3, wxPENSTYLE_SOLID);
632     wxBrush decBrush(decColor, wxBRUSHSTYLE_SOLID);
633     wxBrush idealBrush(idealColor, wxBRUSHSTYLE_SOLID);
634 
635     double xScaleFactor;
636     double yScaleFactor;
637     int xOrigin;
638     int yOrigin;
639     int ptRadius;
640     int graphWindowWidth;
641     int graphWindowHeight;
642     int numNorth;
643     double northInc;
644     int numSouth;
645 
646     // Find the max excursion from the origin in order to scale the points to fit the bitmap
647     double maxDec = -9999.0;
648     double minDec = 9999.0;
649     for (auto it = northSteps.begin(); it != northSteps.end(); ++it)
650     {
651         maxDec = wxMax(maxDec, *it);
652         minDec = wxMin(minDec, *it);
653     }
654 
655     for (auto it = southSteps.begin(); it != southSteps.end(); ++it)
656     {
657         maxDec = wxMax(maxDec, *it);
658         minDec = wxMin(minDec, *it);
659     }
660 
661     graphWindowWidth = bmpWidth;
662     graphWindowHeight = 0.7 * bmpHeight;
663     yScaleFactor = (graphWindowHeight) / (maxDec - minDec + 1);
664     xScaleFactor = (graphWindowWidth) / (northSteps.size() + southSteps.size());
665 
666     // Since we get mount coordinates, north steps will always be in ascending order
667     numNorth = northSteps.size();
668     northInc = (northSteps.at(numNorth - 1) - northSteps.at(0)) / numNorth;
669     numSouth = southSteps.size();       // May not be the same as numNorth if some sort of problem occurred
670 
671     dc.SelectObject(bmp);
672     dc.SetBackground(*wxBLACK_BRUSH);
673 
674     dc.SetFont(wxFont(12, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
675     dc.Clear();
676 
677     // Bottom and top labels
678     xOrigin = graphWindowWidth / 2;
679     yOrigin = graphWindowHeight + 40;           // Leave room at the top for labels and such
680     dc.SetTextForeground(idealColor);
681     dc.DrawText(_("Ideal"), 0.7 * graphWindowWidth, bmpHeight - 25);
682     dc.SetTextForeground(decColor);
683     dc.DrawText(_("Measured"), 0.2 * graphWindowWidth, bmpHeight - 25);
684     dc.DrawText(_("North"), 0.1 * graphWindowWidth, 10);
685     dc.DrawText(_("South"), 0.8 * graphWindowWidth, 10);
686     if (PulseSize > 0)
687     {
688         wxString pulseSzTxt = wxString::Format(_("Pulse size = %d ms"), PulseSize);
689         int w, h;
690         GetTextExtent(pulseSzTxt, &w, &h);
691 
692         dc.DrawText(pulseSzTxt, wxMax(0, graphWindowWidth / 2 - w / 2 - 10), yOrigin - (h + 10));
693     }
694     // Draw the axes
695     dc.SetPen(axisPen);
696     dc.DrawLine(0, yOrigin, graphWindowWidth, yOrigin);    // x
697     dc.DrawLine(xOrigin, yOrigin, xOrigin, 0);             // y
698 
699     // Draw the north steps
700     dc.SetPen(decPen);
701     dc.SetBrush(decBrush);
702     ptRadius = 1;
703 
704     for (int i = 0; i < numNorth; i++)
705     {
706         dc.DrawCircle(wxPoint(i * xScaleFactor, round(yOrigin - (northSteps.at(i) - minDec) * yScaleFactor)), ptRadius);
707     }
708 
709     // Draw the south steps
710     for (int i = 0; i < numSouth; i++)
711     {
712         dc.DrawCircle(wxPoint((i + numNorth) * xScaleFactor, round(yOrigin - (southSteps.at(i) - minDec) * yScaleFactor)), ptRadius);
713     }
714 
715     // Now show an ideal south recovery line
716     dc.SetPen(idealPen);
717     dc.SetBrush(idealBrush);
718 
719     double peakSouth = southSteps.at(0);
720     for (int i = 1; i <= numNorth; i++)
721     {
722         wxPoint where = wxPoint((i + numNorth)* xScaleFactor, round(yOrigin - (peakSouth - i * northInc - minDec) * yScaleFactor));
723         dc.DrawCircle(where, ptRadius);
724     }
725 
726     dc.SelectObject(wxNullBitmap);
727     return bmp;
728 }
729 
730 // -------------------  BacklashTool Implementation
731 
BacklashTool()732 BacklashTool::BacklashTool()
733 {
734     m_scope = TheScope();
735 
736     m_lastDecGuideRate = GetLastDecGuideRate();     // -1 if we aren't calibrated
737     if (m_lastDecGuideRate > 0)
738         m_bltState = BLT_STATE_INITIALIZE;
739     else
740     {
741         m_bltState = BLT_STATE_ABORTED;
742         m_lastStatus = _("Backlash measurement cannot be run - please re-run your mount calibration");
743         Debug.Write("BLT: Could not get calibration data\n");
744     }
745     m_backlashResultPx = 0;
746     m_backlashResultMs = 0;
747     m_cumClearingDistance = 0;
748     m_backlashExemption = false;
749 }
750 
~BacklashTool()751 BacklashTool::~BacklashTool()
752 {
753 
754 }
755 
IsGraphable()756 bool BacklashTool::IsGraphable()
757 {
758     return m_southBLSteps.size() > 0;
759 }
760 
GetLastDecGuideRate()761 double BacklashTool::GetLastDecGuideRate()
762 {
763     double rtnVal;
764     Calibration lastCalibration;
765     m_scope->GetLastCalibration(&lastCalibration);
766 
767     if (lastCalibration.isValid)
768     {
769         rtnVal = lastCalibration.yRate;
770     }
771     else
772     {
773         rtnVal = -1;
774     }
775     return rtnVal;
776 }
777 
StartMeasurement(double DriftPerMin)778 void BacklashTool::StartMeasurement(double DriftPerMin)
779 {
780     m_bltState = BLT_STATE_INITIALIZE;
781     m_driftPerSec = DriftPerMin / 60.0;
782     m_northBLSteps.clear();
783     m_southBLSteps.clear();
784     m_northStats.ClearAll();
785     DecMeasurementStep(pFrame->pGuider->CurrentPosition());
786 }
787 
StopMeasurement()788 void BacklashTool::StopMeasurement()
789 {
790     m_bltState = BLT_STATE_ABORTED;
791     DecMeasurementStep(pFrame->pGuider->CurrentPosition());
792 }
793 
OutOfRoom(const wxSize & frameSize,double camX,double camY,int margin)794 static bool OutOfRoom(const wxSize& frameSize, double camX, double camY, int margin)
795 {
796     return camX < margin ||
797         camY < margin ||
798         camX >= frameSize.GetWidth() - margin ||
799         camY >= frameSize.GetHeight() - margin;
800 }
801 
802 // Measure the apparent backlash by looking at the first south moves, looking to see when the mount moves consistently at the expected rate
803 // Goal is to establish a good seed value for backlash compensation, not to accurately measure the hardware performance
ComputeBacklashPx(double * bltPx,int * bltMs,double * northRate)804 BacklashTool::MeasurementResults BacklashTool::ComputeBacklashPx(double* bltPx, int* bltMs, double* northRate)
805 {
806     double expectedAmount;
807     double expectedMagnitude;
808     double earlySouthMoves = 0;
809     double blPx = 0;
810     double northDelta = 0;
811     double driftPxPerFrame;
812     double nRate = 0.;
813     BacklashTool::MeasurementResults rslt;
814 
815     *bltPx = 0;
816     *bltMs = 0;
817     *northRate = m_lastDecGuideRate;
818     if (m_northBLSteps.size() > 3)
819     {
820         // figure out the drift-related corrections
821         double driftAmtPx = m_driftPerSec * (m_msmtEndTime - m_msmtStartTime) / 1000;               // amount of drift in px for entire north measurement period
822         int stepCount = m_northStats.GetCount();
823         northDelta = m_northStats.GetSum();
824         nRate = fabs((northDelta - driftAmtPx) / (stepCount * m_pulseWidth));                       // drift-corrected empirical measure of north rate
825         driftPxPerFrame = driftAmtPx / stepCount;
826         Debug.Write(wxString::Format("BLT: Drift correction of %0.2f px applied to total north moves of %0.2f px, %0.3f px/frame\n", driftAmtPx, northDelta, driftPxPerFrame));
827         Debug.Write(wxString::Format("BLT: Empirical north rate = %.2f px/s \n", nRate * 1000));
828 
829         // Compute an expected movement of 90% of the median delta north moves (px).  Use the 90% tolerance to accept situations where the south rate
830         // never matches the north rate yet the mount is moving consistently. Allow smoothing for odd mounts that produce sequences of short-long-short-long...
831         expectedAmount = 0.9 * m_northStats.GetMedian();
832         expectedMagnitude = fabs(expectedAmount);
833         int goodSouthMoves = 0;
834         double lastSouthMove = 0;
835         bool smoothing = false;
836         for (int step = 1; step < m_southBLSteps.size(); step++)
837         {
838             double southMove = m_southBLSteps[step] - m_southBLSteps[step-1];
839             earlySouthMoves += southMove;
840             if (southMove < 0 && (fabs(southMove) >= expectedMagnitude || fabs(southMove + lastSouthMove / 2.0) > expectedMagnitude))     // Big enough move and in the correct (south) direction
841             {
842                 if (fabs(southMove) < expectedMagnitude)
843                     smoothing = true;
844                 goodSouthMoves++;
845                 // We want two consecutive south moves that meet or exceed the expected magnitude.  This sidesteps situations where the mount shows a "false start" south
846                 if (goodSouthMoves == 2)
847                 {
848                     if (smoothing)
849                         Debug.Write("BLT: Smoothing applied to south data points\n");
850                     // bl = sum(expected moves) - sum(actual moves) - (drift correction for that period)
851                     blPx = step * expectedMagnitude - fabs(earlySouthMoves - step * driftPxPerFrame);               // drift-corrected backlash amount
852                     if (blPx * nRate < -200)
853                         rslt = MEASUREMENT_SANITY;              // large negative number
854                     else
855                     if (blPx >= 0.7 * northDelta)
856                         rslt = MEASUREMENT_TOO_FEW_NORTH;       // bl large compared to total north moves
857                     else
858                         rslt = MEASUREMENT_VALID;
859                     if (blPx < 0)
860                     {
861                         Debug.Write(wxString::Format("BLT: Negative measurement = %0.2f px, forcing to zero\n", blPx));
862                         blPx = 0;
863                     }
864                     break;
865                 }
866             }
867             else
868             {
869                 if (goodSouthMoves > 0)
870                     goodSouthMoves--;
871             }
872             lastSouthMove = southMove;
873         }
874         if (goodSouthMoves < 2)
875             rslt = MEASUREMENT_TOO_FEW_SOUTH;
876     }
877     else
878         rslt = MEASUREMENT_TOO_FEW_NORTH;
879     // Update the ref variables
880     *bltPx = blPx;
881     *bltMs = (int)(blPx / nRate);
882     *northRate = nRate;
883     return rslt;
884 }
885 
DecMeasurementStep(const PHD_Point & currentCamLoc)886 void BacklashTool::DecMeasurementStep(const PHD_Point& currentCamLoc)
887 {
888     double decDelta = 0.;
889     double amt = 0;
890     // double fakeDeltas []= {0, -5, -2, 2, 4, 5, 5, 5, 5 };
891     PHD_Point currMountLocation;
892     double tol;
893     try
894     {
895         if (m_scope->TransformCameraCoordinatesToMountCoordinates(currentCamLoc, currMountLocation))
896             throw ERROR_INFO("BLT: CamToMount xForm failed");
897         if (m_bltState != BLT_STATE_INITIALIZE)
898         {
899             decDelta = currMountLocation.Y - m_markerPoint.Y;
900             m_cumClearingDistance += decDelta;                                    // use signed value
901             //if (m_bltState == BLT_STATE_CLEAR_NORTH)                            // DEBUG ONLY
902             //    decDelta = fakeDeltas[wxMin(m_stepCount, 7)];
903         }
904         Debug.Write("BLT: Entering DecMeasurementStep, state = " + std::to_string(m_bltState) + "\n");
905         switch (m_bltState)
906         {
907         case BLT_STATE_INITIALIZE:
908             m_stepCount = 0;
909             m_markerPoint = currMountLocation;
910             m_startingPoint = currMountLocation;
911             // Compute pulse size for clearing backlash - just use the last known guide rate
912             if (m_lastDecGuideRate <= 0)
913                 m_lastDecGuideRate = GetLastDecGuideRate();             // try it again, maybe the user has since calibrated
914             if (m_lastDecGuideRate > 0)
915             {
916                 m_pulseWidth = BACKLASH_EXPECTED_DISTANCE * 1.25 / m_lastDecGuideRate;      // px/px_per_ms, bump it to sidestep near misses
917                 m_acceptedMoves = 0;
918                 m_lastClearRslt = 0;
919                 m_cumClearingDistance = 0;
920                 m_backlashExemption = false;
921                 m_Rslt = MEASUREMENT_VALID;
922                 // Get this state machine in synch with the guider state machine - let it drive us, starting with backlash clearing step
923                 m_bltState = BLT_STATE_CLEAR_NORTH;
924                 m_scope->SetGuidingEnabled(true);
925                 pFrame->pGuider->EnableMeasurementMode(true);                   // Measurement results now come to us
926             }
927             else
928             {
929                 m_bltState = BLT_STATE_ABORTED;
930                 m_lastStatus = _("Backlash measurement cannot be run - Dec guide rate not available");
931                 Debug.Write("BLT: Could not get calibration data\n");
932             }
933             break;
934 
935         case BLT_STATE_CLEAR_NORTH:
936             // Want to see the mount moving north for 3 consecutive moves of >= expected distance pixels
937             if (m_stepCount == 0)
938             {
939                 // Get things moving with the first clearing pulse
940                 Debug.Write(wxString::Format("BLT starting North backlash clearing using pulse width of %d,"
941                     " looking for moves >= %d px\n", m_pulseWidth, BACKLASH_EXPECTED_DISTANCE));
942                 pFrame->ScheduleAxisMove(m_scope, NORTH, m_pulseWidth, MOVEOPTS_CALIBRATION_MOVE);
943                 m_stepCount = 1;
944                 m_lastStatus = wxString::Format(_("Clearing North backlash, step %d"), m_stepCount);
945                 m_lastStatusDebug = wxString::Format("Clearing North backlash, step %d", m_stepCount);
946                 break;
947             }
948             if (fabs(decDelta) >= BACKLASH_EXPECTED_DISTANCE)
949             {
950                 if (m_acceptedMoves == 0 || (m_lastClearRslt * decDelta) > 0)    // Just starting or still moving in same direction
951                 {
952                     m_acceptedMoves++;
953                     Debug.Write(wxString::Format("BLT accepted clearing move of %0.2f\n", decDelta));
954                 }
955                 else
956                 {
957                     m_acceptedMoves = 0;            // Reset on a direction reversal
958                     Debug.Write(wxString::Format("BLT rejected clearing move of %0.2f, direction reversal\n", decDelta));
959                 }
960             }
961             else
962                 Debug.Write(wxString::Format("BLT backlash clearing move of %0.2f px was not large enough\n", decDelta));
963             if (m_acceptedMoves < BACKLASH_MIN_COUNT)                    // More work to do
964             {
965                 if (m_stepCount < MAX_CLEARING_STEPS)
966                 {
967                     if (fabs(m_cumClearingDistance) > BACKLASH_EXEMPTION_DISTANCE)
968                     {
969                         // We moved the mount a substantial distance north but the individual moves were too small - probably a bad calibration,
970                         // so let the user proceed with backlash measurement before we push the star too far
971                         Debug.Write(wxString::Format("BLT: Cum backlash of %0.2f px is at least half of expected, continue with backlash measurement\n", m_cumClearingDistance));
972                         m_backlashExemption = true;
973                     }
974                     else
975                     {
976                         if (!OutOfRoom(pCamera->FullSize, currentCamLoc.X, currentCamLoc.Y, pFrame->pGuider->GetMaxMovePixels()))
977                         {
978                             pFrame->ScheduleAxisMove(m_scope, NORTH, m_pulseWidth, MOVEOPTS_CALIBRATION_MOVE);
979                             m_stepCount++;
980                             m_markerPoint = currMountLocation;
981                             m_lastClearRslt = decDelta;
982                             m_lastStatus = wxString::Format(_("Clearing North backlash, step %d (up to limit of %d)"), m_stepCount, MAX_CLEARING_STEPS);
983                             m_lastStatusDebug = wxString::Format("Clearing North backlash, step %d (up to limit of %d)", m_stepCount, MAX_CLEARING_STEPS);
984                             Debug.Write(wxString::Format("BLT: %s, LastDecDelta = %0.2f px\n", m_lastStatusDebug, decDelta));
985                             break;
986                         }
987                     }
988                 }
989                 else
990                 {
991                     m_lastStatus = _("Could not clear North backlash - test failed");
992                     m_Rslt = MEASUREMENT_BL_NOT_CLEARED;
993                     throw (wxString("BLT: Could not clear north backlash"));
994                 }
995             }
996             if (m_acceptedMoves >= BACKLASH_MIN_COUNT || m_backlashExemption || OutOfRoom(pCamera->FullSize, currentCamLoc.X, currentCamLoc.Y, pFrame->pGuider->GetMaxMovePixels()))    // Ok to go ahead with actual backlash measurement
997             {
998                 m_bltState = BLT_STATE_STEP_NORTH;
999                 double totalBacklashCleared = m_stepCount * m_pulseWidth;
1000                 // Want to move the mount North at >=500 ms, regardless of image scale. But reduce pulse width if it would exceed 80% of the tracking rectangle -
1001                 // need to leave some room for seeing deflections and dec drift
1002                 m_pulseWidth = wxMax((int)NORTH_PULSE_SIZE, m_scope->GetCalibrationDuration());
1003                 m_pulseWidth = wxMin(m_pulseWidth, (int)floor(0.7 * (double)pFrame->pGuider->GetMaxMovePixels() / m_lastDecGuideRate));
1004                 m_stepCount = 0;
1005                 // Move 50% more than the backlash we cleared or >=8 secs, whichever is greater.  We want to leave plenty of room
1006                 // for giving South moves time to clear backlash and actually get moving
1007                 m_northPulseCount = wxMax((MAX_NORTH_PULSES + m_pulseWidth - 1) / m_pulseWidth,
1008                                           totalBacklashCleared * 1.5 / m_pulseWidth);  // Up to 8 secs
1009 
1010                 Debug.Write(wxString::Format("BLT: Starting North moves at Dec=%0.2f\n", currMountLocation.Y));
1011                 m_msmtStartTime = ::wxGetUTCTimeMillis().GetValue();
1012                 // falling through to start moving North
1013             }
1014 
1015         case BLT_STATE_STEP_NORTH:
1016             if (m_stepCount < m_northPulseCount && !OutOfRoom(pCamera->FullSize, currentCamLoc.X, currentCamLoc.Y, pFrame->pGuider->GetMaxMovePixels()))
1017             {
1018                 m_lastStatus = wxString::Format(_("Moving North for %d ms, step %d / %d"), m_pulseWidth, m_stepCount + 1, m_northPulseCount);
1019                 m_lastStatusDebug = wxString::Format("Moving North for %d ms, step %d / %d", m_pulseWidth, m_stepCount + 1, m_northPulseCount);
1020                 double deltaN;
1021                 if (m_stepCount >= 1)
1022                 {
1023                     deltaN = currMountLocation.Y - m_northBLSteps.back();
1024                     m_northStats.AddGuideInfo(m_stepCount, deltaN, 0);
1025                 }
1026                 else
1027                 {
1028                     deltaN = 0;
1029                     m_markerPoint = currMountLocation;            // Marker point at start of Dec moves North
1030                 }
1031                 Debug.Write(wxString::Format("BLT: %s, DecLoc = %0.2f, DeltaDec = %0.2f\n", m_lastStatusDebug, currMountLocation.Y, deltaN));
1032                 m_northBLSteps.push_back(currMountLocation.Y);
1033                 pFrame->ScheduleAxisMove(m_scope, NORTH, m_pulseWidth, MOVEOPTS_CALIBRATION_MOVE);
1034                 m_stepCount++;
1035                 break;
1036             }
1037             else
1038             {
1039                 // Either got finished or ran out of room
1040                 m_msmtEndTime = ::wxGetUTCTimeMillis().GetValue();
1041                 double deltaN = 0;
1042                 if (m_stepCount >= 1)
1043                 {
1044                     deltaN = currMountLocation.Y - m_northBLSteps.back();
1045                     m_northStats.AddGuideInfo(m_stepCount, deltaN, 0);
1046                 }
1047                 Debug.Write(wxString::Format("BLT: North pulses ended at Dec location %0.2f, TotalDecDelta=%0.2f px, LastDeltaDec = %0.2f\n", currMountLocation.Y, decDelta, deltaN));
1048                 m_northBLSteps.push_back(currMountLocation.Y);
1049                 if (m_stepCount < m_northPulseCount)
1050                 {
1051                     if (m_stepCount < 0.5 * m_northPulseCount)
1052                     {
1053                         m_lastStatus = _("Star too close to edge for accurate measurement of backlash. Choose a star farther from the edge.");
1054                         m_Rslt = MEASUREMENT_TOO_FEW_NORTH;
1055                         throw (wxString("BLT: Too few north moves"));
1056                     }
1057                     Debug.Write("BLT: North pulses truncated, too close to frame edge\n");
1058                 }
1059                 m_northPulseCount = m_stepCount;
1060                 m_stepCount = 0;
1061                 m_bltState = BLT_STATE_STEP_SOUTH;
1062                 // falling through to moving back South
1063             }
1064 
1065         case BLT_STATE_STEP_SOUTH:
1066             if (m_stepCount < m_northPulseCount)
1067             {
1068                 m_lastStatus = wxString::Format(_("Moving South for %d ms, step %d / %d"), m_pulseWidth, m_stepCount + 1, m_northPulseCount);
1069                 m_lastStatusDebug = wxString::Format("Moving South for %d ms, step %d / %d", m_pulseWidth, m_stepCount + 1, m_northPulseCount);
1070                 Debug.Write(wxString::Format("BLT: %s, DecLoc = %0.2f\n", m_lastStatusDebug, currMountLocation.Y));
1071                 m_southBLSteps.push_back(currMountLocation.Y);
1072                 pFrame->ScheduleAxisMove(m_scope, SOUTH, m_pulseWidth, MOVEOPTS_CALIBRATION_MOVE);
1073                 m_stepCount++;
1074                 break;
1075             }
1076 
1077             // Now see where we ended up - fall through to computing and testing a correction
1078             Debug.Write(wxString::Format("BLT: South pulses ended at Dec location %0.2f\n", currMountLocation.Y));
1079             m_southBLSteps.push_back(currMountLocation.Y);
1080             m_endSouth = currMountLocation;
1081             m_bltState = BLT_STATE_TEST_CORRECTION;
1082             m_stepCount = 0;
1083             // fall through
1084 
1085         case BLT_STATE_TEST_CORRECTION:
1086             if (m_stepCount == 0)
1087             {
1088                 m_Rslt = ComputeBacklashPx(&m_backlashResultPx, &m_backlashResultMs, &m_northRate);
1089                 if (m_Rslt != MEASUREMENT_VALID)
1090                 {
1091                     // Abort the test and show an explanatory status in the GA dialog
1092                     switch (m_Rslt)
1093                     {
1094                     case MEASUREMENT_SANITY:
1095                         m_lastStatus = _("Dec movements too erratic - test failed");
1096                         throw (wxString("BLT: Calculation failed sanity check"));
1097                         break;
1098                     case MEASUREMENT_TOO_FEW_NORTH:
1099                         // Don't throw an exception - the test was completed but the bl result is not accurate - handle it in the GA UI
1100                         break;
1101                     case MEASUREMENT_TOO_FEW_SOUTH:
1102                         m_lastStatus = _("Mount never established consistent south moves - test failed");
1103                         throw (wxString("BLT: Too few acceptable south moves"));
1104                         break;
1105                     default:
1106 			            break;
1107                     }
1108                 }
1109 
1110                 double sigmaPx;
1111                 double sigmaMs;
1112                 GetBacklashSigma(&sigmaPx, &sigmaMs);
1113                 Debug.Write(wxString::Format("BLT: Trial backlash amount is %0.2f px, %d ms, sigma = %0.1f px\n", m_backlashResultPx, m_backlashResultMs,
1114                     sigmaPx));
1115                 if (m_backlashResultMs > 0)
1116                 {
1117                     // Don't push the guide star outside the tracking region
1118                     if (m_backlashResultPx < pFrame->pGuider->GetMaxMovePixels())
1119                     {
1120                         m_lastStatus = wxString::Format(_("Issuing test backlash correction of %d ms"), m_backlashResultMs);
1121                         Debug.Write(m_lastStatus + "\n");
1122                         // This should put us back roughly to where we issued the big North pulse unless the backlash is very large
1123                         pFrame->ScheduleAxisMove(m_scope, SOUTH, m_backlashResultMs, MOVEOPTS_CALIBRATION_MOVE);
1124                         m_stepCount++;
1125                         break;
1126                     }
1127                     else
1128                     {
1129                         int maxFrameMove = (int)floor((double)0.8 * pFrame->pGuider->GetMaxMovePixels() / m_northRate);
1130                         Debug.Write(wxString::Format("BLT: Clearing pulse is very large, issuing max S move of %d\n", maxFrameMove));
1131                         pFrame->ScheduleAxisMove(m_scope, SOUTH, maxFrameMove, MOVEOPTS_CALIBRATION_MOVE); // One more pulse to cycle the state machine
1132                         m_stepCount = 0;
1133                         // Can't fine-tune the pulse size, just try to restore the star to < MaxMove of error
1134                         m_bltState = BLT_STATE_RESTORE;
1135                         break;
1136                     }
1137                 }
1138                 else
1139                 {
1140                     m_bltState = BLT_STATE_RESTORE;
1141                     m_stepCount = 0;
1142                     // fall through, no need for test pulse
1143                 }
1144 
1145             }
1146             // See how close we came, maybe fine-tune a bit
1147             if (m_bltState == BLT_STATE_TEST_CORRECTION)
1148             {
1149                 Debug.Write(wxString::Format("BLT: Trial backlash pulse resulted in net DecDelta = %0.2f px, Dec Location %0.2f\n", decDelta, currMountLocation.Y));
1150                 tol = TRIAL_TOLERANCE_AS / pFrame->GetCameraPixelScale();                           // tolerance in units of px
1151                 if (fabs(decDelta) > tol)                                                           // decDelta = (current - markerPoint)
1152                 {
1153                     double pulse_delta = fabs(currMountLocation.Y - m_endSouth.Y);                  // How far we moved with the test pulse
1154                     double target_delta = fabs(m_markerPoint.Y - m_endSouth.Y);                     // How far we needed to go
1155                     if ((m_endSouth.Y - m_markerPoint.Y) * decDelta < 0)                            // Sign change, went too far
1156                     {
1157                         //m_backlashResultMs *= target_delta / pulse_delta;
1158                         Debug.Write(wxString::Format("BLT: Nominal backlash value over-shot by %0.2f X\n", target_delta / pulse_delta));
1159                     }
1160                     else
1161                     {
1162                         Debug.Write(wxString::Format("BLT: Nominal backlash value under-shot by %0.2f X\n", target_delta / pulse_delta));
1163                     }
1164                 }
1165                 else
1166                     Debug.Write(wxString::Format("BLT: Nominal backlash pulse resulted in final delta of %0.1f a-s\n", fabs(decDelta) * pFrame->GetCameraPixelScale()));
1167             }
1168 
1169             m_bltState = BLT_STATE_RESTORE;
1170             Debug.Write("BLT: normal result, moving to state=restore\n");
1171             m_stepCount = 0;
1172             // fall through
1173 
1174         case BLT_STATE_RESTORE:
1175             // We could be a considerable distance from where we started, so get back close to the starting point without losing the star
1176             if (m_stepCount == 0)
1177             {
1178                 Debug.Write(wxString::Format("BLT: Starting Dec position at %0.2f, Ending Dec position at %0.2f\n", m_markerPoint.Y, currMountLocation.Y));
1179                 amt = fabs(currMountLocation.Y - m_startingPoint.Y);
1180                 if (amt > pFrame->pGuider->GetMaxMovePixels())      // Too big, try to move guide star closer to starting position
1181                 {
1182                     m_restoreCount = (int)floor((amt / m_northRate) / m_pulseWidth);
1183                     m_restoreCount = wxMin(m_restoreCount, 10);             // Don't spend forever at it, something probably went wrong
1184                     Debug.Write(wxString::Format("BLT: Final restore distance is %0.1f px, approx %d steps\n", amt, m_restoreCount));
1185                     m_stepCount = 0;
1186                 }
1187                 else
1188                     m_bltState = BLT_STATE_WRAPUP;
1189             }
1190             if (m_stepCount < m_restoreCount)
1191             {
1192 
1193                 pFrame->ScheduleAxisMove(m_scope, SOUTH, m_pulseWidth, MOVEOPTS_CALIBRATION_MOVE);
1194                 m_stepCount++;
1195                 m_lastStatus = _("Restoring star position");
1196                 Debug.Write(wxString::Format("BLT: Issuing restore pulse count %d of %d ms\n", m_stepCount, m_pulseWidth));
1197                 break;
1198             }
1199             m_bltState = BLT_STATE_WRAPUP;
1200             Debug.Write("BLT: normal result, moving to state=wrap-up\n");
1201             // fall through
1202 
1203         case BLT_STATE_WRAPUP:
1204             m_lastStatus = _("Measurement complete");
1205             CleanUp();
1206             m_bltState = BLT_STATE_COMPLETED;
1207             break;                          // This will cycle the guider state machine and get normal guiding going
1208 
1209         case BLT_STATE_COMPLETED:                           // Shouldn't happen
1210             break;
1211 
1212         case BLT_STATE_ABORTED:
1213             m_lastStatus = _("Measurement halted");
1214             Debug.Write("BLT: measurement process halted by user or by error\n");
1215             CleanUp();
1216             break;
1217         }                       // end of switch on state
1218     }
1219     catch (const wxString& msg)
1220     {
1221         POSSIBLY_UNUSED(msg);
1222         Debug.Write(wxString::Format("BLT: Exception thrown in logical state %d\n", (int)m_bltState));
1223         m_bltState = BLT_STATE_ABORTED;
1224         Debug.Write("BLT: " + m_lastStatus + "\n");
1225         CleanUp();
1226     }
1227 
1228     Debug.Write("BLT: Exiting DecMeasurementStep\n");
1229 }
1230 
GetBacklashSigma(double * SigmaPx,double * SigmaMs)1231 void BacklashTool::GetBacklashSigma(double* SigmaPx, double* SigmaMs)
1232 {
1233     if ((m_Rslt == MEASUREMENT_VALID || m_Rslt == BacklashTool::MEASUREMENT_TOO_FEW_NORTH) && m_northStats.GetCount() > 1)
1234     {
1235         // Sigma of mean for north moves + sigma of two measurements going south, added in quadrature
1236         double variance = m_northStats.GetVariance();
1237         int count = m_northStats.GetCount();
1238         *SigmaPx = sqrt((variance / count) + (2 * variance / (count - 1)));
1239         *SigmaMs = *SigmaPx / m_northRate;
1240     }
1241     else
1242     {
1243         *SigmaPx = 0;
1244         *SigmaMs = 0;
1245     }
1246 }
1247 
1248 // Launch modal dlg to show backlash test
ShowGraph(wxDialog * pGA,const std::vector<double> & northSteps,const std::vector<double> & southSteps,int PulseSize)1249 void BacklashTool::ShowGraph(wxDialog *pGA, const std::vector<double> &northSteps, const std::vector<double> &southSteps, int PulseSize)
1250 {
1251     BacklashGraph dlg(pGA, northSteps, southSteps, PulseSize);
1252     dlg.ShowModal();
1253 }
1254 
CleanUp()1255 void BacklashTool::CleanUp()
1256 {
1257     m_scope->GetBacklashComp()->ResetBLCState();        // Normal guiding will start, don't want old BC state applied
1258     pFrame->pGuider->EnableMeasurementMode(false);
1259     Debug.Write("BLT: Cleanup completed\n");
1260 }
1261 
1262 //------------------------------  End of BacklashTool implementation
1263