[education/kstars] /: Create extensions interface
Jasem Mutlaq
null at kde.org
Wed Aug 21 13:11:35 BST 2024
Git commit e6734ccef699f0997fd9325361ce6378ad95ebc1 by Jasem Mutlaq, on behalf of Ed Lee.
Committed on 21/08/2024 at 12:11.
Pushed by mutlaqja into branch 'master'.
Create extensions interface
Closes #271.
This MR creates the hooks required to detect, start & stop small helper programs (extensions) from within Ekos.
Detection is handled by querying the file system. An 'extensions' directory is created on first run within the KSPaths::writableLocation eg: \~/.local/share/kstars on a typical Linux install. Any executable program found within that directory that also has a valid configuration file is presented as a runnable option within the new extension controls. Detection is performed each time Ekos is started. If no valid extensions are found then the extension controls are not displayed.
![Screenshot from 2024-05-08 14-56-42.png](/uploads/a130106f5538808966087f0d288e2dec/Screenshot_from_2024-05-08_14-56-42.png)
The start/stop/abort control is a single button that toggles between 'Run' & 'Stop', the same as the Ekos Start/Stop and Capture Start/Stop controls. The additional function of abort is enabled if an extension fails to exit within a timeout of the close request triggered by the Stop button. Abort is a second click of the re-enabled Stop button and is also notified by a log message.
Starting, aborting, and message handling of extensions is via a QProcess function. All output from the extension is forwarded to the Ekos log. Gracefully stopping an extension is via an emitted DBus signal that the extension program should handle.
Each extension must have a companion configuration file also located in the extensions directory, named the same as the executable with the addition of a .conf eg: an extension named _example_ must also have a configuration file named _example.conf_ A configuration file is a plain text file that provides configuration settings to the extension program and usage information to the user. A configuration file is only valid if it contains a line starting with: minimum_kstars_version=x.y.z The x.y.z is the minimum release of KStars that the extension is designed/tested against. This value is checked against the current KStars KSTARS_VERSION macro defined in version.h and must be equal or lower for the extension to be considered valid. The extension should also check that this minimum_kstars_version string matches what it expects.
Optionally each extension can also provide an icon file for display in the Extension drop down list. Again the naming should match the extension executable with a valid file extension (_.jpg,_ .bmp, _.gif,_ .png or .svg) and be placed in the same extensions directory. A default icon is used for any extension that does not provide it's own icon.
Several new DBus functions/signals are added to enable general extension use and for a specific upcoming extension. These are:
* ExtensionState function which returns the current Extension state and extensionStateChanged signal which signifies that ExtensionState has changed.
* getJobPlaceholderFormat and getJobPreviewFileName functions which returns the currently set placeholder format and filename - to be used by the upcoming Siril_EAA extension to located the preview image save location.
* setFITSfromFile and previewFile functions which sets whether the Ekos preview viewer received images direct from internal modules (as is current) or from externally passed fits file locations, and passes a file location to be displayed - to be used by the upcoming Sirial_EAA extension to pass the live stacked result back to Ekos.
Two extensions are ready for release (other than final packaging), these being:
* FireCapture launcher (FC_launcher) - disconnects the current primary camera INDI driver and launches FireCapture. Upon close restarts the INDI driver.
* KStars Backup (KS_backup) - provides a GUI for the archiving and restoration of KStars/INDI (and optionally others) configuration directories to/from .tar.gz archives/
Additionally there is a minimal working example extension intended to inform other potential extension authors:
* Minimum skeleton example (example) - just passes a heartbeat message to the log and provides graceful exit.
A third extension is basically complete but requires more extension testing. ~~This is on hold pending the next maintenance release of Siril which should contain my fix for the current intermittent command pipe handling:~~ Merged into Siril 1.2.2, now release, and superseded by 1.2.3.
* Sirial_EEA - provides live stacking of the preview job from the Capture Module and displays it auto-stretched in the Ekos preview window.
All the above extensions are currently hosted in my github at https://github.com/LuckyEddie47/kstars_extensions
A +86 -0 doc/ekos-extensions.docbook
M +2 -2 doc/ekos-user-interface.docbook
M +2 -1 doc/ekos.docbook
M +- -- doc/ekos_summary_cheatsheet.png
M +1 -0 doc/index.docbook
M +1 -0 kstars/CMakeLists.txt
M +22 -0 kstars/ekos/ekos.cpp
M +19 -0 kstars/ekos/ekos.h
A +225 -0 kstars/ekos/extensions.cpp *
A +44 -0 kstars/ekos/extensions.h *
M +98 -1 kstars/ekos/manager.cpp
M +19 -0 kstars/ekos/manager.h
M +121 -12 kstars/ekos/manager.ui
M +3 -0 kstars/org.kde.kstars.Ekos.xml
The files marked with a * at the end have a non valid license. Please read: https://community.kde.org/Policies/Licensing_Policy and use the headers which are listed at that page.
https://invent.kde.org/education/kstars/-/commit/e6734ccef699f0997fd9325361ce6378ad95ebc1
diff --git a/doc/ekos-extensions.docbook b/doc/ekos-extensions.docbook
new file mode 100644
index 0000000000..672ca7c63b
--- /dev/null
+++ b/doc/ekos-extensions.docbook
@@ -0,0 +1,86 @@
+<sect1 id="ekos-extensions">
+ <title>Extensions</title>
+ <indexterm>
+ <primary>Tools</primary>
+ <secondary>Ekos</secondary>
+ <tertiary>Extensions</tertiary>
+ </indexterm>
+ <sect2 id="extensions-Introduction">
+ <title>Introduction</title>
+ <para>
+ Extensions are small programs that can be added to interact with Kstars/Ekos/INDI in order to provide extra functions and features.
+ </para>
+ <note>
+ <para>
+ Extensions are separate from Kstars/Ekos/INDI. They are not provided as part of this software. Only a means to call them is provided for convenience. Make sure that you understand the requirements and risks of using an extension.
+ </para>
+ </note>
+ </sect2>
+ <sect2 id="extensions-Requirements">
+ <title>Requirements</title>
+ <para>
+ Each extension must consist of at least 2 files, with an optional third icon file.
+ <itemizedlist>
+ <listitem>
+ <para>
+ The program file. This is any executable file that will run on the users system. The user under which KStars is running must have execute permission on this file.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ The configuration file. This is a plain text file that defines the required and optional inputs for the extension. It must be named the same as the extension executable with the addition of a .conf file name extension.
+ </para>
+ <para>
+ It is mandatory that the configuration file contains a line starting with <emphasis role="bold">minimum_kstars_version=n.n.n</emphasis> where n.n.n is the lowest version of KStars that supports the extension eg. 3.7.1
+ </para>
+ <para>
+ Optionally (and non-preferred) the configuration file may contain a line starting with <emphasis role="bold">runDetached=true</emphasis>. If present this line makes the extension run independently from KStars. Once it has been started the extension can not pass status information back to Ekos. This should only be used by extensions that are required to continue to run after KStars has closed. The extension must also provide it's own user interface.
+ </para>
+ <para>
+ Additionally the configuration file can contain any other parameters that the extension author decides and free text.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ The icon file, if present, must be named the same as the extension executable with the addition of the appropriate file name extension for the image format, .jpg, .bmp, .gif, .png and .svg are supported. If provided this icon will be used in the extension selection ComboBox. If an icon is not provide a default icon will be used instead.
+ </para>
+ </listitem>
+ </itemizedlist>
+ All files of the extension (executable, configuration, and optional icon) must be copied into the KStars writable data location /extensions eg. ~/.local/share/kstars/extensions
+ </para>
+ <para>
+ Extensions that are present, have a valid configuration file, and have a minimum KStars version no higher than the current installation will be detected when Ekos is started. If there are no valid extensions detected the extension UI element will not be displayed. The extension UI elements are a ComboBox showing the name and icon of each detected extension, and a start/stop button. If the extension fails to close within 10 seconds of the stop button being clicked, it becomes re-enabled as an abort button that will force close the extension. Only one extension can be used at a time.
+ </para>
+ </sect2>
+ <sect2 id="extensions-Development">
+ <title>Development</title>
+ <para>
+ The following describes additional points for developers of extensions.
+ <itemizedlist>
+ <listitem>
+ <para>
+ Some extension sources including an example skelton extension are available in the <ulink url="https://github.com/LuckyEddie47/kstars_extensions">KStars Extension Github</ulink>
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Each extension must check for the existance and validity of it's own configuration file. The minimum_kstars_version configuration file entry must be checked against an internal reference to confirm that the configuration file matches the extension requirements. See the skelton example.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ The calling KStars version is passed to the extension as arg(1) in the launching QProcess call.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ All interaction with KStars/Ekos/INDI should be via the DBus interface. See the skelton exmaple. Useful tools for interrogating, monitoring and understanding DBus include <ulink url="https://wiki.gnome.org/Apps/DFeet>D-Feet">D-Feet</ulink> and <ulink url="https://gitlab.freedesktop.org/bustle/bustle">Bustle</ulink>
+ </para>
+ <para>
+ It may appear on initial investigation that using the Qt DBus Adaptors system would be much easier than direct use of the Qt DBus Interfaces/Messages and KStars does provide the required xml definitions. However currently there is heavy use of custom types the definitions of which are combined with other information in the KStars sources. This results in a large set of files from KStars that require inclusion within an extension in order to make use of the Qt DBus Adaptors. Hopefully this will be addressed in the future.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+ </sect2>
+</sect1>
diff --git a/doc/ekos-user-interface.docbook b/doc/ekos-user-interface.docbook
index fd943492b3..1effb62a79 100644
--- a/doc/ekos-user-interface.docbook
+++ b/doc/ekos-user-interface.docbook
@@ -1,4 +1,4 @@
-<sect1>
+<sect1>
<title>User Interface</title>
<indexterm>
<primary>Tools</primary>
@@ -77,7 +77,7 @@
<term>Summary & Setup Module</term>
<listitem>
<para>
- As its name suggests, this is where you will create and manage your equipment profile, and connect to your devices. It also provides a summary view where the capture progress along with the focus & guide operations is displayed in a compact format to convey the most important information relevant to the user.
+ As its name suggests, this is where you will create and manage your equipment profile, and connect to your devices. It also provides a summary view where the capture progress along with the focus & guide operations is displayed in a compact format to convey the most important information relevant to the user. If any extensions are installed, the selection and start/stop controls will be shown.
</para>
</listitem>
</varlistentry>
diff --git a/doc/ekos.docbook b/doc/ekos.docbook
index 794a3dd486..f969b5a77b 100644
--- a/doc/ekos.docbook
+++ b/doc/ekos.docbook
@@ -1,4 +1,4 @@
-<chapter id="ekos">
+<chapter id="ekos">
<title>Ekos</title>
<indexterm><primary>Ekos</primary></indexterm>
@@ -136,6 +136,7 @@
&tool-ekos-align;
&tool-ekos-scheduler;
&tool-ekos-analyze;
+&tool-ekos-extensions;
&tool-ekos-tutorials;
</chapter>
diff --git a/doc/ekos_summary_cheatsheet.png b/doc/ekos_summary_cheatsheet.png
index d205582bfb..cd3d6dda9e 100644
Binary files a/doc/ekos_summary_cheatsheet.png and b/doc/ekos_summary_cheatsheet.png differ
diff --git a/doc/index.docbook b/doc/index.docbook
index a63579023a..6381e4333a 100644
--- a/doc/index.docbook
+++ b/doc/index.docbook
@@ -66,6 +66,7 @@
<!ENTITY tool-ekos-profile-wizard SYSTEM "ekos-profile-wizard.docbook">
<!ENTITY tool-ekos-scheduler SYSTEM "ekos-scheduler.docbook">
<!ENTITY tool-ekos-analyze SYSTEM "ekos-analyze.docbook">
+ <!ENTITY tool-ekos-extensions SYSTEM "ekos-extensions.docbook">
<!ENTITY tool-ekos-setup SYSTEM "ekos-setup.docbook">
<!ENTITY tool-ekos-tutorials SYSTEM "ekos-tutorials.docbook">
<!ENTITY tool-ekos-user-interface SYSTEM "ekos-user-interface.docbook">
diff --git a/kstars/CMakeLists.txt b/kstars/CMakeLists.txt
index 85a448a640..fe958ead4b 100644
--- a/kstars/CMakeLists.txt
+++ b/kstars/CMakeLists.txt
@@ -214,6 +214,7 @@ if (INDI_FOUND)
ekos/manager/focusprogresswidget.cpp
ekos/manager/guidemanager.cpp
ekos/manager/meridianflipstate.cpp
+ ekos/extensions.cpp
# Auxiliary
ekos/auxiliary/darklibrary.cpp
diff --git a/kstars/ekos/ekos.cpp b/kstars/ekos/ekos.cpp
index 9e948b2373..4e3e2fa918 100644
--- a/kstars/ekos/ekos.cpp
+++ b/kstars/ekos/ekos.cpp
@@ -39,6 +39,10 @@ const QString getSchedulerStatusString(SchedulerState state, bool translated)
{
return translated ? schedulerStates[state].toString() : schedulerStates[state].untranslatedText();
}
+const QString getExtensionStatusString(ExtensionState state, bool translated)
+{
+ return translated ? i18n(extensionStates[state]) : extensionStates[state];
+}
/* Taken from https://codereview.stackexchange.com/questions/71300/wrapper-function-to-do-polynomial-fits-with-gsl */
std::vector<double> gsl_polynomial_fit(const double *const data_x, const double *const data_y, const int n,
@@ -197,3 +201,21 @@ const QDBusArgument &operator>>(const QDBusArgument &argument, Ekos::SchedulerSt
dest = static_cast<Ekos::SchedulerState>(a);
return argument;
}
+
+QDBusArgument &operator<<(QDBusArgument &argument, const Ekos::ExtensionState &source)
+{
+ argument.beginStructure();
+ argument << static_cast<int>(source);
+ argument.endStructure();
+ return argument;
+}
+
+const QDBusArgument &operator>>(const QDBusArgument &argument, Ekos::ExtensionState &dest)
+{
+ int a;
+ argument.beginStructure();
+ argument >> a;
+ argument.endStructure();
+ dest = static_cast<Ekos::ExtensionState>(a);
+ return argument;
+}
diff --git a/kstars/ekos/ekos.h b/kstars/ekos/ekos.h
index a498c82a5d..94ba40da4f 100644
--- a/kstars/ekos/ekos.h
+++ b/kstars/ekos/ekos.h
@@ -207,6 +207,20 @@ typedef enum
Error
} CommunicationStatus;
+typedef enum
+{
+ EXTENSION_START_REQUESTED,
+ EXTENSION_STARTED,
+ EXTENSION_STOP_REQUESTED,
+ EXTENSION_STOPPED,
+} ExtensionState;
+
+const QString getExtensionStatusString(ExtensionState state, bool translated = true);
+
+static const QList<const char *> extensionStates = { I18N_NOOP("Starting"), I18N_NOOP("Started"), I18N_NOOP("Stopping"),
+ I18N_NOOP("Stopped")
+};
+
std::vector<double> gsl_polynomial_fit(const double *const data_x, const double *const data_y, const int n,
const int order, double &chisq);
@@ -248,3 +262,8 @@ const QDBusArgument &operator>>(const QDBusArgument &argument, Ekos::AlignState
Q_DECLARE_METATYPE(Ekos::SchedulerState)
QDBusArgument &operator<<(QDBusArgument &argument, const Ekos::SchedulerState &source);
const QDBusArgument &operator>>(const QDBusArgument &argument, Ekos::SchedulerState &dest);
+
+// Extensions
+Q_DECLARE_METATYPE(Ekos::ExtensionState)
+QDBusArgument &operator<<(QDBusArgument &argument, const Ekos::ExtensionState &source);
+const QDBusArgument &operator>>(const QDBusArgument &argument, Ekos::ExtensionState &dest);
diff --git a/kstars/ekos/extensions.cpp b/kstars/ekos/extensions.cpp
new file mode 100644
index 0000000000..551c70a42e
--- /dev/null
+++ b/kstars/ekos/extensions.cpp
@@ -0,0 +1,225 @@
+#include "extensions.h"
+#include "auxiliary/kspaths.h"
+#include "version.h"
+
+#include <QDir>
+#include <QIcon>
+#include <QDebug>
+#include <QProcess>
+
+extensions::extensions(QObject *parent) : QObject{ parent } {
+ found = new QMap<QString, extDetails>;
+ extensionProcess = new QProcess(this);
+
+ connect(extensionProcess, &QProcess::started, this, [=]()
+ {
+ emit extensionStateChanged(Ekos::EXTENSION_STARTED);
+ });
+ connect(extensionProcess, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, [=]()
+ {
+ emit extensionStateChanged(Ekos::EXTENSION_STOPPED);
+ });
+ connect(extensionProcess, &QProcess::readyRead, this, [this] {
+ emit extensionOutput(extensionProcess->readAll());
+ });
+}
+
+bool extensions::discover()
+{
+ bool sucess = false;
+
+ QDir dir = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/extensions");
+ // Makes directory if not existing, doesn't change anything if already exists
+ if (dir.mkpath(".")) {
+ m_Directory = dir.absolutePath();
+ QStringList filesExe = dir.entryList(QStringList(), QDir::Files | QDir::Executable);
+ QStringList filesConf = dir.entryList(QStringList() << "*.conf", QDir::Files | QDir::Readable);
+ QStringList filesIcons = dir.entryList(QStringList() << "*.jpg" << "*.bmp" << "*.gif" << "*.png" << "*.svg",
+ QDir::Files | QDir::Readable);
+ if (! filesExe.isEmpty()) {
+ qDebug() << "Found extension(s): " << filesExe;
+
+ // Remove any executable without a .conf fileROCm
+ foreach (QString exe, filesExe) {
+ if (!filesConf.contains(QString(exe).append(".conf"))) {
+ qDebug() << QString(".conf file not found for extension %1").arg(exe);
+ filesExe.removeOne(exe);
+ }
+ }
+
+ if (! filesExe.isEmpty()) {
+
+ // Remove any .conf files that don't share filename with an executable
+ foreach (QString conf, filesConf) {
+ if (!filesExe.contains(QString(conf).remove(".conf"))) {
+ qDebug() << QString("Extraneous extension %1 file found without executable").arg(conf);
+ filesConf.removeOne(conf);
+ }
+ // Remove any .conf file that is not valid
+ if (!confValid(conf)) {
+ qDebug() << QString(".conf file %1 is not valid").arg(conf);
+ filesConf.removeOne(conf);
+ }
+ }
+
+ //Check if any executable doesn't have a valid .conf
+ foreach (QString exe, filesExe) {
+ if (!filesConf.contains(QString(exe).append(".conf"))) {
+ filesExe.removeOne(exe);
+ }
+ }
+
+ // Check if we have any executables with valid .conf files and build map
+ if (! filesExe.isEmpty() && (filesExe.count() == filesConf.count())) {
+ foreach (QString exe, filesExe) {
+ extDetails m_ext;
+
+ QString iconName = "";
+ foreach (QString name, filesIcons) {
+ if (name.contains(exe)) {
+ iconName = name;
+ break;
+ }
+ }
+ QIcon icon;
+ if (iconName != "") {
+ QString temp;
+ temp.append(m_Directory).append("/").append(iconName);
+ icon.addFile(temp);
+ } else {
+ icon = QIcon::fromTheme("plugins");
+ }
+
+ QString confFileName;
+ QString tooltip = "";
+ bool runDetached = false;
+ confFileName.append(m_Directory).append("/").append(exe).append(".conf");
+ QFile confFile(confFileName);
+ if (confFile.exists()) {
+ if (confFile.open(QIODevice::ReadOnly) && confFile.isReadable()) {
+ QTextStream confTS = QTextStream(&confFile);
+ while (!confTS.atEnd()) {
+ QString confLine = confTS.readLine();
+ if (confLine.contains("tooltip=")) {
+ tooltip = confLine.right(confLine.length() - (confLine.indexOf("=")) - 1);
+ } else if (confLine.contains("runDetached=true")) {
+ runDetached = true;
+ }
+ }
+ } else qDebug() << QString("Can't access .conf file %1").arg(exe).append(".conf");
+ } else qDebug() << QString(".conf file %1 disappeared").arg(exe).append(".conf");
+
+ m_ext.tooltip = tooltip;
+ m_ext.icon = icon;
+ m_ext.detached = runDetached;
+ found->insert(exe, m_ext);
+
+ sucess = true;
+ }
+ } else qDebug() << "No extensions found with valid .conf files";
+ } else qDebug() << "No extensions found with .conf files";
+ } else qDebug() << "No extensions found";
+ } else qDebug() << "Could not access extensions directory";
+
+ return sucess;
+}
+
+bool extensions::confValid(const QString &filePath)
+{
+ // Check that the passed extension .conf file contains a line starting
+ // minimum_kstars_version=xx.yy.zz
+ // and that the xx.yy.zz is no higher that the KSTARS_VERSION
+
+ bool valid = false;
+
+ QString confFileName;
+ confFileName.append(m_Directory).append("/").append(filePath);
+ QFile confFile(confFileName);
+ if (confFile.exists()) {
+ if (confFile.open(QIODevice::ReadOnly) && confFile.isReadable()) {
+ QTextStream confTS = QTextStream(&confFile);
+ while (!confTS.atEnd()) {
+ QString confLine = confTS.readLine();
+ if (confLine.contains("minimum_kstars_version=")) {
+ QString minVersion = confLine.right(confLine.length() - (confLine.indexOf("=")) - 1);
+ QStringList minVersionElements = minVersion.split(".");
+ if (minVersionElements.count() == 3) {
+ QList <int> minVersionElementInts;
+ foreach (QString element, minVersionElements) {
+ if (element.toInt() || element == "0") {
+ minVersionElementInts.append(element.toInt());
+ } else break;
+ }
+ if (minVersionElementInts.count() == minVersionElements.count()) {
+ QStringList KStarsVersionElements = QString(KSTARS_VERSION).split(".");
+ QList <int> KStarsVersionElementInts;
+ foreach (QString element, KStarsVersionElements) {
+ if (element.toInt() || element == "0") {
+ KStarsVersionElementInts.append(element.toInt());
+ }
+ }
+ if (minVersionElementInts.at(0) <= KStarsVersionElementInts.at(0)) {
+ if (KStarsVersionElementInts.at(0) > minVersionElementInts.at(0)) {
+ valid = true;
+ } else if (minVersionElementInts.at(1) <= KStarsVersionElementInts.at(1)) {
+ if (KStarsVersionElementInts.at(1) > minVersionElementInts.at(1)) {
+ valid = true;
+ } else if (minVersionElementInts.at(2) <= KStarsVersionElementInts.at(2)) {
+ valid = true;
+ }
+ }
+ }
+
+ if (!valid) qDebug() << QString(".conf file %1 requires a minimum KStars version of %2").arg(filePath, minVersion);
+
+ } else qDebug() << QString(".conf file %1 does not contain a valid minimum_kstars_version string").append(filePath);
+ } else qDebug() << QString(".conf file %1 does not contain a valid minimum_kstars_version string").append(filePath);
+ } else qDebug() << QString(".conf file %1 does not contain a valid minimum_kstars_version string").append(filePath);
+ }
+ } else qDebug() << QString("Can't access .conf file %1").arg(filePath);
+ } else qDebug() << QString(".conf file %1 disappeared").arg(filePath);
+
+ return valid;
+}
+
+QIcon extensions::getIcon(const QString &name)
+{
+ if (found->contains(name)) {
+ extDetails m_ext = found->value("name");
+ return m_ext.icon;
+ } else return QIcon();
+}
+
+QString extensions::getTooltip(const QString &name)
+{
+ if (found->contains(name)) {
+ extDetails m_ext = found->value(name);
+ return m_ext.tooltip;
+ } else return "";
+}
+
+void extensions::run(const QString &extension)
+{
+ QString processPath;
+ processPath.append(QString("%1%2%3").arg(m_Directory, "/", extension));
+ QStringList arguments;
+ extensionProcess->setWorkingDirectory(m_Directory);
+ extDetails m_ext = found->value(extension);
+ if (m_ext.detached) {
+ extensionProcess->startDetached(processPath, arguments);
+ } else {
+ extensionProcess->setProcessChannelMode(QProcess::MergedChannels);
+ extensionProcess->start(processPath, arguments);
+ }
+ emit extensionStateChanged(Ekos::EXTENSION_START_REQUESTED);
+}
+
+void extensions::stop()
+{
+ emit extensionStateChanged(Ekos::EXTENSION_STOP_REQUESTED);
+}
+
+void extensions::kill()
+{
+ extensionProcess->kill();
+}
diff --git a/kstars/ekos/extensions.h b/kstars/ekos/extensions.h
new file mode 100644
index 0000000000..2abdb8ccb0
--- /dev/null
+++ b/kstars/ekos/extensions.h
@@ -0,0 +1,44 @@
+#ifndef EXTENSIONS_H
+#define EXTENSIONS_H
+
+#include "ekos.h"
+
+#include <QMap>
+#include <QObject>
+#include <QProcess>
+#include <QIcon>
+
+class extensions : public QObject
+{
+ Q_OBJECT
+ public:
+ explicit extensions(QObject *parent = nullptr);
+ bool discover();
+ void run(const QString &extension = "");
+ void stop();
+ void kill();
+
+ struct extDetails
+ {
+ QString tooltip;
+ QIcon icon;
+ bool detached;
+ };
+ QMap<QString, extDetails>* found;
+
+ public slots:
+ QIcon getIcon(const QString &name);
+ QString getTooltip(const QString &name);
+
+ signals:
+ void extensionStateChanged(Ekos::ExtensionState);
+ void extensionOutput(const QString);
+
+ private:
+ QString m_Directory = "";
+ bool confValid(const QString &filePath);
+ QProcess* extensionProcess;
+
+};
+
+#endif // EXTENSIONS_H
diff --git a/kstars/ekos/manager.cpp b/kstars/ekos/manager.cpp
index 3aea7654ee..079f94f647 100644
--- a/kstars/ekos/manager.cpp
+++ b/kstars/ekos/manager.cpp
@@ -221,6 +221,8 @@ Manager::Manager(QWidget * parent) : QDialog(parent)
indiControlPanelB->setEnabled(status == Ekos::Success);
connectB->setEnabled(false);
disconnectB->setEnabled(false);
+ extensionB->setEnabled(false);
+ extensionCombo->setEnabled(false);
profileGroup->setEnabled(status == Ekos::Idle || status == Ekos::Error);
m_isStarted = (status == Ekos::Success || status == Ekos::Pending);
if (status == Ekos::Success)
@@ -372,6 +374,74 @@ Manager::Manager(QWidget * parent) : QDialog(parent)
numPermanentTabs = index + 1;
+ // Extensions
+ extensionTimer.setSingleShot(true);
+ groupBox_4->setHidden(true);
+ extensionB->setIcon(QIcon::fromTheme("media-playback-start"));
+ connect(extensionB, &QPushButton::clicked, this, [this] {
+ if (extensionB->icon().name() == "media-playback-start") {
+ extensionTimer.setInterval(1000);
+ connect(&extensionTimer, &QTimer::timeout, this, [this] {
+ appendLogText(i18n("Extension '%1' failed to start, aborting", extensionCombo->currentText()));
+ m_extensions.kill();
+ });
+ extensionTimer.start();
+ extensionAbort = false;
+ m_extensions.run(extensionCombo->currentText());
+ } else if (extensionB->icon().name() == "media-playback-stop") {
+ if (!extensionAbort) {
+ extensionTimer.setInterval(10000);
+ connect(&extensionTimer, &QTimer::timeout, this, [this] {
+ appendLogText(i18n("Extension '%1' failed to stop, abort enabled", extensionCombo->currentText()));
+ extensionB->setEnabled(true);
+ extensionAbort = true;
+ });
+ extensionTimer.start();
+ m_extensions.stop();
+ } else {
+ appendLogText(i18n("Extension '%1' aborting", extensionCombo->currentText()));
+ m_extensions.kill();
+ }
+ }
+ });
+ connect(&m_extensions, &extensions::extensionStateChanged, this, [this](Ekos::ExtensionState state) {
+ switch (state) {
+ case EXTENSION_START_REQUESTED:
+ appendLogText(i18n("Extension '%1' start requested", extensionCombo->currentText()));
+ extensionB->setEnabled(false);
+ extensionCombo->setEnabled(false);
+ break;
+ case EXTENSION_STARTED:
+ appendLogText(i18n("Extension '%1' started", extensionCombo->currentText()));
+ extensionB->setIcon(QIcon::fromTheme("media-playback-stop"));
+ extensionB->setEnabled(true);
+ extensionCombo->setEnabled(false);
+ extensionTimer.stop();
+ disconnect(&extensionTimer, &QTimer::timeout, this, nullptr);
+ break;
+ case EXTENSION_STOP_REQUESTED:
+ appendLogText(i18n("Extension '%1' stop requested", extensionCombo->currentText()));
+ extensionB->setEnabled(false);
+ extensionCombo->setEnabled(false);
+ break;
+ case EXTENSION_STOPPED:
+ appendLogText(i18n("Extension '%1 stopped", extensionCombo->currentText()));
+ extensionB->setIcon(QIcon::fromTheme("media-playback-start"));
+ extensionB->setEnabled(true);
+ extensionCombo->setEnabled(true);
+ extensionTimer.stop();
+ disconnect(&extensionTimer, &QTimer::timeout, this, nullptr);
+ }
+ m_extensionStatus = state;
+ emit extensionStatusChanged();
+ });
+ connect(extensionCombo, &QComboBox::currentTextChanged, this, [this] (QString text) {
+ extensionCombo->setToolTip(m_extensions.getTooltip(text));
+ });
+ connect(&m_extensions, &extensions::extensionOutput, this, [this] (QString message) {
+ appendLogText(QString(i18n("Extension '%1': %2", extensionCombo->currentText(), message.trimmed())));
+ });
+
// Temporary fix. Not sure how to resize Ekos Dialog to fit contents of the various tabs in the QScrollArea which are added
// dynamically. I used setMinimumSize() but it doesn't appear to make any difference.
// Also set Layout policy to SetMinAndMaxSize as well. Any idea how to fix this?
@@ -594,6 +664,8 @@ void Manager::reset()
connectB->setEnabled(false);
disconnectB->setEnabled(false);
+ extensionB->setEnabled(false);
+ extensionCombo->setEnabled(false);
//controlPanelB->setEnabled(false);
processINDIB->setEnabled(true);
@@ -633,6 +705,11 @@ void Manager::stop()
profileGroup->setEnabled(true);
setWindowTitle(i18nc("@title:window", "Ekos"));
+
+ // Clear extensions list ready for rediscovery if start is called again
+ extensionCombo->clear();
+ m_extensions.found->clear();
+ groupBox_4->setHidden(true);
}
void Manager::start()
@@ -1063,6 +1140,17 @@ void Manager::start()
runConnection();
}
}
+
+ // Search for extensions
+ if (m_extensions.discover()) {
+ foreach (QString extension, m_extensions.found->keys()) {
+ extensions::extDetails m_ext = m_extensions.found->value(extension);
+ extensionCombo->addItem(m_ext.icon, extension);
+ }
+ }
+ if (extensionCombo->count() > 0) {
+ groupBox_4->setHidden(false);
+ }
}
void Manager::setClientStarted(const QString &host, int port)
@@ -1314,6 +1402,9 @@ void Manager::connectDevices()
connectB->setEnabled(false);
disconnectB->setEnabled(true);
+ extensionCombo->setEnabled(true);
+ if (extensionCombo->currentText() != "")
+ extensionB->setEnabled(true);
appendLogText(i18n("Connecting INDI devices..."));
}
@@ -1434,6 +1525,8 @@ void Manager::processNewDevice(const QSharedPointer<ISD::GenericDevice> &device)
connectB->setEnabled(true);
disconnectB->setEnabled(false);
+ extensionCombo->setEnabled(false);
+ extensionB->setEnabled(false);
if (m_LocalMode == false && m_DriverDevicesCount == 0)
{
@@ -1450,6 +1543,9 @@ void Manager::deviceConnected()
connectB->setEnabled(false);
disconnectB->setEnabled(true);
processINDIB->setEnabled(false);
+ extensionCombo->setEnabled(true);
+ if (extensionCombo->currentText() != "")
+ extensionB->setEnabled(true);
auto device = qobject_cast<ISD::GenericDevice *>(sender());
@@ -1521,6 +1617,8 @@ void Manager::deviceDisconnected()
connectB->setEnabled(true);
disconnectB->setEnabled(false);
processINDIB->setEnabled(true);
+ extensionCombo->setEnabled(false);
+ extensionB->setEnabled(false);
}
void Manager::addMount(ISD::Mount *device)
@@ -2009,7 +2107,6 @@ void Manager::initCapture()
ekosLiveClient.get()->message()->updateCaptureStatus(cStatus);
});
connect(captureModule(), &Ekos::Capture::newStatus, this, &Ekos::Manager::updateCaptureStatus);
- connect(captureModule(), &Ekos::Capture::newImage, this, &Ekos::Manager::updateCaptureProgress);
connect(captureModule(), &Ekos::Capture::driverTimedout, this, &Ekos::Manager::restartDriver);
connect(captureModule(), &Ekos::Capture::newExposureProgress, this, &Ekos::Manager::updateExposureProgress);
capturePreview->setEnabled(true);
diff --git a/kstars/ekos/manager.h b/kstars/ekos/manager.h
index aed920147b..a41c5603a0 100644
--- a/kstars/ekos/manager.h
+++ b/kstars/ekos/manager.h
@@ -21,9 +21,11 @@
#include "ksnotification.h"
#include "auxiliary/opslogs.h"
#include "ekos/capture/rotatorsettings.h"
+#include "extensions.h"
#include <QDialog>
#include <QHash>
+#include <QTimer>
#include <memory>
@@ -83,6 +85,7 @@ class Manager : public QDialog, public Ui::Manager
Q_SCRIPTABLE Q_PROPERTY(Ekos::CommunicationStatus ekosStatus READ ekosStatus NOTIFY ekosStatusChanged)
Q_SCRIPTABLE Q_PROPERTY(Ekos::CommunicationStatus settleStatus READ settleStatus NOTIFY settleStatusChanged)
Q_SCRIPTABLE Q_PROPERTY(bool ekosLiveStatus READ ekosLiveStatus NOTIFY ekosLiveStatusChanged)
+ Q_SCRIPTABLE Q_PROPERTY(Ekos::ExtensionState extensionStatus READ extensionStatus NOTIFY extensionStatusChanged)
Q_SCRIPTABLE Q_PROPERTY(QStringList logText READ logText NOTIFY newLog)
enum class EkosModule
@@ -154,6 +157,8 @@ class Manager : public QDialog, public Ui::Manager
QString getCurrentJobName();
void announceEvent(const QString &message, KSNotification::EventSource source, KSNotification::EventType event);
+ extensions m_extensions;
+
/**
* @brief activateModule Switch tab to specific module name (i.e. CCD) and raise Ekos screen to focus.
* @param name module name CCD, Guide, Focus, Mount, Scheduler, or Observatory.
@@ -315,6 +320,15 @@ class Manager : public QDialog, public Ui::Manager
Q_SCRIPTABLE bool ekosLiveStatus();
+ /**
+ * DBUS interface function.
+ * @return Settle status (0 EXTENSION_START_REQUESTED, 1 EXTENSION_STARTED, 2 EXTENSION_STOP_REQUESTED, 3 EXTENSION_STOPPED)
+ */
+ Q_SCRIPTABLE Ekos::ExtensionState extensionStatus()
+ {
+ return m_extensionStatus;
+ };
+
/**
* DBUS interface function.
* @param enabled Connect to EkosLive if true, otherwise disconnect.
@@ -346,6 +360,7 @@ class Manager : public QDialog, public Ui::Manager
void indiStatusChanged(Ekos::CommunicationStatus status);
void settleStatusChanged(Ekos::CommunicationStatus status);
void ekosLiveStatusChanged(bool status);
+ void extensionStatusChanged(bool = true);
void newLog(const QString &text);
void newModule(const QString &name);
@@ -540,6 +555,7 @@ class Manager : public QDialog, public Ui::Manager
// Settle is used to know once all properties from all devices have been defined
// There is no way to know this for sure so we use a debounace mechanism.
CommunicationStatus m_settleStatus { Ekos::Idle };
+ ExtensionState m_extensionStatus { Ekos::EXTENSION_STOPPED };
std::unique_ptr<QStandardItemModel> profileModel;
QList<QSharedPointer<ProfileInfo>> profiles;
@@ -556,6 +572,9 @@ class Manager : public QDialog, public Ui::Manager
// Preview Frame
QSharedPointer<SummaryFITSView> m_SummaryView;
+ QTimer extensionTimer;
+ bool extensionAbort = false;
+
QSharedPointer<ProfileInfo> m_CurrentProfile;
bool profileWizardLaunched { false };
QString m_PrimaryCamera, m_GuideCamera;
diff --git a/kstars/ekos/manager.ui b/kstars/ekos/manager.ui
index 1ddbf22279..33bc5ec205 100644
--- a/kstars/ekos/manager.ui
+++ b/kstars/ekos/manager.ui
@@ -395,8 +395,7 @@
<string/>
</property>
<property name="icon">
- <iconset theme="folder-cloud">
- <normaloff>.</normaloff>.</iconset>
+ <iconset theme="folder-cloud"/>
</property>
<property name="iconSize">
<size>
@@ -424,8 +423,7 @@
<string/>
</property>
<property name="icon">
- <iconset theme="kstars_indi">
- <normaloff>.</normaloff>.</iconset>
+ <iconset theme="kstars_indi"/>
</property>
<property name="iconSize">
<size>
@@ -453,8 +451,7 @@
<string/>
</property>
<property name="icon">
- <iconset theme="drive-removable-media">
- <normaloff>.</normaloff>.</iconset>
+ <iconset theme="drive-removable-media"/>
</property>
<property name="iconSize">
<size>
@@ -482,8 +479,7 @@
<string/>
</property>
<property name="icon">
- <iconset theme="configure">
- <normaloff>.</normaloff>.</iconset>
+ <iconset theme="configure"/>
</property>
<property name="iconSize">
<size>
@@ -589,6 +585,123 @@
</layout>
</widget>
</item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_4">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>1</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>170</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>170</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="title">
+ <string>Extensions</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <property name="spacing">
+ <number>1</number>
+ </property>
+ <property name="leftMargin">
+ <number>3</number>
+ </property>
+ <property name="topMargin">
+ <number>3</number>
+ </property>
+ <property name="rightMargin">
+ <number>3</number>
+ </property>
+ <property name="bottomMargin">
+ <number>3</number>
+ </property>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,0">
+ <property name="spacing">
+ <number>1</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QComboBox" name="extensionCombo">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>120</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>200</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="extensionB">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="toolTip">
+ <string>Start selected extension</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>30</width>
+ <height>30</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
</layout>
</item>
<item>
@@ -730,7 +843,6 @@
<widget class="QLabel" name="altLabel">
<property name="font">
<font>
- <weight>75</weight>
<bold>true</bold>
</font>
</property>
@@ -746,7 +858,6 @@
<widget class="QLabel" name="azLabel">
<property name="font">
<font>
- <weight>75</weight>
<bold>true</bold>
</font>
</property>
@@ -762,7 +873,6 @@
<widget class="QLabel" name="decLabel">
<property name="font">
<font>
- <weight>75</weight>
<bold>true</bold>
</font>
</property>
@@ -778,7 +888,6 @@
<widget class="QLabel" name="raLabel">
<property name="font">
<font>
- <weight>75</weight>
<bold>true</bold>
</font>
</property>
diff --git a/kstars/org.kde.kstars.Ekos.xml b/kstars/org.kde.kstars.Ekos.xml
index e5ccd74360..f72b473e31 100644
--- a/kstars/org.kde.kstars.Ekos.xml
+++ b/kstars/org.kde.kstars.Ekos.xml
@@ -9,6 +9,7 @@
</property>
<property name="settleStatus" type="u" access="read"/>
<property name="ekosLiveStatus" type="b" access="read"/>
+ <property name="extensionStatus" type="i" access="read"/>
<property name="logText" type="as" access="read"/>
<method name="connectDevices">
<annotation name="org.freedesktop.DBus.Method.NoReply" value="true"/>
@@ -62,6 +63,8 @@
<signal name="ekosLiveStatusChanged">
<arg name="status" type="b" direction="out"/>
</signal>
+ <signal name="extensionStatusChanged">
+ <arg name="status" type="b" direction="out"/>
<signal name="newLog">
<arg name="text" type="s" direction="out"/>
</signal>
More information about the kde-doc-english
mailing list