From 857a33e84435c526840f678385196ef8565c8ef4 Mon Sep 17 00:00:00 2001 From: zomfg Date: Wed, 28 Oct 2020 22:17:29 +0100 Subject: [PATCH 1/2] plugin: config template + better parsing + logs --- Software/res/LightpackResources.qrc | 1 + Software/res/plugin-template.ini | 80 +++++++++++++++++ Software/src/Plugin.cpp | 133 +++++++++++++++++++--------- Software/src/Plugin.hpp | 18 ++-- Software/src/PluginsManager.cpp | 2 +- 5 files changed, 186 insertions(+), 48 deletions(-) create mode 100755 Software/res/plugin-template.ini 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())); } } From b66762cfaf951963fa242bf00aea4605b2a11723 Mon Sep 17 00:00:00 2001 From: zomfg Date: Wed, 28 Oct 2020 22:18:35 +0100 Subject: [PATCH 2/2] readme: plugin template link --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) 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/)