diff --git a/Tests/scheduler/1x1s_Lum.esq b/Tests/scheduler/1x1s_Lum.esq
new file mode 100644
--- /dev/null
+++ b/Tests/scheduler/1x1s_Lum.esq
@@ -0,0 +1,48 @@
+
+
+CCD Simulator
+CCD Simulator
+2
+0
+60
+0
+
+1
+
+1
+1
+
+
+0
+0
+1280
+1024
+
+0
+Luminance
+Light
+
+
+0
+0
+0
+
+1
+1
+/tmp/kstars_tests
+0
+0
+
+
+
+
+Manual
+
+
+Manual
+
+False
+False
+
+
+
diff --git a/Tests/scheduler/1x1s_RGBLumRGB.esq b/Tests/scheduler/1x1s_RGBLumRGB.esq
new file mode 100644
--- /dev/null
+++ b/Tests/scheduler/1x1s_RGBLumRGB.esq
@@ -0,0 +1,282 @@
+
+
+CCD Simulator
+CCD Simulator
+2
+0
+60
+0
+
+1
+
+1
+1
+
+
+0
+0
+1280
+1024
+
+0
+Red
+Light
+
+
+1
+1
+1
+
+1
+1
+/tmp/kstars_tests
+0
+0
+
+
+
+
+Manual
+
+
+Manual
+
+False
+False
+
+
+
+1
+
+1
+1
+
+
+0
+0
+1280
+1024
+
+0
+Green
+Light
+
+
+1
+1
+1
+
+1
+1
+/tmp/kstars_tests
+0
+0
+
+
+
+
+Manual
+
+
+Manual
+
+False
+False
+
+
+
+1
+
+1
+1
+
+
+0
+0
+1280
+1024
+
+0
+Blue
+Light
+
+
+1
+1
+1
+
+1
+1
+/tmp/kstars_tests
+0
+0
+
+
+
+
+Manual
+
+
+Manual
+
+False
+False
+
+
+
+1
+
+1
+1
+
+
+0
+0
+1280
+1024
+
+0
+Luminance
+Light
+
+
+1
+1
+1
+
+1
+1
+/tmp/kstars_tests
+0
+0
+
+
+
+
+Manual
+
+
+Manual
+
+False
+False
+
+
+
+1
+
+1
+1
+
+
+0
+0
+1280
+1024
+
+0
+Red
+Light
+
+
+1
+1
+1
+
+1
+1
+/tmp/kstars_tests
+0
+0
+
+
+
+
+Manual
+
+
+Manual
+
+False
+False
+
+
+
+1
+
+1
+1
+
+
+0
+0
+1280
+1024
+
+0
+Green
+Light
+
+
+1
+1
+1
+
+1
+1
+/tmp/kstars_tests
+0
+0
+
+
+
+
+Manual
+
+
+Manual
+
+False
+False
+
+
+
+1
+
+1
+1
+
+
+0
+0
+1280
+1024
+
+0
+Blue
+Light
+
+
+1
+1
+1
+
+1
+1
+/tmp/kstars_tests
+0
+0
+
+
+
+
+Manual
+
+
+Manual
+
+False
+False
+
+
+
diff --git a/Tests/scheduler/culmination_no_twilight.esl b/Tests/scheduler/culmination_no_twilight.esl
new file mode 100644
--- /dev/null
+++ b/Tests/scheduler/culmination_no_twilight.esl
@@ -0,0 +1,263 @@
+
+
+Default
+
+Alnath
+10
+
+5.43806
+28.6078
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+Culmination
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alnilam
+10
+
+5.60306
+-1.20167
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+Culmination
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alnitak
+10
+
+5.67917
+-1.9425
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+Culmination
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alphard
+10
+
+9.45917
+-8.65806
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+Culmination
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alphecca
+10
+
+15.5781
+26.7142
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+Culmination
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alpheratz
+10
+
+0.139167
+29.0906
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+Culmination
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alphirk
+10
+
+21.4775
+70.5606
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+Culmination
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alshain
+10
+
+19.9217
+6.40667
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+Culmination
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Altair
+10
+
+19.8456
+8.86667
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+Culmination
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Altais
+10
+
+19.2092
+67.6614
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+Culmination
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Aludra
+10
+
+7.40139
+-29.3031
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+Culmination
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alya
+10
+
+18.9367
+4.20306
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+Culmination
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+UnparkMount
+
+
+WarmCCD
+ParkMount
+
+
diff --git a/Tests/scheduler/duplicated_scheduler_jobs_duplicated_sequence_jobs_no_twilight.esl b/Tests/scheduler/duplicated_scheduler_jobs_duplicated_sequence_jobs_no_twilight.esl
new file mode 100644
--- /dev/null
+++ b/Tests/scheduler/duplicated_scheduler_jobs_duplicated_sequence_jobs_no_twilight.esl
@@ -0,0 +1,95 @@
+
+
+Default
+
+Kocab
+10
+
+14.845
+74.1553
+
+/tmp/kstars_tests/1x1s_RGBLumRGB.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Kocab
+10
+
+14.845
+74.1553
+
+/tmp/kstars_tests/1x1s_RGBLumRGB.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Kocab
+11
+
+14.845
+74.1553
+
+/tmp/kstars_tests/1x1s_RGBLumRGB.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Repeat
+
+
+Track
+
+
+
+Kocab
+12
+
+14.845
+74.1553
+
+/tmp/kstars_tests/1x1s_RGBLumRGB.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Repeat
+
+
+Track
+
+
+
+UnparkMount
+
+
+WarmCCD
+ParkMount
+
+
diff --git a/Tests/scheduler/duplicated_scheduler_jobs_no_twilight.esl b/Tests/scheduler/duplicated_scheduler_jobs_no_twilight.esl
new file mode 100644
--- /dev/null
+++ b/Tests/scheduler/duplicated_scheduler_jobs_no_twilight.esl
@@ -0,0 +1,95 @@
+
+
+Default
+
+Kocab
+10
+
+14.845
+74.1553
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Kocab
+10
+
+14.845
+74.1553
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Kocab
+11
+
+14.845
+74.1553
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Repeat
+
+
+Track
+
+
+
+Kocab
+12
+
+14.845
+74.1553
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Repeat
+
+
+Track
+
+
+
+UnparkMount
+
+
+WarmCCD
+ParkMount
+
+
diff --git a/Tests/scheduler/readme.txt b/Tests/scheduler/readme.txt
new file mode 100644
--- /dev/null
+++ b/Tests/scheduler/readme.txt
@@ -0,0 +1,5 @@
+Scheduler test vectors.
+
+Create folder /tmp/kstars_tests and copy the .esq and .esl files there.
+Load them from that folder to test the scheduler.
+To reset the tests, simply remove the capture subfolders that the scheduler creates when running.
diff --git a/Tests/scheduler/simple_test.esl b/Tests/scheduler/simple_test.esl
new file mode 100644
--- /dev/null
+++ b/Tests/scheduler/simple_test.esl
@@ -0,0 +1,275 @@
+
+
+ Default
+
+ Alnath
+ 10
+
+ 5.43806
+ 28.6078
+
+ /tmp/kstars_tests/1x1s_Lum.esq
+
+ ASAP
+
+
+ MinimumAltitude
+ EnforceTwilight
+
+
+ Sequence
+
+
+ Track
+
+
+
+ Alnilam
+ 10
+
+ 5.60333
+ -1.20167
+
+ /tmp/kstars_tests/1x1s_Lum.esq
+
+ ASAP
+
+
+ MinimumAltitude
+ EnforceTwilight
+
+
+ Sequence
+
+
+ Track
+
+
+
+ Alnitak
+ 10
+
+ 5.67917
+ -1.9425
+
+ /tmp/kstars_tests/1x1s_Lum.esq
+
+ ASAP
+
+
+ MinimumAltitude
+ EnforceTwilight
+
+
+ Sequence
+
+
+ Track
+
+
+
+ Alphard
+ 10
+
+ 9.45972
+ -8.65833
+
+ /tmp/kstars_tests/1x1s_Lum.esq
+
+ ASAP
+
+
+ MinimumAltitude
+ EnforceTwilight
+
+
+ Sequence
+
+
+ Track
+
+
+
+ Alphecca
+ 10
+
+ 15.5781
+ 26.7147
+
+ /tmp/kstars_tests/1x1s_Lum.esq
+
+ ASAP
+
+
+ MinimumAltitude
+ EnforceTwilight
+
+
+ Sequence
+
+
+ Track
+
+
+
+ Alpheratz
+ 10
+
+ 0.139722
+ 29.0906
+
+ /tmp/kstars_tests/1x1s_Lum.esq
+
+ ASAP
+
+
+ MinimumAltitude
+ EnforceTwilight
+
+
+ Sequence
+
+
+ Track
+
+
+
+ Alphirk
+ 10
+
+ 21.4775
+ 70.5606
+
+ /tmp/kstars_tests/1x1s_Lum.esq
+
+ ASAP
+
+
+ MinimumAltitude
+ EnforceTwilight
+
+
+ Sequence
+
+
+ Track
+
+
+
+ Alshain
+ 10
+
+ 19.9217
+ 6.40667
+
+ /tmp/kstars_tests/1x1s_Lum.esq
+
+ ASAP
+
+
+ MinimumAltitude
+ EnforceTwilight
+
+
+ Sequence
+
+
+ Track
+
+
+
+ Altair
+ 10
+
+ 19.8461
+ 8.86722
+
+ /tmp/kstars_tests/1x1s_Lum.esq
+
+ ASAP
+
+
+ MinimumAltitude
+ EnforceTwilight
+
+
+ Sequence
+
+
+ Track
+
+
+
+ Altais
+ 10
+
+ 19.2092
+ 67.6614
+
+ /tmp/kstars_tests/1x1s_Lum.esq
+
+ ASAP
+
+
+ MinimumAltitude
+ EnforceTwilight
+
+
+ Sequence
+
+
+ Track
+
+
+
+ Aludra
+ 10
+
+ 7.40139
+ -29.3031
+
+ /tmp/kstars_tests/1x1s_Lum.esq
+
+ ASAP
+
+
+ MinimumAltitude
+ EnforceTwilight
+
+
+ Sequence
+
+
+ Track
+
+
+
+ Alya
+ 10
+
+ 18.9369
+ 4.20333
+
+ /tmp/kstars_tests/1x1s_Lum.esq
+
+ ASAP
+
+
+ MinimumAltitude
+ EnforceTwilight
+
+
+ Sequence
+
+
+ Track
+
+
+
+ UnparkMount
+
+
+ WarmCCD
+ ParkMount
+
+
diff --git a/Tests/scheduler/simple_test_no_twilight.esl b/Tests/scheduler/simple_test_no_twilight.esl
new file mode 100644
--- /dev/null
+++ b/Tests/scheduler/simple_test_no_twilight.esl
@@ -0,0 +1,263 @@
+
+
+Default
+
+Alnath
+10
+
+5.43806
+28.6078
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alnilam
+10
+
+5.60306
+-1.20167
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alnitak
+10
+
+5.67917
+-1.9425
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alphard
+10
+
+9.45944
+-8.65806
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alphecca
+10
+
+15.5781
+26.7144
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alpheratz
+10
+
+0.139444
+29.0906
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alphirk
+10
+
+21.4775
+70.5606
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alshain
+10
+
+19.9217
+6.40667
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Altair
+10
+
+19.8458
+8.86694
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Altais
+10
+
+19.2092
+67.6614
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Aludra
+10
+
+7.40139
+-29.3031
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+Alya
+10
+
+18.9367
+4.20306
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+
+
+Sequence
+
+
+Track
+
+
+
+UnparkMount
+
+
+WarmCCD
+ParkMount
+
+
diff --git a/Tests/scheduler/start_at_finish_at_test.esl b/Tests/scheduler/start_at_finish_at_test.esl
new file mode 100644
--- /dev/null
+++ b/Tests/scheduler/start_at_finish_at_test.esl
@@ -0,0 +1,275 @@
+
+
+Default
+
+Alnath
+10
+
+5.43806
+28.6078
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+EnforceTwilight
+
+
+Sequence
+
+
+Track
+
+
+
+Alnilam
+10
+
+5.60333
+-1.20167
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+EnforceTwilight
+
+
+Sequence
+
+
+Track
+
+
+
+Alnitak
+10
+
+5.67917
+-1.9425
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+EnforceTwilight
+
+
+Sequence
+
+
+Track
+
+
+
+Alphard
+10
+
+9.45944
+-8.65806
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+At
+
+
+MinimumAltitude
+EnforceTwilight
+
+
+Sequence
+
+
+Track
+
+
+
+Alphecca
+10
+
+15.5781
+26.7144
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+At
+
+
+MinimumAltitude
+EnforceTwilight
+
+
+Sequence
+
+
+Track
+
+
+
+Alpheratz
+10
+
+0.139722
+29.0906
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+EnforceTwilight
+
+
+Sequence
+
+
+Track
+
+
+
+Alphirk
+10
+
+21.4775
+70.5606
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+EnforceTwilight
+
+
+At
+
+
+Track
+
+
+
+Alshain
+10
+
+19.9217
+6.40667
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+EnforceTwilight
+
+
+Sequence
+
+
+Track
+
+
+
+Altair
+10
+
+19.8458
+8.86694
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+At
+
+
+MinimumAltitude
+EnforceTwilight
+
+
+At
+
+
+Track
+
+
+
+Altais
+10
+
+19.2092
+67.6614
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+EnforceTwilight
+
+
+Sequence
+
+
+Track
+
+
+
+Aludra
+10
+
+7.40139
+-29.3031
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+EnforceTwilight
+
+
+Sequence
+
+
+Track
+
+
+
+Alya
+10
+
+18.9369
+4.20333
+
+/tmp/kstars_tests/1x1s_Lum.esq
+
+ASAP
+
+
+MinimumAltitude
+EnforceTwilight
+
+
+Sequence
+
+
+Track
+
+
+
+UnparkMount
+
+
+WarmCCD
+ParkMount
+
+
diff --git a/kstars/ekos/scheduler/scheduler.cpp b/kstars/ekos/scheduler/scheduler.cpp
--- a/kstars/ekos/scheduler/scheduler.cpp
+++ b/kstars/ekos/scheduler/scheduler.cpp
@@ -183,31 +183,36 @@
{
if (enable)
{
+ connect(nameEdit, SIGNAL(editingFinished()), this, SLOT(setDirty()));
connect(fitsEdit, SIGNAL(editingFinished()), this, SLOT(setDirty()));
+ connect(sequenceEdit, SIGNAL(editingFinished()), this, SLOT(setDirty()));
connect(startupScript, SIGNAL(editingFinished()), this, SLOT(setDirty()));
connect(shutdownScript, SIGNAL(editingFinished()), this, SLOT(setDirty()));
-
connect(schedulerProfileCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setDirty()));
+
connect(stepsButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty()));
connect(startupButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty()));
connect(constraintButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty()));
connect(completionButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty()));
connect(startupProcedureButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty()));
connect(shutdownProcedureGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty()));
- connect(culminationOffset, SIGNAL(editingFinished()), this, SLOT(setDirty()));
+ connect(culminationOffset, SIGNAL(valueChanged(int)), this, SLOT(setDirty()));
connect(startupTimeEdit, SIGNAL(editingFinished()), this, SLOT(setDirty()));
- connect(minAltitude, SIGNAL(editingFinished()), this, SLOT(setDirty()));
- connect(repeatsSpin, SIGNAL(editingFinished()), this, SLOT(setDirty()));
- connect(minMoonSeparation, SIGNAL(editingFinished()), this, SLOT(setDirty()));
+ connect(minAltitude, SIGNAL(valueChanged(int)), this, SLOT(setDirty()));
+ connect(repeatsSpin, SIGNAL(valueChanged(int)), this, SLOT(setDirty()));
+ connect(minMoonSeparation, SIGNAL(valueChanged(int)), this, SLOT(setDirty()));
connect(completionTimeEdit, SIGNAL(editingFinished()), this, SLOT(setDirty()));
+ connect(prioritySpin, SIGNAL(valueChanged(int)), this, SLOT(setDirty()));
}
else
{
//disconnect(this, SLOT(setDirty()));
+ disconnect(nameEdit, SIGNAL(editingFinished()), this, SLOT(setDirty()));
disconnect(fitsEdit, SIGNAL(editingFinished()), this, SLOT(setDirty()));
+ disconnect(sequenceEdit, SIGNAL(editingFinished()), this, SLOT(setDirty()));
disconnect(startupScript, SIGNAL(editingFinished()), this, SLOT(setDirty()));
disconnect(shutdownScript, SIGNAL(editingFinished()), this, SLOT(setDirty()));
disconnect(schedulerProfileCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setDirty()));
@@ -220,12 +225,13 @@
disconnect(startupProcedureButtonGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty()));
disconnect(shutdownProcedureGroup, SIGNAL(buttonToggled(int, bool)), this, SLOT(setDirty()));
- disconnect(culminationOffset, SIGNAL(editingFinished()), this, SLOT(setDirty()));
+ disconnect(culminationOffset, SIGNAL(valueChanged(int)), this, SLOT(setDirty()));
disconnect(startupTimeEdit, SIGNAL(editingFinished()), this, SLOT(setDirty()));
- disconnect(minAltitude, SIGNAL(editingFinished()), this, SLOT(setDirty()));
- disconnect(repeatsSpin, SIGNAL(editingFinished()), this, SLOT(setDirty()));
- disconnect(minMoonSeparation, SIGNAL(editingFinished()), this, SLOT(setDirty()));
+ disconnect(minAltitude, SIGNAL(valueChanged(int)), this, SLOT(setDirty()));
+ disconnect(repeatsSpin, SIGNAL(valueChanged(int)), this, SLOT(setDirty()));
+ disconnect(minMoonSeparation, SIGNAL(valueChanged(int)), this, SLOT(setDirty()));
disconnect(completionTimeEdit, SIGNAL(editingFinished()), this, SLOT(setDirty()));
+ disconnect(prioritySpin, SIGNAL(valueChanged(int)), this, SLOT(setDirty()));
}
}
@@ -361,6 +367,7 @@
//jobUnderEdit = false;
saveJob();
+ jobEvaluationOnly = true;
evaluateJobs();
}
@@ -380,7 +387,6 @@
if(SchedulerJob::FINISH_LOOP == job->getCompletionCondition())
appendLogText(i18n("Warning! Job '%1' has completion condition set to infinite repeat, other jobs may not execute.",job->getName()));
-
if (nameEdit->text().isEmpty())
{
appendLogText(i18n("Target name is required."));
@@ -459,6 +465,8 @@
/* Store the original startup condition */
job->setFileStartupCondition(job->getStartupCondition());
+ job->setFileStartupTime(job->getStartupTime());
+
// #2 Constraints
@@ -478,6 +486,21 @@
// twilight constraints
job->setEnforceTwilight(twilightCheck->isChecked());
+ /* Verifications */
+ /* FIXME: perhaps use a method more visible to the end-user */
+ if (SchedulerJob::START_AT == job->getFileStartupCondition())
+ {
+ /* Warn if appending a job which startup time doesn't allow proper score */
+ if (calculateJobScore(job, job->getStartupTime()) < 0)
+ appendLogText(i18n("Warning! Job '%1' has startup time %1 resulting in a negative score, and will be marked invalid when processed.",
+ job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat())));
+
+ /* Warn if appending a job with a startup time that is in the past */
+ if (job->getStartupTime() < KStarsData::Instance()->lt())
+ appendLogText(i18n("Warning! Job '%1' has fixed startup time %1 set in the past, and will be marked invalid when evaluated.",
+ job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat())));
+ }
+
// #3 Completion conditions
if (sequenceCompletionR->isChecked())
{
@@ -523,6 +546,25 @@
else
currentRow = queueTable->currentRow();
+ // Warn user if a duplicated job is in the list - same target, same sequence
+ foreach (SchedulerJob *a_job, jobs)
+ {
+ if(a_job == job)
+ {
+ break;
+ }
+ else if(a_job->getName() == job->getName() && a_job->getSequenceFile() == job->getSequenceFile())
+ {
+ appendLogText(i18n("Warning! Job '%1' at row %2 has a duplicate at row %3 (same target, same sequence file), "
+ "the scheduler will consider the same storage for captures!",
+ job->getName(), currentRow,
+ a_job->getNameCell()? a_job->getNameCell()->row()+1 : 0));
+ appendLogText(i18n("Make sure job '%1' at row %2 has a specific startup time or a different priority, "
+ "and a greater repeat count (or disable option 'Remember job progress')",
+ job->getName(), currentRow));
+ }
+ }
+
/* Reset job state to evaluate the changes - so this is equivalent to double-clicking the job */
/* FIXME: should we do that if no change was done to the job? */
/* FIXME: move this to SchedulerJob as a "reset" method */
@@ -713,11 +755,11 @@
case SchedulerJob::FINISH_REPEAT:
repeatCompletionR->setChecked(true);
+ repeatsSpin->setValue(job->getRepeatsRequired());
break;
case SchedulerJob::FINISH_LOOP:
loopCompletionR->setChecked(true);
- repeatsSpin->setValue(job->getRepeatsRequired());
break;
case SchedulerJob::FINISH_AT:
@@ -760,6 +802,7 @@
startB->setEnabled(true);
//removeFromQueueB->setToolTip(i18n("Remove observation job from list."));
+ jobEvaluationOnly = true;
evaluateJobs();
}
@@ -844,6 +887,7 @@
if (job->getState() <= SchedulerJob::JOB_BUSY)
{
+ appendLogText(i18n("Job '%1' has not been processed upon scheduler stop, marking aborted.", job->getName()));
job->setState(SchedulerJob::JOB_ABORTED);
wasAborted = true;
}
@@ -974,8 +1018,8 @@
startB->setToolTip(i18n("Stop Scheduler"));
pauseB->setEnabled(true);
- if (Dawn < 0)
- calculateDawnDusk();
+ /* Recalculate dawn and dusk astronomical times - unconditionally in case date changed */
+ calculateDawnDusk();
state = SCHEDULER_RUNNIG;
@@ -1027,37 +1071,80 @@
if (currentJob)
{
currentJob->setStageLabel(jobStatus);
+ queueTable->selectRow(currentJob->getStartupCell()->row());
}
else
{
jobStatus->setText(i18n("No job running"));
+ queueTable->clearSelection();
}
}
void Scheduler::evaluateJobs()
{
+ /* FIXME: it is possible to evaluate jobs while KStars has a time offset, so warn the user about this */
+ QDateTime const now = KStarsData::Instance()->lt();
+
/* Start by refreshing the number of captures already present */
updateCompletedJobsCount();
+ /* Update dawn and dusk astronomical times - unconditionally in case date changed */
+ calculateDawnDusk();
+
+ /* First, filter out non-schedulable jobs */
+ /* FIXME: jobs in state JOB_ERROR should not be in the list, reorder states */
+ QList sortedJobs = jobs;
+ sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(),[](SchedulerJob* job)
+ { return SchedulerJob::JOB_ABORTED < job->getState(); }), sortedJobs.end());
+
+ /* Then reorder jobs by priority */
+ /* FIXME: refactor so all sorts are using the same predicates */
+ /* FIXME: use std::sort as qSort is deprecated */
+ if (Options::sortSchedulerJobs())
+ qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder);
+
/* Then enumerate SchedulerJobs, scheduling only what is required */
- foreach (SchedulerJob *job, jobs)
+ foreach (SchedulerJob *job, sortedJobs)
{
/* Let aborted jobs be rescheduled later instead of forgetting them */
/* FIXME: minimum altitude and altitude cutoff may cause loops here */
- if (job->getState() == SchedulerJob::JOB_ABORTED)
- job->setState(SchedulerJob::JOB_EVALUATION);
+ switch (job->getState())
+ {
+ /* If job is idle, set it for evaluation */
+ case SchedulerJob::JOB_IDLE:
+ job->setState(SchedulerJob::JOB_EVALUATION);
+ job->setEstimatedTime(-1);
+ break;
- if (job->getState() > SchedulerJob::JOB_SCHEDULED)
- continue;
+ /* If job is aborted, reset it for evaluation */
+ case SchedulerJob::JOB_ABORTED:
+ job->setState(SchedulerJob::JOB_EVALUATION);
+ break;
- // If job is idle, let's set it up for evaluation.
- if (job->getState() == SchedulerJob::JOB_IDLE)
- {
- job->setState(SchedulerJob::JOB_EVALUATION);
- job->setEstimatedTime(-1);
+ /* If job is scheduled, quick-check startup and bypass evaluation if in future */
+ case SchedulerJob::JOB_SCHEDULED:
+ if (job->getStartupTime() < now)
+ break;
+ continue;
+
+ /* If job is in error, invalid or complete, bypass evaluation */
+ case SchedulerJob::JOB_ERROR:
+ case SchedulerJob::JOB_INVALID:
+ case SchedulerJob::JOB_COMPLETE:
+ continue;
+
+ /* If job is busy, edge case, bypass evaluation */
+ case SchedulerJob::JOB_BUSY:
+ continue;
+
+ /* Else evaluate */
+ case SchedulerJob::JOB_EVALUATION:
+ default:
+ break;
}
// In case of a repeating jobs, let's make sure we have more runs left to go
+ /* FIXME: if finding a repeated job with no repeats remaining, state this is a safeguard - Is it really? Set complete? */
if (job->getCompletionCondition() == SchedulerJob::FINISH_REPEAT)
{
if (job->getRepeatsRemaining() == 0)
@@ -1068,19 +1155,6 @@
}
}
- // Warn user if a duplicated job is in the list - same target, same sequence
- foreach (SchedulerJob *a_job, jobs)
- if(a_job == job)
- break;
- else if(a_job->getName() == job->getName() && a_job->getSequenceFile() == job->getSequenceFile())
- appendLogText(i18n("Warning! Job '%1' is duplicated (same target, same sequence file), the scheduler will consider the same storage for captures!"));
-
- int16_t score = 0;
-
- QDateTime now = KStarsData::Instance()->lt();
-
- /* FIXME: it is possible to evaluate jobs while KStars has a time offset, so warn the user about this */
-
// -1 = Job is not estimated yet
// -2 = Job is estimated but time is unknown
// > 0 Job is estimated and time is known
@@ -1104,15 +1178,11 @@
{
// #1.1 ASAP?
case SchedulerJob::START_ASAP:
- // If not light frames are required, run it now
- if (job->getLightFramesRequired())
- score = calculateJobScore(job, now);
- else
- score = 1000;
-
- job->setScore(score);
+ {
+ /* Job is to be started as soon as possible, so check its current score */
+ int16_t const score = calculateJobScore(job, now);
- // If we can't start now, let's schedule it
+ /* If it's not possible to run the job now, find proper altitude time */
if (score < 0)
{
// If Altitude or Dark score are negative, we try to schedule a better time for altitude and dark sky period.
@@ -1122,166 +1192,181 @@
//appendLogText(i18n("%1 observation job is scheduled at %2", job->getName(), job->getStartupTime().toString()));
job->setState(SchedulerJob::JOB_SCHEDULED);
// Since it's scheduled, we need to skip it now and re-check it later since its startup condition changed to START_AT
- job->setScore(BAD_SCORE);
- continue;
+ /*job->setScore(BAD_SCORE);
+ continue;*/
}
else
{
job->setState(SchedulerJob::JOB_INVALID);
appendLogText(i18n("Ekos failed to schedule %1.", job->getName()));
}
+
+ /* Keep the job score for current time, score will refresh as scheduler progresses */
+ /* score = calculateJobScore(job, job->getStartupTime()); */
+ job->setScore(score);
}
+ /* If it's possible to run the job now, check weather */
else if (isWeatherOK(job) == false)
{
- appendLogText(i18n("Job '%1' cannot run because of bad weather.", job->getName()));
+ appendLogText(i18n("Job '%1' cannot run now because of bad weather.", job->getName()));
+ job->setState(SchedulerJob::JOB_ABORTED);
job->setScore(BAD_SCORE);
}
+ /* If weather is ok, schedule the job to run now */
else
{
appendLogText(i18n("Job '%1' is due to run as soon as possible.", job->getName()));
/* Give a proper start time, so that job can be rescheduled if others also start asap */
job->setStartupTime(now);
+ job->setState(SchedulerJob::JOB_SCHEDULED);
+ job->setScore(score);
}
- break;
+ }
+ break;
// #1.2 Culmination?
case SchedulerJob::START_CULMINATION:
+ {
if (calculateCulmination(job))
{
- appendLogText(i18n("Job '%1' is scheduled at %2", job->getName(),
+ appendLogText(i18n("Job '%1' is scheduled at %2 for culmination.", job->getName(),
job->getStartupTime().toString()));
job->setState(SchedulerJob::JOB_SCHEDULED);
// Since it's scheduled, we need to skip it now and re-check it later since its startup condition changed to START_AT
- job->setScore(BAD_SCORE);
- continue;
+ /*job->setScore(BAD_SCORE);
+ continue;*/
}
else
+ {
+ appendLogText(i18n("Job '%1' culmination cannot be scheduled, marking invalid.", job->getName()));
job->setState(SchedulerJob::JOB_INVALID);
- break;
+ }
+ }
+ break;
// #1.3 Start at?
case SchedulerJob::START_AT:
{
if (job->getCompletionCondition() == SchedulerJob::FINISH_AT)
{
- if (job->getStartupTime().secsTo(job->getCompletionTime()) <= 0)
+ if (job->getCompletionTime() <= job->getStartupTime())
{
appendLogText(i18n("Job '%1' completion time (%2) could not be achieved before start up time (%3), marking invalid", job->getName(),
job->getCompletionTime().toString(), job->getStartupTime().toString()));
job->setState(SchedulerJob::JOB_INVALID);
continue;
}
}
- QDateTime startupTime = job->getStartupTime();
- int timeUntil = KStarsData::Instance()->lt().secsTo(startupTime);
- // If starting time already passed by 5 minutes (default), we mark the job as invalid
- /* FIXME: altitude calculation will change the job start condition to START_AT, so this might be a deadend while the end-user didn't request so */
+ int const timeUntil = now.secsTo(job->getStartupTime());
+
+ // If starting time already passed by 5 minutes (default), we mark the job as invalid or aborted
if (timeUntil < (-1 * Options::leadTime() * 60))
{
- dms const passedUp(timeUntil / 3600.0);
- if (job->getState() == SchedulerJob::JOB_EVALUATION)
+ dms const passedUp(-timeUntil / 3600.0);
+
+ /* Mark the job invalid only if its startup time was a user request, else just abort it for later reschedule */
+ if (job->getFileStartupCondition() == SchedulerJob::START_AT)
{
- appendLogText(i18n("Job '%1' startup time is fixed, and is already passed by %2, marking invalid.",
- job->getName(), passedUp.toHMSString()));
+ appendLogText(i18n("Job '%1' startup time was fixed at %2, and is already passed by %3, marking invalid.",
+ job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()), passedUp.toHMSString()));
job->setState(SchedulerJob::JOB_INVALID);
}
else
{
- appendLogText(i18n("Job '%1' startup time already passed by %2, marking aborted.", job->getName(),
- passedUp.toHMSString()));
+ appendLogText(i18n("Job '%1' startup time was %2, and is already passed by %3, marking aborted.",
+ job->getName(), job->getStartupTime().toString(job->getDateTimeDisplayFormat()), passedUp.toHMSString()));
job->setState(SchedulerJob::JOB_ABORTED);
}
continue;
}
// Start scoring once we reach startup time
else if (timeUntil <= 0)
{
- /*score += getAltitudeScore(job, now);
- score += getMoonSeparationScore(job, now);
- score += getDarkSkyScore(now);*/
- score = calculateJobScore(job, now);
+ /* Consolidate altitude, moon separation and sky darkness scores */
+ int16_t const score = calculateJobScore(job, now);
if (score < 0)
{
/* If job score is already negative, silently abort the job to avoid spamming the user */
- if (job->getScore() < 0)
- {
- job->setState(SchedulerJob::JOB_ABORTED);
- }
- else
+ if (0 < job->getScore())
{
if (job->getState() == SchedulerJob::JOB_EVALUATION)
- {
- appendLogText(
- i18n("Job '%1' evaluation failed with a score of %2, marking aborted.",
- job->getName(), score));
- job->setState(SchedulerJob::JOB_ABORTED);
- }
+ appendLogText(i18n("Job '%1' evaluation failed with a score of %2, marking aborted.",
+ job->getName(), score));
+ else if (timeUntil == 0)
+ appendLogText(i18n("Job '%1' updated score is %2 at startup time, marking aborted.",
+ job->getName(), score));
else
- {
- if (timeUntil == 0)
- appendLogText(i18n(
- "Job '%1' updated score is %2 at startup time, marking aborted.", job->getName(), score));
- else
- appendLogText(i18n(
- "Job '%1' updated score is %2 %3 seconds after startup time, marking aborted.",
- job->getName(), score, abs(timeUntil)));
- job->setState(SchedulerJob::JOB_ABORTED);
- }
+ appendLogText(i18n("Job '%1' updated score is %2 %3 seconds after startup time, marking aborted.",
+ job->getName(), score, abs(timeUntil)));
}
+ job->setState(SchedulerJob::JOB_ABORTED);
+ job->setScore(score);
+
continue;
}
- // If job is already scheduled, we check the weather, and if it is not OK, we set bad score until weather improves.
+ /* Positive score means job is already scheduled, so we check the weather, and if it is not OK, we set bad score until weather improves. */
else if (isWeatherOK(job) == false)
- score += BAD_SCORE;
+ {
+ appendLogText(i18n("Job '%1' cannot run now because of bad weather.", job->getName()));
+ job->setState(SchedulerJob::JOB_ABORTED);
+ job->setScore(BAD_SCORE);
+ }
+ /* Else record current score */
+ else job->setScore(score);
}
+#if 0
// If it is in the future and originally was designated as ASAP job
// Job must be less than 12 hours away to be considered for re-evaluation
else if (timeUntil > (Options::leadTime() * 60) && (timeUntil < 12 * 3600) &&
job->getFileStartupCondition() == SchedulerJob::START_ASAP)
{
QDateTime nextJobTime = now.addSecs(Options::leadTime() * 60);
if (job->getEnforceTwilight() == false || (now > duskDateTime && now < preDawnDateTime))
{
- appendLogText(i18n("Job '%1' is imminent, scheduled to run at %2.",
+ appendLogText(i18n("Job '%1' can be scheduled under 12 hours, but will be re-evaluated at %2.",
job->getName(), nextJobTime.toString(job->getDateTimeDisplayFormat())));
job->setStartupTime(nextJobTime);
}
- score += BAD_SCORE;
+ job->setScore(BAD_SCORE);
}
// If time is far in the future, we make the score negative
else
{
if (job->getState() == SchedulerJob::JOB_EVALUATION &&
calculateJobScore(job, job->getStartupTime()) < 0)
{
- appendLogText(i18n("Job '%1' evaluation failed with a score of %2, marking aborted.",
- job->getName(), score));
+ appendLogText(i18n("Job '%1' can only be scheduled in more than 12 hours, marking aborted.",
+ job->getName()));
job->setState(SchedulerJob::JOB_ABORTED);
continue;
}
- score += BAD_SCORE;
+ /*score += BAD_SCORE;*/
}
+#endif
- job->setScore(score);
}
break;
}
- // appendLogText(i18n("Job total score is %1", score));
//if (score > 0 && job->getState() == SchedulerJob::JOB_EVALUATION)
if (job->getState() == SchedulerJob::JOB_EVALUATION)
job->setState(SchedulerJob::JOB_SCHEDULED);
}
+ /*
+ * At this step, we scheduled all jobs that had to be scheduled because they could not start as soon as possible.
+ * Now we check the amount of jobs we have to run.
+ */
+
int invalidJobs = 0, completedJobs = 0, abortedJobs = 0, upcomingJobs = 0;
- // Find invalid jobs
+ /* Partition jobs into invalid/aborted/completed/upcoming jobs */
foreach (SchedulerJob *job, jobs)
{
switch (job->getState())
@@ -1344,25 +1429,32 @@
return;
}
- SchedulerJob *bestCandidate = nullptr;
+ /*
+ * At this step, we still have jobs to run.
+ * We filter out jobs that won't run now, and make sure jobs are not all starting at the same time.
+ */
updatePreDawn();
- QList sortedJobs = jobs;
-
- sortedJobs.erase(std::remove_if(sortedJobs.begin(), sortedJobs.end(),[](SchedulerJob* job)
- { return job->getState() > SchedulerJob::JOB_SCHEDULED || job->getScore() < 0;}), sortedJobs.end());
-
+ /* If there jobs left to run in the filtered list, check reschedule */
if (sortedJobs.isEmpty())
+ {
+ if (startupState == STARTUP_COMPLETE)
+ {
+ appendLogText(i18n("No jobs left in the scheduler queue, starting shutdown procedure..."));
+ // Let's start shutdown procedure
+ checkShutdownState();
+ }
+ else
+ stop();
+
return;
+ }
- /* FIXME: refactor so all sorts are using the same predicates */
- /* FIXME: use std::sort as qSort is deprecated */
+ /* Now that jobs are scheduled, possibly at the same time, reorder by altitude and priority again */
if (Options::sortSchedulerJobs())
{
- // Order by altitude, greater altitude first
qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::decreasingAltitudeOrder);
- // Then by priority, lower priority value first
qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder);
}
@@ -1432,199 +1524,117 @@
return;
}
- // Find best score
- /*foreach(SchedulerJob *job, jobs)
- {
- if (job->getState() != SchedulerJob::JOB_SCHEDULED)
- continue;
-
- int jobScore = job->getScore();
- int jobPriority = job->getPriority();
-
- if (jobPriority <= maxPriority)
- {
- maxPriority = jobPriority;
+ /*
+ * At this step, we finished evaluating jobs.
+ * We select the first job that has to be run, per schedule.
+ */
- if (jobScore > 0 && jobScore > maxScore)
- {
- maxScore = jobScore;
- bestCandidate = job;
- }
- }
- }*/
+ // Sort again by schedule, sooner first, as some jobs may have shifted during the last step
+ qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingStartupTimeOrder);
-#if 0
- /* FIXME: refactor so all sorts are using the same predicates */
- /* FIXME: use std::sort as qSort is deprecated */
- if (Options::sortSchedulerJobs())
- {
- // Order by score score first
- qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::decreasingScoreOrder);
- // Then by priority
- qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder);
+ setCurrentJob(sortedJobs.first());
- foreach (SchedulerJob *job, sortedJobs)
- {
- if (job->getState() != SchedulerJob::JOB_SCHEDULED || job->getScore() <= 0)
- continue;
+ int nextObservationTime = now.secsTo(currentJob->getStartupTime());
- bestCandidate = job;
- break;
- }
- }
- else
- {
- // Get the first job that can run.
- for (SchedulerJob *job : sortedJobs)
- {
- if (job->getScore() > 0)
- {
- bestCandidate = job;
- break;
- }
- }
- }
-#endif
+ /* Check if job can be processed right now */
+ if (currentJob->getFileStartupCondition() == SchedulerJob::START_ASAP)
+ if( 0 < calculateJobScore(currentJob, now))
+ nextObservationTime = 0;
- /* FIXME: refactor so all sorts are using the same predicates */
- /* FIXME: use std::sort as qSort is deprecated */
- if (Options::sortSchedulerJobs())
- {
- // Order by score first
- qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::decreasingScoreOrder);
- // Then by priority
- qSort(sortedJobs.begin(), sortedJobs.end(), SchedulerJob::increasingPriorityOrder);
- }
+ appendLogText(i18n("Job '%1' is selected for next observation with priority #%2 and score %3.",
+ currentJob->getName(), currentJob->getPriority(), currentJob->getScore()));
- // Get the first job that can run.
- for (SchedulerJob *job : sortedJobs)
+ // If mount was previously parked awaiting job activation, we unpark it.
+ if (parkWaitState == PARKWAIT_PARKED)
{
- if (job->getScore() > 0)
- {
- bestCandidate = job;
- break;
- }
+ parkWaitState = PARKWAIT_UNPARK;
+ return;
}
- if (bestCandidate != nullptr)
- {
- // If mount was previously parked awaiting job activation, we unpark it.
- if (parkWaitState == PARKWAIT_PARKED)
- {
- parkWaitState = PARKWAIT_UNPARK;
- return;
- }
-
- appendLogText(i18n("Job '%1' is selected for next observation with priority #%2 and score %3.",
- bestCandidate->getName(), bestCandidate->getPriority(), bestCandidate->getScore()));
-
- queueTable->selectRow(bestCandidate->getStartupCell()->row());
- setCurrentJob(bestCandidate);
- }
// If we already started, we check when the next object is scheduled at.
// If it is more than 30 minutes in the future, we park the mount if that is supported
// and we unpark when it is due to start.
- else // if (startupState == STARTUP_COMPLETE)
- {
- int nextObservationTime = 1e6;
- SchedulerJob *nextObservationJob = nullptr;
- foreach (SchedulerJob *job, sortedJobs)
+ // If start up procedure is complete and the user selected pre-emptive shutdown, let us check if the next observation time exceed
+ // the pre-emptive shutdown time in hours (default 2). If it exceeds that, we perform complete shutdown until next job is ready
+ if (startupState == STARTUP_COMPLETE && Options::preemptiveShutdown() &&
+ nextObservationTime > (Options::preemptiveShutdownTime() * 3600))
+ {
+ appendLogText(i18n("Job '%1' scheduled for execution at %2. Observatory scheduled for "
+ "shutdown until next job is ready.",
+ currentJob->getName(), currentJob->getStartupTime().toString()));
+ preemptiveShutdown = true;
+ weatherCheck->setEnabled(false);
+ weatherLabel->hide();
+ checkShutdownState();
+
+ // Wake up when job is due
+ //sleepTimer.setInterval((nextObservationTime * 1000 - (1000 * Options::leadTime() * 60)));
+ sleepTimer.setInterval(( (nextObservationTime+1) * 1000));
+ //connect(&sleepTimer, SIGNAL(timeout()), this, SLOT(wakeUpScheduler()));
+ sleepTimer.start();
+ }
+ // Otherise, sleep until job is ready
+ /* FIXME: if not parking, stop tracking maybe? this would prevent crashes or scheduler stops from leaving the mount to track and bump the pier */
+ //else if (nextObservationTime > (Options::leadTime() * 60))
+ else if (nextObservationTime > 1)
+ {
+ // If start up procedure is already complete, and we didn't issue any parking commands before and parking is checked and enabled
+ // Then we park the mount until next job is ready. But only if the job uses TRACK as its first step, otherwise we cannot get into position again.
+ // This is also only performed if next job is due more than the default lead time (5 minutes).
+ // If job is due sooner than that is not worth parking and we simply go into sleep or wait modes.
+ if ((nextObservationTime > (Options::leadTime() * 60)) &&
+ startupState == STARTUP_COMPLETE &&
+ parkWaitState == PARKWAIT_IDLE &&
+ (currentJob->getStepPipeline() & SchedulerJob::USE_TRACK) &&
+ parkMountCheck->isEnabled() &&
+ parkMountCheck->isChecked())
{
- if (job->getState() != SchedulerJob::JOB_SCHEDULED || job->getStartupCondition() != SchedulerJob::START_AT)
- continue;
-
- int timeLeft = KStarsData::Instance()->lt().secsTo(job->getStartupTime());
-
- if (timeLeft > 0 && timeLeft < nextObservationTime)
- {
- nextObservationTime = timeLeft;
- nextObservationJob = job;
- }
+ appendLogText(i18n("Job '%1' scheduled for execution at %2. Parking the mount until "
+ "the job is ready.",
+ currentJob->getName(), currentJob->getStartupTime().toString()));
+ parkWaitState = PARKWAIT_PARK;
}
-
- if (nextObservationJob)
- {
- // If start up procedure is complete and the user selected pre-emptive shutdown, let us check if the next observation time exceed
- // the pre-emptive shutdown time in hours (default 2). If it exceeds that, we perform complete shutdown until next job is ready
- if (startupState == STARTUP_COMPLETE && Options::preemptiveShutdown() &&
- nextObservationTime > (Options::preemptiveShutdownTime() * 3600))
- {
- appendLogText(i18n("Job '%1' scheduled for execution at %2. Observatory scheduled for "
- "shutdown until next job is ready.",
- nextObservationJob->getName(), nextObservationJob->getStartupTime().toString()));
- preemptiveShutdown = true;
- weatherCheck->setEnabled(false);
- weatherLabel->hide();
- checkShutdownState();
-
- // Wake up when job is due
- //sleepTimer.setInterval((nextObservationTime * 1000 - (1000 * Options::leadTime() * 60)));
- sleepTimer.setInterval(( (nextObservationTime+1) * 1000));
- //connect(&sleepTimer, SIGNAL(timeout()), this, SLOT(wakeUpScheduler()));
- sleepTimer.start();
- }
- // Otherise, sleep until job is ready
- /* FIXME: if not parking, stop tracking maybe? this would prevent crashes or scheduler stops from leaving the mount to track and bump the pier */
- //else if (nextObservationTime > (Options::leadTime() * 60))
- else if (nextObservationTime > 1)
- {
- // If start up procedure is already complete, and we didn't issue any parking commands before and parking is checked and enabled
- // Then we park the mount until next job is ready. But only if the job uses TRACK as its first step, otherwise we cannot get into position again.
- // This is also only performed if next job is due more than the default lead time (5 minutes).
- // If job is due sooner than that is not worth parking and we simply go into sleep or wait modes.
- if ((nextObservationTime > (Options::leadTime() * 60)) &&
- startupState == STARTUP_COMPLETE &&
- parkWaitState == PARKWAIT_IDLE &&
- (nextObservationJob->getStepPipeline() & SchedulerJob::USE_TRACK) &&
- parkMountCheck->isEnabled() &&
- parkMountCheck->isChecked())
- {
- appendLogText(i18n("Job '%1' scheduled for execution at %2. Parking the mount until "
- "the job is ready.",
- nextObservationJob->getName(), nextObservationJob->getStartupTime().toString()));
- parkWaitState = PARKWAIT_PARK;
- }
- // If mount was pre-emptivally parked OR if parking is not supported or if start up procedure is IDLE then go into
- // sleep mode until next job is ready.
+ // If mount was pre-emptivally parked OR if parking is not supported or if start up procedure is IDLE then go into
+ // sleep mode until next job is ready.
#if 0
- else if ((nextObservationTime > (Options::leadTime() * 60)) &&
- (parkWaitState == PARKWAIT_PARKED ||
- parkMountCheck->isEnabled() == false ||
- parkMountCheck->isChecked() == false ||
- startupState == STARTUP_IDLE))
- {
- appendLogText(i18n("Sleeping until observation job %1 is ready at %2...", nextObservationJob->getName(),
- KStars::Instance()->data()->lt().addSecs(nextObservationTime+1).toString()));
- sleepLabel->setToolTip(i18n("Scheduler is in sleep mode"));
- schedulerTimer.stop();
- sleepLabel->show();
-
- // Wake up when job is ready.
- // N.B. Waking 5 minutes before is useless now because we evaluate ALL scheduled jobs each second
- // So just wake it up when it is exactly due
- sleepTimer.setInterval(( (nextObservationTime+1) * 1000));
- sleepTimer.start();
- }
+ else if ((nextObservationTime > (Options::leadTime() * 60)) &&
+ (parkWaitState == PARKWAIT_PARKED ||
+ parkMountCheck->isEnabled() == false ||
+ parkMountCheck->isChecked() == false ||
+ startupState == STARTUP_IDLE))
+ {
+ appendLogText(i18n("Sleeping until observation job %1 is ready at %2...", currentJob->getName(),
+ now.addSecs(nextObservationTime+1).toString()));
+ sleepLabel->setToolTip(i18n("Scheduler is in sleep mode"));
+ schedulerTimer.stop();
+ sleepLabel->show();
+
+ // Wake up when job is ready.
+ // N.B. Waking 5 minutes before is useless now because we evaluate ALL scheduled jobs each second
+ // So just wake it up when it is exactly due
+ sleepTimer.setInterval(( (nextObservationTime+1) * 1000));
+ sleepTimer.start();
+ }
#endif
- // The only difference between sleep and wait modes is the time. If the time more than lead time (5 minutes by default)
- // then we sleep, otherwise we wait. It's the same thing, just different labels.
- else
- {
- appendLogText(i18n("Sleeping until observation job %1 is ready at %2...", nextObservationJob->getName(),
- KStars::Instance()->data()->lt().addSecs(nextObservationTime+1).toString()));
- sleepLabel->setToolTip(i18n("Scheduler is in sleep mode"));
- schedulerTimer.stop();
- sleepLabel->show();
-
- // Wake up when job is ready.
- // N.B. Waking 5 minutes before is useless now because we evaluate ALL scheduled jobs each second
- // So just wake it up when it is exactly due
- sleepTimer.setInterval(( (nextObservationTime+1) * 1000));
- //connect(&sleepTimer, SIGNAL(timeout()), this, SLOT(wakeUpScheduler()));
- sleepTimer.start();
- }
- }
+ // The only difference between sleep and wait modes is the time. If the time more than lead time (5 minutes by default)
+ // then we sleep, otherwise we wait. It's the same thing, just different labels.
+ else
+ {
+ appendLogText(i18n("Sleeping until observation job %1 is ready at %2...", currentJob->getName(),
+ now.addSecs(nextObservationTime+1).toString()));
+ sleepLabel->setToolTip(i18n("Scheduler is in sleep mode"));
+ schedulerTimer.stop();
+ sleepLabel->show();
+
+ /* FIXME: stop tracking now */
+
+ // Wake up when job is ready.
+ // N.B. Waking 5 minutes before is useless now because we evaluate ALL scheduled jobs each second
+ // So just wake it up when it is exactly due
+ sleepTimer.setInterval(( (nextObservationTime+1) * 1000));
+ //connect(&sleepTimer, SIGNAL(timeout()), this, SLOT(wakeUpScheduler()));
+ sleepTimer.start();
}
}
}
@@ -1676,66 +1686,76 @@
bool Scheduler::calculateAltitudeTime(SchedulerJob *job, double minAltitude, double minMoonAngle)
{
// We wouldn't stat observation 30 mins (default) before dawn.
- double earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0);
- double altitude = 0;
- QDateTime lt(KStarsData::Instance()->lt().date(), QTime());
- KStarsDateTime ut = geo->LTtoUT(KStarsDateTime(lt));
+ double const earlyDawn = Dawn - Options::preDawnTime() / (60.0 * 24.0);
+ /* Compute UTC for beginning of today */
+ QDateTime const lt(KStarsData::Instance()->lt().date(), QTime());
+ KStarsDateTime const ut = geo->LTtoUT(KStarsDateTime(lt));
+
+ /* Retrieve target coordinates to be converted to horizontal to determine altitude */
SkyPoint target = job->getTargetCoords();
- QTime now = KStarsData::Instance()->lt().time();
- double fraction = now.hour() + now.minute() / 60.0 + now.second() / 3600;
+ /* Retrieve the current fraction of the day */
+ QTime const now = KStarsData::Instance()->lt().time();
+ double const fraction = now.hour() + now.minute() / 60.0 + now.second() / 3600;
/* This attempts to locate the first minute of the next 24 hours when the job target matches the altitude and moon constraints */
for (double hour = fraction; hour < (fraction + 24); hour += 1.0 / 60.0)
{
- double rawFrac = 0;
- KStarsDateTime myUT = ut.addSecs(hour * 3600.0);
-
- rawFrac = (hour > 24 ? (hour - 24) : hour) / 24.0;
+ double const rawFrac = (hour > 24 ? (hour - 24) : hour) / 24.0;
/* Test twilight enforcement, and if enforced, bail out if start time is during day */
- if (!job->getEnforceTwilight() || rawFrac < Dawn || rawFrac > Dusk)
+ /* FIXME: rework day fraction loop to shift to dusk directly */
+ if (job->getEnforceTwilight() && Dawn <= rawFrac && rawFrac <= Dusk)
+ continue;
+
+ /* Compute altitude of target for the current fraction of the day */
+ KStarsDateTime const myUT = ut.addSecs(hour * 3600.0);
+ CachingDms const LST = geo->GSTtoLST(myUT.gst());
+ target.EquatorialToHorizontal(&LST, geo->lat());
+ double const altitude = target.alt().Degrees();
+
+ if (altitude > minAltitude)
{
- CachingDms LST = geo->GSTtoLST(myUT.gst());
- target.EquatorialToHorizontal(&LST, geo->lat());
- altitude = target.alt().Degrees();
+ QDateTime const startTime = geo->UTtoLT(myUT);
- if (altitude > minAltitude)
+ /* Test twilight enforcement, and if enforced, bail out if start time is too close to dawn */
+ if (job->getEnforceTwilight() && earlyDawn < rawFrac && rawFrac < Dawn)
{
- QDateTime startTime = geo->UTtoLT(myUT);
-
- /* Test twilight enforcement, and if enforced, bail out if start time is too close to dawn */
- if (job->getEnforceTwilight() && rawFrac > earlyDawn && rawFrac < Dawn)
- {
- appendLogText(i18n("Job '%1' reaches an altitude of %2 degrees at %3 but will not be scheduled due to "
- "close proximity to astronomical twilight rise.",
- job->getName(), QString::number(minAltitude, 'g', 3), startTime.toString(job->getDateTimeDisplayFormat())));
- return false;
- }
+ appendLogText(i18n("Job '%1' reaches an altitude of %2 degrees at %3 but will not be scheduled due to "
+ "close proximity to astronomical twilight rise.",
+ job->getName(), QString::number(minAltitude, 'g', 3), startTime.toString(job->getDateTimeDisplayFormat())));
+ return false;
+ }
- if (minMoonAngle > 0 && getMoonSeparationScore(job, startTime) < 0)
- continue;
+ /* Continue searching if Moon separation is not good enough */
+ if (minMoonAngle > 0 && getMoonSeparationScore(job, startTime) < 0)
+ continue;
- /* FIXME: the name of the function doesn't suggest the job can be modified */
- job->setStartupTime(startTime);
- /* Kept the informative log because of the reschedule of aborted jobs */
- appendLogText(i18n("Job '%1' is scheduled to start at %2 where its altitude is %3 degrees.", job->getName(),
- startTime.toString(job->getDateTimeDisplayFormat()), QString::number(altitude, 'g', 3)));
- return true;
- }
+ /* FIXME: the name of the function doesn't suggest the job can be modified */
+ job->setStartupTime(startTime);
+ /* Kept the informative log because of the reschedule of aborted jobs */
+ appendLogText(i18n("Job '%1' is scheduled to start at %2 where its altitude is %3 degrees.", job->getName(),
+ startTime.toString(job->getDateTimeDisplayFormat()), QString::number(altitude, 'g', 3)));
+ return true;
}
}
/* FIXME: move this to the caller too to comment the decision to reject the job */
if (minMoonAngle == -1)
- appendLogText(i18n("Job '%1' cannot rise above minimum altitude of %2 degrees in the next 24 hours, marking invalid.", job->getName(),
- QString::number(minAltitude, 'g', 3)));
- else
- appendLogText(i18n("Job '%1' cannot rise above minimum altitude of %2 degrees with minimum moon "
- "separation of %3 degrees in the next 24 hours, marking invalid.",
- job->getName(), QString::number(minAltitude, 'g', 3),
- QString::number(minMoonAngle, 'g', 3)));
+ {
+ if (job->getEnforceTwilight())
+ {
+ appendLogText(i18n("Job '%1' has no night time with an altitude above %2 degrees during the next 24 hours, marking invalid.",
+ job->getName(), QString::number(minAltitude, 'g', 3)));
+ }
+ else appendLogText(i18n("Job '%1' can't rise to an altitude above %2 degrees in the next 24 hours, marking invalid.",
+ job->getName(), QString::number(minAltitude, 'g', 3)));
+ }
+ else appendLogText(i18n("Job '%1' can't be scheduled with an altitude above %2 degrees with minimum moon "
+ "separation of %3 degrees in the next 24 hours, marking invalid.",
+ job->getName(), QString::number(minAltitude, 'g', 3),
+ QString::number(minMoonAngle, 'g', 3)));
return false;
}
@@ -1920,16 +1940,24 @@
int16_t Scheduler::calculateJobScore(SchedulerJob *job, QDateTime when)
{
+ /* Only consolidate the score if light frames are required, calibration frames can run whenever needed */
+ if (!job->getLightFramesRequired())
+ return 1000;
+
int16_t total = 0;
- /* FIXME: as soon as one score is negative, it's a no-go and other scores are unneeded */
+ /* As soon as one score is negative, it's a no-go and other scores are unneeded */
if (job->getEnforceTwilight())
total += getDarkSkyScore(when);
- if (job->getStepPipeline() != SchedulerJob::USE_NONE)
+
+ if (0 <= total && job->getStepPipeline() != SchedulerJob::USE_NONE)
total += getAltitudeScore(job, when);
- total += getMoonSeparationScore(job, when);
+ if (0 <= total)
+ total += getMoonSeparationScore(job, when);
+
+ appendLogText(i18n("Job '%1' has a total score of %2", job->getName(), total));
return total;
}
@@ -1939,7 +1967,9 @@
double currentAlt = findAltitude(job->getTargetCoords(), when);
if (currentAlt < 0)
+ {
score = BAD_SCORE;
+ }
// If minimum altitude is specified
else if (job->getMinAltitude() > 0)
{
@@ -1973,14 +2003,18 @@
}
// If it's below minimum hard altitude (15 degrees now), set score to 10% of altitude value
else if (currentAlt < minAltitude->minimum())
+ {
score = currentAlt / 10.0;
+ }
// If no minimum altitude, then adjust altitude score to account for current target altitude
else
+ {
score = (1.5 * pow(1.06, currentAlt)) - (minAltitude->minimum() / 10.0);
+ }
/* Kept the informative log now that scores are displayed */
- appendLogText(i18n("Job '%1' target altitude is %3 degrees at %2, resulting in a score of %4.", job->getName(), when.toString(job->getDateTimeDisplayFormat()),
- QString::number(currentAlt, 'g', 3), score));
+ appendLogText(i18n("Job '%1' target altitude is %3 degrees at %2 (score %4).", job->getName(), when.toString(job->getDateTimeDisplayFormat()),
+ QString::number(currentAlt, 'g', 3), QString::asprintf("%+d", score)));
return score;
}
@@ -2063,7 +2097,7 @@
score /= 5.0;
/* Kept the informative log now that score is displayed */
- appendLogText(i18n("Job '%1' target is %3 degrees from Moon, resulting in a score of %2.", job->getName(), score, separation));
+ appendLogText(i18n("Job '%1' target is %3 degrees from Moon (score %2).", job->getName(), QString::asprintf("%+d", score), separation));
return score;
}
@@ -3317,9 +3351,9 @@
dirPath = QUrl(fileURL.url(QUrl::RemoveFilename));
- loadScheduler(fileURL.toLocalFile());
-
- evaluateJobs();
+ /* Run a job idle evaluation after a successful load */
+ if (loadScheduler(fileURL.toLocalFile()))
+ startJobEvaluation();
}
bool Scheduler::loadScheduler(const QString &fileURL)
@@ -3654,7 +3688,7 @@
else if (job->getFileStartupCondition() == SchedulerJob::START_CULMINATION)
outstream << "Culmination" << endl;
else if (job->getFileStartupCondition() == SchedulerJob::START_AT)
- outstream << "At"
+ outstream << "At"
<< endl;
outstream << "" << endl;
@@ -3876,7 +3910,10 @@
// We're done whether the job completed successfully or not.
else if (currentJob->getCompletionCondition() == SchedulerJob::FINISH_SEQUENCE)
{
- currentJob->setState(SchedulerJob::JOB_COMPLETE);
+ /* Mark the job idle as well as all its duplicates for re-evaluation */
+ foreach(SchedulerJob *a_job, jobs)
+ if (a_job == currentJob || a_job->isDuplicateOf(currentJob))
+ a_job->setState(SchedulerJob::JOB_IDLE);
captureBatch = 0;
// Stop Guiding if it was used
stopGuiding();
@@ -3892,7 +3929,10 @@
// If we're done
if (currentJob->getRepeatsRemaining() == 0)
{
- currentJob->setState(SchedulerJob::JOB_COMPLETE);
+ /* Mark the job idle as well as all its duplicates for re-evaluation */
+ foreach(SchedulerJob *a_job, jobs)
+ if (a_job == currentJob || a_job->isDuplicateOf(currentJob))
+ a_job->setState(SchedulerJob::JOB_IDLE);
stopCurrentJobAction();
stopGuiding();
@@ -3908,17 +3948,24 @@
currentJob->setState(SchedulerJob::JOB_BUSY);
- /* If we are guiding, continue capturing, else realign */
+ /* If we are guiding, continue capturing */
if (currentJob->getStepPipeline() & SchedulerJob::USE_GUIDE)
{
currentJob->setStage(SchedulerJob::STAGE_CAPTURING);
startCapture();
}
- else
+ /* If we are using alignment, realign */
+ else if (currentJob->getStepPipeline() & SchedulerJob::USE_ALIGN)
{
currentJob->setStage(SchedulerJob::STAGE_ALIGNING);
startAstrometry();
}
+ /* Else just slew back to target - no-op probably, but having only 'track' checked is an edge case */
+ else
+ {
+ currentJob->setStage(SchedulerJob::STAGE_SLEWING);
+ startSlew();
+ }
appendLogText(i18np("Job '%1' is repeating, #%2 batch remaining.",
"Job '%1' is repeating, #%2 batches remaining.",
@@ -3942,7 +3989,10 @@
{
if (KStarsData::Instance()->lt().secsTo(currentJob->getCompletionTime()) <= 0)
{
- currentJob->setState(SchedulerJob::JOB_COMPLETE);
+ /* Mark the job idle as well as all its duplicates for re-evaluation */
+ foreach(SchedulerJob *a_job, jobs)
+ if (a_job == currentJob || a_job->isDuplicateOf(currentJob))
+ a_job->setState(SchedulerJob::JOB_IDLE);
stopCurrentJobAction();
stopGuiding();
captureBatch = 0;
@@ -4136,8 +4186,6 @@
void Scheduler::updateCompletedJobsCount()
{
- /* QMap finishedFramesCount; see later FIXME in that function */
-
/* Use a temporary map in order to limit the number of file searches */
QMap newFramesCount;
@@ -4186,13 +4234,7 @@
}
/* Count captures already stored */
- int const completed = getCompletedFiles(signature, oneSeqJob->getFullPrefix());
-
- /* FIXME: finishedFramesCount isn't documented, and is getting in the way of counting the amount of captures in the storage */
- newFramesCount[signature] += completed; /* - finishedFramesCount[signature]; */
-
- /* if (oneJob->getState() == SchedulerJob::JOB_COMPLETE)
- finishedFramesCount[signature] += oneSeqJob->getCount(); */
+ newFramesCount[signature] = getCompletedFiles(signature, oneSeqJob->getFullPrefix());
}
}
@@ -4262,11 +4304,15 @@
// If the previous job signature matches the current, reduce completion count to compare duplicates
if (!signature.compare(prevJob->getLocalDir() + prevJob->getDirectoryPostfix()))
{
- appendLogText(i18n("%1 has a previous duplicate with %2 completed captures.", seqName, prevJob->getCount()));
+ appendLogText(i18n("%1 has a previous duplicate sequence requiring %2 captures.", seqName, prevJob->getCount()));
completed -= prevJob->getCount();
}
}
+ // Now completed count can be needlessly negative for this job, so clamp to zero
+ if (completed < 0)
+ completed = 0;
+
// Update the completion count for this signature if we still have captures to take
QMap fMap = schedJob->getCapturedFramesMap();
fMap[signature] = completed < job->getCount() ? completed : job->getCount();
@@ -4285,7 +4331,7 @@
{
if(areJobCapturesComplete)
{
- appendLogText(i18n("%1 completed its sequence of %2 light frames.", seqName, job->getCount()));
+ appendLogText(i18n("%1 completed its sequence of %2 light frames.", seqName, job->getCount()*schedJob->getRepeatsRequired()));
}
else
{
@@ -4316,7 +4362,7 @@
if (schedJob->getCompletionCondition() == SchedulerJob::FINISH_LOOP)
totalImagingTime += fabs((job->getExposure() + job->getDelay()) * 1);
else
- totalImagingTime += fabs((job->getExposure() + job->getDelay()) * (job->getCount() - completed));
+ totalImagingTime += fabs((job->getExposure() + job->getDelay()) * (job->getCount()*schedJob->getRepeatsRequired() - completed));
/* If we have light frames to process, add focus/dithering delay */
if (job->getFrameType() == FRAME_LIGHT)
@@ -4437,7 +4483,7 @@
if (mountReply.error().type() != QDBusError::NoError)
{
- appendLogText(i18n("Warning! Mount getParkingStatus request received DBUS error: %2", QDBusError::errorString(mountReply.error().type())));
+ appendLogText(i18n("Warning! Mount getParkingStatus request received DBUS error: %1", QDBusError::errorString(mountReply.error().type())));
status = Mount::PARKING_ERROR;
}
@@ -4477,7 +4523,7 @@
if (mountReply.error().type() != QDBusError::NoError)
{
- appendLogText(i18n("Warning! Mount getParkingStatus request received DBUS error: %2", QDBusError::errorString(mountReply.error().type())));
+ appendLogText(i18n("Warning! Mount getParkingStatus request received DBUS error: %1", QDBusError::errorString(mountReply.error().type())));
status = Mount::PARKING_ERROR;
}
@@ -4554,7 +4600,7 @@
if (mountReply.error().type() != QDBusError::NoError)
{
- appendLogText(i18n("Warning! Mount getParkingStatus request received DBUS error: %2", QDBusError::errorString(mountReply.error().type())));
+ appendLogText(i18n("Warning! Mount getParkingStatus request received DBUS error: %1", QDBusError::errorString(mountReply.error().type())));
status = Mount::PARKING_ERROR;
}
@@ -4568,7 +4614,7 @@
if (domeReply.error().type() != QDBusError::NoError)
{
- appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %2", QDBusError::errorString(domeReply.error().type())));
+ appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %1", QDBusError::errorString(domeReply.error().type())));
status = Dome::PARKING_ERROR;
}
@@ -4594,7 +4640,7 @@
if (domeReply.error().type() != QDBusError::NoError)
{
- appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %2", QDBusError::errorString(domeReply.error().type())));
+ appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %1", QDBusError::errorString(domeReply.error().type())));
status = Dome::PARKING_ERROR;
}
@@ -4623,7 +4669,7 @@
if (domeReply.error().type() != QDBusError::NoError)
{
- appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %2", QDBusError::errorString(domeReply.error().type())));
+ appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %1", QDBusError::errorString(domeReply.error().type())));
status = Dome::PARKING_ERROR;
}
@@ -4691,7 +4737,7 @@
if (domeReply.error().type() != QDBusError::NoError)
{
- appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %2", QDBusError::errorString(domeReply.error().type())));
+ appendLogText(i18n("Warning! Dome getParkingStatus request received DBUS error: %1", QDBusError::errorString(domeReply.error().type())));
status = Dome::PARKING_ERROR;
}
@@ -4705,7 +4751,7 @@
if (capReply.error().type() != QDBusError::NoError)
{
- appendLogText(i18n("Warning! Cap getParkingStatus request received DBUS error: %2", QDBusError::errorString(capReply.error().type())));
+ appendLogText(i18n("Warning! Cap getParkingStatus request received DBUS error: %1", QDBusError::errorString(capReply.error().type())));
status = DustCap::PARKING_ERROR;
}
@@ -4731,7 +4777,7 @@
if (capReply.error().type() != QDBusError::NoError)
{
- appendLogText(i18n("Warning! Cap getParkingStatus request received DBUS error: %2", QDBusError::errorString(capReply.error().type())));
+ appendLogText(i18n("Warning! Cap getParkingStatus request received DBUS error: %1", QDBusError::errorString(capReply.error().type())));
status = DustCap::PARKING_ERROR;
}
@@ -4760,7 +4806,7 @@
if (capReply.error().type() != QDBusError::NoError)
{
- appendLogText(i18n("Warning! Cap getParkingStatus request received DBUS error: %2", QDBusError::errorString(capReply.error().type())));
+ appendLogText(i18n("Warning! Cap getParkingStatus request received DBUS error: %1", QDBusError::errorString(capReply.error().type())));
status = DustCap::PARKING_ERROR;
}
@@ -4822,15 +4868,12 @@
void Scheduler::startJobEvaluation()
{
- jobEvaluationOnly = true;
- if (Dawn < 0)
- calculateDawnDusk();
-
// Reset ALL scheduler jobs to IDLE and re-evalute them all again
for(SchedulerJob *job : jobs)
job->reset();
// Now evaluate all pending jobs per the conditions set in each
+ jobEvaluationOnly = true;
evaluateJobs();
}
diff --git a/kstars/ekos/scheduler/schedulerjob.h b/kstars/ekos/scheduler/schedulerjob.h
--- a/kstars/ekos/scheduler/schedulerjob.h
+++ b/kstars/ekos/scheduler/schedulerjob.h
@@ -26,14 +26,14 @@
/** @brief States of a SchedulerJob. */
typedef enum {
- JOB_IDLE,
- JOB_EVALUATION,
- JOB_SCHEDULED,
- JOB_BUSY,
- JOB_ERROR,
- JOB_ABORTED,
- JOB_INVALID,
- JOB_COMPLETE
+ JOB_IDLE, /**< Job was just created, and is not evaluated yet */
+ JOB_EVALUATION, /**< Job is being evaluated */
+ JOB_SCHEDULED, /**< Job was evaluated, and has a schedule */
+ JOB_BUSY, /**< Job is being processed */
+ JOB_ERROR, /**< Job encountered a fatal issue while processing, and must be reset manually */
+ JOB_ABORTED, /**< Job encountered a transitory issue while processing, and will be rescheduled */
+ JOB_INVALID, /**< Job has an incorrect configuration, and cannot proceed */
+ JOB_COMPLETE /**< Job finished all required captures */
} JOBStatus;
/** @brief Running stages of a SchedulerJob. */
@@ -147,8 +147,17 @@
void setDateTimeDisplayFormat(const QString &value);
/** @} */
+ /** @brief Original startup condition, as entered by the user. */
+ /** @{ */
StartupCondition getFileStartupCondition() const { return fileStartupCondition; }
void setFileStartupCondition(const StartupCondition &value);
+ /** @} */
+
+ /** @brief Original time at which the job must start, as entered by the user. */
+ /** @{ */
+ QDateTime getFileStartupTime() const { return fileStartupTime; }
+ void setFileStartupTime(const QDateTime &value);
+ /** @} */
/** @brief Whether this job requires re-focus while running its capture sequence. */
/** @{ */
@@ -180,7 +189,12 @@
void setNameCell(QTableWidgetItem *cell);
/** @} */
- /** @brief Current state of the scheduler job. */
+ /** @brief Current state of the scheduler job.
+ * Setting state to JOB_ABORTED automatically resets the startup characteristics.
+ * Setting state to JOB_INVALID automatically resets the startup characteristics and the duration estimation.
+ * @see SchedulerJob::setStartupCondition, SchedulerJob::setFileStartupCondition, SchedulerJob::setStartupTime
+ * and SchedulerJob::setFileStartupTime.
+ */
/** @{ */
JOBStatus getState() const { return state; }
void setState(const JOBStatus &value);
@@ -304,6 +318,13 @@
*/
void reset();
+ /** @brief Determining whether a SchedulerJob is a duplicate of another.
+ * @param a_job is the other SchedulerJob to test duplication against.
+ * @return True if objects are different, but name and sequence file are identical, else false.
+ * @fixme This is a weak comparison, but that's what the scheduler looks at to decide completion.
+ */
+ bool isDuplicateOf(SchedulerJob const *a_job) const { return this != a_job && name == a_job->name && sequenceFile == a_job->sequenceFile; }
+
/** @brief Compare ::SchedulerJob instances based on score. This is a qSort predicate, deprecated in QT5.
* @arg a, b are ::SchedulerJob instances to compare.
* @return true if the score of b is lower than the score of a.
@@ -325,6 +346,13 @@
*/
static bool decreasingAltitudeOrder(SchedulerJob const *a, SchedulerJob const *b);
+ /** @brief Compare ::SchedulerJob instances based on startup time. This is a qSort predicate, deprecated in QT5.
+ * @arg a, b are ::SchedulerJob instances to compare.
+ * @return true if the startup time of a is sooner than the priority of b.
+ * @return false if the startup time of a is later than or equal to the priority of b.
+ */
+ static bool increasingStartupTimeOrder(SchedulerJob const *a, SchedulerJob const *b);
+
private:
QString name;
SkyPoint targetCoords;
@@ -338,6 +366,7 @@
int sequenceCount { 0 };
int completedCount { 0 };
+ QDateTime fileStartupTime;
QDateTime startupTime;
QDateTime completionTime;
diff --git a/kstars/ekos/scheduler/schedulerjob.cpp b/kstars/ekos/scheduler/schedulerjob.cpp
--- a/kstars/ekos/scheduler/schedulerjob.cpp
+++ b/kstars/ekos/scheduler/schedulerjob.cpp
@@ -38,9 +38,12 @@
void SchedulerJob::setStartupTime(const QDateTime &value)
{
startupTime = value;
+
if (value.isValid())
startupCondition = START_AT;
- updateJobCell();
+
+ /* Refresh estimated time - which update job cells */
+ setEstimatedTime(estimatedTime);
}
void SchedulerJob::setSequenceFile(const QUrl &value)
@@ -70,13 +73,26 @@
void SchedulerJob::setCompletionTime(const QDateTime &value)
{
- completionTime = value;
- updateJobCell();
+ /* If argument completion time is valid, automatically switch condition to FINISH_AT */
+ if (value.isValid())
+ {
+ setCompletionCondition(FINISH_AT);
+ completionTime = value;
+ }
+ /* If completion time is not valid, but startup time is, deduce completion from startup and duration */
+ else if (startupTime.isValid())
+ {
+ completionTime = startupTime.addSecs(estimatedTime);
+ }
+
+ /* Refresh estimated time - which update job cells */
+ setEstimatedTime(estimatedTime);
}
void SchedulerJob::setCompletionCondition(const CompletionCondition &value)
{
completionCondition = value;
+ updateJobCell();
}
void SchedulerJob::setStepPipeline(const StepPipeline &value)
@@ -92,6 +108,21 @@
if (JOB_ERROR == state)
KNotification::event(QLatin1String("EkosSchedulerJobFail"), i18n("Ekos job failed (%1)", getName()));
+ /* If job becomes invalid, automatically reset its startup characteristics, and force its duration to be reestimated */
+ if (JOB_INVALID == value)
+ {
+ setStartupCondition(fileStartupCondition);
+ setStartupTime(fileStartupTime);
+ setEstimatedTime(-1);
+ }
+
+ /* If job is aborted, automatically reset its startup characteristics */
+ if (JOB_ABORTED == value)
+ {
+ setStartupCondition(fileStartupCondition);
+ /* setStartupTime(fileStartupTime); */
+ }
+
updateJobCell();
}
@@ -128,30 +159,54 @@
{
statusCell = value;
updateJobCell();
+ if (statusCell)
+ statusCell->setToolTip(i18n("Current status of job '%1', managed by the Scheduler.\n"
+ "If invalid, the Scheduler was not able to find a proper observation time for the target.\n"
+ "If aborted, the Scheduler missed the scheduled time or encountered transitory issues and will reschedule the job.\n"
+ "If complete, the Scheduler verified that all sequence captures requested were stored, including repeats.",
+ name));
}
void SchedulerJob::setStartupCell(QTableWidgetItem *value)
{
startupCell = value;
updateJobCell();
+ if (startupCell)
+ startupCell->setToolTip(i18n("Startup time for job '%1', as estimated by the Scheduler.\n"
+ "Fixed time from user or culmination time is marked with a chronometer symbol. ",
+ name));
}
void SchedulerJob::setCompletionCell(QTableWidgetItem *value)
{
completionCell = value;
updateJobCell();
+ if (completionCell)
+ completionCell->setToolTip(i18n("Completion time for job '%1', as estimated by the Scheduler.\n"
+ "Can be specified by the user to limit duration of looping jobs.\n"
+ "Fixed time from user is marked with a chronometer symbol. ",
+ name));
}
void SchedulerJob::setCaptureCountCell(QTableWidgetItem *value)
{
captureCountCell = value;
updateJobCell();
+ if (captureCountCell)
+ captureCountCell->setToolTip(i18n("Count of captures stored for job '%1', based on its sequence job.\n"
+ "This is a summary, additional specific frame types may be required to complete the job.",
+ name));
}
void SchedulerJob::setScoreCell(QTableWidgetItem *value)
{
scoreCell = value;
updateJobCell();
+ if (scoreCell)
+ scoreCell->setToolTip(i18n("Current score for job '%1', from its altitude, moon separation and sky darkness.\n"
+ "Negative if adequate altitude is not achieved yet or if there is no proper observation time today.\n"
+ "The Scheduler will refresh scores when picking a new candidate job.",
+ name));
}
void SchedulerJob::setDateTimeDisplayFormat(const QString &value)
@@ -183,9 +238,34 @@
fileStartupCondition = value;
}
+void SchedulerJob::setFileStartupTime(const QDateTime &value)
+{
+ fileStartupTime = value;
+}
+
void SchedulerJob::setEstimatedTime(const int64_t &value)
{
- estimatedTime = value;
+ /* If startup and completion times are fixed, estimated time cannot change */
+ if (START_AT == startupCondition && FINISH_AT == completionCondition)
+ {
+ estimatedTime = startupTime.secsTo(completionTime);
+ }
+ /* If startup time is fixed but not completion time, estimated time adjusts completion time */
+ else if (START_AT == startupCondition)
+ {
+ estimatedTime = value;
+ completionTime = startupTime.addSecs(value);
+ }
+ /* If completion time is fixed but not startup time, estimated time adjusts startup time */
+ /* FIXME: adjusting startup time will probably not work, because jobs are scheduled from first available altitude */
+ else if (FINISH_AT == completionCondition)
+ {
+ estimatedTime = value;
+ startupTime = completionTime.addSecs(-value);
+ }
+ /* Else estimated time is simply stored as is */
+ else estimatedTime = value;
+
updateJobCell();
}
@@ -208,6 +288,10 @@
{
estimatedTimeCell = value;
updateJobCell();
+ if (estimatedTimeCell)
+ estimatedTimeCell->setToolTip(i18n("Duration job '%1' will take to complete when started, as estimated by the Scheduler.\n"
+ "Depends on the actions to be run, and the sequence job to be processed.",
+ name));
}
void SchedulerJob::setLightFramesRequired(bool value)
@@ -240,7 +324,6 @@
targetCoords.updateCoordsNow(KStarsData::Instance()->updateNum());
}
-/* FIXME: unrelated to model, move this in the view */
void SchedulerJob::updateJobCell()
{
if (nameCell)
@@ -276,7 +359,7 @@
if (stageCell || stageLabel)
{
- /* Translated string cache - overkill, probably, and doesn't warn about missing enums like switch/case shouldi ; also, not thread-safe */
+ /* Translated string cache - overkill, probably, and doesn't warn about missing enums like switch/case should ; also, not thread-safe */
/* FIXME: this should work with a static initializer in C++11, but QT versions are touchy on this, and perhaps i18n can't be used? */
static QMap stageStrings;
static QString stageStringUnknown;
@@ -312,13 +395,73 @@
if (startupCell)
{
- startupCell->setText(startupTime.toString(dateTimeDisplayFormat));
+ /* Display a startup time if job is running, scheduled to run or about to be re-scheduled */
+ if (JOB_SCHEDULED == state || JOB_BUSY == state || JOB_ABORTED == state) switch (fileStartupCondition)
+ {
+ /* If the original condition is START_AT/START_CULMINATION, startup time is fixed */
+ case START_AT:
+ case START_CULMINATION:
+ startupCell->setText(startupTime.toString(dateTimeDisplayFormat));
+ startupCell->setIcon(QIcon::fromTheme("chronometer"));
+ break;
+
+ /* If the original condition is START_ASAP, startup time is informational */
+ case START_ASAP:
+ startupCell->setText(startupTime.toString(dateTimeDisplayFormat));
+ startupCell->setIcon(QIcon());
+ break;
+
+ /* Else do not display any startup time */
+ default:
+ startupCell->setText(QString());
+ startupCell->setIcon(QIcon());
+ break;
+ }
+ /* Display a missed startup time if job is invalid */
+ else if (JOB_INVALID == state && START_AT == fileStartupCondition)
+ {
+ startupCell->setText(startupTime.toString(dateTimeDisplayFormat));
+ startupCell->setIcon(QIcon::fromTheme("chronometer"));
+ }
+ /* Else do not display any startup time */
+ else
+ {
+ startupCell->setText(QString());
+ startupCell->setIcon(QIcon());
+ }
+
startupCell->tableWidget()->resizeColumnToContents(startupCell->column());
}
if (completionCell)
{
- completionCell->setText(completionTime.toString(dateTimeDisplayFormat));
+ /* Display a completion time if job is running, scheduled to run or about to be re-scheduled */
+ if (JOB_SCHEDULED == state || JOB_BUSY == state || JOB_ABORTED == state) switch (completionCondition)
+ {
+ case FINISH_LOOP:
+ completionCell->setText(QString("-"));
+ completionCell->setIcon(QIcon());
+ break;
+
+ case FINISH_AT:
+ completionCell->setText(completionTime.toString(dateTimeDisplayFormat));
+ completionCell->setIcon(QIcon::fromTheme("chronometer"));
+ break;
+
+ case FINISH_SEQUENCE:
+ case FINISH_REPEAT:
+ default:
+ completionCell->setText(completionTime.toString(dateTimeDisplayFormat));
+ completionCell->setIcon(QIcon());
+ break;
+ }
+ /* Else do not display any completion time */
+ else
+ {
+ completionCell->setText(QString());
+ completionCell->setIcon(QIcon());
+ }
+
completionCell->tableWidget()->resizeColumnToContents(completionCell->column());
}
@@ -361,7 +504,7 @@
stage = STAGE_IDLE;
estimatedTime = -1;
startupCondition = fileStartupCondition;
- startupTime = fileStartupCondition == START_AT ? startupTime : QDateTime();
+ startupTime = fileStartupCondition == START_AT ? fileStartupTime : QDateTime();
/* No change to culmination offset */
repeatsRemaining = repeatsRequired;
}
@@ -381,3 +524,8 @@
return Ekos::Scheduler::findAltitude(job1->getTargetCoords(), job1->getStartupTime()) >
Ekos::Scheduler::findAltitude(job2->getTargetCoords(), job2->getStartupTime());
}
+
+bool SchedulerJob::increasingStartupTimeOrder(SchedulerJob const *job1, SchedulerJob const *job2)
+{
+ return job1->getStartupTime() < job2->getStartupTime();
+}