diff --git a/CMakeLists.txt b/CMakeLists.txt index 31b47ec6..6248c3d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -276,6 +276,9 @@ if (ENABLE_TESTS) add_subdirectory(test) endif () +# Add resources directory +add_subdirectory(resources) + # Add the doxygen generation directory add_subdirectory(doc) diff --git a/assets/webconfig/js/ledsim.js b/assets/webconfig/js/ledsim.js index 0b3666e2..30ccdd47 100644 --- a/assets/webconfig/js/ledsim.js +++ b/assets/webconfig/js/ledsim.js @@ -235,16 +235,6 @@ $(document).ready(function() { dialog.open(); }); - function resetImage(){ - imageCanvasNodeCtx.fillStyle = "rgb(225,225,225)" - imageCanvasNodeCtx.fillRect(0, 0, canvas_width, canvas_height); - var image = new Image(); - image.onload = function() { - imageCanvasNodeCtx.drawImage(image, canvas_width*0.3, canvas_height*0.38); - }; - image.src ='data:image/png;base64,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LV199dXo37//58ePH/9pYWHhk0Z5bm7uXdu2bdvCGNuW6B92paCsC4vbiRLBWQDoeEodHocydzpvpaY3sGnq1AlXrbaSJwz/WQvKjjjH2Lp1a1dtmySRoiiUlpbGR44cyadNm2b3foiz/2932xN6gfH9tCuuuKLmyJEj9xQVFf1NkqQsvZP94Fe/+tXm0aNHvzhixAhWVlZ2RgaLyy+/nNra2vYR0QnGWBYAKTMzc8qdd9456FNMvgwAABKUSURBVMYbbzyFbjrI28na+neeCD1gFwBqamqqWLduXbUuRnT7vV577TUA4E8++eQL995771y/33+lzhX0Hz58+M+feOKJhbfddltDvH8tOfQiikdd4gWqhihz2oDaavUyDpslDgw5FvH9gc11chvTSLJrp9gqWvRwidBZ2UTUyfK7/oeAs0vas2cPBg4cuLmmpmaZWD5ixIgVr7/++tiysrIzGtxs3bp1xzjnW4zv4vP5Si699NKrJEnyjh49+ox+HM65jFi0hjRDSbV///4tn332WS1jrEcAnTRpEgDQI4880rply5almqYdFWTVK6+44orrAUi6g0sSAJXiKomcTjWDSTtr53trV2bpjysqkZA4wgMXlA+2czgTyNeOJpJLkmRHjZ14PYkgxX9aMsJT9u/f/6lgMPiS8M6ZX/va1/5nxowZmWdKgVNdXY1rr71WPXXq1HOITcIGABQWFv74o48+mrpnzx5at25djymnHjgbjLH7AHQ4sCiKUn355Zf/VVfi9Ihab9u2rWP/sssu233ixIkHxfPFxcUPrFmzZvz+/fu5PQVlJrbWXiZ1OsHaLoaQVRQFOARDF4DaRC6wXbvFanaITo3h4H261G8FJJ1lhgPK20Xm/N+YZH0mrBIbNmx4QFXVDsdSn8837aWXXroFgMwYM6hGt1O/fv0wffp05OXlfdzW1vasMBh4J0+e/MaHH3444corr+zxt9K5lZsA3CeWL1++/N5QKBRkjLXjDNtei4uLVwWDweeFd0q78sorX927d28O51yzBSgJwLQJBBRvcnVCQNmwx/HqgQ07SZxzZmKbrahtF/DX1dVZUnid9XW6ZGDcZMQEgvWqZ5achsHW/qdST/P3WrlyZXlNTc0jBnVhjHn69u3746NHj04CIIlUoztp+fLl2LhxIwAgEAj8RFGU7UKHTr344ov/dvjw4YvE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'; - } - // ------------------------------------------------------------------ $(hyperion).on("cmd-settings-update",function(event){ var obj = event.response.data @@ -254,4 +244,14 @@ $(document).ready(function() { leds = serverConfig.leds updateLedLayout(); }); + + function resetImage(){ + imageCanvasNodeCtx.fillStyle = "rgb(225,225,225)" + imageCanvasNodeCtx.fillRect(0, 0, canvas_width, canvas_height); + var image = new Image(); + image.onload = function() { + imageCanvasNodeCtx.drawImage(image, canvas_width*0.3, canvas_height*0.38); + }; + image.src ='data:image/png;base64,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LV199dXo37//58ePH/9pYWHhk0Z5bm7uXdu2bdvCGNuW6B92paCsC4vbiRLBWQDoeEodHocydzpvpaY3sGnq1AlXrbaSJwz/WQvKjjjH2Lp1a1dtmySRoiiUlpbGR44cyadNm2b3foiz/2932xN6gfH9tCuuuKLmyJEj9xQVFf1NkqQsvZP94Fe/+tXm0aNHvzhixAhWVlZ2RgaLyy+/nNra2vYR0QnGWBYAKTMzc8qdd9456FNMvgwAABKUSURBVMYbbzyFbjrI28na+neeCD1gFwBqamqqWLduXbUuRnT7vV577TUA4E8++eQL995771y/33+lzhX0Hz58+M+feOKJhbfddltDvH8tOfQiikdd4gWqhihz2oDaavUyDpslDgw5FvH9gc11chvTSLJrp9gqWvRwidBZ2UTUyfK7/oeAs0vas2cPBg4cuLmmpmaZWD5ixIgVr7/++tiysrIzGtxs3bp1xzjnW4zv4vP5Si699NKrJEnyjh49+ox+HM65jFi0hjRDSbV///4tn332WS1jrEcAnTRpEgDQI4880rply5almqYdFWTVK6+44orrAUi6g0sSAJXiKomcTjWDSTtr53trV2bpjysqkZA4wgMXlA+2czgTyNeOJpJLkmRHjZ14PYkgxX9aMsJT9u/f/6lgMPiS8M6ZX/va1/5nxowZmWdKgVNdXY1rr71WPXXq1HOITcIGABQWFv74o48+mrpnzx5at25djymnHjgbjLH7AHQ4sCiKUn355Zf/VVfi9Ihab9u2rWP/sssu233ixIkHxfPFxcUPrFmzZvz+/fu5PQVlJrbWXiZ1OsHaLoaQVRQFOARDF4DaRC6wXbvFanaITo3h4H261G8FJJ1lhgPK20Xm/N+YZH0mrBIbNmx4QFXVDsdSn8837aWXXroFgMwYM6hGt1O/fv0wffp05OXlfdzW1vasMBh4J0+e/MaHH3444corr+zxt9K5lZsA3CeWL1++/N5QKBRkjLXjDNtei4uLVwWDweeFd0q78sorX927d28O51yzBSgJwLQJBBRvcnVCQNmwx/HqgQ07SZxzZmKbrahtF/DX1dVZUnid9XW6ZGDcZMQEgvWqZ5achsHW/qdST/P3WrlyZXlNTc0jBnVhjHn69u3746NHj04CIIlUoztp+fLl2LhxIwAgEAj8RFGU7UKHTr344ov/dvjw4YvE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'; + } }); diff --git a/cmake/FindGitVersion.cmake b/cmake/FindGitVersion.cmake index bbcdfb79..b48f70ca 100644 --- a/cmake/FindGitVersion.cmake +++ b/cmake/FindGitVersion.cmake @@ -1,9 +1,10 @@ execute_process( COMMAND git log -1 --format=%cn-%t/%h-%ct WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE BUILD_ID ERROR_QUIET ) execute_process( COMMAND sh -c "git branch | grep '^*' | sed 's;^*;;g' " WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE VERSION_ID ERROR_QUIET ) +execute_process( COMMAND sh -c "git remote --verbose | grep origin | grep fetch | cut -f2 | cut -d' ' -f1" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_REMOTE_PATH ERROR_QUIET ) STRING ( STRIP "${BUILD_ID}" BUILD_ID ) STRING ( STRIP "${VERSION_ID}" VERSION_ID ) -SET ( HYPERION_BUILD_ID "${VERSION_ID} (${BUILD_ID})" ) +STRING ( STRIP "${GIT_REMOTE_PATH}" GIT_REMOTE_PATH ) +SET ( HYPERION_BUILD_ID "${VERSION_ID} (${BUILD_ID}) Git Remote: ${GIT_REMOTE_PATH}" ) message ( STATUS "Current Version: ${HYPERION_BUILD_ID}" ) - diff --git a/cmake/macos/Hyperion.icns b/cmake/osxbundle/Hyperion.icns similarity index 100% rename from cmake/macos/Hyperion.icns rename to cmake/osxbundle/Hyperion.icns diff --git a/cmake/macos/Info.plist b/cmake/osxbundle/Info.plist similarity index 100% rename from cmake/macos/Info.plist rename to cmake/osxbundle/Info.plist diff --git a/cmake/osxbundle/launch.sh b/cmake/osxbundle/launch.sh new file mode 100644 index 00000000..0c3ea053 --- /dev/null +++ b/cmake/osxbundle/launch.sh @@ -0,0 +1,5 @@ +#!/bin/sh +cd "$(dirname "$0")" +# Path to hyperiond!? +cd ../Resources/bin +exec ./hyperiond "$@" diff --git a/cmake/packages.cmake b/cmake/packages.cmake index 0d016880..5ae0d5fe 100644 --- a/cmake/packages.cmake +++ b/cmake/packages.cmake @@ -1,38 +1,63 @@ # cmake file for generating distribution packages IF (APPLE) - SET ( CPACK_GENERATOR "TGZ" "Bundle") # "RPM" -ELSE() + SET ( CPACK_GENERATOR "TGZ" "Bundle") +ELSEIF (UNIX) SET ( CPACK_GENERATOR "DEB" "TGZ" "STGZ") # "RPM" +ELSEIF (WIN32) + SET ( CPACK_GENERATOR "ZIP" "NSIS") ENDIF() -SET ( CPACK_PACKAGE_NAME "hyperion" ) +# Apply to all packages, some of these can be overwritten with generator specific content +# https://cmake.org/cmake/help/v3.0/module/CPack.html + +SET ( CPACK_PACKAGE_NAME "Hyperion" ) SET ( CPACK_PACKAGE_DESCRIPTION_SUMMARY "Hyperion is an open source ambient light implementation" ) SET ( CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_SOURCE_DIR}/README.md" ) +SET ( CPACK_PACKAGE_FILE_NAME "Hyperion-${HYPERION_VERSION_MAJOR}.${HYPERION_VERSION_MINOR}.${HYPERION_VERSION_PATCH}") +SET ( CPACK_PACKAGE_CONTACT "packages@hyperion-project.org") +SET ( CPACK_PACKAGE_EXECUTABLES "hyperiond;Hyperion" ) +SET ( CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/resources/icons/hyperion-icon-32px.png") +SET ( CPACK_PACKAGE_VERSION_MAJOR "${HYPERION_VERSION_MAJOR}") +SET ( CPACK_PACKAGE_VERSION_MINOR "${HYPERION_VERSION_MINOR}") +SET ( CPACK_PACKAGE_VERSION_PATCH "${HYPERION_VERSION_PATCH}") SET ( CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE" ) +SET ( CPACK_CREATE_DESKTOP_LINKS "hyperiond;Hyperion" ) + + +# Specific CPack Package Generators +# https://cmake.org/Wiki/CMake:CPackPackageGenerators +# .deb files for dpkg -SET ( CPACK_DEBIAN_PACKAGE_MAINTAINER "Hyperion Team") -SET ( CPACK_DEBIAN_PACKAGE_NAME "Hyperion" ) SET ( CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_CURRENT_SOURCE_DIR}/cmake/debian/preinst;${CMAKE_CURRENT_SOURCE_DIR}/cmake/debian/postinst;${CMAKE_CURRENT_SOURCE_DIR}/cmake/debian/prerm" ) -SET ( CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://www.hyperion-project.org" ) -SET ( CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5core5a (>= 5.5.0), libqt5network5 (>= 5.5.0), libqt5gui5 (>= 5.5.0), libqt5serialport5 (>= 5.5.0), libqt5sql5 (>= 5.5.0), libavahi-core7 (>= 0.6.31), libavahi-compat-libdnssd1 (>= 0.6.31), libusb-1.0-0, libpython3.5, libc6" ) +SET ( CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5core5a (>= 5.5.0), libqt5network5 (>= 5.5.0), libqt5gui5 (>= 5.5.0), libqt5serialport5 (>= 5.5.0), libqt5sql5 (>= 5.5.0), libqt5sql5-sqlite (>= 5.5.0), libavahi-core7 (>= 0.6.31), libavahi-compat-libdnssd1 (>= 0.6.31), libusb-1.0-0, libpython3.5, libc6" ) SET ( CPACK_DEBIAN_PACKAGE_SECTION "Miscellaneous" ) -SET ( CPACK_RPM_PACKAGE_NAME "Hyperion" ) -SET ( CPACK_RPM_PACKAGE_URL "https://github.com/hyperion-project/hyperion.ng" ) +# .rpm for rpm +SET ( CPACK_RPM_PACKAGE_RELEASE 1) +SET ( CPACK_RPM_PACKAGE_LICENSE "unknown") +SET ( CPACK_RPM_PACKAGE_GROUP "unknown") +SET ( CPACK_RPM_PACKAGE_REQUIRES "libqt5core5a >= 5.5.0, libqt5network5 >= 5.5.0, libqt5gui5 >= 5.5.0, libqt5serialport5 >= 5.5.0, libqt5sql5 >= 5.5.0, libqt5sql5-sqlite >= 5.5.0, libavahi-core7 >= 0.6.31, libavahi-compat-libdnssd1 >= 0.6.31, libusb-1.0-0, libpython3.5, libc6") SET ( CPACK_RPM_POST_INSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/rpm/postinst" ) -SET ( CPACK_PACKAGE_FILE_NAME "Hyperion") -SET ( CPACK_PACKAGE_ICON ${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/Hyperion.icns ) +# OSX "Bundle" generator TODO Add more osx generators +# https://cmake.org/cmake/help/v3.10/module/CPackBundle.html SET ( CPACK_BUNDLE_NAME "Hyperion" ) -SET ( CPACK_BUNDLE_ICON ${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/Hyperion.icns ) -SET ( CPACK_BUNDLE_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/Info.plist ) -#SET ( CPACK_BUNDLE_STARTUP_COMMAND - path to a file that will be executed when the user opens the bundle. Could be a shell-script or a binary. ) +SET ( CPACK_BUNDLE_ICON ${CMAKE_CURRENT_SOURCE_DIR}/cmake/osxbundle/Hyperion.icns ) +SET ( CPACK_BUNDLE_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/cmake/osxbundle/Info.plist ) +SET ( CPACK_BUNDLE_STARTUP_COMMAND "${CMAKE_SOURCE_DIR}/cmake/osxbundle/launch.sh" ) -SET(CPACK_PACKAGE_VERSION_MAJOR "${HYPERION_VERSION_MAJOR}") -SET(CPACK_PACKAGE_VERSION_MINOR "${HYPERION_VERSION_MINOR}") -SET(CPACK_PACKAGE_VERSION_PATCH "${HYPERION_VERSION_PATCH}") +# NSIS for windows, requires NSIS TODO finish +SET ( CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/cmake/nsis/installer.ico") +SET ( CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/cmake/nsis/uninstaller.ico") +#SET ( CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/cmake/nsis/installer.bmp") #bmp required? If so, wrap in WIN32 check else: Use default icon instead +SET ( CPACK_NSIS_MODIFY_PATH ON) +SET ( CPACK_NSIS_DISPLAY_NAME "Hyperion Installer") +SET ( CPACK_NSIS_INSTALLED_ICON_NAME "Link to .exe") +SET ( CPACK_NSIS_HELP_LINK "https://www.hyperion-project.org") +SET ( CPACK_NSIS_URL_INFO_ABOUT "https://www.hyperion-project.org") +# define the install components SET ( CPACK_COMPONENTS_ALL "${PLATFORM}" ) SET ( CPACK_ARCHIVE_COMPONENT_INSTALL ON ) SET ( CPACK_DEB_COMPONENT_INSTALL ON ) diff --git a/dependencies/build/tinkerforge/ip_connection.c b/dependencies/build/tinkerforge/ip_connection.c index 31cf4aee..62278442 100644 --- a/dependencies/build/tinkerforge/ip_connection.c +++ b/dependencies/build/tinkerforge/ip_connection.c @@ -7,7 +7,7 @@ */ #ifndef _WIN32 - #define _BSD_SOURCE // for usleep from unistd.h + #define _DEFAULT_SOURCE // for usleep from unistd.h #endif #include diff --git a/include/api/JsonAPI.h b/include/api/JsonAPI.h index a7619be4..7c7caf13 100644 --- a/include/api/JsonAPI.h +++ b/include/api/JsonAPI.h @@ -7,33 +7,10 @@ #include // qt includess -#include #include #include #include -// createEffect helper -struct find_schema: std::unary_function -{ - QString pyFile; - find_schema(QString pyFile):pyFile(pyFile) { } - bool operator()(EffectSchema const& schema) const - { - return schema.pyFile == pyFile; - } -}; - -// deleteEffect helper -struct find_effect: std::unary_function -{ - QString effectName; - find_effect(QString effectName) :effectName(effectName) { } - bool operator()(EffectDefinition const& effectDefinition) const - { - return effectDefinition.name == effectName; - } -}; - class JsonCB; class JsonAPI : public QObject @@ -59,8 +36,11 @@ public: void handleMessage(const QString & message); public slots: - /// _timer_ledcolors requests ledcolor updates (if enabled) - void streamLedcolorsUpdate(); + /// + /// @brief is called whenever the current Hyperion instance pushes new led raw values (if enabled) + /// @param ledColors The current ledColors + /// + void streamLedcolorsUpdate(const std::vector& ledColors); /// push images whenever hyperion emits (if enabled) void setImage(const Image & image); @@ -80,11 +60,9 @@ signals: void forwardJsonMessage(QJsonObject); private: - - // The JsonCB instance which handles data subscription/notifications - JsonCB* _jsonCB; // true if further callbacks are forbidden (http) bool _noListener; + /// The peer address of the client QString _peerAddress; @@ -94,8 +72,8 @@ private: /// Hyperion instance Hyperion* _hyperion; - /// timer for ledcolors streaming - QTimer _timer_ledcolors; + // The JsonCB instance which handles data subscription/notifications + JsonCB* _jsonCB; // streaming buffers QJsonObject _streaming_leds_reply; @@ -108,9 +86,15 @@ private: /// mutex to determine state of image streaming QMutex _image_stream_mutex; + /// mutex to determine state of image streaming + QMutex _led_stream_mutex; + /// timeout for live video refresh volatile qint64 _image_stream_timeout; + /// timeout for led color refresh + volatile qint64 _led_stream_timeout; + /// /// Handle an incoming JSON Color message /// diff --git a/include/effectengine/EffectEngine.h b/include/effectengine/EffectEngine.h index 138e6ebf..c96aff38 100644 --- a/include/effectengine/EffectEngine.h +++ b/include/effectengine/EffectEngine.h @@ -17,30 +17,43 @@ #include #include -// pre-declarioation +// pre-declaration class Effect; +class EffectFileHandler; class EffectEngine : public QObject { Q_OBJECT public: - EffectEngine(Hyperion * hyperion, const QJsonObject & jsonEffectConfig); + EffectEngine(Hyperion * hyperion); virtual ~EffectEngine(); - void readEffects(); - - const std::list & getEffects() const - { - return _availableEffects; - }; + const std::list & getEffects() const { return _availableEffects; }; const std::list & getActiveEffects(); - const std::list & getEffectSchemas() - { - return _effectSchemas; - }; + /// + /// Get available schemas from EffectFileHandler + /// @return all schemas + /// + const std::list & getEffectSchemas(); + + /// + /// @brief Save an effect with EffectFileHandler + /// @param obj The effect args + /// @param[out] resultMsg The feedback message + /// @return True on success else false + /// + const bool saveEffect(const QJsonObject& obj, QString& resultMsg); + + /// + /// @brief Delete an effect by name. + /// @param[in] effectName The effect name to delete + /// @param[out] resultMsg The message on error + /// @return True on success else false + /// + const bool deleteEffect(const QString& effectName, QString& resultMsg); /// /// @brief Get all init data of the running effects and stop them @@ -72,19 +85,18 @@ public slots: private slots: void effectFinished(); + /// + /// @brief is called whenever the EffectFileHandler emits updated effect list + /// + void handleUpdatedEffectList(); + private: - bool loadEffectDefinition(const QString & path, const QString & effectConfigFile, EffectDefinition &effectDefinition); - - bool loadEffectSchema(const QString & path, const QString & effectSchemaFile, EffectSchema &effectSchema); - /// Run the specified effect on the given priority channel and optionally specify a timeout int runEffectScript(const QString &script, const QString &name, const QJsonObject & args, int priority, int timeout = -1, const QString & origin="System", unsigned smoothCfg=0); private: Hyperion * _hyperion; - QJsonObject _effectConfig; - std::list _availableEffects; std::list _activeEffects; @@ -93,7 +105,8 @@ private: std::list _cachedActiveEffects; - std::list _effectSchemas; - Logger * _log; + + // The global effect file handler + EffectFileHandler* _effectFileHandler; }; diff --git a/include/effectengine/EffectFileHandler.h b/include/effectengine/EffectFileHandler.h new file mode 100644 index 00000000..cdcc76d9 --- /dev/null +++ b/include/effectengine/EffectFileHandler.h @@ -0,0 +1,86 @@ +#pragma once + +// util +#include +#include +#include +#include + +class EffectFileHandler : public QObject +{ + Q_OBJECT +private: + friend class HyperionDaemon; + EffectFileHandler(const QString& rootPath, const QJsonDocument& effectConfig, QObject* parent = nullptr); + +public: + static EffectFileHandler* efhInstance; + static EffectFileHandler* getInstance() { return efhInstance; }; + + /// + /// @brief Get all available effects + /// + const std::list & getEffects() const { return _availableEffects; }; + + /// + /// @brief Get all available schemas + /// + const std::list & getEffectSchemas() { return _effectSchemas; }; + + /// + /// @brief Save an effect + /// @param obj The effect args + /// @param[out] resultMsg The feedback message + /// @return True on success else false + /// + const bool saveEffect(const QJsonObject& obj, QString& resultMsg); + + /// + /// @brief Delete an effect by name. + /// @param[in] effectName The effect name to delete + /// @param[out] resultMsg The message on error + /// @return True on success else false + /// + const bool deleteEffect(const QString& effectName, QString& resultMsg); + +public slots: + /// + /// @brief Handle settings update from Hyperion Settingsmanager emit + /// @param type settingyType from enum + /// @param config configuration object + /// + void handleSettingsUpdate(const settings::type& type, const QJsonDocument& config); + +signals: + /// + /// @brief Emits whenever the data changes for an effect + /// + void effectListChanged(); + +private: + /// + /// @brief refresh available schemas and effects + /// + void updateEffects(); + + /// + /// @brief Load the effect definition, called by updateEffects() + /// + bool loadEffectDefinition(const QString & path, const QString & effectConfigFile, EffectDefinition &effectDefinition); + + /// + /// @brief load effect schemas, called by updateEffects() + /// + bool loadEffectSchema(const QString & path, const QString & effectSchemaFile, EffectSchema &effectSchema); + +private: + QJsonObject _effectConfig; + Logger* _log; + const QString _rootPath; + + // available effects + std::list _availableEffects; + + // all schemas + std::list _effectSchemas; +}; diff --git a/include/hyperion/CaptureCont.h b/include/hyperion/CaptureCont.h index dd15e122..a5b5059e 100644 --- a/include/hyperion/CaptureCont.h +++ b/include/hyperion/CaptureCont.h @@ -54,6 +54,11 @@ private slots: /// void setV4lInactive(); + /// + /// @brief Is called from _systemInactiveTimer to set source after specific time to inactive + /// + void setSystemInactive(); + private: /// Hyperion instance Hyperion* _hyperion; @@ -61,6 +66,7 @@ private: /// Reflect state of System capture and prio bool _systemCaptEnabled; quint8 _systemCaptPrio; + QTimer* _systemInactiveTimer; /// Reflect state of v4l capture and prio bool _v4lCaptEnabled; diff --git a/include/hyperion/Hyperion.h b/include/hyperion/Hyperion.h index e981e1b5..9bb9a569 100644 --- a/include/hyperion/Hyperion.h +++ b/include/hyperion/Hyperion.h @@ -163,8 +163,21 @@ public: /// const InputInfo getPriorityInfo(const int priority) const; - /// Reload the list of available effects - void reloadEffects(); + /// + /// @brief Save an effect + /// @param obj The effect args + /// @param[out] resultMsg The feedback message + /// @return True on success else false + /// + const bool saveEffect(const QJsonObject& obj, QString& resultMsg); + + /// + /// @brief Delete an effect by name. + /// @param[in] effectName The effect name to delete + /// @param[out] resultMsg The message on error + /// @return True on success else false + /// + const bool deleteEffect(const QString& effectName, QString& resultMsg); /// Get the list of available effects /// @return The list of available effects @@ -215,12 +228,6 @@ public: /// @return the state bool sourceAutoSelectEnabled(); - /// - /// @brief Get the last untransformed/unadjusted led colors - /// @return The _rawLedBuffer leds - /// - const std::vector& getRawLedBuffer() { return _rawLedBuffer; }; - /// /// @brief Enable/Disable components during runtime, called from external API (requests) /// @@ -433,6 +440,11 @@ signals: /// void v4lImage(const Image & image); + /// + /// @brief Emits whenever new untransformed ledColos data is available, reflects the current visible device + /// + void rawLedColors(const std::vector& ledValues); + private slots: /// /// Updates the priority muxer with the current time and (re)writes the led color with applied @@ -548,8 +560,6 @@ private: /// buffer for leds (with adjustment) std::vector _ledBuffer; - /// buffer for leds (without adjustment) - std::vector _rawLedBuffer; /// Boblight instance BoblightServer* _boblightServer; diff --git a/include/hyperion/SettingsManager.h b/include/hyperion/SettingsManager.h index 2394cc7b..33a43777 100644 --- a/include/hyperion/SettingsManager.h +++ b/include/hyperion/SettingsManager.h @@ -9,7 +9,7 @@ class Hyperion; /// -/// @brief Manage the settings read write from/to database, on settings changed will emit a signal to update components accordingly +/// @brief Manage the settings read write from/to config file, on settings changed will emit a signal to update components accordingly /// class SettingsManager : public QObject { @@ -37,7 +37,7 @@ public: const bool saveSettings(QJsonObject config, const bool& correct = false); /// - /// @brief get a single setting json from database + /// @brief get a single setting json from config /// @param type The settings::type from enum /// @return The requested json data as QJsonDocument /// @@ -51,7 +51,7 @@ public: signals: /// - /// @brief Emits whenever a config part changed. Comparison of database and new data to prevent false positive + /// @brief Emits whenever a config part changed. /// @param type The settings type from enum /// @param data The data as QJsonDocument /// @@ -60,10 +60,13 @@ signals: private: /// Hyperion instance Hyperion* _hyperion; + /// Logger instance Logger* _log; + /// the schema static QJsonObject schemaJson; + /// the current config of this instance QJsonObject _qconfig; }; diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 88ff4893..fcbfb7d3 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -12,9 +12,9 @@ #include #include #include -#include -#include -#include +// #include +// #include +// #include #include // hyperion includes @@ -41,13 +41,14 @@ using namespace hyperion; JsonAPI::JsonAPI(QString peerAddress, Logger* log, QObject* parent, bool noListener) : QObject(parent) - , _jsonCB(new JsonCB(this)) , _noListener(noListener) , _peerAddress(peerAddress) , _log(log) , _hyperion(Hyperion::getInstance()) + , _jsonCB(new JsonCB(this)) , _streaming_logging_activated(false) , _image_stream_timeout(0) + , _led_stream_timeout(0) { // the JsonCB creates json messages you can subscribe to e.g. data change events; forward them to the parent client connect(_jsonCB, &JsonCB::newCallback, this, &JsonAPI::callbackMessage); @@ -55,9 +56,8 @@ JsonAPI::JsonAPI(QString peerAddress, Logger* log, QObject* parent, bool noListe // notify hyperion about a jsonMessageForward connect(this, &JsonAPI::forwardJsonMessage, _hyperion, &Hyperion::forwardJsonMessage); - // led color stream update timer - connect(&_timer_ledcolors, SIGNAL(timeout()), this, SLOT(streamLedcolorsUpdate())); _image_stream_mutex.unlock(); + _led_stream_mutex.unlock(); } void JsonAPI::handleMessage(const QString& messageString) @@ -207,105 +207,20 @@ void JsonAPI::handleEffectCommand(const QJsonObject& message, const QString& com void JsonAPI::handleCreateEffectCommand(const QJsonObject& message, const QString &command, const int tan) { - if (!message["args"].toObject().isEmpty()) - { - QString scriptName; - (message["script"].toString().mid(0, 1) == ":" ) - ? scriptName = ":/effects//" + message["script"].toString().mid(1) - : scriptName = message["script"].toString(); - - std::list effectsSchemas = _hyperion->getEffectSchemas(); - std::list::iterator it = std::find_if(effectsSchemas.begin(), effectsSchemas.end(), find_schema(scriptName)); - - if (it != effectsSchemas.end()) - { - if(!JsonUtils::validate("JsonRpc@"+_peerAddress, message["args"].toObject(), it->schemaFile, _log)) - { - sendErrorReply("Error during arg validation against schema, please consult the Hyperion Log", command, tan); - return; - } - - QJsonObject effectJson; - QJsonArray effectArray; - effectArray = _hyperion->getQJsonConfig()["effects"].toObject()["paths"].toArray(); - - if (effectArray.size() > 0) - { - if (message["name"].toString().trimmed().isEmpty() || message["name"].toString().trimmed().startsWith(".")) - { - sendErrorReply("Can't save new effect. Effect name is empty or begins with a dot.", command, tan); - return; - } - - effectJson["name"] = message["name"].toString(); - effectJson["script"] = message["script"].toString(); - effectJson["args"] = message["args"].toObject(); - - std::list availableEffects = _hyperion->getEffects(); - std::list::iterator iter = std::find_if(availableEffects.begin(), availableEffects.end(), find_effect(message["name"].toString())); - - QFileInfo newFileName; - if (iter != availableEffects.end()) - { - newFileName.setFile(iter->file); - if (newFileName.absoluteFilePath().mid(0, 1) == ":") - { - sendErrorReply("The effect name '" + message["name"].toString() + "' is assigned to an internal effect. Please rename your effekt.", command, tan); - return; - } - } else - { - QString f = FileUtils::convertPath(effectArray[0].toString() + "/" + message["name"].toString().replace(QString(" "), QString("")) + QString(".json")); - newFileName.setFile(f); - } - - if(!JsonUtils::write(newFileName.absoluteFilePath(), effectJson, _log)) - { - sendErrorReply("Error while saving effect, please check the Hyperion Log", command, tan); - return; - } - - Info(_log, "Reload effect list"); - _hyperion->reloadEffects(); - sendSuccessReply(command, tan); - } else - { - sendErrorReply("Can't save new effect. Effect path empty", command, tan); - return; - } - } else - sendErrorReply("Missing schema file for Python script " + message["script"].toString(), command, tan); - } else - sendErrorReply("Missing or empty Object 'args'", command, tan); + QString resultMsg; + if(_hyperion->saveEffect(message, resultMsg)) + sendSuccessReply(command, tan); + else + sendErrorReply(resultMsg, command, tan); } void JsonAPI::handleDeleteEffectCommand(const QJsonObject& message, const QString& command, const int tan) { - QString effectName = message["name"].toString(); - std::list effectsDefinition = _hyperion->getEffects(); - std::list::iterator it = std::find_if(effectsDefinition.begin(), effectsDefinition.end(), find_effect(effectName)); - - if (it != effectsDefinition.end()) - { - QFileInfo effectConfigurationFile(it->file); - if (effectConfigurationFile.absoluteFilePath().mid(0, 1) != ":" ) - { - if (effectConfigurationFile.exists()) - { - bool result = QFile::remove(effectConfigurationFile.absoluteFilePath()); - if (result) - { - Info(_log, "Reload effect list"); - _hyperion->reloadEffects(); - sendSuccessReply(command, tan); - } else - sendErrorReply("Can't delete effect configuration file: " + effectConfigurationFile.absoluteFilePath() + ". Please check permissions", command, tan); - } else - sendErrorReply("Can't find effect configuration file: " + effectConfigurationFile.absoluteFilePath(), command, tan); - } else - sendErrorReply("Can't delete internal effect: " + message["name"].toString(), command, tan); - } else - sendErrorReply("Effect " + message["name"].toString() + " not found", command, tan); + QString resultMsg; + if(_hyperion->deleteEffect(message["name"].toString(), resultMsg)) + sendSuccessReply(command, tan); + else + sendErrorReply(resultMsg, command, tan); } void JsonAPI::handleSysInfoCommand(const QJsonObject&, const QString& command, const int tan) @@ -358,10 +273,9 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject& message, const QString& const Hyperion::InputInfo & priorityInfo = _hyperion->getPriorityInfo(priority); QJsonObject item; item["priority"] = priority; - if (int(priorityInfo.timeoutTime_ms - now) > -1 ) - { + if (priorityInfo.timeoutTime_ms > 0 ) item["duration_ms"] = int(priorityInfo.timeoutTime_ms - now); - } + // owner has optional informations to the component if(!priorityInfo.owner.isEmpty()) item["owner"] = priorityInfo.owner; @@ -861,12 +775,11 @@ void JsonAPI::handleLedColorsCommand(const QJsonObject& message, const QString & _streaming_leds_reply["success"] = true; _streaming_leds_reply["command"] = command+"-ledstream-update"; _streaming_leds_reply["tan"] = tan; - _timer_ledcolors.setInterval(125); - _timer_ledcolors.start(125); + connect(_hyperion, &Hyperion::rawLedColors, this, &JsonAPI::streamLedcolorsUpdate, Qt::UniqueConnection); } else if (subcommand == "ledstream-stop") { - _timer_ledcolors.stop(); + disconnect(_hyperion, &Hyperion::rawLedColors, this, &JsonAPI::streamLedcolorsUpdate); } else if (subcommand == "imagestream-start") { @@ -984,32 +897,37 @@ void JsonAPI::sendErrorReply(const QString &error, const QString &command, const } -void JsonAPI::streamLedcolorsUpdate() +void JsonAPI::streamLedcolorsUpdate(const std::vector& ledColors) { - QJsonObject result; - QJsonArray leds; - - const std::vector & ledColors = _hyperion->getRawLedBuffer(); - for(auto color = ledColors.begin(); color != ledColors.end(); ++color) + if ( (_led_stream_timeout+100) < QDateTime::currentMSecsSinceEpoch() && _led_stream_mutex.tryLock(0) ) { - QJsonObject item; - item["index"] = int(color - ledColors.begin()); - item["red"] = color->red; - item["green"] = color->green; - item["blue"] = color->blue; - leds.append(item); + _led_stream_timeout = QDateTime::currentMSecsSinceEpoch(); + QJsonObject result; + QJsonArray leds; + + for(auto color = ledColors.begin(); color != ledColors.end(); ++color) + { + QJsonObject item; + item["index"] = int(color - ledColors.begin()); + item["red"] = color->red; + item["green"] = color->green; + item["blue"] = color->blue; + leds.append(item); + } + + result["leds"] = leds; + _streaming_leds_reply["result"] = result; + + // send the result + emit callbackMessage(_streaming_leds_reply); + + _led_stream_mutex.unlock(); } - - result["leds"] = leds; - _streaming_leds_reply["result"] = result; - - // send the result - emit callbackMessage(_streaming_leds_reply); } void JsonAPI::setImage(const Image & image) { - if ( (_image_stream_timeout+250) < QDateTime::currentMSecsSinceEpoch() && _image_stream_mutex.tryLock(0) ) + if ( (_image_stream_timeout+100) < QDateTime::currentMSecsSinceEpoch() && _image_stream_mutex.tryLock(0) ) { _image_stream_timeout = QDateTime::currentMSecsSinceEpoch(); diff --git a/libsrc/api/JsonCB.cpp b/libsrc/api/JsonCB.cpp index f1f8e109..1fe0be1c 100644 --- a/libsrc/api/JsonCB.cpp +++ b/libsrc/api/JsonCB.cpp @@ -139,10 +139,9 @@ void JsonCB::handlePriorityUpdate() const Hyperion::InputInfo priorityInfo = _prioMuxer->getInputInfo(priority); QJsonObject item; item["priority"] = priority; - if (int(priorityInfo.timeoutTime_ms - now) > -1 ) - { + if (priorityInfo.timeoutTime_ms > 0 ) item["duration_ms"] = int(priorityInfo.timeoutTime_ms - now); - } + // owner has optional informations to the component if(!priorityInfo.owner.isEmpty()) item["owner"] = priorityInfo.owner; diff --git a/libsrc/effectengine/EffectEngine.cpp b/libsrc/effectengine/EffectEngine.cpp index 26bed8a7..7c89f66d 100644 --- a/libsrc/effectengine/EffectEngine.cpp +++ b/libsrc/effectengine/EffectEngine.cpp @@ -18,33 +18,46 @@ #include #include #include +#include #include "HyperionConfig.h" -EffectEngine::EffectEngine(Hyperion * hyperion, const QJsonObject & jsonEffectConfig) +EffectEngine::EffectEngine(Hyperion * hyperion) : _hyperion(hyperion) - , _effectConfig(jsonEffectConfig) , _availableEffects() , _activeEffects() , _log(Logger::getInstance("EFFECTENGINE")) + , _effectFileHandler(EffectFileHandler::getInstance()) { Q_INIT_RESOURCE(EffectEngine); qRegisterMetaType>("std::vector"); - qRegisterMetaType>("Image"); qRegisterMetaType("hyperion::Components"); // connect the Hyperion channel clear feedback connect(_hyperion, SIGNAL(channelCleared(int)), this, SLOT(channelCleared(int))); connect(_hyperion, SIGNAL(allChannelsCleared()), this, SLOT(allChannelsCleared())); - // read all effects - readEffects(); + // get notifications about refreshed effect list + connect(_effectFileHandler, &EffectFileHandler::effectListChanged, this, &EffectEngine::handleUpdatedEffectList); + + // register smooth cfgs and fill available effects + handleUpdatedEffectList(); } EffectEngine::~EffectEngine() { } +const bool EffectEngine::saveEffect(const QJsonObject& obj, QString& resultMsg) +{ + return _effectFileHandler->saveEffect(obj, resultMsg); +} + +const bool EffectEngine::deleteEffect(const QString& effectName, QString& resultMsg) +{ + return _effectFileHandler->deleteEffect(effectName, resultMsg); +} + const std::list &EffectEngine::getActiveEffects() { _availableActiveEffects.clear(); @@ -63,6 +76,11 @@ const std::list &EffectEngine::getActiveEffects() return _availableActiveEffects; } +const std::list & EffectEngine::getEffectSchemas() +{ + return _effectFileHandler->getEffectSchemas(); +} + void EffectEngine::cacheRunningEffects() { _cachedActiveEffects.clear(); @@ -90,175 +108,26 @@ void EffectEngine::startCachedEffects() _cachedActiveEffects.clear(); } -bool EffectEngine::loadEffectDefinition(const QString &path, const QString &effectConfigFile, EffectDefinition & effectDefinition) +void EffectEngine::handleUpdatedEffectList() { - QString fileName = path + QDir::separator() + effectConfigFile; - - // Read and parse the effect json config file - QJsonObject configEffect; - if(!JsonUtils::readFile(fileName, configEffect, _log)) - return false; - - Q_INIT_RESOURCE(EffectEngine); - // validate effect config with effect schema(path) - if(!JsonUtils::validate(fileName, configEffect, ":effect-schema", _log)) - return false; - - // setup the definition - effectDefinition.file = fileName; - QJsonObject config = configEffect; - QString scriptName = config["script"].toString(); - effectDefinition.name = config["name"].toString(); - if (scriptName.isEmpty()) - return false; - - QFile fileInfo(scriptName); - - if (scriptName.mid(0, 1) == ":" ) - { - (!fileInfo.exists()) - ? effectDefinition.script = ":/effects/"+scriptName.mid(1) - : effectDefinition.script = scriptName; - } else - { - (!fileInfo.exists()) - ? effectDefinition.script = path + QDir::separator() + scriptName - : effectDefinition.script = scriptName; - } - - effectDefinition.args = config["args"].toObject(); - effectDefinition.smoothCfg = SMOOTHING_MODE_PAUSE; - if (effectDefinition.args["smoothing-custom-settings"].toBool()) - { - effectDefinition.smoothCfg = _hyperion->addSmoothingConfig( - effectDefinition.args["smoothing-time_ms"].toInt(), - effectDefinition.args["smoothing-updateFrequency"].toDouble(), - 0 ); - } - else - { - effectDefinition.smoothCfg = _hyperion->addSmoothingConfig(true); - } - return true; -} - -bool EffectEngine::loadEffectSchema(const QString &path, const QString &effectSchemaFile, EffectSchema & effectSchema) -{ - QString fileName = path + "schema/" + QDir::separator() + effectSchemaFile; - - // Read and parse the effect schema file - QJsonObject schemaEffect; - if(!JsonUtils::readFile(fileName, schemaEffect, _log)) - return false; - - // setup the definition - QString scriptName = schemaEffect["script"].toString(); - effectSchema.schemaFile = fileName; - fileName = path + QDir::separator() + scriptName; - QFile pyFile(fileName); - - if (scriptName.isEmpty() || !pyFile.open(QIODevice::ReadOnly)) - { - fileName = path + "schema/" + QDir::separator() + effectSchemaFile; - Error( _log, "Python script '%s' in effect schema '%s' could not be loaded", QSTRING_CSTR(scriptName), QSTRING_CSTR(fileName)); - return false; - } - - pyFile.close(); - - effectSchema.pyFile = (scriptName.mid(0, 1) == ":" ) ? ":/effects/"+scriptName.mid(1) : path + QDir::separator() + scriptName; - effectSchema.pySchema = schemaEffect; - - return true; -} - -void EffectEngine::readEffects() -{ - // clear all lists _availableEffects.clear(); - _effectSchemas.clear(); - // read all effects - const QJsonArray & paths = _effectConfig["paths"].toArray(); - const QJsonArray & disabledEfx = _effectConfig["disable"].toArray(); - - QStringList efxPathList; - efxPathList << ":/effects/"; - QStringList disableList; - - for(auto p : paths) + for (auto def : _effectFileHandler->getEffects()) { - efxPathList << p.toString().replace("$ROOT",_hyperion->getRootPath()); - } - for(auto efx : disabledEfx) - { - disableList << efx.toString(); - } - - QMap availableEffects; - for (const QString & path : efxPathList ) - { - QDir directory(path); - if (!directory.exists()) + // add smoothing configs to Hyperion + if (def.args["smoothing-custom-settings"].toBool()) { - if(directory.mkpath(path)) - { - Warning(_log, "New Effect path \"%s\" created successfull", QSTRING_CSTR(path) ); - } - else - { - Warning(_log, "Failed to create Effect path \"%s\", please check permissions", QSTRING_CSTR(path) ); - } + def.smoothCfg = _hyperion->addSmoothingConfig( + def.args["smoothing-time_ms"].toInt(), + def.args["smoothing-updateFrequency"].toDouble(), + 0 ); } else { - int efxCount = 0; - QStringList filenames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase); - for (const QString & filename : filenames) - { - EffectDefinition def; - if (loadEffectDefinition(path, filename, def)) - { - InfoIf(availableEffects.find(def.name) != availableEffects.end(), _log, - "effect overload effect '%s' is now taken from '%s'", QSTRING_CSTR(def.name), QSTRING_CSTR(path) ); - - if ( disableList.contains(def.name) ) - { - Info(_log, "effect '%s' not loaded, because it is disabled in hyperion config", QSTRING_CSTR(def.name)); - } - else - { - availableEffects[def.name] = def; - efxCount++; - } - } - } - Info(_log, "%d effects loaded from directory %s", efxCount, QSTRING_CSTR(path)); - - // collect effect schemas - efxCount = 0; - directory = path.endsWith("/") ? (path + "schema/") : (path + "/schema/"); - QStringList pynames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase); - for (const QString & pyname : pynames) - { - EffectSchema pyEffect; - if (loadEffectSchema(path, pyname, pyEffect)) - { - _effectSchemas.push_back(pyEffect); - efxCount++; - } - } - InfoIf(efxCount > 0, _log, "%d effect schemas loaded from directory %s", efxCount, QSTRING_CSTR((path + "schema/"))); + def.smoothCfg = _hyperion->addSmoothingConfig(true); } + _availableEffects.push_back(def); } - - for(auto item : availableEffects) - { - _availableEffects.push_back(item); - } - - ErrorIf(_availableEffects.size()==0, _log, "no effects found, check your effect directories"); - emit effectListUpdated(); } diff --git a/libsrc/effectengine/EffectFileHandler.cpp b/libsrc/effectengine/EffectFileHandler.cpp new file mode 100644 index 00000000..584369ec --- /dev/null +++ b/libsrc/effectengine/EffectFileHandler.cpp @@ -0,0 +1,322 @@ +#include + +// util +#include + +// qt +#include +#include +#include +#include + +// createEffect helper +struct find_schema: std::unary_function +{ + QString pyFile; + find_schema(QString pyFile):pyFile(pyFile) { } + bool operator()(EffectSchema const& schema) const + { + return schema.pyFile == pyFile; + } +}; + +// deleteEffect helper +struct find_effect: std::unary_function +{ + QString effectName; + find_effect(QString effectName) :effectName(effectName) { } + bool operator()(EffectDefinition const& effectDefinition) const + { + return effectDefinition.name == effectName; + } +}; + +EffectFileHandler* EffectFileHandler::efhInstance; + +EffectFileHandler::EffectFileHandler(const QString& rootPath, const QJsonDocument& effectConfig, QObject* parent) + : QObject(parent) + , _effectConfig() + , _log(Logger::getInstance("EFFECTFILES")) + , _rootPath(rootPath) +{ + EffectFileHandler::efhInstance = this; + + Q_INIT_RESOURCE(EffectEngine); + + // init + handleSettingsUpdate(settings::EFFECTS, effectConfig); +} + +void EffectFileHandler::handleSettingsUpdate(const settings::type& type, const QJsonDocument& config) +{ + if(type == settings::EFFECTS) + { + _effectConfig = config.object(); + // update effects and schemas + updateEffects(); + } +} + +const bool EffectFileHandler::deleteEffect(const QString& effectName, QString& resultMsg) +{ + std::list effectsDefinition = getEffects(); + std::list::iterator it = std::find_if(effectsDefinition.begin(), effectsDefinition.end(), find_effect(effectName)); + + if (it != effectsDefinition.end()) + { + QFileInfo effectConfigurationFile(it->file); + if (effectConfigurationFile.absoluteFilePath().mid(0, 1) != ":" ) + { + if (effectConfigurationFile.exists()) + { + bool result = QFile::remove(effectConfigurationFile.absoluteFilePath()); + if (result) + { + updateEffects(); + return true; + } else + resultMsg = "Can't delete effect configuration file: " + effectConfigurationFile.absoluteFilePath() + ". Please check permissions"; + } else + resultMsg = "Can't find effect configuration file: " + effectConfigurationFile.absoluteFilePath(); + } else + resultMsg = "Can't delete internal effect: " + effectName; + } else + resultMsg = "Effect " + effectName + " not found"; + + return false; +} + +const bool EffectFileHandler::saveEffect(const QJsonObject& message, QString& resultMsg) +{ + if (!message["args"].toObject().isEmpty()) + { + QString scriptName; + (message["script"].toString().mid(0, 1) == ":" ) + ? scriptName = ":/effects//" + message["script"].toString().mid(1) + : scriptName = message["script"].toString(); + + std::list effectsSchemas = getEffectSchemas(); + std::list::iterator it = std::find_if(effectsSchemas.begin(), effectsSchemas.end(), find_schema(scriptName)); + + if (it != effectsSchemas.end()) + { + if(!JsonUtils::validate("EffectFileHandler", message["args"].toObject(), it->schemaFile, _log)) + { + resultMsg = "Error during arg validation against schema, please consult the Hyperion Log"; + return false; + } + + QJsonObject effectJson; + QJsonArray effectArray; + effectArray = _effectConfig["paths"].toArray(); + + if (effectArray.size() > 0) + { + if (message["name"].toString().trimmed().isEmpty() || message["name"].toString().trimmed().startsWith(".")) + { + resultMsg = "Can't save new effect. Effect name is empty or begins with a dot."; + return false; + } + + effectJson["name"] = message["name"].toString(); + effectJson["script"] = message["script"].toString(); + effectJson["args"] = message["args"].toObject(); + + std::list availableEffects = getEffects(); + std::list::iterator iter = std::find_if(availableEffects.begin(), availableEffects.end(), find_effect(message["name"].toString())); + + QFileInfo newFileName; + if (iter != availableEffects.end()) + { + newFileName.setFile(iter->file); + if (newFileName.absoluteFilePath().mid(0, 1) == ":") + { + resultMsg = "The effect name '" + message["name"].toString() + "' is assigned to an internal effect. Please rename your effekt."; + return false; + } + } else + { + // TODO global special keyword handling + QString f = effectArray[0].toString().replace("$ROOT",_rootPath) + "/" + message["name"].toString().replace(QString(" "), QString("")) + QString(".json"); + newFileName.setFile(f); + } + + if(!JsonUtils::write(newFileName.absoluteFilePath(), effectJson, _log)) + { + resultMsg = "Error while saving effect, please check the Hyperion Log"; + return false; + } + + Info(_log, "Reload effect list"); + updateEffects(); + resultMsg = ""; + return true; + } else + resultMsg = "Can't save new effect. Effect path empty"; + } else + resultMsg = "Missing schema file for Python script " + message["script"].toString(); + } else + resultMsg = "Missing or empty Object 'args'"; + + return false; +} + +void EffectFileHandler::updateEffects() +{ + // clear all lists + _availableEffects.clear(); + _effectSchemas.clear(); + + // read all effects + const QJsonArray & paths = _effectConfig["paths"].toArray(); + const QJsonArray & disabledEfx = _effectConfig["disable"].toArray(); + + QStringList efxPathList; + efxPathList << ":/effects/"; + QStringList disableList; + + for(auto p : paths) + { + efxPathList << p.toString().replace("$ROOT",_rootPath); + } + for(auto efx : disabledEfx) + { + disableList << efx.toString(); + } + + QMap availableEffects; + for (const QString & path : efxPathList ) + { + QDir directory(path); + if (!directory.exists()) + { + if(directory.mkpath(path)) + { + Info(_log, "New Effect path \"%s\" created successfull", QSTRING_CSTR(path) ); + } + else + { + Warning(_log, "Failed to create Effect path \"%s\", please check permissions", QSTRING_CSTR(path) ); + } + } + else + { + int efxCount = 0; + QStringList filenames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase); + for (const QString & filename : filenames) + { + EffectDefinition def; + if (loadEffectDefinition(path, filename, def)) + { + InfoIf(availableEffects.find(def.name) != availableEffects.end(), _log, + "effect overload effect '%s' is now taken from '%s'", QSTRING_CSTR(def.name), QSTRING_CSTR(path) ); + + if ( disableList.contains(def.name) ) + { + Info(_log, "effect '%s' not loaded, because it is disabled in hyperion config", QSTRING_CSTR(def.name)); + } + else + { + availableEffects[def.name] = def; + efxCount++; + } + } + } + Info(_log, "%d effects loaded from directory %s", efxCount, QSTRING_CSTR(path)); + + // collect effect schemas + efxCount = 0; + directory = path.endsWith("/") ? (path + "schema/") : (path + "/schema/"); + QStringList pynames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase); + for (const QString & pyname : pynames) + { + EffectSchema pyEffect; + if (loadEffectSchema(path, pyname, pyEffect)) + { + _effectSchemas.push_back(pyEffect); + efxCount++; + } + } + InfoIf(efxCount > 0, _log, "%d effect schemas loaded from directory %s", efxCount, QSTRING_CSTR((path + "schema/"))); + } + } + + for(auto item : availableEffects) + { + _availableEffects.push_back(item); + } + + ErrorIf(_availableEffects.size()==0, _log, "no effects found, check your effect directories"); + + emit effectListChanged(); +} + +bool EffectFileHandler::loadEffectDefinition(const QString &path, const QString &effectConfigFile, EffectDefinition & effectDefinition) +{ + QString fileName = path + QDir::separator() + effectConfigFile; + + // Read and parse the effect json config file + QJsonObject configEffect; + if(!JsonUtils::readFile(fileName, configEffect, _log)) + return false; + + // validate effect config with effect schema(path) + if(!JsonUtils::validate(fileName, configEffect, ":effect-schema", _log)) + return false; + + // setup the definition + effectDefinition.file = fileName; + QJsonObject config = configEffect; + QString scriptName = config["script"].toString(); + effectDefinition.name = config["name"].toString(); + if (scriptName.isEmpty()) + return false; + + QFile fileInfo(scriptName); + + if (scriptName.mid(0, 1) == ":" ) + { + (!fileInfo.exists()) + ? effectDefinition.script = ":/effects/"+scriptName.mid(1) + : effectDefinition.script = scriptName; + } else + { + (!fileInfo.exists()) + ? effectDefinition.script = path + QDir::separator() + scriptName + : effectDefinition.script = scriptName; + } + + effectDefinition.args = config["args"].toObject(); + effectDefinition.smoothCfg = 1; // pause config + return true; +} + +bool EffectFileHandler::loadEffectSchema(const QString &path, const QString &effectSchemaFile, EffectSchema & effectSchema) +{ + QString fileName = path + "schema/" + QDir::separator() + effectSchemaFile; + + // Read and parse the effect schema file + QJsonObject schemaEffect; + if(!JsonUtils::readFile(fileName, schemaEffect, _log)) + return false; + + // setup the definition + QString scriptName = schemaEffect["script"].toString(); + effectSchema.schemaFile = fileName; + fileName = path + QDir::separator() + scriptName; + QFile pyFile(fileName); + + if (scriptName.isEmpty() || !pyFile.open(QIODevice::ReadOnly)) + { + fileName = path + "schema/" + QDir::separator() + effectSchemaFile; + Error( _log, "Python script '%s' in effect schema '%s' could not be loaded", QSTRING_CSTR(scriptName), QSTRING_CSTR(fileName)); + return false; + } + + pyFile.close(); + + effectSchema.pyFile = (scriptName.mid(0, 1) == ":" ) ? ":/effects/"+scriptName.mid(1) : path + QDir::separator() + scriptName; + effectSchema.pySchema = schemaEffect; + + return true; +} diff --git a/libsrc/effectengine/EffectModule.cpp b/libsrc/effectengine/EffectModule.cpp index 65166c19..528ab5d2 100644 --- a/libsrc/effectengine/EffectModule.cpp +++ b/libsrc/effectengine/EffectModule.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -44,10 +45,9 @@ PyObject *EffectModule::json2python(const QJsonValue &jsonData) return Py_BuildValue(""); case QJsonValue::Double: { - if (std::rint(jsonData.toDouble()) != jsonData.toDouble()) - { + if (std::round(jsonData.toDouble()) != jsonData.toDouble()) return Py_BuildValue("d", jsonData.toDouble()); - } + return Py_BuildValue("i", jsonData.toInt()); } case QJsonValue::Bool: diff --git a/libsrc/hyperion/CaptureCont.cpp b/libsrc/hyperion/CaptureCont.cpp index f50ea338..71baa73c 100644 --- a/libsrc/hyperion/CaptureCont.cpp +++ b/libsrc/hyperion/CaptureCont.cpp @@ -7,6 +7,7 @@ CaptureCont::CaptureCont(Hyperion* hyperion) : QObject() , _hyperion(hyperion) , _systemCaptEnabled(false) + , _systemInactiveTimer(new QTimer(this)) , _v4lCaptEnabled(false) , _v4lInactiveTimer(new QTimer(this)) { @@ -16,6 +17,11 @@ CaptureCont::CaptureCont(Hyperion* hyperion) // comp changes connect(_hyperion, &Hyperion::componentStateChanged, this, &CaptureCont::componentStateChanged); + // inactive timer system + connect(_systemInactiveTimer, &QTimer::timeout, this, &CaptureCont::setSystemInactive); + _systemInactiveTimer->setSingleShot(true); + _systemInactiveTimer->setInterval(5000); + // inactive timer v4l connect(_v4lInactiveTimer, &QTimer::timeout, this, &CaptureCont::setV4lInactive); _v4lInactiveTimer->setSingleShot(true); @@ -38,6 +44,7 @@ void CaptureCont::handleV4lImage(const Image & image) void CaptureCont::handleSystemImage(const Image& image) { + _systemInactiveTimer->start(); _hyperion->setInputImage(_systemCaptPrio, image); } @@ -118,3 +125,8 @@ void CaptureCont::setV4lInactive() { _hyperion->setInputInactive(_v4lCaptPrio); } + +void CaptureCont::setSystemInactive() +{ + _hyperion->setInputInactive(_systemCaptPrio); +} diff --git a/libsrc/hyperion/Hyperion.cpp b/libsrc/hyperion/Hyperion.cpp index 12345e72..c38bc916 100644 --- a/libsrc/hyperion/Hyperion.cpp +++ b/libsrc/hyperion/Hyperion.cpp @@ -95,9 +95,7 @@ Hyperion::Hyperion(HyperionDaemon* daemon, const quint8& instance, const QString , _ledBuffer(_ledString.leds().size(), ColorRgb::BLACK) { if (!_raw2ledAdjustment->verifyAdjustments()) - { Warning(_log, "At least one led has no color calibration, please add all leds from your led layout to an 'LED index' field!"); - } // handle hwLedCount _hwLedCount = qMax(unsigned(getSetting(settings::DEVICE).object()["hardwareLedCount"].toInt(getLedCount())), getLedCount()); @@ -107,6 +105,7 @@ Hyperion::Hyperion(HyperionDaemon* daemon, const quint8& instance, const QString { _ledStringColorOrder.push_back(led.colorOrder); } + for (Led& led : _ledStringClone.leds()) { _ledStringColorOrder.insert(_ledStringColorOrder.begin() + led.index, led.colorOrder); @@ -135,7 +134,7 @@ Hyperion::Hyperion(HyperionDaemon* daemon, const quint8& instance, const QString getComponentRegister().componentStateChanged(hyperion::COMP_LEDDEVICE, _device->componentState()); // create the effect engine and pipe the updateEmit; must be initialized after smoothing! - _effectEngine = new EffectEngine(this,getSetting(settings::EFFECTS).object()); + _effectEngine = new EffectEngine(this); connect(_effectEngine, &EffectEngine::effectListUpdated, this, &Hyperion::effectListUpdated); // setup config state checks and initial shot @@ -178,7 +177,6 @@ void Hyperion::freeObjects(bool emitCloseSignal) { // switch off all leds clearall(true); - _device->switchOff(); if (emitCloseSignal) { @@ -507,9 +505,14 @@ const Hyperion::InputInfo Hyperion::getPriorityInfo(const int priority) const return _muxer.getInputInfo(priority); } -void Hyperion::reloadEffects() +const bool Hyperion::saveEffect(const QJsonObject& obj, QString& resultMsg) { - _effectEngine->readEffects(); + return _effectEngine->saveEffect(obj, resultMsg); +} + +const bool Hyperion::deleteEffect(const QString& effectName, QString& resultMsg) +{ + return _effectEngine->deleteEffect(effectName, resultMsg); } const std::list & Hyperion::getEffects() const @@ -625,12 +628,14 @@ void Hyperion::update() { _ledBuffer = priorityInfo.ledColors; } - // copy rawLedColors before adjustments - _rawLedBuffer = _ledBuffer; + + // emit rawLedColors before transform + emit rawLedColors(_ledBuffer); // apply adjustments if(compChanged) _raw2ledAdjustment->setBacklightEnabled((_prevCompId != hyperion::COMP_COLOR && _prevCompId != hyperion::COMP_EFFECT)); + _raw2ledAdjustment->applyAdjustment(_ledBuffer); // insert cloned leds into buffer diff --git a/libsrc/hyperion/PriorityMuxer.cpp b/libsrc/hyperion/PriorityMuxer.cpp index 40292939..c348a7c3 100644 --- a/libsrc/hyperion/PriorityMuxer.cpp +++ b/libsrc/hyperion/PriorityMuxer.cpp @@ -302,11 +302,10 @@ void PriorityMuxer::setCurrentTime(void) if(infoIt->timeoutTime_ms >= -1) newPriority = qMin(newPriority, infoIt->priority); - // call timeTrigger when effect or color is running with timeout > -1, blacklist prio 255 - if(infoIt->priority < 254 && infoIt->timeoutTime_ms > -1 && (infoIt->componentId == hyperion::COMP_EFFECT || infoIt->componentId == hyperion::COMP_COLOR)) - { + // call timeTrigger when effect or color is running with timeout > 0, blacklist prio 255 + if(infoIt->priority < 254 && infoIt->timeoutTime_ms > 0 && (infoIt->componentId == hyperion::COMP_EFFECT || infoIt->componentId == hyperion::COMP_COLOR)) emit signalTimeTrigger(); // as signal to prevent Threading issues - } + ++infoIt; } } diff --git a/libsrc/hyperion/resource.qrc b/libsrc/hyperion/resource.qrc index 5eeb9163..2d8c1c36 100644 --- a/libsrc/hyperion/resource.qrc +++ b/libsrc/hyperion/resource.qrc @@ -1,6 +1,5 @@ - hyperion-icon_32px.png hyperion.schema.json ../../config/hyperion.config.json.default schema/schema-general.json diff --git a/libsrc/leddevice/dev_net/LedDeviceAurora.cpp b/libsrc/leddevice/dev_net/LedDeviceAurora.cpp index 9e13599a..9ebb91b2 100644 --- a/libsrc/leddevice/dev_net/LedDeviceAurora.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceAurora.cpp @@ -171,7 +171,7 @@ int LedDeviceAurora::write(const std::vector & ledValues) udpbuffer[i++] = panelCount; for (const ColorRgb& color : ledValues) { - if (i & ledValues) udpbuffer[i++] = 0; // W not set manually udpbuffer[i++] = 1; // currently fixed at value 1 which corresponds to 100ms } - if(panelCounter > panelCount) { + if((unsigned)panelCounter > panelCount) { break; } //printf ("c.red %d sz c.red %d\n", color.red, sizeof(color.red)); diff --git a/libsrc/ssdp/SSDPDiscover.cpp b/libsrc/ssdp/SSDPDiscover.cpp index 87130f24..c19d49e6 100644 --- a/libsrc/ssdp/SSDPDiscover.cpp +++ b/libsrc/ssdp/SSDPDiscover.cpp @@ -88,7 +88,7 @@ const QString SSDPDiscover::getFirstService(const searchType& type, const QStrin //Info(_log, "Received msearch response from '%s:%d'. Search target: %s",QSTRING_CSTR(sender.toString()), senderPort, QSTRING_CSTR(headers.value("st"))); if(type == STY_WEBSERVER) { - Info(_log, "Found Hyperion server at: %s:%d", QSTRING_CSTR(url.host()), url.port()); + Info(_log, "Found Hyperion server at: %s:%s", QSTRING_CSTR(url.host()), url.port()); return url.host()+":"+QString::number(url.port()); } @@ -101,7 +101,7 @@ const QString SSDPDiscover::getFirstService(const searchType& type, const QStrin } else { - Info(_log, "Found Hyperion server at: %s:%d", QSTRING_CSTR(url.host()), fbsport); + Info(_log, "Found Hyperion server at: %s:%s", QSTRING_CSTR(url.host()), QSTRING_CSTR(fbsport)); return url.host()+":"+fbsport; } } diff --git a/libsrc/webserver/QtHttpServer.cpp b/libsrc/webserver/QtHttpServer.cpp index bccdb07b..4a118f24 100644 --- a/libsrc/webserver/QtHttpServer.cpp +++ b/libsrc/webserver/QtHttpServer.cpp @@ -1,4 +1,3 @@ - #include "QtHttpServer.h" #include "QtHttpRequest.h" #include "QtHttpReply.h" @@ -9,95 +8,114 @@ const QString & QtHttpServer::HTTP_VERSION = QStringLiteral ("HTTP/1.1"); QtHttpServerWrapper::QtHttpServerWrapper (QObject * parent) - : QTcpServer (parent) - , m_useSsl (false) -{ } - -QtHttpServerWrapper::~QtHttpServerWrapper (void) { } - -void QtHttpServerWrapper::setUseSecure (const bool ssl) { - m_useSsl = ssl; + : QTcpServer (parent) + , m_useSsl (false) +{ } -void QtHttpServerWrapper::incomingConnection (qintptr handle) { - QTcpSocket * sock = (m_useSsl - ? new QSslSocket (this) - : new QTcpSocket (this)); - if (sock->setSocketDescriptor (handle)) { - addPendingConnection (sock); - } - else { - delete sock; - } +QtHttpServerWrapper::~QtHttpServerWrapper (void) +{ +} + +void QtHttpServerWrapper::setUseSecure (const bool ssl) { + m_useSsl = ssl; +} + +void QtHttpServerWrapper::incomingConnection (qintptr handle) +{ + QTcpSocket * sock = (m_useSsl + ? new QSslSocket (this) + : new QTcpSocket (this)); + (sock->setSocketDescriptor (handle)) + ? addPendingConnection (sock) + : delete sock; } QtHttpServer::QtHttpServer (QObject * parent) - : QObject (parent) - , m_useSsl (false) - , m_serverName (QStringLiteral ("The Qt5 HTTP Server")) + : QObject (parent) + , m_useSsl (false) + , m_serverName (QStringLiteral ("The Qt5 HTTP Server")) { - m_sockServer = new QtHttpServerWrapper (this); - connect (m_sockServer, &QtHttpServerWrapper::newConnection, this, &QtHttpServer::onClientConnected); + m_sockServer = new QtHttpServerWrapper (this); + connect (m_sockServer, &QtHttpServerWrapper::newConnection, this, &QtHttpServer::onClientConnected); } -const QString & QtHttpServer::getServerName (void) const { - return m_serverName; +const QString & QtHttpServer::getServerName (void) const +{ + return m_serverName; } -quint16 QtHttpServer::getServerPort (void) const { - return m_sockServer->serverPort (); +quint16 QtHttpServer::getServerPort (void) const +{ + return m_sockServer->serverPort (); } -QString QtHttpServer::getErrorString (void) const { - return m_sockServer->errorString (); +QString QtHttpServer::getErrorString (void) const +{ + return m_sockServer->errorString (); } -void QtHttpServer::start (quint16 port) { +void QtHttpServer::start (quint16 port) +{ if(!m_sockServer->isListening()) + (m_sockServer->listen (QHostAddress::Any, port)) + ? emit started (m_sockServer->serverPort ()) + : emit error (m_sockServer->errorString ()); +} + +void QtHttpServer::stop (void) +{ + if (m_sockServer->isListening ()) { - if (m_sockServer->listen (QHostAddress::Any, port)) { - emit started (m_sockServer->serverPort ()); - } - else { - emit error (m_sockServer->errorString ()); + m_sockServer->close (); + + // disconnect clients + const QList socks = m_socksClientsHash.keys(); + for(auto sock : socks) + { + sock->close(); } + + emit stopped (); } } -void QtHttpServer::stop (void) { - if (m_sockServer->isListening ()) { - m_sockServer->close (); - emit stopped (); - } +void QtHttpServer::setServerName (const QString & serverName) +{ + m_serverName = serverName; } -void QtHttpServer::setServerName (const QString & serverName) { - m_serverName = serverName; +void QtHttpServer::setUseSecure (const bool ssl) +{ + m_useSsl = ssl; + m_sockServer->setUseSecure (m_useSsl); } -void QtHttpServer::setUseSecure (const bool ssl) { - m_useSsl = ssl; - m_sockServer->setUseSecure (m_useSsl); +void QtHttpServer::setPrivateKey (const QSslKey & key) +{ + m_sslKey = key; } -void QtHttpServer::setPrivateKey (const QSslKey & key) { - m_sslKey = key; +void QtHttpServer::setCertificates (const QList & certs) +{ + m_sslCerts = certs; } -void QtHttpServer::setCertificates (const QList & certs) { - m_sslCerts = certs; -} - -void QtHttpServer::onClientConnected (void) { - while (m_sockServer->hasPendingConnections ()) { - if (QTcpSocket * sock = m_sockServer->nextPendingConnection ()) { +void QtHttpServer::onClientConnected (void) +{ + while (m_sockServer->hasPendingConnections ()) + { + if (QTcpSocket * sock = m_sockServer->nextPendingConnection ()) + { connect (sock, &QTcpSocket::disconnected, this, &QtHttpServer::onClientDisconnected); - if (m_useSsl) { - if (QSslSocket * ssl = qobject_cast (sock)) { + if (m_useSsl) + { + if (QSslSocket * ssl = qobject_cast (sock)) + { connect (ssl, SslErrorSignal (&QSslSocket::sslErrors), this, &QtHttpServer::onClientSslErrors); - connect (ssl, &QSslSocket::encrypted, this, &QtHttpServer::onClientSslEncrypted); - connect (ssl, &QSslSocket::peerVerifyError, this, &QtHttpServer::onClientSslPeerVerifyError); - connect (ssl, &QSslSocket::modeChanged, this, &QtHttpServer::onClientSslModeChanged); + connect (ssl, &QSslSocket::encrypted, this, &QtHttpServer::onClientSslEncrypted); + connect (ssl, &QSslSocket::peerVerifyError, this, &QtHttpServer::onClientSslPeerVerifyError); + connect (ssl, &QSslSocket::modeChanged, this, &QtHttpServer::onClientSslModeChanged); ssl->setLocalCertificateChain (m_sslCerts); ssl->setPrivateKey (m_sslKey); ssl->setPeerVerifyMode (QSslSocket::AutoVerifyPeer); @@ -107,30 +125,38 @@ void QtHttpServer::onClientConnected (void) { QtHttpClientWrapper * wrapper = new QtHttpClientWrapper (sock, this); m_socksClientsHash.insert (sock, wrapper); emit clientConnected (wrapper->getGuid ()); - } - } + } + } } -void QtHttpServer::onClientSslEncrypted (void) { } - -void QtHttpServer::onClientSslPeerVerifyError (const QSslError & err) { - Q_UNUSED (err) +void QtHttpServer::onClientSslEncrypted (void) +{ } -void QtHttpServer::onClientSslErrors (const QList & errors) { - Q_UNUSED (errors) +void QtHttpServer::onClientSslPeerVerifyError (const QSslError & err) +{ + Q_UNUSED (err) } -void QtHttpServer::onClientSslModeChanged (QSslSocket::SslMode mode) { - Q_UNUSED (mode) +void QtHttpServer::onClientSslErrors (const QList & errors) +{ + Q_UNUSED (errors) } -void QtHttpServer::onClientDisconnected (void) { - if (QTcpSocket * sockClient = qobject_cast (sender ())) { - if (QtHttpClientWrapper * wrapper = m_socksClientsHash.value (sockClient, Q_NULLPTR)) { - emit clientDisconnected (wrapper->getGuid ()); - wrapper->deleteLater (); - m_socksClientsHash.remove (sockClient); - } - } +void QtHttpServer::onClientSslModeChanged (QSslSocket::SslMode mode) +{ + Q_UNUSED (mode) +} + +void QtHttpServer::onClientDisconnected (void) +{ + if (QTcpSocket * sockClient = qobject_cast (sender ())) + { + if (QtHttpClientWrapper * wrapper = m_socksClientsHash.value (sockClient, Q_NULLPTR)) + { + emit clientDisconnected (wrapper->getGuid ()); + wrapper->deleteLater (); + m_socksClientsHash.remove (sockClient); + } + } } diff --git a/libsrc/webserver/WebServer.cpp b/libsrc/webserver/WebServer.cpp index 876a025c..c570736a 100644 --- a/libsrc/webserver/WebServer.cpp +++ b/libsrc/webserver/WebServer.cpp @@ -13,12 +13,11 @@ WebServer::WebServer(const QJsonDocument& config, QObject * parent) - : QObject(parent) + : QObject(parent) , _config(config) , _log(Logger::getInstance("WEBSERVER")) , _server() { - } WebServer::~WebServer() diff --git a/libsrc/webserver/WebSocketClient.cpp b/libsrc/webserver/WebSocketClient.cpp index 938175a6..7a9c2cdd 100644 --- a/libsrc/webserver/WebSocketClient.cpp +++ b/libsrc/webserver/WebSocketClient.cpp @@ -227,8 +227,8 @@ void WebSocketClient::sendClose(int status, QString reason) void WebSocketClient::handleBinaryMessage(QByteArray &data) { - uint8_t priority = data.at(0); - unsigned duration_s = data.at(1); + //uint8_t priority = data.at(0); + //unsigned duration_s = data.at(1); unsigned imgSize = data.size() - 4; unsigned width = ((data.at(2) << 8) & 0xFF00) | (data.at(3) & 0xFF); unsigned height = imgSize / width; @@ -244,7 +244,7 @@ void WebSocketClient::handleBinaryMessage(QByteArray &data) memcpy(image.memptr(), data.data()+4, imgSize); //_hyperion->registerInput(); - _hyperion->setInputImage(priority, image, duration_s*1000); + //_hyperion->setInputImage(priority, image, duration_s*1000); } qint64 WebSocketClient::sendMessage(QJsonObject obj) diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt new file mode 100644 index 00000000..0305dec7 --- /dev/null +++ b/resources/CMakeLists.txt @@ -0,0 +1,22 @@ +# Global resource file to embed static data into code, available as static lib 'resources'. File names needs to be unique in the /resources directory and below. +# All files are available with their file name by calling ":/FILENAME". Probably you need to call Q_INIT_RESOURCE("resources") once +# +# Define the current source locations +SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/resources) + +# catch all files +FILE ( GLOB Hyperion_RESFILES "${CURRENT_SOURCE_DIR}/icons/*" ) + +# fill resources.qrc with RESFILES +FOREACH( f ${Hyperion_RESFILES} ) + get_filename_component(fname ${f} NAME) + SET(HYPERION_RES "${HYPERION_RES}\n\t\t${f}") +ENDFOREACH() + +# prep file +CONFIGURE_FILE(${CURRENT_SOURCE_DIR}/resources.qrc.in ${CMAKE_BINARY_DIR}/resources.qrc ) +SET(Hyperion_RES ${CMAKE_BINARY_DIR}/resources.qrc) + +add_library(resources + ${Hyperion_RES} +) diff --git a/libsrc/hyperion/hyperion-icon_32px.png b/resources/icons/hyperion-icon-32px.png similarity index 100% rename from libsrc/hyperion/hyperion-icon_32px.png rename to resources/icons/hyperion-icon-32px.png diff --git a/resources/resources.qrc.in b/resources/resources.qrc.in new file mode 100644 index 00000000..e15247f3 --- /dev/null +++ b/resources/resources.qrc.in @@ -0,0 +1,5 @@ + + + ${HYPERION_RES} + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 428a45e6..cac3a381 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,7 +1,7 @@ add_subdirectory(hyperiond) add_subdirectory(hyperion-remote) -# The following clients depend on the protobuf library +# The following binaries are just compiled if requested if (ENABLE_AMLOGIC) add_subdirectory(hyperion-aml) endif() diff --git a/src/hyperion-aml/CMakeLists.txt b/src/hyperion-aml/CMakeLists.txt index 4d7c1315..861e46bf 100644 --- a/src/hyperion-aml/CMakeLists.txt +++ b/src/hyperion-aml/CMakeLists.txt @@ -23,9 +23,7 @@ add_executable(${PROJECT_NAME} ) target_link_libraries(${PROJECT_NAME} - effectengine commandline - blackborder hyperion-utils flatbufserver flatbuffers diff --git a/src/hyperion-aml/hyperion-aml.cpp b/src/hyperion-aml/hyperion-aml.cpp index 21f387ca..02a7aca1 100644 --- a/src/hyperion-aml/hyperion-aml.cpp +++ b/src/hyperion-aml/hyperion-aml.cpp @@ -37,7 +37,7 @@ int main(int argc, char ** argv) // create the option parser and initialize all parser Parser parser("AmLogic capture application for Hyperion. Will automatically search a Hyperion server if -a option isn't used. Please note that if you have more than one server running it's more or less random which one will be used."); - IntOption & argFps = parser.add ('f', "framerate", "Capture frame rate [default: %1]", "10"); + IntOption & argFps = parser.add ('f', "framerate", "Capture frame rate [default: %1]", "10", 1, 25); IntOption & argWidth = parser.add (0x0, "width", "Width of the captured image [default: %1]", "160", 160, 4096); IntOption & argHeight = parser.add (0x0, "height", "Height of the captured image [default: %1]", "160", 160, 4096); BooleanOption & argScreenshot = parser.add(0x0, "screenshot", "Take a single screenshot, save it to file and quit"); diff --git a/src/hyperion-dispmanx/CMakeLists.txt b/src/hyperion-dispmanx/CMakeLists.txt index 52300bce..ad81ea16 100644 --- a/src/hyperion-dispmanx/CMakeLists.txt +++ b/src/hyperion-dispmanx/CMakeLists.txt @@ -30,9 +30,7 @@ add_executable( ${PROJECT_NAME} ) target_link_libraries( ${PROJECT_NAME} - effectengine commandline - blackborder hyperion-utils flatbufserver flatbuffers diff --git a/src/hyperion-dispmanx/hyperion-dispmanx.cpp b/src/hyperion-dispmanx/hyperion-dispmanx.cpp index 0f626023..3a8cfe08 100644 --- a/src/hyperion-dispmanx/hyperion-dispmanx.cpp +++ b/src/hyperion-dispmanx/hyperion-dispmanx.cpp @@ -36,9 +36,9 @@ int main(int argc, char ** argv) // create the option parser and initialize all parameters Parser parser("Dispmanx capture application for Hyperion. Will automatically search a Hyperion server if -a option isn't used. Please note that if you have more than one server running it's more or less random which one will be used."); - IntOption & argFps = parser.add ('f', "framerate", "Capture frame rate [default: %1]", "10"); - IntOption & argWidth = parser.add (0x0, "width", "Width of the captured image [default: %1]", "64", 32, 4096); - IntOption & argHeight = parser.add (0x0, "height", "Height of the captured image [default: %1]", "64", 32, 4096); + IntOption & argFps = parser.add ('f', "framerate", "Capture frame rate [default: %1]", "10", 1, 25); + IntOption & argWidth = parser.add (0x0, "width", "Width of the captured image [default: %1]", "64", 64); + IntOption & argHeight = parser.add (0x0, "height", "Height of the captured image [default: %1]", "64", 64); BooleanOption & argScreenshot = parser.add(0x0, "screenshot", "Take a single screenshot, save it to file and quit"); Option & argAddress = parser.add