diff --git a/README.md b/README.md
index 1d97d9709..998a9a872 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ Lightpack project with Prismatik flavour
[Short Description](#lightpack-project-with-prismatik-flavour)
[Main Features](#main-features)
[Supported Devices and Protocols](#supported-devices-and-protocols)
+ [Making Plugins](#making-plugins)
[Useful URLs](#useful-urls)
[Build Prismatik with Windows](#prismatik-build-instructions-for-windows)
[Build with Linux](#build-instructions-for-linux)
@@ -68,6 +69,11 @@ handle other devices with Prismatik such as Adalight, Ardulight, or even Alienwa
* ESP8266/ESP32 ([WLED](https://github.com/Aircoookie/WLED) firmware highly recommended)
+##### Making Plugins:
+* [template](Software/res/plugin-template.ini)
+* [code samples](Software/apiexamples)
+
+
##### Useful URLs:
* [Project mothership](https://github.com/psieg/Lightpack/)
* [Original project mothership](https://github.com/woodenshark/Lightpack/)
diff --git a/Software/res/LightpackResources.qrc b/Software/res/LightpackResources.qrc
index 5978ad618..7ddbd1e89 100644
--- a/Software/res/LightpackResources.qrc
+++ b/Software/res/LightpackResources.qrc
@@ -39,5 +39,6 @@
icons/Prismatik-pixmap.png
text/cast.html
icons/persist.png
+ plugin-template.ini
diff --git a/Software/res/plugin-template.ini b/Software/res/plugin-template.ini
new file mode 100755
index 000000000..d364cbad4
--- /dev/null
+++ b/Software/res/plugin-template.ini
@@ -0,0 +1,80 @@
+# This a template for Prismatik plugin configuration
+# How to make a plugin:
+# - create a folder in Prismatik/Plugins (in your user's home dir), for ex "myplugin"
+# - copy this INI file into that folder with the same name as the folder, for ex "myplugin.ini"
+# Prismatik will do this automatically on launch (or when you reload the plugin list from the UI)
+# - edit the INI file to suit your needs
+# - to make your code interact with Prismatik enable the web server and checkout Software/apiexamples
+# - plugin output will be merged with Prismatik logs, make sure to enable those if needed
+
+# [Main] is a required section
+[Main]
+
+# this will be displayed in the Plugins tab, defaults to plugin folder name
+;Name=My custom plugin
+Name=
+
+# command to make your plugin run, REQUIRED
+# - the process will start in plugin's folder so your scripts will be local
+# - system env vars are passed to the process
+# - if you need external/system commands (interpreters / shell...)
+# either use full path
+# Execute=/bin/sh myplugin.sh
+# or make sure it's in the PATH env var
+# Execute=node myplugin.js
+# shebangs should work too
+# - this can be pretty much any executable and not related to Prismatik
+# it could trigger something on the system when Prismatik launches
+# - AVOID SPACES IN PATHS AND NAMES
+# this won't work
+# Execute=C:/Program Files/python.exe "my custom plugin.py"
+# if spaces in file names are required, wrap the command in a script
+# - on WINDOWS use forward slash "/" as file separator
+# Execute=D:/Some/Path/curl.exe -s http://example.com/api/foo
+Execute=
+
+# platform specific command if needed, overrides Execute= on a given platform
+# REQUIRED if Execute= is not used
+# use forward slash "/" as file separator on WINDOWS
+;ExecuteOnWindows=python.exe myscript.py
+;ExecuteOnOSX=/some/path/sh myscript.sh
+;ExecuteOnNix=myscript.sh
+
+
+# these will be displayed in Plugins tab
+
+# icon file (bmp, gif, jpg, png) within your plugin folder, defaults to a built-in icon
+;Icon=myicon.png
+Icon=
+Author=
+# version can be a number/string/hash/date...
+Version=0.1
+
+# description can be plain text
+;Description=My plugin that does this and that
+# or basic HTML, see https://doc.qt.io/qt-5/richtext-html-subset.html
+Description="
+
+
+
+
+
+ Make sure to update the INI file in the plugin folder
+
+
+"
+
+
+
+
+# here you can define any custom sections and settings to use in your plugin
+;[MyCustomSettings]
+;foo=bar
+
+;[SomeOtherSection]
+;port=6969
diff --git a/Software/src/Plugin.cpp b/Software/src/Plugin.cpp
index 21327e54d..72d461f65 100644
--- a/Software/src/Plugin.cpp
+++ b/Software/src/Plugin.cpp
@@ -1,8 +1,8 @@
#include "Plugin.hpp"
#include
-#include
-#include
+#include
#include
+#include
#include "Settings.hpp"
#include "../common/defs.h"
@@ -23,24 +23,46 @@ Plugin::Plugin(QString name, QString path, QObject *parent) :
_pathPlugin = path;
QDir pluginPath(_pathPlugin);
- QString fileName = path+"/"+name+".ini";
- QSettings settings( fileName, QSettings::IniFormat );
- settings.beginGroup("Main");
- this->_name = settings.value( "Name", "Error").toString();
- if (settings.contains(kOsSpecificExecuteKey)) {
- this->_exec = settings.value( kOsSpecificExecuteKey, "").toString();
- } else {
- this->_exec = settings.value( "Execute", "").toString();
+ const QString fileName(path+"/"+name+".ini");
+ if (!QFile::exists(fileName)) {
+ qWarning() << Q_FUNC_INFO << name << fileName << "does not exist, generating defaults, make sure to edit the file!";
+ if (!QFile::copy(":/plugin-template.ini", fileName))
+ qWarning() << Q_FUNC_INFO << name << "failed to generate" << fileName;
+ else if (!QFile::setPermissions(fileName, QFile::permissions(fileName) | QFileDevice::WriteOwner))
+ qWarning() << Q_FUNC_INFO << name << "could not set write permissions to" << fileName;
}
- this->_guid = settings.value( "Guid", "").toString();
- this->_author = settings.value( "Author", "").toString();
- this->_description = settings.value( "Description", "").toString();
- this->_version = settings.value( "Version", "").toString();
- this->_icon = pluginPath.absoluteFilePath(settings.value( "Icon", "").toString());
+ QSettings settings(fileName, QSettings::IniFormat);
+ settings.beginGroup("Main");
+ _name = settings.value("Name", "").toString();
+ if (_name.isEmpty())
+ _name = name;
+
+ if (settings.contains(kOsSpecificExecuteKey))
+ _arguments = settings.value(kOsSpecificExecuteKey, "").toString().split(' ');
+ else
+ _arguments = settings.value("Execute", "").toString().split(' ');
+
+ if (!_arguments.isEmpty())
+ _exec = _arguments.takeFirst();
+
+ if (_exec.isEmpty())
+ qWarning() << Q_FUNC_INFO << name << "no executable, check" << fileName;
+
+ _guid = settings.value("Guid", "").toString();
+ _author = settings.value("Author", "").toString();
+ _description = settings.value("Description", "").toString();
+ _version = settings.value("Version", "").toString();
+
+ const QString iconName = settings.value("Icon", "").toString();
+ const QString iconPath = pluginPath.absoluteFilePath(iconName);
+ if (!iconName.isEmpty() && QFile::exists(iconPath) && !QImageReader::imageFormat(iconPath).isEmpty())
+ _icon = QIcon(iconPath);
+ if (_icon.isNull())
+ _icon = QIcon(":/icons/plugins.png");
+
settings.endGroup();
process = new QProcess(this);
-
}
Plugin::~Plugin()
@@ -48,17 +70,15 @@ Plugin::~Plugin()
Stop();
}
-QString Plugin::Name() const
-{
+QString Plugin::Name() const {
return _name;
}
-QString Plugin::Guid() const
-{
+QString Plugin::Guid() const {
return _guid;
}
-QString Plugin::Author() const {
+QString Plugin::Author() const {
return _author;
}
@@ -71,54 +91,56 @@ QString Plugin::Version() const {
}
-QIcon Plugin::Icon() const {
- QFileInfo f(_icon);
- if (f.exists())
- return QIcon(_icon);
- return QIcon(":/icons/plugins.png");
+QIcon Plugin::Icon() const {
+ return _icon;
}
int Plugin::getPriority() const {
- QString key = this->_name+"/Priority";
+ const QString key = _name+"/Priority";
return Settings::valueMain(key).toInt();
}
void Plugin::setPriority(int priority) {
- QString key = this->_name+"/Priority";
+ const QString key = _name+"/Priority";
Settings::setValueMain(key,priority);
}
bool Plugin::isEnabled() const {
- QString key = this->_name+"/Enable";
+ const QString key = _name+"/Enable";
return Settings::valueMain(key).toBool();
}
-void Plugin::setEnabled(bool enable){
- DEBUG_LOW_LEVEL << Q_FUNC_INFO << enable;
- QString key = this->_name+"/Enable";
+void Plugin::setEnabled(bool enable) {
+ DEBUG_LOW_LEVEL << Q_FUNC_INFO << _name << enable;
+ const QString key = _name+"/Enable";
Settings::setValueMain(key,enable);
- if (!enable) this->Stop();
- if (enable) this->Start();
+ if (!enable) Stop();
+ if (enable) Start();
}
void Plugin::Start()
{
- DEBUG_LOW_LEVEL << Q_FUNC_INFO << _exec;
-
- //QStringList arguments;
- //arguments << "-style" << "fusion";
+ DEBUG_LOW_LEVEL << Q_FUNC_INFO << _name << "starting" << _exec << _arguments;
QDir dir(_pathPlugin);
QDir::setCurrent(dir.absolutePath());
process->disconnect();
- connect(process, SIGNAL(stateChanged(QProcess::ProcessState)), this, SIGNAL(stateChanged(QProcess::ProcessState)));
+
+ connect(process, SIGNAL(stateChanged(QProcess::ProcessState)), this, SLOT(stateChanged(QProcess::ProcessState)));
+ connect(process, SIGNAL(errorOccurred(QProcess::ProcessError)), this, SLOT(errorOccurred(QProcess::ProcessError)));
+
+ connect(process, SIGNAL(started()), this, SLOT(started()));
+ connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(finished(int, QProcess::ExitStatus)));
+
+ connect(process, SIGNAL(readyReadStandardError()), this, SLOT(readyReadStandardError()));
+ connect(process, SIGNAL(readyReadStandardOutput()), this, SLOT(readyReadStandardOutput()));
process->setEnvironment(QProcess::systemEnvironment());
-// process->setProcessChannelMode(QProcess::ForwardedChannels);
process->setProgram(_exec);
+ process->setArguments(_arguments);
process->start();
}
@@ -132,3 +154,34 @@ QProcess::ProcessState Plugin::state() const
return process->state();
}
+// QProcess slots
+void Plugin::stateChanged(QProcess::ProcessState newState)
+{
+ DEBUG_LOW_LEVEL << Q_FUNC_INFO << _name << newState;
+ emit pluginStateChanged(newState);
+}
+
+void Plugin::errorOccurred(QProcess::ProcessError error)
+{
+ qWarning() << Q_FUNC_INFO << _name << error << _exec << _arguments;
+}
+
+void Plugin::started()
+{
+ DEBUG_LOW_LEVEL << Q_FUNC_INFO << _name;
+}
+
+void Plugin::finished(int exitCode, QProcess::ExitStatus exitStatus)
+{
+ DEBUG_LOW_LEVEL << Q_FUNC_INFO << _name << "exitCode=" << exitCode << exitStatus;
+}
+
+void Plugin::readyReadStandardError()
+{
+ qWarning() << Q_FUNC_INFO << _name << process->readAllStandardError();
+}
+
+void Plugin::readyReadStandardOutput()
+{
+ DEBUG_LOW_LEVEL << Q_FUNC_INFO << _name << process->readAllStandardOutput();
+}
diff --git a/Software/src/Plugin.hpp b/Software/src/Plugin.hpp
index 5f4363b3e..2112480c3 100644
--- a/Software/src/Plugin.hpp
+++ b/Software/src/Plugin.hpp
@@ -2,6 +2,7 @@
#include
#include
+#include
#include "debug.h"
class Plugin : public QObject
@@ -30,11 +31,16 @@ class Plugin : public QObject
signals:
+ void pluginStateChanged(QProcess::ProcessState newState);
- void stateChanged(QProcess::ProcessState);
-
public slots:
-
+ void stateChanged(QProcess::ProcessState newState);
+ void errorOccurred(QProcess::ProcessError error);
+ void started();
+ void finished(int exitCode, QProcess::ExitStatus exitStatus);
+ void readyReadStandardError();
+ void readyReadStandardOutput();
+
private:
QString _guid;
@@ -42,12 +48,10 @@ public slots:
QString _description;
QString _author;
QString _version;
- QString _icon;
+ QIcon _icon;
QString _exec;
- QString _arguments;
+ QStringList _arguments;
QString _pathPlugin;
QProcess *process;
};
-
-
diff --git a/Software/src/PluginsManager.cpp b/Software/src/PluginsManager.cpp
index 8faf0a3ef..a9816bc27 100644
--- a/Software/src/PluginsManager.cpp
+++ b/Software/src/PluginsManager.cpp
@@ -93,7 +93,7 @@ void PluginsManager::StartPlugins()
p->disconnect();
if (p->isEnabled())
p->Start();
- connect(p, SIGNAL(stateChanged(QProcess::ProcessState)), this, SLOT(onPluginStateChangedHandler()));
+ connect(p, SIGNAL(pluginStateChanged(QProcess::ProcessState)), this, SLOT(onPluginStateChangedHandler()));
}
}