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

KDE's Doxygen guidelines are available online.