Kstars

meridianflipstate.cpp
1/* Ekos state machine for the meridian flip
2 SPDX-FileCopyrightText: Wolfgang Reissenberger <sterne-jaeger@openfuture.de>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "meridianflipstate.h"
8#include "ekos/mount/mount.h"
9#include "Options.h"
10
11#include "kstarsdata.h"
12#include "indicom.h"
13
14#include <ekos_capture_debug.h>
15
16namespace Ekos
17{
18MeridianFlipState::MeridianFlipState(QObject *parent) : QObject(parent)
19{
20}
21
22QString MeridianFlipState::MFStageString(MFStage stage)
23{
24 switch(stage)
25 {
26 case MF_NONE:
27 return "MF_NONE";
28 case MF_REQUESTED:
29 return "MF_REQUESTED";
30 case MF_READY:
31 return "MF_READY";
32 case MF_INITIATED:
33 return "MF_INITIATED";
34 case MF_FLIPPING:
35 return "MF_FLIPPING";
36 case MF_COMPLETED:
37 return "MF_COMPLETED";
38 case MF_ALIGNING:
39 return "MF_ALIGNING";
40 case MF_GUIDING:
41 return "MF_GUIDING";
42 }
43 return "MFStage unknown.";
44}
45
46void MeridianFlipState::setEnabled(bool value)
47{
48 m_enabled = value;
49 // reset meridian flip if disabled
50 if (m_enabled == false)
51 updateMFMountState(MOUNT_FLIP_NONE);
52}
53
54void MeridianFlipState::connectMount(Mount *mount)
55{
56 connect(mount, &Mount::newCoords, this, &MeridianFlipState::updateTelescopeCoord, Qt::UniqueConnection);
57 connect(mount, &Mount::newStatus, this, &MeridianFlipState::setMountStatus, Qt::UniqueConnection);
58}
59
60void MeridianFlipState::updateMeridianFlipStage(const MFStage &stage)
61{
62 qCDebug(KSTARS_EKOS_CAPTURE) << "updateMeridianFlipStage: " << MeridianFlipState::MFStageString(stage);
63
64 if (meridianFlipStage != stage)
65 {
66 switch (stage)
67 {
68 case MeridianFlipState::MF_NONE:
69 meridianFlipStage = stage;
70 break;
71
72 case MeridianFlipState::MF_READY:
73 if (getMeridianFlipStage() == MeridianFlipState::MF_REQUESTED)
74 {
75 // we keep the stage on requested until the mount starts the meridian flip
76 updateMFMountState(MeridianFlipState::MOUNT_FLIP_ACCEPTED);
77 }
78 else if (m_CaptureState == CAPTURE_PAUSED)
79 {
80 // paused after meridian flip requested
81 meridianFlipStage = stage;
82 updateMFMountState(MeridianFlipState::MOUNT_FLIP_ACCEPTED);
83 }
84 else if (!(checkMeridianFlipRunning()
85 || getMeridianFlipStage() == MeridianFlipState::MF_COMPLETED))
86 {
87 // if neither a MF has been requested (checked above) or is in a post
88 // MF calibration phase, no MF needs to take place.
89 // Hence we set to the stage to NONE
90 meridianFlipStage = MeridianFlipState::MF_NONE;
91 break;
92 }
93 // in any other case, ignore it
94 break;
95
96 case MeridianFlipState::MF_INITIATED:
97 meridianFlipStage = MeridianFlipState::MF_INITIATED;
98 break;
99
100 case MeridianFlipState::MF_REQUESTED:
101 if (m_CaptureState == CAPTURE_PAUSED)
102 // paused before meridian flip requested
103 updateMFMountState(MeridianFlipState::MOUNT_FLIP_ACCEPTED);
104 else
105 updateMFMountState(MeridianFlipState::MOUNT_FLIP_WAITING);
106 meridianFlipStage = stage;
107 break;
108
109 case MeridianFlipState::MF_COMPLETED:
110 meridianFlipStage = MeridianFlipState::MF_COMPLETED;
111 break;
112
113 default:
114 meridianFlipStage = stage;
115 break;
116 }
117 }
118}
119
120
121
122bool MeridianFlipState::checkMeridianFlip(dms lst)
123{
124 // checks if a flip is possible
125 if (m_hasMount == false)
126 {
127 publishMFMountStatusText(i18n("Meridian flip inactive (no scope connected)"));
128 updateMFMountState(MOUNT_FLIP_NONE);
129 return false;
130 }
131
132 if (isEnabled() == false)
133 {
134 publishMFMountStatusText(i18n("Meridian flip inactive (flip not requested)"));
135 return false;
136 }
137
138 // Will never get called when parked!
139 if (m_MountParkStatus == ISD::PARK_PARKED)
140 {
141 publishMFMountStatusText(i18n("Meridian flip inactive (parked)"));
142 return false;
143 }
144
145 if (targetPosition.valid == false || isEnabled() == false)
146 {
147 publishMFMountStatusText(i18n("Meridian flip inactive (no target set)"));
148 return false;
149 }
150
151 // get the time after the meridian that the flip is called for (Degrees --> Hours)
152 double offset = rangeHA(m_offset / 15.0);
153
154 double hrsToFlip = 0; // time to go to the next flip - hours -ve means a flip is required
155
156 double ha = currentPosition.ha.HoursHa(); // -12 to 0 to +12
157
158 // calculate time to next flip attempt. This uses the current hour angle, the pier side if available
159 // and the meridian flip offset to get the time to the flip
160 //
161 // *** should it use the target position so it will continue to track the target even if the mount is not tracking?
162 //
163 // Note: the PierSide code relies on the mount reporting the pier side correctly
164 // It is possible that a mount can flip before the meridian and this has caused problems so hrsToFlip is calculated
165 // assuming the mount can flip up to three hours early.
166
167 static ISD::Mount::PierSide initialPierSide; // used when the flip has completed to determine if the flip was successful
168
169 // adjust ha according to the pier side.
170 switch (currentPosition.pierSide)
171 {
172 case ISD::Mount::PierSide::PIER_WEST:
173 // this is the normal case, tracking from East to West, flip is near Ha 0.
174 break;
175 case ISD::Mount::PierSide::PIER_EAST:
176 // this is the below the pole case, tracking West to East, flip is near Ha 12.
177 // shift ha by 12h
178 ha = rangeHA(ha + 12);
179 break;
180 default:
181 // This is the case where the PierSide is not available, make one attempt only
182 setFlipDelayHrs(0);
183 // we can only attempt a flip if the mount started before the meridian, assumed in the unflipped state
184 if (initialPositionHA() >= 0)
185 {
186 publishMFMountStatusText(i18n("Meridian flip inactive (slew after meridian)"));
187 if (getMeridianFlipMountState() == MOUNT_FLIP_NONE)
188 return false;
189 }
190 break;
191 }
192 // get the time to the next flip, allowing for the pier side and
193 // the possibility of an early flip
194 // adjust ha so an early flip is allowed for
195 if (ha >= 9.0)
196 ha -= 24.0;
197 hrsToFlip = offset + getFlipDelayHrs() - ha;
198
199 int hh = static_cast<int> (hrsToFlip);
200 int mm = static_cast<int> ((hrsToFlip - hh) * 60);
201 int ss = static_cast<int> ((hrsToFlip - hh - mm / 60.0) * 3600);
202 QString message = i18n("Meridian flip in %1", QTime(hh, mm, ss).toString(Qt::TextDate));
203
204 // handle the meridian flip state machine
205 switch (getMeridianFlipMountState())
206 {
207 case MOUNT_FLIP_NONE:
208 publishMFMountStatusText(message);
209
210 if (hrsToFlip <= 0)
211 {
212 // signal that a flip can be done
213 qCInfo(KSTARS_EKOS_MOUNT) << "Meridian flip planned with LST=" <<
214 lst.toHMSString() <<
215 " scope RA=" << currentPosition.position.ra().toHMSString() <<
216 " ha=" << ha <<
217 ", meridian diff=" << offset <<
218 ", hrstoFlip=" << hrsToFlip <<
219 ", flipDelayHrs=" << getFlipDelayHrs() <<
220 ", " << ISD::Mount::pierSideStateString(currentPosition.pierSide);
221
222 initialPierSide = currentPosition.pierSide;
223 updateMFMountState(MOUNT_FLIP_PLANNED);
224 }
225 break;
226
227 case MOUNT_FLIP_PLANNED:
228 // handle the case where there is no Capture module
229 if (m_hasCaptureInterface == false)
230 {
231 qCDebug(KSTARS_EKOS_MOUNT) << "no capture interface, starting flip slew.";
232 updateMFMountState(MOUNT_FLIP_ACCEPTED);
233 return true;
234 }
235 return false;
236
237 case MOUNT_FLIP_ACCEPTED:
238 // set by the Capture module when it's ready
239 return true;
240
241 case MOUNT_FLIP_RUNNING:
242 if (m_MountStatus == ISD::Mount::MOUNT_TRACKING)
243 {
244 if (minMeridianFlipEndTime <= KStarsData::Instance()->clock()->utc())
245 {
246 // meridian flip slew completed, did it work?
247 // check tracking only when the minimal flip duration has passed
248 bool flipFailed = false;
249
250 // pointing state change check only for mounts that report pier side
251 if (currentPosition.pierSide == ISD::Mount::PIER_UNKNOWN)
252 {
253 appendLogText(i18n("Assuming meridian flip completed, but pier side unknown."));
254 // signal that capture can resume
255 updateMFMountState(MOUNT_FLIP_COMPLETED);
256 return false;
257 }
258 else if (currentPosition.pierSide == initialPierSide)
259 {
260 flipFailed = true;
261 qCWarning(KSTARS_EKOS_MOUNT) << "Meridian flip failed, pier side not changed";
262 }
263
264 if (flipFailed)
265 {
266 if (getFlipDelayHrs() <= 1.0)
267 {
268 // Set next flip attempt to be 4 minutes in the future.
269 // These depend on the assignment to flipDelayHrs above.
270 constexpr double delayHours = 4.0 / 60.0;
271 if (currentPosition.pierSide == ISD::Mount::PierSide::PIER_EAST)
272 setFlipDelayHrs(rangeHA(ha + 12 + delayHours) - offset);
273 else
274 setFlipDelayHrs(ha + delayHours - offset);
275
276 // check to stop an infinite loop, 1.0 hrs for now but should use the Ha limit
277 appendLogText(i18n("meridian flip failed, retrying in 4 minutes"));
278 }
279 else
280 {
281 appendLogText(i18n("No successful Meridian Flip done, delay too long"));
282 }
283 updateMFMountState(MOUNT_FLIP_COMPLETED); // this will resume imaging and try again after the extra delay
284 }
285 else
286 {
287 setFlipDelayHrs(0);
288 appendLogText(i18n("Meridian flip completed OK."));
289 // signal that capture can resume
290 updateMFMountState(MOUNT_FLIP_COMPLETED);
291 }
292 }
293 else
294 qCInfo(KSTARS_EKOS_MOUNT) << "Tracking state during meridian flip reached too early, ignored.";
295 }
296 break;
297
298 case MOUNT_FLIP_COMPLETED:
299 updateMFMountState(MOUNT_FLIP_NONE);
300 break;
301
302 default:
303 break;
304 }
305 return false;
306}
307
308void MeridianFlipState::startMeridianFlip()
309{
310 if (/*initialHA() > 0 || */ targetPosition.valid == false)
311 {
312 // no meridian flip necessary
313 qCDebug(KSTARS_EKOS_MOUNT) << "No meridian flip: no target defined";
314 return;
315 }
316
317 if (m_MountStatus != ISD::Mount::MOUNT_TRACKING)
318 {
319 // this should never happen
320 if (m_hasMount == false)
321 qCWarning(KSTARS_EKOS_MOUNT()) << "No mount connected!";
322
323 // no meridian flip necessary
324 qCInfo(KSTARS_EKOS_MOUNT) << "No meridian flip: mount not tracking, current state:" <<
325 ISD::Mount::mountStates[m_MountStatus];
326 return;
327 }
328
329 dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
330 double HA = rangeHA(lst.Hours() - targetPosition.position.ra().Hours());
331
332 // execute meridian flip
333 qCInfo(KSTARS_EKOS_MOUNT) << "Meridian flip: slewing to RA=" <<
334 targetPosition.position.ra().toHMSString() <<
335 "DEC=" << targetPosition.position.dec().toDMSString() <<
336 " Hour Angle " << dms(HA).toHMSString();
337
338 updateMinMeridianFlipEndTime();
339 updateMFMountState(MeridianFlipState::MOUNT_FLIP_RUNNING);
340
341 // start the re-slew to the current target expecting that the mount firmware changes the pier side
342 emit slewTelescope(targetPosition.position);
343}
344
345bool MeridianFlipState::resetMeridianFlip()
346{
347
348 // reset the meridian flip status if the slew is not the meridian flip itself
349 if (meridianFlipMountState != MOUNT_FLIP_RUNNING)
350 {
351 updateMFMountState(MOUNT_FLIP_NONE);
352 setFlipDelayHrs(0);
353 qCDebug(KSTARS_EKOS_MOUNT) << "flipDelayHrs set to zero in slew, m_MFStatus=" <<
354 meridianFlipStatusString(meridianFlipMountState);
355 // meridian flip not running, no need for post MF handling
356 return false;
357 }
358 // don't interrupt a meridian flip directly
359 return true;
360}
361
362void MeridianFlipState::processFlipCompleted()
363{
364 appendLogText(i18n("Telescope completed the meridian flip."));
365 if (m_CaptureState == CAPTURE_IDLE || m_CaptureState == CAPTURE_ABORTED || m_CaptureState == CAPTURE_COMPLETE)
366 {
367 // reset the meridian flip stage and jump directly MF_NONE, since no
368 // restart of guiding etc. necessary
369 updateMeridianFlipStage(MeridianFlipState::MF_NONE);
370 return;
371 }
372
373}
374
375
376void MeridianFlipState::setMeridianFlipMountState(MeridianFlipMountState newMeridianFlipMountState)
377{
378 qCDebug (KSTARS_EKOS_MOUNT) << "Setting meridian flip status to " << meridianFlipStatusString(newMeridianFlipMountState);
379 publishMFMountStatus(newMeridianFlipMountState);
380 meridianFlipMountState = newMeridianFlipMountState;
381}
382
383void MeridianFlipState::appendLogText(QString message)
384{
385 qCInfo(KSTARS_EKOS_MOUNT) << message;
386 emit newLog(message);
387}
388
389void MeridianFlipState::updateMinMeridianFlipEndTime()
390{
391 minMeridianFlipEndTime = KStarsData::Instance()->clock()->utc().addSecs(Options::minFlipDuration());
392}
393
394void MeridianFlipState::updateMFMountState(MeridianFlipMountState status)
395{
396 if (getMeridianFlipMountState() != status)
397 {
398 if (status == MOUNT_FLIP_ACCEPTED)
399 {
400 // ignore accept signal if none was requested
401 if (meridianFlipStage != MF_REQUESTED)
402 return;
403 }
404 // in all other cases, handle it straight forward
405 setMeridianFlipMountState(status);
406 emit newMountMFStatus(status);
407 }
408}
409
410void MeridianFlipState::publishMFMountStatus(MeridianFlipMountState status)
411{
412 // avoid double entries
413 if (status == meridianFlipMountState)
414 return;
415
416 switch (status)
417 {
418 case MOUNT_FLIP_NONE:
419 publishMFMountStatusText(i18n("Status: inactive"));
420 break;
421
422 case MOUNT_FLIP_PLANNED:
423 publishMFMountStatusText(i18n("Meridian flip planned..."));
424 break;
425
426 case MOUNT_FLIP_WAITING:
427 publishMFMountStatusText(i18n("Meridian flip waiting..."));
428 break;
429
430 case MOUNT_FLIP_ACCEPTED:
431 publishMFMountStatusText(i18n("Meridian flip ready to start..."));
432 break;
433
434 case MOUNT_FLIP_RUNNING:
435 publishMFMountStatusText(i18n("Meridian flip running..."));
436 break;
437
438 case MOUNT_FLIP_COMPLETED:
439 publishMFMountStatusText(i18n("Meridian flip completed."));
440 break;
441
442 default:
443 break;
444 }
445
446}
447
448void MeridianFlipState::publishMFMountStatusText(QString text)
449{
450 // avoid double entries
451 if (text != m_lastStatusText)
452 {
453 emit newMeridianFlipMountStatusText(text);
454 m_lastStatusText = text;
455 }
456}
457
458QString MeridianFlipState::meridianFlipStatusString(MeridianFlipMountState status)
459{
460 switch (status)
461 {
462 case MOUNT_FLIP_NONE:
463 return "MOUNT_FLIP_NONE";
464 case MOUNT_FLIP_PLANNED:
465 return "MOUNT_FLIP_PLANNED";
466 case MOUNT_FLIP_WAITING:
467 return "MOUNT_FLIP_WAITING";
468 case MOUNT_FLIP_ACCEPTED:
469 return "MOUNT_FLIP_ACCEPTED";
470 case MOUNT_FLIP_RUNNING:
471 return "MOUNT_FLIP_RUNNING";
472 case MOUNT_FLIP_COMPLETED:
473 return "MOUNT_FLIP_COMPLETED";
474 case MOUNT_FLIP_ERROR:
475 return "MOUNT_FLIP_ERROR";
476 }
477 return "not possible";
478}
479
480
481
482
483
484void MeridianFlipState::setMountStatus(ISD::Mount::Status status)
485{
486 qCDebug(KSTARS_EKOS_MOUNT) << "New mount state for MF:" << ISD::Mount::mountStates[status];
487 m_PrevMountStatus = m_MountStatus;
488 m_MountStatus = status;
489}
490
491void MeridianFlipState::setMountParkStatus(ISD::ParkStatus status)
492{
493 // clear the meridian flip when parking
494 if (status == ISD::PARK_PARKING || status == ISD::PARK_PARKED)
495 updateMFMountState(MOUNT_FLIP_NONE);
496
497 m_MountParkStatus = status;
498}
499
500
501void MeridianFlipState::updatePosition(MountPosition &pos, const SkyPoint &position, ISD::Mount::PierSide pierSide,
502 const dms &ha, const bool isValid)
503{
504 pos.position = position;
505 pos.pierSide = pierSide;
506 pos.ha = ha;
507 pos.valid = isValid;
508}
509
510void MeridianFlipState::updateTelescopeCoord(const SkyPoint &position, ISD::Mount::PierSide pierSide, const dms &ha)
511{
512 updatePosition(currentPosition, position, pierSide, ha, true);
513
514 // If we just finished a slew, let's update initialHA and the current target's position,
515 // but only if the meridian flip is enabled
516 if (m_MountStatus == ISD::Mount::MOUNT_TRACKING && m_PrevMountStatus == ISD::Mount::MOUNT_SLEWING
517 && isEnabled())
518 {
519 if (meridianFlipMountState == MOUNT_FLIP_NONE)
520 {
521 setFlipDelayHrs(0);
522 }
523 // set the target position
524 updatePosition(targetPosition, currentPosition.position, currentPosition.pierSide, currentPosition.ha, true);
525 qCDebug(KSTARS_EKOS_MOUNT) << "Slew finished, MFStatus " <<
526 meridianFlipStatusString(meridianFlipMountState);
527 // ensure that this is executed only once
528 m_PrevMountStatus = m_MountStatus;
529 }
530
531 QChar sgn(ha.Hours() <= 12.0 ? '+' : '-');
532
533 dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
534
535 // don't check the meridian flip while in motion
536 bool inMotion = (m_MountStatus == ISD::Mount::MOUNT_SLEWING || m_MountStatus == ISD::Mount::MOUNT_MOVING
537 || m_MountStatus == ISD::Mount::MOUNT_PARKING);
538 if ((inMotion == false) && checkMeridianFlip(lst))
539 startMeridianFlip();
540 else
541 {
542 const QString message(i18n("Meridian flip inactive (parked)"));
543 if (m_MountParkStatus == ISD::PARK_PARKED /* && meridianFlipStatusWidget->getStatus() != message */)
544 {
545 publishMFMountStatusText(message);
546 }
547 }
548}
549
550void MeridianFlipState::setTargetPosition(SkyPoint *pos)
551{
552 if (pos != nullptr)
553 {
554 dms lst = KStarsData::Instance()->geo()->GSTtoLST(KStarsData::Instance()->clock()->utc().gst());
555 dms ha = dms(lst.Degrees() - pos->ra().Degrees());
556
557 qCDebug(KSTARS_EKOS_MOUNT) << "Setting target RA=" << pos->ra().toHMSString() << "DEC=" << pos->dec().toDMSString();
558 updatePosition(targetPosition, *pos, ISD::Mount::PIER_UNKNOWN, ha, true);
559 }
560 else
561 {
562 clearTargetPosition();
563 }
564}
565
566double MeridianFlipState::initialPositionHA() const
567{
568 double HA = targetPosition.ha.HoursHa();
569 return HA;
570}
571
572
573} // namespace
574
The sky coordinates of a point in the sky.
Definition skypoint.h:45
const CachingDms & dec() const
Definition skypoint.h:269
const CachingDms & ra() const
Definition skypoint.h:263
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
double Hours() const
Definition dms.h:168
const QString toHMSString(const bool machineReadable=false, const bool highPrecision=false) const
Definition dms.cpp:378
const double & Degrees() const
Definition dms.h:141
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:78
bool isValid(QStringView ifopt)
NETWORKMANAGERQT_EXPORT NetworkManager::Status status()
UniqueConnection
TextDate
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:19:03 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.