diff --git a/.gitignore b/.gitignore index 029eb5ba..1a539693 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,15 @@ NULL # Docker deploy folder deploy/* + +# ccache/buildcache +.*cache/ + +# release-deps/debug-deps +*-deps/ + +# User defined CMake preset file. +CMakeUserPresets.json + +#Configurations created under config for testing +/configs diff --git a/.version b/.version index 40259166..0f238926 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.0.17-beta.1 +2.0.17-beta.2 diff --git a/.vs/launch.vs.json b/.vs/launch.vs.json index 37627ab1..d2573ae3 100644 --- a/.vs/launch.vs.json +++ b/.vs/launch.vs.json @@ -6,12 +6,42 @@ "type": "default", "project": "CMakeLists.txt", "projectTarget": "hyperiond.exe (bin\\hyperiond.exe)", - "name": "Run hyperion with debug option and external console", + "name": "Run Hyperion" + }, + { + "type": "default", + "project": "CMakeLists.txt", + "projectTarget": "hyperiond.exe (bin\\hyperiond.exe)", + "name": "Run hyperion with debug logging and external console", "args": [ "-d", "-c" ], "externalConsole": true + }, + { + "type": "default", + "project": "CMakeLists.txt", + "projectTarget": "hyperiond.exe (bin\\hyperiond.exe)", + "name": "Run hyperion with verbose logging with external console", + "args": [ + "-v", + "-c" + ], + "externalConsole": true + }, + { + "type": "default", + "project": "CMakeLists.txt", + "projectTarget": "hyperiond.exe (bin\\hyperiond.exe)", + "name": "Run hyperion with debug logging and a test configuration DB", + "args": [ + "-d", + "-c", + "-u", + "${workspaceRoot}\\configs\\testConfig" + ], + "externalConsole": true } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index c892f259..d605e4ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,14 +10,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **JSON-API** - Align JSON subscription update elements. `ledcolors-imagestream-update, ledcolors-ledstream-update, logmsg-update` now return data via `data` and not `result +- Global global configuration elements are now separated form instance specific ones ### Added - Support for ftdi chip based LED-devices with ws2812, sk6812 apa102 LED types (Many thanks to @nurikk) (#1746) -- Support for Skydimo devices (being an Adalight variant) +- Support for Skydimo devices - Support gaps on Matrix Layout (#1696) - Windows: Added a new grabber that uses the DXGI DDA (Desktop Duplication API). This has much better performance than the DX grabber as it does more of its work on the GPU. +- Support to import, export and backup Hyperion's full configuration via the UI, JSON-API and commandline (`--importConfig, --exportConfig`) (#804) +- Allow to force starting Hyperion in read-only mode (`--readonlyMode`) +- JSON-API: Support to query for a dedicated set of configuration items for a set of instances +- JSON-API: Support to save a dedicated set of configuration items for a set of instances + **JSON-API** - New subscription support for event updates, i.e. `Suspend, Resume, Idle, idleResume, Restart, Quit`. - Support direct or multiple instance addressing via single requests (#809) @@ -33,12 +39,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed: Kodi Color Calibration, Refactor Wizards (#1674) - Fixed: Token Dialog not closing - Fixed: Philip Hue APIv2 support without Entertainment group defined (#1742) +- Refactored: Database access layer +- Refactored: Hyperion's configuration database is validated before start-up (and migrated, if required) +- Refactored: Python to enable parallel effect processing under Python 3.12 +- Fixed: Python 3.12 crashes (#1747) +- osX Grabber: Use ScreenCaptureKit under macOS 15 and above **JSON-API** - Refactored JSON-API to ensure consistent authorization behaviour across sessions and single requests with token authorization. - Provide additional error details with API responses, esp. on JSON parsing, validation or token errors. - Generate random TANs for every API request from the Hyperion UI - Fixed: Handling of IP4 addresses wrapped in IPv6 for external network connections- +- Fixed: Local Admin API Authentication rejects valid tokens (#1251) ### Removed diff --git a/CMakeLists.txt b/CMakeLists.txt index d27cbc4f..50e5faa8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,11 +20,10 @@ PROJECT(hyperion) include (${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.cmake) file (STRINGS ".version" HYPERION_VERSION) SetVersionNumber(HYPERION ${HYPERION_VERSION}) -set(DEFAULT_JSON_CONFIG_FILE ${CMAKE_CURRENT_SOURCE_DIR}/config/hyperion.config.json.default) +set(DEFAULT_JSON_CONFIG_FILE ${CMAKE_CURRENT_SOURCE_DIR}/settings/hyperion.settings.json.default) file(READ ${DEFAULT_JSON_CONFIG_FILE} DEFAULT_JSON_CONFIG_VAR) string(REPLACE "configVersionValue" ${HYPERION_VERSION} DEFAULT_JSON_CONFIG_VAR "${DEFAULT_JSON_CONFIG_VAR}") -string(REPLACE "previousVersionValue" ${HYPERION_VERSION} DEFAULT_JSON_CONFIG_VAR "${DEFAULT_JSON_CONFIG_VAR}") -file(WRITE ${CMAKE_BINARY_DIR}/config/hyperion.config.json.default "${DEFAULT_JSON_CONFIG_VAR}") +file(WRITE ${CMAKE_BINARY_DIR}/settings/hyperion.settings.json.default "${DEFAULT_JSON_CONFIG_VAR}") # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) @@ -227,7 +226,7 @@ message(STATUS "HYPERION_LIGHT = ${HYPERION_LIGHT}") if(HYPERION_LIGHT) message(STATUS "HYPERION_LIGHT: Hyperion is build with a reduced set of functionality.") - # Disable Grabbers + # Disable Screen/Video Grabbers SET ( DEFAULT_AMLOGIC OFF ) SET ( DEFAULT_DISPMANX OFF ) SET ( DEFAULT_DX OFF ) @@ -237,17 +236,20 @@ if(HYPERION_LIGHT) SET ( DEFAULT_OSX OFF ) SET ( DEFAULT_QT OFF ) SET ( DEFAULT_V4L2 OFF ) - SET ( DEFAULT_AUDIO OFF ) SET ( DEFAULT_X11 OFF ) SET ( DEFAULT_XCB OFF ) + # Disable Audio Grabbers + SET ( DEFAULT_AUDIO OFF ) + # LED-Devices - SET ( DEFAULT_DEV_NETWORK OFF ) - SET ( DEFAULT_DEV_SERIAL OFF ) - SET ( DEFAULT_DEV_SPI OFF ) - SET ( DEFAULT_DEV_TINKERFORGE OFF ) - SET ( DEFAULT_DEV_USB_HID OFF ) - SET ( DEFAULT_DEV_WS281XPWM OFF ) + #SET ( DEFAULT_DEV_NETWORK OFF ) + #SET ( DEFAULT_DEV_FTDI OFF ) + #SET ( DEFAULT_DEV_SERIAL OFF ) + #SET ( DEFAULT_DEV_SPI OFF ) + #SET ( DEFAULT_DEV_TINKERFORGE OFF ) + #SET ( DEFAULT_DEV_USB_HID OFF ) + #SET ( DEFAULT_DEV_WS281XPWM OFF ) # Disable Input Servers SET ( DEFAULT_BOBLIGHT_SERVER OFF ) @@ -260,13 +262,14 @@ if(HYPERION_LIGHT) SET ( DEFAULT_FLATBUF_CONNECT OFF ) # Disable Services - SET ( DEFAULT_EXPERIMENTAL OFF ) - SET ( DEFAULT_MDNS ON ) - SET ( DEFAULT_REMOTE_CTL OFF ) SET ( DEFAULT_EFFECTENGINE OFF ) + SET ( DEFAULT_EXPERIMENTAL OFF ) + #SET ( DEFAULT_MDNS OFF ) + SET ( DEFAULT_REMOTE_CTL OFF ) + SET ( ENABLE_JSONCHECKS ON ) + SET ( ENABLE_DEPLOY_DEPENDENCIES ON ) endif() - message(STATUS "Grabber options:") addIndent(" - ") @@ -441,7 +444,7 @@ endif() if(ENABLE_JSONCHECKS) # check all json files file (GLOB_RECURSE HYPERION_SCHEMAS RELATIVE ${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/libsrc/*schema*.json) - set(JSON_FILES ${CMAKE_BINARY_DIR}/config/hyperion.config.json.default ${HYPERION_SCHEMAS}) + set(JSON_FILES ${CMAKE_BINARY_DIR}/settings/hyperion.settings.json.default ${HYPERION_SCHEMAS}) execute_process ( COMMAND ${PYTHON_EXECUTABLE} test/jsonchecks/checkjson.py ${JSON_FILES} @@ -464,7 +467,7 @@ if(ENABLE_JSONCHECKS) endif() execute_process ( - COMMAND ${PYTHON_EXECUTABLE} test/jsonchecks/checkschema.py ${CMAKE_BINARY_DIR}/config/hyperion.config.json.default libsrc/hyperion/hyperion.schema.json + COMMAND ${PYTHON_EXECUTABLE} test/jsonchecks/checkschema.py ${CMAKE_BINARY_DIR}/settings/hyperion.settings.json.default libsrc/hyperion/schema/schema-settings-default.json WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} RESULT_VARIABLE CHECK_CONFIG_FAILED ) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 00000000..1f0dc222 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,317 @@ +{ + "version": 8, + "configurePresets": [ + { + "name": "base", + "description": "Base settings that apply to all configurations", + "hidden": true, + "binaryDir": "${sourceDir}/build" + }, + { + "name": "ccache", + "description": "Set ccache variables", + "hidden": true, + "cacheVariables": { + "USE_COMPILER_CACHE": "ON" + }, + "environment": { + "CCACHE_NODEBUG": "true", + "CCACHE_DIRECT": "true", + "CCACHE_MAXSIZE": "500M", + "CCACHE_COMPRESS": "true", + "CCACHE_COMPRESSLEVEL": "1", + "CCACHE_NOSTATS": "true", + "CCACHE_DIR": "${sourceDir}/.ccache" + } + }, + { + "name": "buildcache", + "description": "Set buildcache variables", + "hidden": true, + "cacheVariables": { + "USE_COMPILER_CACHE": "ON" + }, + "environment": { + "BUILDCACHE_DEBUG": "-1", + "BUILDCACHE_DIRECT_MODE": "true", + "BUILDCACHE_MAX_CACHE_SIZE": "524288000", + "BUILDCACHE_COMPRESS": "true", + "BUILDCACHE_COMPRESS_FORMAT": "LZ4", + "BUILDCACHE_DIR": "${sourceDir}/.buildcache" + } + }, + { + "name": "hyperion-light", + "hidden": true, + "cacheVariables": { + "HYPERION_LIGHT": "ON" + } + }, + { + "name": "hyperion-bare-minimum", + "hidden": true, + "cacheVariables": { + "ENABLE_AMLOGIC": "OFF", + "ENABLE_DDA": "OFF", + "ENABLE_DISPMANX": "OFF", + "ENABLE_DX": "OFF", + "ENABLE_FB": "OFF", + "ENABLE_MF": "OFF", + "ENABLE_OSX": "OFF", + "ENABLE_QT": "OFF", + "ENABLE_V4L2": "OFF", + "ENABLE_X11": "OFF", + "ENABLE_XCB": "OFF", + "ENABLE_AUDIO": "OFF", + "ENABLE_DEV_FTDI": "OFF", + "ENABLE_DEV_NETWORK": "OFF", + "ENABLE_DEV_SERIAL": "ON", + "ENABLE_DEV_SPI": "OFF", + "ENABLE_DEV_TINKERFORGE": "OFF", + "ENABLE_DEV_USB_HID": "OFF", + "ENABLE_DEV_WS281XPWM": "OFF", + "ENABLE_BOBLIGHT_SERVER": "OFF", + "ENABLE_CEC": "OFF", + "ENABLE_FLATBUF_SERVER": "OFF", + "ENABLE_PROTOBUF_SERVER": "OFF", + "ENABLE_FORWARDER": "OFF", + "ENABLE_FLATBUF_CONNECT": "OFF", + "ENABLE_EXPERIMENTAL": "OFF", + "ENABLE_MDNS": "OFF", + "ENABLE_REMOTE_CTL": "OFF", + "ENABLE_EFFECTENGINE": "OFF", + "ENABLE_JSONCHECKS": "ON", + "ENABLE_DEPLOY_DEPENDENCIES": "ON" + } + }, + { + "name": "debug", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "release", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "clang", + "hidden": true, + "generator": "Ninja", + "cacheVariables": { + "CMAKE_C_COMPILER": "/usr/bin/clang", + "CMAKE_CXX_COMPILER": "/usr/bin/clang++" + } + }, + { + "name": "msvc", + "hidden": true, + "generator": "Visual Studio 17 2022", + "architecture": "x64", + "toolset": "host=x64" + }, + { + "name": "gcc", + "hidden": true, + "generator": "Ninja", + "cacheVariables": { + "CMAKE_C_COMPILER": "/usr/bin/gcc", + "CMAKE_CXX_COMPILER": "/usr/bin/g++" + } + }, + { + "name": "macos-release", + "displayName": "MacOS (release) (clang)", + "description": "Build with Clang as Release without Debug Symbols", + "inherits": [ "base", "release", "clang" ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "macos-release-deps", + "displayName": "MacOS Dependencies (release) (clang)", + "description": "Build Dependencies with Clang as Release without Debug Symbols", + "inherits": [ "macos-release" ], + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/${presetName}" + } + }, + { + "name": "macos-debug", + "displayName": "MacOS Debug (clang)", + "description": "Build with Clang with Debug Symbols", + "inherits": [ "base", "debug", "clang" ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "macos-debug-deps", + "displayName": "MacOS Dependencies (debug) (clang)", + "description": "Build Dependencies with Clang with Debug Symbols", + "inherits": [ "macos-debug" ], + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/${presetName}" + } + }, + { + "name": "windows-release", + "displayName": "Windows Release (msvc)", + "description": "Build with MSVC's CL as Release without Debug Symbols", + "inherits": [ "base", "msvc" ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows-debug", + "displayName": "Windows Debug (msvc)", + "description": "Build with MSVC's CL with Debug Symbols", + "inherits": [ "base", "msvc" ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "linux-release", + "displayName": "Linux Release (gcc)", + "description": "Build with GCC as Release without Debug Symbols", + "inherits": [ "base", "release", "gcc" ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "linux-debug", + "displayName": "Linux Debug (gcc)", + "description": "Build with GCC with Debug Symbols", + "inherits": [ "base", "debug", "gcc" ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + } + ], + "buildPresets": [ + { + "name": "macos-release", + "displayName": "MacOS (release) (clang)", + "description": "Build with Clang as Release without Debug Symbols", + "targets": "all", + "configurePreset": "macos-release", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + + }, + { + "name": "macos-release-deps", + "displayName": "MacOS Dependencies (release) (clang)", + "description": "Build Dependencies with Clang as Release without Debug Symbols", + "targets": "dependencies/all", + "configurePreset": "macos-release-deps", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + + }, + { + "name": "macos-debug", + "displayName": "MacOS (debug) (clang)", + "description": "Build with Clang as Debug", + "targets": "all", + "configurePreset": "macos-debug", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + + }, + { + "name": "macos-debug-deps", + "displayName": "MacOS Dependencies (debug) (clang)", + "description": "Build Dependencies with Clang as Debug", + "targets": "dependencies/all", + "configurePreset": "macos-debug-deps", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "windows-release", + "displayName": "Windows (release) (msvc)", + "description": "Build with MSVC's CL as Release without Debug Symbols", + "configuration": "Release", + "targets": "ALL_BUILD", + "configurePreset": "windows-release", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + + }, + { + "name": "windows-debug", + "displayName": "Windows (debug) (msvc)", + "description": "Build with MSVC's CL with Debug Symbols", + "configuration": "Debug", + "targets": "ALL_BUILD", + "configurePreset": "windows-debug", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + + }, + { + "name": "linux-release", + "displayName": "Linux (release) (gcc)", + "description": "Build with GCC as Release without Debug Symbols", + "targets": "all", + "configurePreset": "linux-release", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + + }, + { + "name": "linux-debug", + "displayName": "Linux (debug) (gcc)", + "description": "Build with GCC with Debug Symbols", + "targets": "all", + "configurePreset": "linux-debug", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + } + ] +} diff --git a/CMakeSettings.json b/CMakeSettings.json deleted file mode 100644 index a6c02f16..00000000 --- a/CMakeSettings.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "configurations": [ - { - "name": "x64-Debug", - "generator": "Ninja", - "configurationType": "Debug", - "inheritEnvironments": [ "msvc_x64_x64" ], - "buildRoot": "${projectDir}\\build-${name}", - "installRoot": "${projectDir}\\install-${name}", - "cmakeCommandArgs": "", - "buildCommandArgs": "", - "ctestCommandArgs": "", - "variables": [ - // Replace path with your TurboJPEG installation path - //{ "name": "TurboJPEG_INCLUDE_DIRS", "type": "PATH", "value": "YourTurboJPEGPath/libjpeg-turbo64/include" }, - //{ "name": "TurboJPEG_LIBRARY", "value": "YourTurboJPEGPath/libjpeg-turbo64/lib/turbojpeg.lib", "type": "FILEPATH" } - ], - "environments": [ - // Replace path with your installation path - //{ "QTDIR": "C:/Qt/6.2.2/msvc2019_64/" }, - //{ "VULKAN_SDK": "C:/VulkanSDK/1.2.182.0" } - ] - }, - { - "name": "x64-Release", - "generator": "Ninja", - "configurationType": "Release", - "buildRoot": "${projectDir}\\build-${name}", - "installRoot": "${projectDir}\\install-${name}", - "cmakeCommandArgs": "", - "buildCommandArgs": "package", - "ctestCommandArgs": "", - "inheritEnvironments": [ "msvc_x64_x64" ], - "variables": [ - // Replace path with your TurboJPEG installation path - //{ "name": "TurboJPEG_INCLUDE_DIRS", "type": "PATH", "value": "YourTurboJPEGPath/libjpeg-turbo64/include" }, - //{ "name": "TurboJPEG_LIBRARY", "value": "YourTurboJPEGPath/libjpeg-turbo64/lib/turbojpeg.lib", "type": "FILEPATH" } - ], - "environments": [ - // Replace path with your installation path - //{ "QTDIR": "C:/Qt/6.2.2/msvc2019_64/" }, - //{ "VULKAN_SDK": "C:/VulkanSDK/1.2.182.0" } - ] - } - ] -} diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 852fe202..14fae3e6 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -715,7 +715,6 @@ "edt_dev_spec_port_expl": "Service Port [1-65535]", "edt_dev_spec_port_title": "Port", "edt_dev_spec_printTimeStamp_title": "Add timestamp", - "edt_dev_spec_skydimo_mode_title": "Skydimo Mode", "edt_dev_spec_stream_protocol_title": "Streaming protocol", "edt_dev_spec_pwmChannel_title": "PWM channel", "edt_dev_spec_razer_device_title": "Razer Chroma Device", @@ -1019,8 +1018,8 @@ "infoDialog_import_comperror_text": "Sad! Your browser doesn't support importing. Please try again with another browser.", "infoDialog_import_confirm_text": "Are you sure to import \"$1\"? This process can't be reverted!", "infoDialog_import_confirm_title": "Confirm import", - "infoDialog_import_hyperror_text": "The selected configuration file \"$1\" can't be imported. It's not compatible with Hyperion 2.0 and higher!", "infoDialog_import_jsonerror_text": "The selected configuration file \"$1\" is not a .json file, or it's corrupted. Error message: ($2)", + "infoDialog_import_version_error_text": "The selected configuration file \"$1\" can not be imported. It's not compatible with Hyperion 2.0.17 and higher!", "infoDialog_wizrgb_text": "Your RGB Byte Order is already well adjusted.", "infoDialog_writeconf_error_text": "Saving your configuration failed.", "infoDialog_writeimage_error_text": "The selected file \"$1\" is not an image file, or it's corrupted! Please select another image file.", diff --git a/assets/webconfig/js/content_general.js b/assets/webconfig/js/content_general.js index 3a25045a..6b732e46 100644 --- a/assets/webconfig/js/content_general.js +++ b/assets/webconfig/js/content_general.js @@ -28,11 +28,6 @@ $(document).ready(function () { // Instance handling function handleInstanceRename(e) { - conf_editor.on('change', function () { - window.readOnlyMode ? $('#btn_cl_save').prop('disabled', true) : $('#btn_submit').prop('disabled', false); - window.readOnlyMode ? $('#btn_ma_save').prop('disabled', true) : $('#btn_submit').prop('disabled', false); - }); - var inst = e.currentTarget.id.split("_")[1]; showInfoDialog('renInst', $.i18n('conf_general_inst_renreq_t'), getInstanceNameByIndex(inst)); @@ -119,14 +114,14 @@ $(document).ready(function () { //check file is json var check = isJsonString(content); if (check.length != 0) { - showInfoDialog('error', "", $.i18n('infoDialog_import_jsonerror_text', f.name, JSON.stringify(check))); + showInfoDialog('error', "", $.i18n('infoDialog_import_jsonerror_text', f.name, JSON.stringify(check.message))); dis_imp_btn(true); } else { content = JSON.parse(content); //check for hyperion json - if (typeof content.leds === 'undefined' || typeof content.general === 'undefined') { - showInfoDialog('error', "", $.i18n('infoDialog_import_hyperror_text', f.name)); + if (typeof content.global === 'undefined' || typeof content.instances === 'undefined') { + showInfoDialog('error', "", $.i18n('infoDialog_import_version_error_text', f.name)); dis_imp_btn(true); } else { @@ -143,10 +138,10 @@ $(document).ready(function () { $('#btn_import_conf').off().on('click', function () { showInfoDialog('import', $.i18n('infoDialog_import_confirm_title'), $.i18n('infoDialog_import_confirm_text', confName)); - $('#id_btn_import').off().on('click', function () { + $('#id_btn_import').off().on('click', function () { requestRestoreConfig(importedConf); - setTimeout(initRestart, 100); }); + }); $('#select_import_conf').off().on('change', function (e) { @@ -157,18 +152,17 @@ $(document).ready(function () { }); //export - $('#btn_export_conf').off().on('click', function () { - var name = window.serverConfig.general.name; + $('#btn_export_conf').off().on('click', async () => + { + const d = new Date(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const timestamp = `${d.getFullYear()}-${month}-${day}`; - var d = new Date(); - var month = d.getMonth() + 1; - var day = d.getDate(); - - var timestamp = d.getFullYear() + '.' + - (month < 10 ? '0' : '') + month + '.' + - (day < 10 ? '0' : '') + day; - - download(JSON.stringify(window.serverConfig, null, "\t"), 'Hyperion-' + window.currentVersion + '-Backup (' + name + ') ' + timestamp + '.json', "application/json"); + const configBackup = await requestConfig(); + if (configBackup.success === true) { + download(JSON.stringify(configBackup.info, null, "\t"), 'HyperionBackup-' + timestamp + '_v' + window.currentVersion + '.json', "application/json"); + } }); //create introduction @@ -180,3 +174,8 @@ $(document).ready(function () { removeOverlay(); }); + +$(window.hyperion).on("cmd-config-restoreconfig", function (event) { + setTimeout(initRestart, 100); +}); + diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index 70d9ce18..1cb2060f 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -128,7 +128,7 @@ $(document).ready(function () { requestSysInfo(); }); - $(window.hyperion).on("cmd-config-getconfig", function (event) { + $(window.hyperion).on("cmd-config-getconfig-old", function (event) { window.serverConfig = event.response.info; window.showOptHelp = window.serverConfig.general.showOptHelp; @@ -278,7 +278,7 @@ $(document).ready(function () { window.currentHyperionInstance = 0; window.currentHyperionInstanceName = getInstanceNameByIndex(0); - requestServerConfig(); + requestServerConfigOld(); setTimeout(requestServerInfo, 100) setTimeout(requestTokenInfo, 200) } @@ -296,7 +296,7 @@ $(document).ready(function () { }); $(window.hyperion).on("cmd-instance-switchTo", function (event) { - requestServerConfig(); + requestServerConfigOld(); setTimeout(requestServerInfo, 200) setTimeout(requestTokenInfo, 400) }); @@ -338,11 +338,6 @@ $(function () { }); }); -// hotfix body padding when bs modals overlap -$(document.body).on('hide.bs.modal,hidden.bs.modal', function () { - $('body').css('padding-right', '0'); -}); - //Dark Mode $("#btn_darkmode").off().on("click", function (e) { if (getStorage("darkMode") != "on") { diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index f8b6c080..356e42e0 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -23,7 +23,7 @@ var devFTDI = ['apa102_ftdi', 'sk6812_ftdi', 'ws2812_ftdi']; var devRPiPWM = ['ws281x']; var devRPiGPIO = ['piblaster']; var devNET = ['atmoorb', 'cololight', 'fadecandy', 'homeassistant', 'philipshue', 'nanoleaf', 'razer', 'tinkerforge', 'tpm2net', 'udpe131', 'udpartnet', 'udpddp', 'udph801', 'udpraw', 'wled', 'yeelight']; -var devSerial = ['adalight', 'dmx', 'atmo', 'sedu', 'tpm2', 'karate']; +var devSerial = ['adalight', 'dmx', 'atmo', 'sedu', 'skydimo', 'tpm2', 'karate']; var devHID = ['hyperionusbasp', 'lightpack', 'paintpack', 'rawhid']; var infoTextDefault = '' + $.i18n("conf_leds_device_info_log") + ' ' + $.i18n("main_menu_logging_token") + ''; @@ -1122,6 +1122,7 @@ $(document).ready(function () { case "dmx": case "karate": case "sedu": + case "skydimo": case "tpm2": //FTDI devices @@ -1231,6 +1232,7 @@ $(document).ready(function () { case "karate": case "dmx": case "sedu": + case "skydimo": case "tpm2": { let currentDeviceType = window.serverConfig.device.type; if ($.inArray(currentDeviceType, devSerial) === -1) { @@ -1462,6 +1464,7 @@ $(document).ready(function () { case "adalight": case "dmx": case "sedu": + case "skydimo": case "tpm2": case "apa102": case "apa104": @@ -1801,6 +1804,7 @@ $(document).ready(function () { break; case "adalight": + case "skydimo": var currentLedCount = conf_editor.getEditor("root.generalOptions.hardwareLedCount").getValue(); params = Object.assign(conf_editor.getEditor("root.generalOptions").getValue(), conf_editor.getEditor("root.specificOptions").getValue(), @@ -1942,6 +1946,7 @@ function saveLedConfig(genDefLayout = false) { case "dmx": case "karate": case "sedu": + case "skydimo": case "tpm2": case "apa102": case "apa104": @@ -1976,7 +1981,7 @@ function saveLedConfig(genDefLayout = false) { break; } - //Rewrite whole LED & Layout configuration, in case changes were done accross tabs and no default layout + //Rewrite whole LED & Layout configuration, in case changes were done across tabs and no default layout if (genDefLayout !== true) { result.ledConfig = getLedConfig(); result.leds = JSON.parse(aceEdt.getText()); @@ -2105,6 +2110,7 @@ var updateOutputSelectList = function (ledType, discoveryInfo) { case "dmx": case "karate": case "sedu": + case "skydimo": case "tpm2": for (const device of discoveryInfo.devices) { if (device.udev) { diff --git a/assets/webconfig/js/hyperion.js b/assets/webconfig/js/hyperion.js index 8153e827..a06dbdaa 100644 --- a/assets/webconfig/js/hyperion.js +++ b/assets/webconfig/js/hyperion.js @@ -34,36 +34,29 @@ tokenList = {}; const ENDLESS = -1; -function initRestart() -{ +function initRestart() { $(window.hyperion).off(); - requestServerConfigReload(); window.watchdog = 10; connectionLostDetection('restart'); } -function connectionLostDetection(type) -{ - if ( window.watchdog > 2 ) - { - var interval_id = window.setInterval(function(){clearInterval(interval_id);}, 9999); // Get a reference to the last +function connectionLostDetection(type) { + if (window.watchdog > 2) { + var interval_id = window.setInterval(function () { clearInterval(interval_id); }, 9999); // Get a reference to the last for (var i = 1; i < interval_id; i++) window.clearInterval(i); - if(type == 'restart') - { + if (type == 'restart') { $("body").html($("#container_restart").html()); // setTimeout delay for probably slower systems, some browser don't execute THIS action - setTimeout(restartAction,250); + setTimeout(restartAction, 250); } - else - { + else { $("body").html($("#container_connection_lost").html()); connectionLostAction(); } } - else - { - $.get( "/cgi/cfg_jsonserver", function() {window.watchdog=0}).fail(function() {window.watchdog++;}); + else { + $.get("/cgi/cfg_jsonserver", function () { window.watchdog = 0 }).fail(function () { window.watchdog++; }); } } @@ -71,25 +64,22 @@ setInterval(connectionLostDetection, 3000); // init websocket to hyperion and bind socket events to jquery events of $(hyperion) object -function initWebSocket() -{ - if ("WebSocket" in window) - { - if (window.websocket == null) - { +function initWebSocket() { + if ("WebSocket" in window) { + if (window.websocket == null) { window.jsonPort = ''; - if(document.location.port == '' && document.location.protocol == "http:") + if (document.location.port == '' && document.location.protocol == "http:") window.jsonPort = '80'; else if (document.location.port == '' && document.location.protocol == "https:") window.jsonPort = '443'; else window.jsonPort = document.location.port; - window.websocket = (document.location.protocol == "https:") ? new WebSocket('wss://'+document.location.hostname+":"+window.jsonPort) : new WebSocket('ws://'+document.location.hostname+":"+window.jsonPort); + window.websocket = (document.location.protocol == "https:") ? new WebSocket('wss://' + document.location.hostname + ":" + window.jsonPort) : new WebSocket('ws://' + document.location.hostname + ":" + window.jsonPort); window.websocket.onopen = function (event) { - $(window.hyperion).trigger({type:"open"}); + $(window.hyperion).trigger({ type: "open" }); - $(window.hyperion).on("cmd-serverinfo", function(event) { + $(window.hyperion).on("cmd-serverinfo", function (event) { window.watchdog = 0; }); }; @@ -97,8 +87,7 @@ function initWebSocket() window.websocket.onclose = function (event) { // See http://tools.ietf.org/html/rfc6455#section-7.4.1 var reason; - switch(event.code) - { + switch (event.code) { case 1000: reason = "Normal closure, meaning that the purpose for which the connection was established has been fulfilled."; break; case 1001: reason = "An endpoint is \"going away\", such as a server going down or a browser having navigated away from a page."; break; case 1002: reason = "An endpoint is terminating the connection due to a protocol error"; break; @@ -114,71 +103,66 @@ function initWebSocket() case 1015: reason = "The connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified)."; break; default: reason = "Unknown reason"; } - $(window.hyperion).trigger({type:"close", reason:reason}); + $(window.hyperion).trigger({ type: "close", reason: reason }); window.watchdog = 10; connectionLostDetection(); }; window.websocket.onmessage = function (event) { - try - { + try { var response = JSON.parse(event.data); var success = response.success; var cmd = response.command; var tan = response.tan - if (success || typeof(success) == "undefined") - { - $(window.hyperion).trigger({type:"cmd-"+cmd, response:response}); + if (success || typeof (success) == "undefined") { + $(window.hyperion).trigger({ type: "cmd-" + cmd, response: response }); } - else - { - // skip tan -1 error handling - if(tan != -1){ - var error = response.hasOwnProperty("error")? response.error : "unknown"; - if (error == "Service Unavailable") { - window.location.reload(); - } else { - $(window.hyperion).trigger({type:"error",reason:error}); - } - console.log("[window.websocket::onmessage] ",error) + else { + // skip tan -1 error handling + if (tan != -1) { + var error = response.hasOwnProperty("error") ? response.error : "unknown"; + if (error == "Service Unavailable") { + window.location.reload(); + } else { + $(window.hyperion).trigger({ type: "error", reason: error }); } + let errorData = response.hasOwnProperty("errorData") ? response.errorData : ""; + console.log("[window.websocket::onmessage] ", error, ", Description:", errorData); + } } } - catch(exception_error) - { - $(window.hyperion).trigger({type:"error",reason:exception_error}); - console.log("[window.websocket::onmessage] ",exception_error) + catch (exception_error) { + $(window.hyperion).trigger({ type: "error", reason: exception_error }); + console.log("[window.websocket::onmessage] ", exception_error) } }; window.websocket.onerror = function (error) { - $(window.hyperion).trigger({type:"error",reason:error}); - console.log("[window.websocket::onerror] ",error) + $(window.hyperion).trigger({ type: "error", reason: error }); + console.log("[window.websocket::onerror] ", error) }; } } - else - { + else { $(window.hyperion).trigger("error"); alert("Websocket is not supported by your browser"); return; } } -function sendToHyperion(command, subcommand, msg) -{ +function sendToHyperion(command, subcommand, msg) { if (typeof subcommand != 'undefined' && subcommand.length > 0) - subcommand = ',"subcommand":"'+subcommand+'"'; + subcommand = ',"subcommand":"' + subcommand + '"'; else subcommand = ""; if (typeof msg != 'undefined' && msg.length > 0) - msg = ","+msg; + msg = "," + msg; else msg = ""; - window.wsTan = Math.floor(Math.random() * 1000) - window.websocket.send('{"command":"'+command+'", "tan":'+window.wsTan+subcommand+msg+'}'); + window.wsTan = Math.floor(Math.random() * 1000) + window.websocket.send('{"command":"' + command + '", "tan":' + window.wsTan + subcommand + msg + '}'); } // Send a json message to Hyperion and wait for a matching response @@ -188,9 +172,9 @@ function sendToHyperion(command, subcommand, msg) // data: The json data as Object // tan: The optional tan, default 1. If the tan is -1, we skip global response error handling // Returns data of response or false if timeout -async function sendAsyncToHyperion (command, subcommand, data, tan = Math.floor(Math.random() * 1000) ) { +async function sendAsyncToHyperion(command, subcommand, data, tan = Math.floor(Math.random() * 1000)) { let obj = { command, tan } - if (subcommand) {Object.assign(obj, {subcommand})} + if (subcommand) { Object.assign(obj, { subcommand }) } if (data) { Object.assign(obj, data) } //if (process.env.DEV || sstore.getters['common/getDebugState']) console.log('SENDAS', obj) @@ -200,7 +184,7 @@ async function sendAsyncToHyperion (command, subcommand, data, tan = Math.floor( // Send a json message to Hyperion and wait for a matching response // A response matches, when command(+subcommand) of request and response is the same // Returns data of response or false if timeout -async function __sendAsync (data) { +async function __sendAsync(data) { return new Promise((resolve, reject) => { let cmd = data.command let subc = data.subcommand @@ -213,7 +197,7 @@ async function __sendAsync (data) { try { rdata = JSON.parse(e.data) } catch (error) { - console.error("[window.websocket::onmessage] ",error) + console.error("[window.websocket::onmessage] ", error) resolve(false) } if (rdata.command == cmd && rdata.tan == tan) { @@ -232,274 +216,273 @@ async function __sendAsync (data) { // wrapped server commands // Test if admin requires authentication -function requestRequiresAdminAuth() -{ - sendToHyperion("authorize","adminRequired"); +function requestRequiresAdminAuth() { + sendToHyperion("authorize", "adminRequired"); } // Test if the default password needs to be changed -function requestRequiresDefaultPasswortChange() -{ - sendToHyperion("authorize","newPasswordRequired"); +function requestRequiresDefaultPasswortChange() { + sendToHyperion("authorize", "newPasswordRequired"); } // Change password -function requestChangePassword(oldPw, newPw) -{ - sendToHyperion("authorize","newPassword",'"password": "'+oldPw+'", "newPassword":"'+newPw+'"'); +function requestChangePassword(oldPw, newPw) { + sendToHyperion("authorize", "newPassword", '"password": "' + oldPw + '", "newPassword":"' + newPw + '"'); } -function requestAuthorization(password) -{ - sendToHyperion("authorize","login",'"password": "' + password + '"'); +function requestAuthorization(password) { + sendToHyperion("authorize", "login", '"password": "' + password + '"'); } -function requestTokenAuthorization(token) -{ - sendToHyperion("authorize","login",'"token": "' + token + '"'); +function requestTokenAuthorization(token) { + sendToHyperion("authorize", "login", '"token": "' + token + '"'); } -function requestToken(comment) -{ - sendToHyperion("authorize","createToken",'"comment": "'+comment+'"'); +function requestToken(comment) { + sendToHyperion("authorize", "createToken", '"comment": "' + comment + '"'); } -function requestTokenInfo() -{ - sendToHyperion("authorize","getTokenList",""); +function requestTokenInfo() { + sendToHyperion("authorize", "getTokenList", ""); } -function requestGetPendingTokenRequests (id, state) { +function requestGetPendingTokenRequests(id, state) { sendToHyperion("authorize", "getPendingTokenRequests", ""); } -function requestHandleTokenRequest(id, state) -{ - sendToHyperion("authorize","answerRequest",'"id":"'+id+'", "accept":'+state); +function requestHandleTokenRequest(id, state) { + sendToHyperion("authorize", "answerRequest", '"id":"' + id + '", "accept":' + state); } -function requestTokenDelete(id) -{ - sendToHyperion("authorize","deleteToken",'"id":"'+id+'"'); +function requestTokenDelete(id) { + sendToHyperion("authorize", "deleteToken", '"id":"' + id + '"'); } -function requestInstanceRename(inst, name) -{ - sendToHyperion("instance", "saveName",'"instance": '+inst+', "name": "'+name+'"'); +function requestInstanceRename(inst, name) { + sendToHyperion("instance", "saveName", '"instance": ' + inst + ', "name": "' + name + '"'); } -function requestInstanceStartStop(inst, start) -{ - if(start) - sendToHyperion("instance","startInstance",'"instance": '+inst); +function requestInstanceStartStop(inst, start) { + if (start) + sendToHyperion("instance", "startInstance", '"instance": ' + inst); else - sendToHyperion("instance","stopInstance",'"instance": '+inst); + sendToHyperion("instance", "stopInstance", '"instance": ' + inst); } -function requestInstanceDelete(inst) -{ - sendToHyperion("instance","deleteInstance",'"instance": '+inst); +function requestInstanceDelete(inst) { + sendToHyperion("instance", "deleteInstance", '"instance": ' + inst); } -function requestInstanceCreate(name) -{ - sendToHyperion("instance","createInstance",'"name": "'+name+'"'); +function requestInstanceCreate(name) { + sendToHyperion("instance", "createInstance", '"name": "' + name + '"'); } -function requestInstanceSwitch(inst) -{ - sendToHyperion("instance","switchTo",'"instance": '+inst); +function requestInstanceSwitch(inst) { + sendToHyperion("instance", "switchTo", '"instance": ' + inst); } -function requestServerInfo() -{ - sendToHyperion("serverinfo","",'"subscribe":["components-update", "priorities-update", "imageToLedMapping-update", "adjustment-update", "videomode-update", "effects-update", "settings-update", "instance-update"]'); +function requestServerInfo() { + sendToHyperion("serverinfo", "", '"subscribe":["components-update", "priorities-update", "imageToLedMapping-update", "adjustment-update", "videomode-update", "effects-update", "settings-update", "instance-update"]'); } -function requestSysInfo() -{ +function requestSysInfo() { sendToHyperion("sysinfo"); } -function requestSystemSuspend() -{ - sendToHyperion("system","suspend"); +function requestSystemSuspend() { + sendToHyperion("system", "suspend"); } -function requestSystemResume() -{ - sendToHyperion("system","resume"); +function requestSystemResume() { + sendToHyperion("system", "resume"); } -function requestSystemRestart() -{ - sendToHyperion("system","restart"); +function requestSystemRestart() { + sendToHyperion("system", "restart"); } -function requestServerConfigSchema() -{ - sendToHyperion("config","getschema"); +function requestServerConfigSchema() { + sendToHyperion("config", "getschema"); } -function requestServerConfig() -{ +function requestServerConfig() { sendToHyperion("config", "getconfig"); } -function requestServerConfigReload() -{ +function requestServerConfigOld() { + sendToHyperion("config", "getconfig-old"); +} + +function requestServerConfigReload() { sendToHyperion("config", "reload"); } -function requestLedColorsStart() -{ - window.ledStreamActive=true; +function requestLedColorsStart() { + window.ledStreamActive = true; sendToHyperion("ledcolors", "ledstream-start"); } -function requestLedColorsStop() -{ - window.ledStreamActive=false; +function requestLedColorsStop() { + window.ledStreamActive = false; sendToHyperion("ledcolors", "ledstream-stop"); } -function requestLedImageStart() -{ - window.imageStreamActive=true; +function requestLedImageStart() { + window.imageStreamActive = true; sendToHyperion("ledcolors", "imagestream-start"); } -function requestLedImageStop() -{ - window.imageStreamActive=false; +function requestLedImageStop() { + window.imageStreamActive = false; sendToHyperion("ledcolors", "imagestream-stop"); } -function requestPriorityClear(prio) -{ - if(typeof prio !== 'number') +function requestPriorityClear(prio) { + if (typeof prio !== 'number') prio = window.webPrio; - $(window.hyperion).trigger({type:"stopBrowerScreenCapture"}); - sendToHyperion("clear", "", '"priority":'+prio+''); + $(window.hyperion).trigger({ type: "stopBrowerScreenCapture" }); + sendToHyperion("clear", "", '"priority":' + prio + ''); } -function requestClearAll() -{ - $(window.hyperion).trigger({type:"stopBrowerScreenCapture"}); +function requestClearAll() { + $(window.hyperion).trigger({ type: "stopBrowerScreenCapture" }); requestPriorityClear(-1) } -function requestPlayEffect(effectName, duration) -{ - $(window.hyperion).trigger({type:"stopBrowerScreenCapture"}); - sendToHyperion("effect", "", '"effect":{"name":"'+effectName+'"},"priority":'+window.webPrio+',"duration":'+validateDuration(duration)+',"origin":"'+window.webOrigin+'"'); +function requestPlayEffect(effectName, duration) { + $(window.hyperion).trigger({ type: "stopBrowerScreenCapture" }); + sendToHyperion("effect", "", '"effect":{"name":"' + effectName + '"},"priority":' + window.webPrio + ',"duration":' + validateDuration(duration) + ',"origin":"' + window.webOrigin + '"'); } -function requestSetColor(r,g,b,duration) -{ - $(window.hyperion).trigger({type:"stopBrowerScreenCapture"}); - sendToHyperion("color", "", '"color":['+r+','+g+','+b+'], "priority":'+window.webPrio+',"duration":'+validateDuration(duration)+',"origin":"'+window.webOrigin+'"'); +function requestSetColor(r, g, b, duration) { + $(window.hyperion).trigger({ type: "stopBrowerScreenCapture" }); + sendToHyperion("color", "", '"color":[' + r + ',' + g + ',' + b + '], "priority":' + window.webPrio + ',"duration":' + validateDuration(duration) + ',"origin":"' + window.webOrigin + '"'); } -function requestSetImage(data,duration,name) -{ - sendToHyperion("image", "", '"imagedata":"'+data+'", "priority":'+window.webPrio+',"duration":'+validateDuration(duration)+', "format":"auto", "origin":"'+window.webOrigin+'", "name":"'+name+'"'); +function requestSetImage(data, duration, name) { + sendToHyperion("image", "", '"imagedata":"' + data + '", "priority":' + window.webPrio + ',"duration":' + validateDuration(duration) + ', "format":"auto", "origin":"' + window.webOrigin + '", "name":"' + name + '"'); } -function requestSetComponentState(comp, state) -{ +function requestSetComponentState(comp, state) { var state_str = state ? "true" : "false"; - sendToHyperion("componentstate", "", '"componentstate":{"component":"'+comp+'","state":'+state_str+'}'); + sendToHyperion("componentstate", "", '"componentstate":{"component":"' + comp + '","state":' + state_str + '}'); } -function requestSetSource(prio) -{ - if ( prio == "auto" ) +function requestSetSource(prio) { + if (prio == "auto") sendToHyperion("sourceselect", "", '"auto":true'); else - sendToHyperion("sourceselect", "", '"priority":'+prio); + sendToHyperion("sourceselect", "", '"priority":' + prio); } -function requestWriteConfig(config, full) -{ - if(full === true) - window.serverConfig = config; - else - { - jQuery.each(config, function(i, val) { - window.serverConfig[i] = val; - }); +// Function to transform the legacy config into thee new API format +function transformConfig(configInput, instanceId = 0) { + const globalConfig = {}; + const instanceSettings = {}; + + // Populate globalConfig and instanceSettings based on the specified properties + for (const [key, value] of Object.entries(configInput)) { + if (window.schema.propertiesTypes.globalProperties.includes(key)) { + globalConfig[key] = value; + } else if (window.schema.propertiesTypes.instanceProperties.includes(key)) { + instanceSettings[key] = value; + } } - sendToHyperion("config","setconfig", '"config":'+JSON.stringify(window.serverConfig)); + // Initialize the final transformed configuration + const transformedConfig = {}; + + // Add `global` only if it has properties + if (Object.keys(globalConfig).length > 0) { + transformedConfig.global = { settings: globalConfig }; + } + + // Add `instance` only if there are instance settings + if (Object.keys(instanceSettings).length > 0) { + transformedConfig.instances = [ + { + id: instanceId, + settings: instanceSettings + } + ]; + } + + return transformedConfig; +} + +function requestWriteConfig(singleInstanceConfig, full) { + let newConfig = ""; + const instance = Number(window.currentHyperionInstance); + + if (full === true) { + window.serverConfig = singleInstanceConfig; + newConfig = transformConfig(window.serverConfig, instance); + } + else { + jQuery.each(singleInstanceConfig, function (i, val) { + window.serverConfig[i] = val; + }); + newConfig = transformConfig(singleInstanceConfig, instance); + } + + sendToHyperion("config", "setconfig", '"config":' + JSON.stringify(newConfig)); } function requestRestoreConfig(config) { sendToHyperion("config", "restoreconfig", '"config":' + JSON.stringify(config)); } -function requestWriteEffect(effectName,effectPy,effectArgs,data) -{ +function requestWriteEffect(effectName, effectPy, effectArgs, data) { var cutArgs = effectArgs.slice(1, -1); - sendToHyperion("create-effect", "", '"name":"'+effectName+'", "script":"'+effectPy+'", '+cutArgs+',"imageData":"'+data+'"'); + sendToHyperion("create-effect", "", '"name":"' + effectName + '", "script":"' + effectPy + '", ' + cutArgs + ',"imageData":"' + data + '"'); } -function requestTestEffect(effectName,effectPy,effectArgs,data) -{ - sendToHyperion("effect", "", '"effect":{"name":"'+effectName+'", "args":'+effectArgs+'}, "priority":'+window.webPrio+', "origin":"'+window.webOrigin+'", "pythonScript":"'+effectPy+'", "imageData":"'+data+'"'); +function requestTestEffect(effectName, effectPy, effectArgs, data) { + sendToHyperion("effect", "", '"effect":{"name":"' + effectName + '", "args":' + effectArgs + '}, "priority":' + window.webPrio + ', "origin":"' + window.webOrigin + '", "pythonScript":"' + effectPy + '", "imageData":"' + data + '"'); } -function requestDeleteEffect(effectName) -{ - sendToHyperion("delete-effect", "", '"name":"'+effectName+'"'); +function requestDeleteEffect(effectName) { + sendToHyperion("delete-effect", "", '"name":"' + effectName + '"'); } -function requestLoggingStart() -{ - window.loggingStreamActive=true; +function requestLoggingStart() { + window.loggingStreamActive = true; sendToHyperion("logging", "start"); } -function requestLoggingStop() -{ - window.loggingStreamActive=false; +function requestLoggingStop() { + window.loggingStreamActive = false; sendToHyperion("logging", "stop"); } -function requestMappingType(type) -{ - sendToHyperion("processing", "", '"mappingType": "'+type+'"'); +function requestMappingType(type) { + sendToHyperion("processing", "", '"mappingType": "' + type + '"'); } -function requestVideoMode(newMode) -{ - sendToHyperion("videomode", "", '"videoMode": "'+newMode+'"'); +function requestVideoMode(newMode) { + sendToHyperion("videomode", "", '"videoMode": "' + newMode + '"'); } -function requestAdjustment(type, value, complete) -{ - if(complete === true) - sendToHyperion("adjustment", "", '"adjustment": '+type+''); +function requestAdjustment(type, value, complete) { + if (complete === true) + sendToHyperion("adjustment", "", '"adjustment": ' + type + ''); else - sendToHyperion("adjustment", "", '"adjustment": {"'+type+'": '+value+'}'); + sendToHyperion("adjustment", "", '"adjustment": {"' + type + '": ' + value + '}'); } -async function requestLedDeviceDiscovery(type, params) -{ +async function requestLedDeviceDiscovery(type, params) { let data = { ledDeviceType: type, params: params }; return sendAsyncToHyperion("leddevice", "discover", data); } -async function requestLedDeviceProperties(type, params) -{ +async function requestLedDeviceProperties(type, params) { let data = { ledDeviceType: type, params: params }; return sendAsyncToHyperion("leddevice", "getProperties", data); } -function requestLedDeviceIdentification(type, params) -{ - let data = { ledDeviceType: type, params: params }; +function requestLedDeviceIdentification(type, params) { + let data = { ledDeviceType: type, params: params }; return sendAsyncToHyperion("leddevice", "identify", data); } @@ -522,3 +505,12 @@ async function requestServiceDiscovery(type, params) { return sendAsyncToHyperion("service", "discover", data); } +async function requestConfig(globalTypes, instances, instanceTypes) { + let globalFilter = { "global": { "types": globalTypes } }; + let instanceFilter = { "instances": { "ids": instances, "types": instanceTypes } }; + let filter = { "configFilter": { globalFilter, instanceFilter } }; + + return sendAsyncToHyperion("config", "getconfig", filter); +} + + diff --git a/assets/webconfig/js/ledsim.js b/assets/webconfig/js/ledsim.js index af633ba7..52a75ced 100644 --- a/assets/webconfig/js/ledsim.js +++ b/assets/webconfig/js/ledsim.js @@ -129,7 +129,7 @@ $(document).ready(function () { } }); // apply new serverinfos - $(window.hyperion).on("cmd-config-getconfig", function (event) { + $(window.hyperion).on("cmd-config-getconfig-old", function (event) { leds = event.response.info.leds; grabberConfig = event.response.info.grabberV4L2; updateLedLayout(); diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index 578e3128..ace8b830 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -239,7 +239,7 @@ function showInfoDialog(type, header, message) { $('#id_body').html(''); if (header == "") $('#id_body').append('

' + $.i18n('infoDialog_general_error_title') + '

'); - $('#id_footer').html(''); + $('#id_footer').html(''); } else if (type == "select") { $('#id_body').html(''); @@ -256,9 +256,9 @@ function showInfoDialog(type, header, message) { $('#id_footer').html('' + $.i18n('InfoDialog_nowrite_foottext') + ''); } else if (type == "import") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); } else if (type == "delInst") { $('#id_body').html(''); @@ -1222,7 +1222,7 @@ function getSystemInfo() { info += '- Avail Video Cap.: ' + window.serverInfo.grabbers.video.available + '\n'; info += '- Avail Audio Cap.: ' + window.serverInfo.grabbers.audio.available + '\n'; info += '- Avail Services: ' + window.serverInfo.services + '\n'; - info += '- Config path: ' + shy.rootPath + '\n'; + info += '- Config database: ' + shy.configDatabaseFile + '\n'; info += '- Database: ' + (shy.readOnlyMode ? "ready-only" : "read/write") + '\n'; info += '- Mode: ' + (shy.isGuiMode ? "GUI" : "Non-GUI") + '\n'; diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index caf5663d..01142373 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -9,14 +9,15 @@ macro(DeployMacOS TARGET) OUTPUT_STRIP_TRAILING_WHITESPACE ) - install(CODE "set(TARGET_FILE \"${TARGET_FILE}\")" COMPONENT "Hyperion") - install(CODE "set(TARGET_BUNDLE_NAME \"${TARGET}.app\")" COMPONENT "Hyperion") - install(CODE "set(PLUGIN_DIR \"${QT_PLUGIN_DIR}\")" COMPONENT "Hyperion") - install(CODE "set(BUILD_DIR \"${CMAKE_BINARY_DIR}\")" COMPONENT "Hyperion") + install(CODE "set(TARGET_FILE \"${TARGET_FILE}\")" COMPONENT "Hyperion") + install(CODE "set(TARGET_BUNDLE_NAME \"${TARGET}.app\")" COMPONENT "Hyperion") + install(CODE "set(PLUGIN_DIR \"${QT_PLUGIN_DIR}\")" COMPONENT "Hyperion") install(CODE "set(ENABLE_EFFECTENGINE \"${ENABLE_EFFECTENGINE}\")" COMPONENT "Hyperion") install(CODE [[ + set(BUNDLE_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}") + file(GET_RUNTIME_DEPENDENCIES EXECUTABLES ${TARGET_FILE} RESOLVED_DEPENDENCIES_VAR resolved_deps @@ -28,13 +29,13 @@ macro(DeployMacOS TARGET) if (${_index} GREATER -1) file(INSTALL FILES "${dependency}" - DESTINATION "${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}/Contents/Frameworks" + DESTINATION "${BUNDLE_INSTALL_DIR}/Contents/Frameworks" TYPE SHARED_LIBRARY ) else() file(INSTALL FILES "${dependency}" - DESTINATION "${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}/Contents/lib" + DESTINATION "${BUNDLE_INSTALL_DIR}/Contents/lib" TYPE SHARED_LIBRARY FOLLOW_SYMLINK_CHAIN ) @@ -58,7 +59,7 @@ macro(DeployMacOS TARGET) foreach(DEPENDENCY ${PLUGINS}) file(INSTALL - DESTINATION "${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}/Contents/lib" + DESTINATION "${BUNDLE_INSTALL_DIR}/Contents/lib" TYPE SHARED_LIBRARY FILES ${DEPENDENCY} FOLLOW_SYMLINK_CHAIN @@ -66,10 +67,10 @@ macro(DeployMacOS TARGET) endforeach() get_filename_component(singleQtLib ${file} NAME) - list(APPEND QT_PLUGINS "${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}/Contents/plugins/${PLUGIN}/${singleQtLib}") + list(APPEND QT_PLUGINS "${BUNDLE_INSTALL_DIR}/Contents/plugins/${PLUGIN}/${singleQtLib}") file(INSTALL FILES ${file} - DESTINATION "${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}/Contents/plugins/${PLUGIN}" + DESTINATION "${BUNDLE_INSTALL_DIR}/Contents/plugins/${PLUGIN}" TYPE SHARED_LIBRARY ) @@ -78,10 +79,10 @@ macro(DeployMacOS TARGET) endforeach() include(BundleUtilities) - fixup_bundle("${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}" "${QT_PLUGINS}" "${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}/Contents/lib" IGNORE_ITEM "python;python3;Python;Python3;.Python;.Python3") + fixup_bundle("${BUNDLE_INSTALL_DIR}" "${QT_PLUGINS}" "${BUNDLE_INSTALL_DIR}/Contents/lib" IGNORE_ITEM "python;python3;Python;Python3;.Python;.Python3") + file(REMOVE_RECURSE "${BUNDLE_INSTALL_DIR}/Contents/lib") if(ENABLE_EFFECTENGINE) - # Detect the Python version and modules directory if(NOT CMAKE_VERSION VERSION_LESS "3.12") find_package(Python3 COMPONENTS Interpreter Development REQUIRED) @@ -98,24 +99,37 @@ macro(DeployMacOS TARGET) # Copy Python modules to '/../Frameworks/Python.framework/Versions/Current/lib/PythonMAJOR.MINOR' and ignore the unnecessary stuff listed below if (PYTHON_MODULES_DIR) + set(PYTHON_FRAMEWORK "${BUNDLE_INSTALL_DIR}/Contents/Frameworks/Python.framework") file( COPY ${PYTHON_MODULES_DIR}/ - DESTINATION "${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}/Contents/Frameworks/Python.framework/Versions/Current/lib/python${PYTHON_VERSION_MAJOR_MINOR}" - PATTERN "*.pyc" EXCLUDE # compiled bytecodes - PATTERN "__pycache__" EXCLUDE # any cache - PATTERN "config-${PYTHON_VERSION_MAJOR_MINOR}*" EXCLUDE # static libs - PATTERN "lib2to3" EXCLUDE # automated Python 2 to 3 code translation - PATTERN "tkinter" EXCLUDE # Tk interface - PATTERN "turtledemo" EXCLUDE # Tk demo folder - PATTERN "turtle.py" EXCLUDE # Tk demo file - PATTERN "test" EXCLUDE # unittest module - PATTERN "sitecustomize.py" EXCLUDE # site-specific configs + DESTINATION "${PYTHON_FRAMEWORK}/Versions/Current/lib/python${PYTHON_VERSION_MAJOR_MINOR}" + PATTERN "*.pyc" EXCLUDE # compiled bytecodes + PATTERN "__pycache__" EXCLUDE # any cache + PATTERN "config-${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}*" EXCLUDE # static libs + PATTERN "lib2to3" EXCLUDE # automated Python 2 to 3 code translation + PATTERN "tkinter" EXCLUDE # Tk interface + PATTERN "lib-dynload/_tkinter.*" EXCLUDE + PATTERN "idlelib" EXCLUDE + PATTERN "turtle.py" EXCLUDE # Tk demo + PATTERN "test" EXCLUDE # unittest module + PATTERN "sitecustomize.py" EXCLUDE # site-specific configs ) endif(PYTHON_MODULES_DIR) endif(ENABLE_EFFECTENGINE) - file(REMOVE_RECURSE "${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}/Contents/lib") - file(REMOVE_RECURSE "${CMAKE_INSTALL_PREFIX}/share") + file(GLOB_RECURSE LIBS FOLLOW_SYMLINKS "${BUNDLE_INSTALL_DIR}/*.dylib") + file(GLOB FRAMEWORKS FOLLOW_SYMLINKS LIST_DIRECTORIES ON "${BUNDLE_INSTALL_DIR}/Contents/Frameworks/*") + foreach(item ${LIBS} ${FRAMEWORKS} ${PYTHON_FRAMEWORK} ${BUNDLE_INSTALL_DIR}) + set(cmd codesign --deep --force --sign - "${item}") + execute_process( + COMMAND ${cmd} + RESULT_VARIABLE codesign_result + ) + + if(NOT codesign_result EQUAL 0) + message(WARNING "macOS signing failed; ${cmd} returned ${codesign_result}") + endif() + endforeach() ]] COMPONENT "Hyperion") diff --git a/cmake/osxbundle/AppleScript.scpt b/cmake/osxbundle/AppleScript.scpt index f3a08e21..9e1f3288 100644 --- a/cmake/osxbundle/AppleScript.scpt +++ b/cmake/osxbundle/AppleScript.scpt @@ -49,11 +49,6 @@ on run argv delay 1 close - -- one last open and close so you can see everything looks correct - open - delay 5 - close - end tell delay 1 diff --git a/cmake/osxbundle/Info.plist.in b/cmake/osxbundle/Info.plist.in index 9774dd79..d6c77d50 100644 --- a/cmake/osxbundle/Info.plist.in +++ b/cmake/osxbundle/Info.plist.in @@ -26,6 +26,8 @@ APPL LSUIElement 1 + NSCameraUsageDescription + Hyperion uses this access to record screencasts NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} Source Code diff --git a/config/hyperion.config.json.default b/config/hyperion.config.json.default deleted file mode 100644 index 06c33c33..00000000 --- a/config/hyperion.config.json.default +++ /dev/null @@ -1,277 +0,0 @@ -{ - "general": { - "name": "My Hyperion Config", - "configVersion": "configVersionValue", - "previousVersion": "previousVersionValue", - "watchedVersionBranch": "Stable", - "showOptHelp": true - }, - "logger": { - "level": "warn" - }, - - "device": { - "type": "file", - "hardwareLedCount": 1, - "autoStart": true, - "output": "/dev/null", - "colorOrder": "rgb", - "latchTime": 0, - "rewriteTime": 0, - "enableAttempts": 6, - "enableAttemptsInterval": 15 - }, - - "schedEvents": { - "enable": false - }, - - "cecEvents": { - "actions": [ - { - "action": "Suspend", - "event": "standby" - }, - { - "action": "Resume", - "event": "set stream path" - } - ], - "enable": false - }, - - "color": { - "imageToLedMappingType": "multicolor_mean", - "channelAdjustment": [ - { - "id": "default", - "leds": "*", - "white": [ 255, 255, 255 ], - "red": [ 255, 0, 0 ], - "green": [ 0, 255, 0 ], - "blue": [ 0, 0, 255 ], - "cyan": [ 0, 255, 255 ], - "magenta": [ 255, 0, 255 ], - "yellow": [ 255, 255, 0 ], - "gammaRed": 2.2, - "gammaGreen": 2.2, - "gammaBlue": 2.2, - "backlightThreshold": 0, - "backlightColored": false, - "brightness": 100, - "brightnessCompensation": 100, - "saturationGain": 1.0, - "brightnessGain": 1.0, - "temperature" : 6600 - } - ] - }, - - "smoothing": { - "enable": true, - "type": "linear", - "time_ms": 150, - "updateFrequency": 25.0000, - "interpolationRate": 25.0000, - "decay": 1, - "dithering": false, - "updateDelay": 0 - }, - - "grabberV4L2": { - "enable": false, - "device": "none", - "input": 0, - "encoding": "NO_CHANGE", - "width": 0, - "height": 0, - "fps": 15, - "flip": "NO_CHANGE", - "fpsSoftwareDecimation": 0, - "sizeDecimation": 8, - "cropLeft": 0, - "cropRight": 0, - "cropTop": 0, - "cropBottom": 0, - "redSignalThreshold": 0, - "greenSignalThreshold": 100, - "blueSignalThreshold": 0, - "signalDetection": false, - "noSignalCounterThreshold": 200, - "sDVOffsetMin": 0.1, - "sDVOffsetMax": 0.9, - "sDHOffsetMin": 0.4, - "sDHOffsetMax": 0.46, - "hardware_brightness": 0, - "hardware_contrast": 0, - "hardware_saturation": 0, - "hardware_hue": 0 - }, - - "grabberAudio": { - "enable": false, - "device": "auto", - "audioEffect": "vuMeter", - "vuMeter": { - "flip": "NO_CHANGE", - "hotColor": [ 255, 0, 0 ], - "multiplier": 1, - "safeColor": [ 0, 255, 0 ], - "safeValue": 45, - "tolerance": 5, - "warnColor": [ 255, 255, 0 ], - "warnValue": 80 - } - }, - - "framegrabber": { - "enable": false, - "device": "auto", - "input": 0, - "width": 80, - "height": 45, - "fps": 10, - "pixelDecimation": 8, - "cropLeft": 0, - "cropRight": 0, - "cropTop": 0, - "cropBottom": 0 - }, - - "blackborderdetector": { - "enable": true, - "threshold": 5, - "unknownFrameCnt": 600, - "borderFrameCnt": 50, - "maxInconsistentCnt": 10, - "blurRemoveCnt": 1, - "mode": "default" - }, - - "foregroundEffect": { - "enable": true, - "type": "effect", - "color": [ 0, 0, 255 ], - "effect": "Rainbow swirl fast", - "duration_ms": 3000 - }, - - "backgroundEffect": { - "enable": false, - "type": "effect", - "color": [ 255, 138, 0 ], - "effect": "Warm mood blobs" - }, - - "forwarder": { - "enable": false, - "jsonapi": [], - "flatbuffer": [] - }, - - "jsonServer": { - "port": 19444 - }, - - "flatbufServer": { - "enable": true, - "port": 19400, - "timeout": 5 - }, - - "protoServer": { - "enable": true, - "port": 19445, - "timeout": 5 - }, - - "boblightServer": { - "enable": false, - "port": 19333, - "priority": 128 - }, - - "webConfig": { - "document_root": "", - "port": 8090, - "sslPort": 8092, - "crtPath": "", - "keyPath": "", - "keyPassPhrase": "" - }, - - "effects": { - "paths": [ "$ROOT/custom-effects" ], - "disable": [ "" ] - }, - - "instCapture": { - "systemEnable": false, - "systemGrabberDevice": "NONE", - "systemPriority": 250, - "v4lEnable": false, - "v4lGrabberDevice": "NONE", - "v4lPriority": 240, - "audioEnable": false, - "audioGrabberDevice": "NONE", - "audioPriority": 230 - }, - - "network": { - "internetAccessAPI": false, - "restirctedInternetAccessAPI": false, - "ipWhitelist": [], - "localApiAuth": false - }, - - "ledConfig": { - "classic": { - "top": 1, - "bottom": 0, - "left": 0, - "right": 0, - "glength": 0, - "gpos": 0, - "position": 0, - "reverse": false, - "hdepth": 8, - "vdepth": 5, - "overlap": 0, - "edgegap": 0, - "ptlh": 0, - "ptlv": 0, - "ptrh": 100, - "ptrv": 0, - "pblh": 0, - "pblv": 100, - "pbrh": 100, - "pbrv": 100 - }, - - "matrix": { - "ledshoriz": 1, - "ledsvert": 1, - "cabling": "snake", - "direction": "horizontal", - "start": "top-left" - } - }, - - "leds": [ - { - "hmax": 1, - "hmin": 0, - "vmax": 0.08, - "vmin": 0 - } - ], - - "osEvents": { - "suspendEnable": true, - "lockEnable": true - }, - - "cecEvents": { - "enable": false - } -} diff --git a/effects/collision.py b/effects/collision.py index 5c22fc83..09afa311 100644 --- a/effects/collision.py +++ b/effects/collision.py @@ -1,5 +1,3 @@ -# Two projectiles are sent from random positions and collide with each other -# Template from https://github.com/nickpesce/lit/blob/master/lit/effects/collision.py import hyperion, time, colorsys, random # Get parameters @@ -7,6 +5,11 @@ sleepTime = max(0.02, float(hyperion.args.get('speed', 100))/1000.0) trailLength = max(3, int(hyperion.args.get('trailLength', 5))) explodeRadius = int(hyperion.args.get('explodeRadius', 8)) +# Ensure that the range for pixel indices stays within bounds +maxPixelIndex = hyperion.ledCount - 1 +if trailLength > maxPixelIndex or explodeRadius > maxPixelIndex: + exit(f"Error: Color length ({trailLength}) and detonation range ({explodeRadius}) must be less than number of LEDs configured ({hyperion.ledCount})") + # Create additional variables increment = None projectiles = [] @@ -14,47 +17,64 @@ projectiles = [] # Initialize the led data ledData = bytearray() for i in range(hyperion.ledCount): - ledData += bytearray((0,0,0)) + ledData += bytearray((0,0,0)) # Start the write data loop while not hyperion.abort(): - if (len(projectiles) != 2): - projectiles = [ [0, 1, random.uniform(0.0, 1.0)], [hyperion.ledCount-1, -1, random.uniform(0.0, 1.0)] ] - increment = -random.randint(0, hyperion.ledCount-1) if random.choice([True, False]) else random.randint(0, hyperion.ledCount-1) + if len(projectiles) != 2: + projectiles = [ + [0, 1, random.uniform(0.0, 1.0)], # Start positions of projectiles + [hyperion.ledCount-1, -1, random.uniform(0.0, 1.0)] + ] + increment = -random.randint(0, hyperion.ledCount-1) if random.choice([True, False]) else random.randint(0, hyperion.ledCount-1) - ledDataBuf = ledData[:] - for i, v in enumerate(projectiles): - projectiles[i][0] = projectiles[i][0]+projectiles[i][1] - for t in range(0, trailLength): - pixel = v[0] - v[1]*t - if pixel + 2 < 0: - pixel += hyperion.ledCount - if pixel + 2 > hyperion.ledCount-1: - pixel -= hyperion.ledCount-1 - rgb = colorsys.hsv_to_rgb(v[2], 1, (trailLength - 1.0*t)/trailLength) - ledDataBuf[3*pixel ] = int(255*rgb[0]) - ledDataBuf[3*pixel + 1] = int(255*rgb[1]) - ledDataBuf[3*pixel + 2] = int(255*rgb[2]) + # Backup the LED data + ledDataBuf = ledData[:] + for i, v in enumerate(projectiles): + # Update projectile positions + projectiles[i][0] = projectiles[i][0] + projectiles[i][1] + + for t in range(0, trailLength): + # Calculate pixel index for the trail + pixel = v[0] - v[1] * t + if pixel < 0: + pixel += hyperion.ledCount + if pixel >= hyperion.ledCount: + pixel -= hyperion.ledCount - hyperion.setColor(ledDataBuf[-increment:] + ledDataBuf[:-increment]) + # Make sure pixel is within bounds + if pixel < 0 or pixel >= hyperion.ledCount: + continue - for i1, p1 in enumerate(projectiles): - for i2, p2 in enumerate(projectiles): - if (p1 is not p2): - prev1 = p1[0] - p1[1] - prev2 = p2[0] - p2[1] - if (prev1 - prev2 < 0) != (p1[0] - p2[0] < 0): - for d in range(0, explodeRadius): - for pixel in range(p1[0] - d, p1[0] + d): - rgb = colorsys.hsv_to_rgb(random.choice([p1[2], p2[2]]), 1, (1.0 * explodeRadius - d) / explodeRadius) - ledDataBuf[3*pixel ] = int(255*rgb[0]) - ledDataBuf[3*pixel + 1] = int(255*rgb[1]) - ledDataBuf[3*pixel + 2] = int(255*rgb[2]) + rgb = colorsys.hsv_to_rgb(v[2], 1, (trailLength - 1.0 * t) / trailLength) + ledDataBuf[3*pixel] = int(255 * rgb[0]) + ledDataBuf[3*pixel + 1] = int(255 * rgb[1]) + ledDataBuf[3*pixel + 2] = int(255 * rgb[2]) - hyperion.setColor(ledDataBuf[-increment:] + ledDataBuf[:-increment]) - time.sleep(sleepTime) + hyperion.setColor(ledDataBuf[-increment:] + ledDataBuf[:-increment]) - projectiles.remove(p1) - projectiles.remove(p2) + # Check for collision and handle explosion + for i1, p1 in enumerate(projectiles): + for i2, p2 in enumerate(projectiles): + if p1 is not p2: + prev1 = p1[0] - p1[1] + prev2 = p2[0] - p2[1] + if (prev1 - prev2 < 0) != (p1[0] - p2[0] < 0): + for d in range(0, explodeRadius): + for pixel in range(p1[0] - d, p1[0] + d): + # Check if pixel is out of bounds + if pixel < 0 or pixel >= hyperion.ledCount: + continue - time.sleep(sleepTime) + rgb = colorsys.hsv_to_rgb(random.choice([p1[2], p2[2]]), 1, (1.0 * explodeRadius - d) / explodeRadius) + ledDataBuf[3 * pixel] = int(255 * rgb[0]) + ledDataBuf[3 * pixel + 1] = int(255 * rgb[1]) + ledDataBuf[3 * pixel + 2] = int(255 * rgb[2]) + + hyperion.setColor(ledDataBuf[-increment:] + ledDataBuf[:-increment]) + time.sleep(sleepTime) + + projectiles.remove(p1) + projectiles.remove(p2) + + time.sleep(sleepTime) diff --git a/include/api/API.h b/include/api/API.h index 96e1f9b8..8b6476e6 100644 --- a/include/api/API.h +++ b/include/api/API.h @@ -239,18 +239,6 @@ protected: QString saveEffect(const QJsonObject &data); #endif - /// - /// @brief Save settings object. Requires ADMIN ACCESS - /// @param data The data object - /// - bool saveSettings(const QJsonObject &data); - - /// - /// @brief Restore settings object. Requires ADMIN ACCESS - /// @param data The data object - /// - bool restoreSettings(const QJsonObject &data); - /// /// @brief Set the authorizationn state /// @param authorized True, if authorized diff --git a/include/api/JsonAPI.h b/include/api/JsonAPI.h index 083a6850..c08d0237 100644 --- a/include/api/JsonAPI.h +++ b/include/api/JsonAPI.h @@ -186,7 +186,7 @@ private: /// void handleSourceSelectCommand(const QJsonObject &message, const JsonApiCommand& cmd); - /// Handle an incoming JSON GetConfig message and check subcommand + /// Handle an incoming JSON Config message and check subcommand /// /// @param message the incoming message /// @@ -204,6 +204,12 @@ private: /// void handleConfigSetCommand(const QJsonObject &message, const JsonApiCommand& cmd); + /// Handle an incoming JSON GetConfig message from handleConfigCommand() + /// + /// @param message the incoming message + /// + void handleConfigGetCommand(const QJsonObject &message, const JsonApiCommand& cmd); + /// Handle an incoming JSON RestoreConfig message from handleConfigCommand() /// /// @param message the incoming message diff --git a/include/api/JsonApiCommand.h b/include/api/JsonApiCommand.h index c63d968b..4345b56a 100644 --- a/include/api/JsonApiCommand.h +++ b/include/api/JsonApiCommand.h @@ -84,6 +84,7 @@ public: DeleteToken, Discover, GetConfig, + GetConfigOld, GetInfo, GetPendingTokenRequests, GetProperties, @@ -134,6 +135,7 @@ public: case DeleteToken: return "deleteToken"; case Discover: return "discover"; case GetConfig: return "getconfig"; + case GetConfigOld: return "getconfig-old"; case GetInfo: return "getInfo"; case GetPendingTokenRequests: return "getPendingTokenRequests"; case GetProperties: return "getProperties"; @@ -274,6 +276,7 @@ public: { {"color", ""}, { Command::Color, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, { {"componentstate", ""}, { Command::ComponentState, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, { {"config", "getconfig"}, { Command::Config, SubCommand::GetConfig, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"config", "getconfig-old"}, { Command::Config, SubCommand::GetConfigOld, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, { {"config", "getschema"}, { Command::Config, SubCommand::GetSchema, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, { {"config", "reload"}, { Command::Config, SubCommand::Reload, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, { {"config", "restoreconfig"}, { Command::Config, SubCommand::RestoreConfig, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, diff --git a/include/api/JsonInfo.h b/include/api/JsonInfo.h index 1346f97e..f3a9922c 100644 --- a/include/api/JsonInfo.h +++ b/include/api/JsonInfo.h @@ -31,6 +31,8 @@ public: static QJsonObject getSystemInfo(const Hyperion* hyperion); QJsonObject discoverSources (const QString& sourceType, const QJsonObject& params); + static QJsonObject getConfiguration(const QList& instances = {}, const QStringList& instanceFilteredTypes = {}, const QStringList& globalFilterTypes = {} ); + private: template diff --git a/include/db/AuthTable.h b/include/db/AuthTable.h index 161c3856..f2ecb50e 100644 --- a/include/db/AuthTable.h +++ b/include/db/AuthTable.h @@ -1,12 +1,7 @@ -#pragma once +#ifndef AUTHSTABLE_H +#define AUTHSTABLE_H -// hyperion #include -#include - -// qt -#include -#include namespace hyperion { const char DEFAULT_USER[] = "Hyperion"; @@ -21,69 +16,30 @@ class AuthTable : public DBManager public: /// construct wrapper with auth table - AuthTable(const QString& rootPath = "", QObject* parent = nullptr, bool readonlyMode = false) - : DBManager(parent) - { - setReadonlyMode(readonlyMode); - if(!rootPath.isEmpty()){ - // Init Hyperion database usage - setRootPath(rootPath); - setDatabaseName("hyperion"); - } - // init Auth table - setTable("auth"); - // create table columns - createTable(QStringList()<<"user TEXT"<<"password BLOB"<<"token BLOB"<<"salt BLOB"<<"comment TEXT"<<"id TEXT"<<"created_at TEXT"<<"last_use TEXT"); - }; + explicit AuthTable(QObject* parent = nullptr); /// /// @brief Create a user record, if called on a existing user the auth is recreated /// @param[in] user The username - /// @param[in] pw The password + /// @param[in] password The password /// @return true on success else false /// - inline bool createUser(const QString& user, const QString& pw) - { - // new salt - QByteArray salt = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex(); - QVariantMap map; - map["user"] = user; - map["salt"] = salt; - map["password"] = hashPasswordWithSalt(pw,salt); - map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("user",user)); - return createRecord(cond, map); - } + bool createUser(const QString& user, const QString& password); /// /// @brief Test if user record exists /// @param[in] user The user id /// @return true on success else false /// - inline bool userExist(const QString& user) - { - VectorPair cond; - cond.append(CPair("user",user)); - return recordExists(cond); - } + bool userExist(const QString& user); /// /// @brief Test if a user is authorized for access with given pw. - /// @param user The user name - /// @param pw The password - /// @return True on success else false + /// @param user The user name + /// @param password The password + /// @return True on success else false /// - inline bool isUserAuthorized(const QString& user, const QString& pw) - { - if(userExist(user) && (calcPasswordHashOfUser(user, pw) == getPasswordHashOfUser(user))) - { - updateUserUsed(user); - return true; - } - return false; - } + bool isUserAuthorized(const QString& user, const QString& password); /// /// @brief Test if a user token is authorized for access. @@ -91,197 +47,92 @@ public: /// @param token The token /// @return True on success else false /// - inline bool isUserTokenAuthorized(const QString& usr, const QString& token) - { - if(getUserToken(usr) == token.toUtf8()) - { - updateUserUsed(usr); - return true; - } - return false; - } + bool isUserTokenAuthorized(const QString& usr, const QString& token); /// /// @brief Update token of a user. It's an alternate login path which is replaced on startup. This token is NOT hashed(!) /// @param user The user name /// @return True on success else false /// - inline bool setUserToken(const QString& user) - { - QVariantMap map; - map["token"] = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex(); - - VectorPair cond; - cond.append(CPair("user", user)); - return updateRecord(cond, map); - } + bool setUserToken(const QString& user); /// /// @brief Get token of a user. This token is NOT hashed(!) /// @param user The user name /// @return The token /// - inline const QByteArray getUserToken(const QString& user) - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("user", user)); - getRecord(cond, results, QStringList()<<"token"); - - return results["token"].toByteArray(); - } + const QByteArray getUserToken(const QString& user); /// /// @brief update password of given user. The user should be tested (isUserAuthorized) to verify this change - /// @param user The user name - /// @param newPw The new password to set - /// @return True on success else false + /// @param user The user name + /// @param newassword The new password to set + /// @return True on success else false /// - inline bool updateUserPassword(const QString& user, const QString& newPw) - { - QVariantMap map; - map["password"] = calcPasswordHashOfUser(user, newPw); - - VectorPair cond; - cond.append(CPair("user", user)); - return updateRecord(cond, map); - } + bool updateUserPassword(const QString& user, const QString& newPassword); /// /// @brief Reset password of Hyperion user !DANGER! Used in Hyperion main.cpp /// @return True on success else false /// - inline bool resetHyperionUser() - { - QVariantMap map; - map["password"] = calcPasswordHashOfUser(hyperion::DEFAULT_USER, hyperion::DEFAULT_PASSWORD); - - VectorPair cond; - cond.append(CPair("user", hyperion::DEFAULT_USER)); - return updateRecord(cond, map); - } + bool resetHyperionUser(); /// /// @brief Update 'last_use' column entry for the corresponding user /// @param[in] user The user to search for /// - inline void updateUserUsed(const QString& user) - { - QVariantMap map; - map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("user", user)); - updateRecord(cond, map); - } + void updateUserUsed(const QString& user); /// /// @brief Test if token record exists, updates last_use on success /// @param[in] token The token id /// @return true on success else false /// - inline bool tokenExist(const QString& token) - { - QVariantMap map; - map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("token", hashToken(token))); - if(recordExists(cond)) - { - // update it - createRecord(cond,map); - return true; - } - return false; - } + bool tokenExist(const QString& token); /// /// @brief Create a new token record with comment /// @param[in] token The token id as plaintext /// @param[in] comment The comment for the token (eg a human readable identifier) - /// @param[in] id The id for the token + /// @param[in] identifier The identifier for the token /// @return true on success else false /// - inline bool createToken(const QString& token, const QString& comment, const QString& id) - { - QVariantMap map; - map["comment"] = comment; - map["id"] = idExist(id) ? QUuid::createUuid().toString().remove("{").remove("}").left(5) : id; - map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("token", hashToken(token))); - return createRecord(cond, map); - } + bool createToken(const QString& token, const QString& comment, const QString& identifier); /// - /// @brief Delete token record by id - /// @param[in] id The token id + /// @brief Delete token record by identifier + /// @param[in] identifier The token identifier /// @return true on success else false /// - inline bool deleteToken(const QString& id) - { - VectorPair cond; - cond.append(CPair("id", id)); - return deleteRecord(cond); - } + bool deleteToken(const QString& identifier); /// - /// @brief Rename token record by id - /// @param[in] id The token id + /// @brief Rename token record by identifier + /// @param[in] identifier The token identifier /// @param[in] comment The new comment /// @return true on success else false /// - inline bool renameToken(const QString &id, const QString &comment) - { - QVariantMap map; - map["comment"] = comment; - - VectorPair cond; - cond.append(CPair("id", id)); - return updateRecord(cond, map); - } + bool renameToken(const QString &identifier, const QString &comment); /// /// @brief Get all 'comment', 'last_use' and 'id' column entries /// @return A vector of all lists /// - inline const QVector getTokenList() - { - QVector results; - getRecords(results, QStringList() << "comment" << "id" << "last_use"); - - return results; - } + const QVector getTokenList(); /// - /// @brief Test if id exists - /// @param[in] id The id + /// @brief Test if identifier exists + /// @param[in] identifier The identifier /// @return true on success else false /// - inline bool idExist(const QString& id) - { - - VectorPair cond; - cond.append(CPair("id", id)); - return recordExists(cond); - } + bool identifierExist(const QString& identifier); /// /// @brief Get the passwort hash of a user from db /// @param user The user name /// @return password as hash /// - inline const QByteArray getPasswordHashOfUser(const QString& user) - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("user", user)); - getRecord(cond, results, QStringList()<<"password"); - - return results["password"].toByteArray(); - } + const QByteArray getPasswordHashOfUser(const QString& user); /// /// @brief Calc the password hash of a user based on user name and password @@ -289,36 +140,22 @@ public: /// @param pw The password /// @return The calced password hash /// - inline const QByteArray calcPasswordHashOfUser(const QString& user, const QString& pw) - { - // get salt - QVariantMap results; - VectorPair cond; - cond.append(CPair("user", user)); - getRecord(cond, results, QStringList()<<"salt"); - - // calc - return hashPasswordWithSalt(pw,results["salt"].toByteArray()); - } + const QByteArray calcPasswordHashOfUser(const QString& user, const QString& password); /// /// @brief Create a password hash of plaintex password + salt - /// @param pw The plaintext password + /// @param password The plaintext password /// @param salt The salt /// @return The password hash with salt /// - inline const QByteArray hashPasswordWithSalt(const QString& pw, const QByteArray& salt) - { - return QCryptographicHash::hash(pw.toUtf8().append(salt), QCryptographicHash::Sha512).toHex(); - } + const QByteArray hashPasswordWithSalt(const QString& password, const QByteArray& salt); /// /// @brief Create a token hash /// @param token The plaintext token /// @return The token hash /// - inline const QByteArray hashToken(const QString& token) - { - return QCryptographicHash::hash(token.toUtf8(), QCryptographicHash::Sha512).toHex(); - } + const QByteArray hashToken(const QString& token); }; + +#endif // AUTHSTABLE_H diff --git a/include/db/DBConfigManager.h b/include/db/DBConfigManager.h new file mode 100644 index 00000000..652e26ad --- /dev/null +++ b/include/db/DBConfigManager.h @@ -0,0 +1,42 @@ +#ifndef DBCONFGMANAGER_H +#define DBConfigManager_H + +#include +#include "db/SettingsTable.h" +#include "db/InstanceTable.h" + +class DBConfigManager : public DBManager +{ +public: + DBConfigManager(QObject* parent = nullptr); + + QPair importJson(const QString& configFile); + bool exportJson(const QString& path = "") const; + + QJsonObject getConfiguration(const QList& instances = {}, const QStringList& instanceFilteredTypes = {}, const QStringList& globalFilterTypes = {} ) const; + + QPair validateConfiguration(); + QPair validateConfiguration(QJsonObject& config, bool doCorrections = false); + + QPair addMissingDefaults(); + + QPair updateConfiguration(); + QPair updateConfiguration(QJsonObject& config, bool doCorrections = false); + + QPair migrateConfiguration(); + +private: + // Function to import global settings from the configuration + bool importGlobalSettings(const QJsonObject& config, QStringList& errorList); + + // Function to import all instances from the configuration + bool importInstances(const QJsonObject& config, QStringList& errorList); + + // Function to import a single instance + bool importInstance(InstanceTable& instanceTable, const QJsonObject& instanceConfig, quint8 instanceIdx, QStringList& errorList); + + // Function to import settings for a specific instance + bool importInstanceSettings(SettingsTable& settingsTable, const QJsonObject& instanceSettings, QStringList& errorList); +}; + +#endif // DBCONFGMANAGER_H diff --git a/include/db/DBManager.h b/include/db/DBManager.h index 4d36471d..a5d85802 100644 --- a/include/db/DBManager.h +++ b/include/db/DBManager.h @@ -1,10 +1,17 @@ #pragma once +#ifdef Unsorted +#undef Unsorted +#endif + #include #include #include #include #include +#include +#include +#include class QSqlDatabase; class QSqlQuery; @@ -26,13 +33,22 @@ class DBManager : public QObject Q_OBJECT public: - DBManager(QObject* parent = nullptr); - ~DBManager() override; + explicit DBManager(QObject* parent = nullptr); + + static void initializeDatabase(const QDir& dataDirectory, bool isReadOnly); + + static QDir getDataDirectory() { return _dataDirectory;} + static QDir getDirectory() { return _databaseDirectory;} + static QFileInfo getFileInfo() { return _databaseFile;} + static bool isReadOnly() { return _isReadOnly; } + + /// + /// @brief Sets the database in read-only mode. + /// Updates will not written to the tables + /// @param[in] readOnly True read-only, false - read/write + /// + static void setReadonly(bool isReadOnly) { _isReadOnly = isReadOnly; } - /// set root path - void setRootPath(const QString& rootPath); - /// define the database to work with - void setDatabaseName(const QString& dbn) { _dbn = dbn; }; /// set a table to work with void setTable(const QString& table); @@ -61,6 +77,8 @@ public: /// bool recordExists(const VectorPair& conditions) const; + bool recordsNotExisting(const QVariantList& testValues,const QString& column, QStringList& nonExistingRecs, const QString& condition ) const; + /// /// @brief Create a new record in table when the conditions find no existing entry. Add additional key:value pairs in columns /// DO NOT repeat column keys between 'conditions' and 'columns' as they will be merged on creation @@ -98,6 +116,18 @@ public: /// bool getRecords(QVector& results, const QStringList& tColumns = QStringList(), const QStringList& tOrder = QStringList()) const; + /// + /// @brief Get data of multiple records, you need to specify the columns. This search is without conditions. Good to grab all data from db + /// @param[in] conditions condition to search for (WHERE) + /// @param[out] results results of query + /// @param[in] tColumns target columns to search in (optional) if not provided returns all columns + /// @param[in] tOrder target order columns with order by ASC/DESC (optional) + /// @return True on success else false + /// + bool getRecords(const VectorPair& conditions, QVector& results, const QStringList& tColumns = {}, const QStringList& tOrder = {}) const; + + bool getRecords(const QString& condition, const QVariantList& bindValues, QVector& results, const QStringList& tColumns = {}, const QStringList& tOrder = {}) const; + /// /// @brief Delete a record determined by conditions /// @param[in] conditions conditions of the row to delete it (WHERE) @@ -119,23 +149,36 @@ public: /// bool deleteTable(const QString& table) const; - /// - /// @brief Sets a table in read-only mode. - /// Updates will not written to the table - /// @param[in] readOnly True read-only, false - read/write - /// - void setReadonlyMode(bool readOnly) { _readonlyMode = readOnly; }; + bool executeQuery(QSqlQuery& query) const; + + bool startTransaction(QSqlDatabase& idb) const; + bool startTransaction(QSqlDatabase& idb, QStringList& errorList); + bool commiTransaction(QSqlDatabase& idb) const; + bool commiTransaction(QSqlDatabase& idb, QStringList& errorList); + bool rollbackTransaction(QSqlDatabase& idb) const; + bool rollbackTransaction(QSqlDatabase& idb, QStringList& errorList); + + // Utility function to log errors and append to error list + void logErrorAndAppend(const QString& errorText, QStringList& errorList); + +protected: + Logger* _log; private: + static QDir _dataDirectory; + static QDir _databaseDirectory; + static QFileInfo _databaseFile; + static QThreadStorage _databasePool; + static bool _isReadOnly; - Logger* _log; - /// databse connection & file name, defaults to hyperion - QString _dbn = "hyperion"; - /// table in database - QString _table; + /// databse connection & file name, defaults to hyperion + QString _dbn = "hyperion"; - bool _readonlyMode; + /// table in database + QString _table; - /// addBindValue to query given by QVariantList - void doAddBindValue(QSqlQuery& query, const QVariantList& variants) const; + /// addBindValues to query given by QVariantList + void addBindValues(QSqlQuery& query, const QVariantList& variants) const; + + QString constructExecutedQuery(const QSqlQuery& query) const; }; diff --git a/include/db/DBMigrationManager.h b/include/db/DBMigrationManager.h new file mode 100644 index 00000000..be315cd1 --- /dev/null +++ b/include/db/DBMigrationManager.h @@ -0,0 +1,33 @@ +#ifndef DBMIGRATIONMANAGER_H +#define DBMIGRATIONMANAGER_H + +#include +#include + +#include + +class DBMigrationManager : public DBManager +{ + Q_OBJECT +public: + explicit DBMigrationManager(QObject *parent = nullptr); + + bool isMigrationRequired(); + bool migrateSettings(QJsonObject& config); + +private: + + bool upgradeGlobalSettings(const semver::version& currentVersion, QJsonObject& config); + bool upgradeGlobalSettings_alpha_9(semver::version& currentVersion, QJsonObject& config); + bool upgradeGlobalSettings_2_0_12(semver::version& currentVersion, QJsonObject& config); + bool upgradeGlobalSettings_2_0_16(semver::version& currentVersion, QJsonObject& config); + bool upgradeGlobalSettings_2_1_0(semver::version& currentVersion, QJsonObject& config); + + bool upgradeInstanceSettings(const semver::version& currentVersion, quint8 instance, QJsonObject& config); + bool upgradeInstanceSettings_alpha_9(semver::version& currentVersion, quint8 instance, QJsonObject& config); + bool upgradeInstanceSettings_2_0_12(semver::version& currentVersion, quint8 instance, QJsonObject& config); + bool upgradeInstanceSettings_2_0_13(semver::version& currentVersion, quint8 instance, QJsonObject& config); + bool upgradeInstanceSettings_2_0_16(semver::version& currentVersion, quint8 instance, QJsonObject& config); +}; + +#endif // DBMIGRATIONMANAGER_H diff --git a/include/db/InstanceTable.h b/include/db/InstanceTable.h index 912ecbaf..7fbb580e 100644 --- a/include/db/InstanceTable.h +++ b/include/db/InstanceTable.h @@ -1,11 +1,7 @@ -#pragma once +#ifndef INSTANCETABLE_H +#define INSTANCETABLE_H -// db #include -#include - -// qt -#include /// /// @brief Hyperion instance manager specific database interface. prepares also the Hyperion database for all follow up usage (Init QtSqlConnection) along with db name @@ -14,22 +10,7 @@ class InstanceTable : public DBManager { public: - InstanceTable(const QString& rootPath, QObject* parent = nullptr, bool readonlyMode = false) - : DBManager(parent) - { - - setReadonlyMode(readonlyMode); - // Init Hyperion database usage - setRootPath(rootPath); - setDatabaseName("hyperion"); - - // Init instance table - setTable("instances"); - createTable(QStringList()<<"instance INTEGER"<<"friendly_name TEXT"<<"enabled INTEGER DEFAULT 0"<<"last_use TEXT"); - - // start/create the first Hyperion instance index 0 - createInstance(); - }; + explicit InstanceTable(QObject* parent = nullptr); /// /// @brief Create a new Hyperion instance entry, the name needs to be unique @@ -37,53 +18,19 @@ public: /// @param[out] inst The id that has been assigned /// @return True on success else false /// - inline bool createInstance(const QString& name, quint8& inst) - { - VectorPair fcond; - fcond.append(CPair("friendly_name",name)); + bool createInstance(const QString& name, quint8& inst); - // check duplicate - if(!recordExists(fcond)) - { - inst = 0; - VectorPair cond; - cond.append(CPair("instance",inst)); - - // increment to next avail index - while(recordExists(cond)) - { - inst++; - cond.removeFirst(); - cond.append(CPair("instance",inst)); - } - // create - QVariantMap data; - data["friendly_name"] = name; - data["instance"] = inst; - VectorPair lcond; - return createRecord(lcond, data); - } - return false; - } + /// + /// @brief Create first Hyperion instance entry, if index 0 is not found. + /// + void createDefaultInstance(); /// /// @brief Delete a Hyperion instance /// @param inst The id that has been assigned /// @return True on success else false /// - inline bool deleteInstance(quint8 inst) - { - VectorPair cond; - cond.append(CPair("instance",inst)); - if(deleteRecord(cond)) - { - // delete settings entries - SettingsTable settingsTable(inst); - settingsTable.deleteInstance(); - return true; - } - return false; - } + bool deleteInstance(quint8 inst); /// /// @brief Assign a new name for the given instance @@ -91,141 +38,59 @@ public: /// @param name The new name of the instance /// @return True on success else false (instance not found) /// - inline bool saveName(quint8 inst, const QString& name) - { - VectorPair fcond; - fcond.append(CPair("friendly_name",name)); - - // check duplicate - if(!recordExists(fcond)) - { - if(instanceExist(inst)) - { - VectorPair cond; - cond.append(CPair("instance",inst)); - QVariantMap data; - data["friendly_name"] = name; - - return updateRecord(cond, data); - } - } - return false; - } - - + bool saveName(quint8 inst, const QString& name); /// /// @brief Get all instances with all columns - /// @param justEnabled return just enabled instances if true + /// @param onlyEnabled return only enabled instances if true /// @return The found instances /// - inline QVector getAllInstances(bool justEnabled = false) - { - QVector results; - getRecords(results, QStringList(), QStringList() << "instance ASC"); - if(justEnabled) - { - for (auto it = results.begin(); it != results.end();) - { - if( ! (*it)["enabled"].toBool()) - { - it = results.erase(it); - continue; - } - ++it; - } - } - return results; - } + QVector getAllInstances(bool onlyEnabled = false); + + /// + /// @brief Get all instance IDs + /// @param onlyEnabled return only enabled instance IDs if true + /// @return The found instances + /// + QList getAllInstanceIDs (bool onlyEnabled = false); /// /// @brief Test if instance record exists /// @param[in] user The user id /// @return true on success else false /// - inline bool instanceExist(quint8 inst) - { - VectorPair cond; - cond.append(CPair("instance",inst)); - return recordExists(cond); - } + bool instanceExist(quint8 inst); /// /// @brief Get instance name by instance index /// @param index The index to search for /// @return The name of this index, may return NOT FOUND if not found /// - inline const QString getNamebyIndex(quint8 index) - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("instance", index)); - getRecord(cond, results, QStringList("friendly_name")); - - QString name = results["friendly_name"].toString(); - return name.isEmpty() ? "NOT FOUND" : name; - } + QString getNamebyIndex(quint8 index); /// /// @brief Update 'last_use' timestamp /// @param inst The instance to update + /// @return True on success else false /// - inline void setLastUse(quint8 inst) - { - VectorPair cond; - cond.append(CPair("instance", inst)); - QVariantMap map; - map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - updateRecord(cond, map); - } + bool setLastUse(quint8 inst); /// /// @brief Update 'enabled' column by instance index /// @param inst The instance to update /// @param newState True when enabled else false + /// @return True on success else false /// - inline void setEnable(quint8 inst, bool newState) - { - VectorPair cond; - cond.append(CPair("instance", inst)); - QVariantMap map; - map["enabled"] = newState; - updateRecord(cond, map); - } + bool setEnable(quint8 inst, bool newState); /// /// @brief Get state of 'enabled' column by instance index /// @param inst The instance to get /// @return True when enabled else false /// - inline bool isEnabled(quint8 inst) - { - VectorPair cond; - cond.append(CPair("instance", inst)); - QVariantMap results; - getRecord(cond, results); - - return results["enabled"].toBool(); - } + bool isEnabled(quint8 inst); private: - /// - /// @brief Create first Hyperion instance entry, if index 0 is not found. - /// - inline void createInstance() - { - if(instanceExist(0)) - setEnable(0, true); - else - { - QVariantMap data; - data["friendly_name"] = "First LED Hardware instance"; - VectorPair cond; - cond.append(CPair("instance", 0)); - if(createRecord(cond, data)) - setEnable(0, true); - else - throw std::runtime_error("Failed to create Hyperion root instance in db! This should never be the case..."); - } - } }; + +#endif // INSTANCETABLE_H diff --git a/include/db/MetaTable.h b/include/db/MetaTable.h index e1861568..3da71904 100644 --- a/include/db/MetaTable.h +++ b/include/db/MetaTable.h @@ -1,14 +1,9 @@ -#pragma once +#ifndef METATABLE_H +#define METATABLE_H // hyperion #include -// qt -#include -#include -#include -#include - /// /// @brief meta table specific database interface /// @@ -17,47 +12,13 @@ class MetaTable : public DBManager public: /// construct wrapper with plugins table and columns - MetaTable(QObject* parent = nullptr, bool readonlyMode = false) - : DBManager(parent) - { - setReadonlyMode(readonlyMode); - - setTable("meta"); - createTable(QStringList()<<"uuid TEXT"<<"created_at TEXT"); - }; + explicit MetaTable(QObject* parent = nullptr); /// /// @brief Get the uuid, if the uuid is not set it will be created /// @return The uuid /// - inline QString getUUID() const - { - QVector results; - getRecords(results, QStringList() << "uuid"); - - for(const auto & entry : results) - { - if(!entry["uuid"].toString().isEmpty()) - return entry["uuid"].toString(); - } - - // create new uuidv5 based on net adapter MAC, save to db and return - QString hash; - foreach(QNetworkInterface interface, QNetworkInterface::allInterfaces()) - { - if (!(interface.flags() & QNetworkInterface::IsLoopBack)) - { - hash = QCryptographicHash::hash(interface.hardwareAddress().toLocal8Bit(),QCryptographicHash::Sha1).toHex(); - break; - } - } - const QString newUuid = QUuid::createUuidV5(QUuid(), hash).toString().mid(1, 36); - VectorPair cond; - cond.append(CPair("uuid",newUuid)); - QVariantMap map; - map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - createRecord(cond, map); - - return newUuid; - } + QString getUUID() const; }; + +#endif // METATABLE_H diff --git a/include/db/SettingsTable.h b/include/db/SettingsTable.h index d1b31c21..66c6634d 100644 --- a/include/db/SettingsTable.h +++ b/include/db/SettingsTable.h @@ -1,12 +1,20 @@ -#pragma once +#ifndef SETTINGSTABLE_H +#define SETTINGSTABLE_H + +#include + +#ifdef WIN32 + #undef max +#endif -// hyperion #include +#include -// qt -#include #include +const int GLOABL_INSTANCE_ID = std::numeric_limits::max();; +const char DEFAULT_CONFIG_VERSION[] = "2.0.0-alpha.8"; + /// /// @brief settings table db interface /// @@ -15,14 +23,7 @@ class SettingsTable : public DBManager public: /// construct wrapper with settings table - SettingsTable(quint8 instance, QObject* parent = nullptr) - : DBManager(parent) - , _hyperion_inst(instance) - { - setTable("settings"); - // create table columns - createTable(QStringList()<<"type TEXT"<<"config TEXT"<<"hyperion_inst INTEGER"<<"updated_at TEXT"); - }; + SettingsTable(quint8 instance = GLOABL_INSTANCE_ID, QObject* parent = nullptr); /// /// @brief Create or update a settings record @@ -30,19 +31,7 @@ public: /// @param[in] config The configuration data /// @return true on success else false /// - inline bool createSettingsRecord(const QString& type, const QString& config) const - { - QVariantMap map; - map["config"] = config; - map["updated_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("type",type)); - // when a setting is not global we are searching also for the instance - if(!isSettingGlobal(type)) - cond.append(CPair("AND hyperion_inst",_hyperion_inst)); - return createRecord(cond, map); - } + bool createSettingsRecord(const QString& type, const QString& config) const; /// /// @brief Test if record exist, type can be global setting or local (instance) @@ -50,76 +39,64 @@ public: /// @param[in] hyperion_inst The instance of hyperion assigned (might be empty) /// @return true on success else false /// - inline bool recordExist(const QString& type) const - { - VectorPair cond; - cond.append(CPair("type",type)); - // when a setting is not global we are searching also for the instance - if(!isSettingGlobal(type)) - cond.append(CPair("AND hyperion_inst",_hyperion_inst)); - return recordExists(cond); - } + bool recordExist(const QString& type) const; /// /// @brief Get 'config' column of settings entry as QJsonDocument /// @param[in] type The settings type /// @return The QJsonDocument /// - inline QJsonDocument getSettingsRecord(const QString& type) const - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("type",type)); - // when a setting is not global we are searching also for the instance - if(!isSettingGlobal(type)) - cond.append(CPair("AND hyperion_inst",_hyperion_inst)); - getRecord(cond, results, QStringList("config")); - return QJsonDocument::fromJson(results["config"].toByteArray()); - } + QJsonDocument getSettingsRecord(const QString& type) const; /// /// @brief Get 'config' column of settings entry as QString /// @param[in] type The settings type /// @return The QString /// - inline QString getSettingsRecordString(const QString& type) const - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("type",type)); - // when a setting is not global we are searching also for the instance - if(!isSettingGlobal(type)) - cond.append(CPair("AND hyperion_inst",_hyperion_inst)); - getRecord(cond, results, QStringList("config")); - return results["config"].toString(); - } + QString getSettingsRecordString(const QString& type) const; + + QJsonObject getSettings(const QStringList& filteredTypes = {} ) const; + QJsonObject getSettings(const QVariant& instance, const QStringList& filteredTypes = {} ) const; + + QStringList nonExtingTypes() const; + QPair addMissingDefaults(); /// /// @brief Delete all settings entries associated with this instance, called from InstanceTable of HyperionIManager /// - inline void deleteInstance() const - { - VectorPair cond; - cond.append(CPair("hyperion_inst",_hyperion_inst)); - deleteRecord(cond); - } + void deleteInstance() const; - inline bool isSettingGlobal(const QString& type) const - { - // list of global settings - QStringList list; - // server port services - list << "jsonServer" << "protoServer" << "flatbufServer" << "forwarder" << "webConfig" << "network" - // capture - << "framegrabber" << "grabberV4L2" << "grabberAudio" - //Events - << "osEvents" << "cecEvents" << "schedEvents" - // other - << "logger" << "general"; + const QVector& getGlobalSettingTypes() const; + bool isGlobalSettingType(const QString& type) const; - return list.contains(type); - } + const QVector& getInstanceSettingTypes() const; + bool isInstanceSettingType(const QString& type) const; + + const QJsonObject& getDefaultSettings() const; + + semver::version getConfigVersion(); + QString getConfigVersionString(); + + bool resolveConfigVersion(); + bool resolveConfigVersion(QJsonObject generalConfig); private: - const quint8 _hyperion_inst; + QString fixVersion(const QString& version); + + QVector initializeGlobalSettingTypes() const; + static QVector globalSettingTypes; + static bool areGlobalSettingTypesInitialised; + + QVector initializeInstanceSettingTypes() const; + static QVector instanceSettingTypes; + static bool areInstanceSettingTypesInitialised; + + QJsonObject initializeDefaultSettings() const; + static QJsonObject defaultSettings; + static bool areDefaultSettingsInitialised; + + const quint8 _instance; + semver::version _configVersion; }; + +#endif // SETTINGSTABLE_H diff --git a/include/effectengine/Effect.h b/include/effectengine/Effect.h index 4766f372..294f5efc 100644 --- a/include/effectengine/Effect.h +++ b/include/effectengine/Effect.h @@ -24,13 +24,13 @@ public: friend class EffectModule; - Effect(Hyperion *hyperion - , int priority - , int timeout - , const QString &script - , const QString &name - , const QJsonObject &args = QJsonObject() - , const QString &imageData = "" + Effect(Hyperion* hyperion + , int priority + , int timeout + , const QString& script + , const QString& name + , const QJsonObject& args = QJsonObject() + , const QString& imageData = "" ); ~Effect() override; @@ -64,20 +64,20 @@ public: QString getScript() const { return _script; } QString getName() const { return _name; } - int getTimeout() const {return _timeout; } + int getTimeout() const { return _timeout; } bool isEndless() const { return _isEndless; } QJsonObject getArgs() const { return _args; } signals: - void setInput(int priority, const std::vector &ledColors, int timeout_ms, bool clearEffect); - void setInputImage(int priority, const Image &image, int timeout_ms, bool clearEffect); + void setInput(int priority, const std::vector& ledColors, int timeout_ms, bool clearEffect); + void setInputImage(int priority, const Image& image, int timeout_ms, bool clearEffect); private: - void setModuleParameters(); + bool setModuleParameters(); void addImage(); - Hyperion *_hyperion; + Hyperion* _hyperion; const int _priority; @@ -95,12 +95,12 @@ private: /// Buffer for colorData QVector _colors; - Logger *_log; + Logger* _log; // Reflects whenever this effects should interrupt (timeout or external request) - std::atomic _interupt {}; + std::atomic _interupt{}; QSize _imageSize; QImage _image; - QPainter *_painter; + QPainter* _painter; QVector _imageStack; }; diff --git a/include/effectengine/EffectModule.h b/include/effectengine/EffectModule.h index f76e7f62..a9c5a4fd 100644 --- a/include/effectengine/EffectModule.h +++ b/include/effectengine/EffectModule.h @@ -8,47 +8,41 @@ class Effect; -class EffectModule: public QObject +class EffectModule : public QObject { Q_OBJECT public: - // Python 3 module def - static struct PyModuleDef moduleDef; - - // Init module - static PyObject* PyInit_hyperion(); - // Register module once static void registerHyperionExtensionModule(); // json 2 python - static PyObject * json2python(const QJsonValue & jsonData); + static PyObject* json2python(const QJsonValue& jsonData); // Wrapper methods for Python interpreter extra buildin methods static PyMethodDef effectMethods[]; - static PyObject* wrapSetColor (PyObject *self, PyObject *args); - static PyObject* wrapSetImage (PyObject *self, PyObject *args); - static PyObject* wrapGetImage (PyObject *self, PyObject *args); - static PyObject* wrapAbort (PyObject *self, PyObject *args); - static PyObject* wrapImageShow (PyObject *self, PyObject *args); - static PyObject* wrapImageLinearGradient (PyObject *self, PyObject *args); - static PyObject* wrapImageConicalGradient (PyObject *self, PyObject *args); - static PyObject* wrapImageRadialGradient (PyObject *self, PyObject *args); - static PyObject* wrapImageSolidFill (PyObject *self, PyObject *args); - static PyObject* wrapImageDrawLine (PyObject *self, PyObject *args); - static PyObject* wrapImageDrawPoint (PyObject *self, PyObject *args); - static PyObject* wrapImageDrawRect (PyObject *self, PyObject *args); - static PyObject* wrapImageDrawPolygon (PyObject *self, PyObject *args); - static PyObject* wrapImageDrawPie (PyObject *self, PyObject *args); - static PyObject* wrapImageSetPixel (PyObject *self, PyObject *args); - static PyObject* wrapImageGetPixel (PyObject *self, PyObject *args); - static PyObject* wrapImageSave (PyObject *self, PyObject *args); - static PyObject* wrapImageMinSize (PyObject *self, PyObject *args); - static PyObject* wrapImageWidth (PyObject *self, PyObject *args); - static PyObject* wrapImageHeight (PyObject *self, PyObject *args); - static PyObject* wrapImageCRotate (PyObject *self, PyObject *args); - static PyObject* wrapImageCOffset (PyObject *self, PyObject *args); - static PyObject* wrapImageCShear (PyObject *self, PyObject *args); - static PyObject* wrapImageResetT (PyObject *self, PyObject *args); + static PyObject* wrapSetColor(PyObject* self, PyObject* args); + static PyObject* wrapSetImage(PyObject* self, PyObject* args); + static PyObject* wrapGetImage(PyObject* self, PyObject* args); + static PyObject* wrapAbort(PyObject* self, PyObject* args); + static PyObject* wrapImageShow(PyObject* self, PyObject* args); + static PyObject* wrapImageLinearGradient(PyObject* self, PyObject* args); + static PyObject* wrapImageConicalGradient(PyObject* self, PyObject* args); + static PyObject* wrapImageRadialGradient(PyObject* self, PyObject* args); + static PyObject* wrapImageSolidFill(PyObject* self, PyObject* args); + static PyObject* wrapImageDrawLine(PyObject* self, PyObject* args); + static PyObject* wrapImageDrawPoint(PyObject* self, PyObject* args); + static PyObject* wrapImageDrawRect(PyObject* self, PyObject* args); + static PyObject* wrapImageDrawPolygon(PyObject* self, PyObject* args); + static PyObject* wrapImageDrawPie(PyObject* self, PyObject* args); + static PyObject* wrapImageSetPixel(PyObject* self, PyObject* args); + static PyObject* wrapImageGetPixel(PyObject* self, PyObject* args); + static PyObject* wrapImageSave(PyObject* self, PyObject* args); + static PyObject* wrapImageMinSize(PyObject* self, PyObject* args); + static PyObject* wrapImageWidth(PyObject* self, PyObject* args); + static PyObject* wrapImageHeight(PyObject* self, PyObject* args); + static PyObject* wrapImageCRotate(PyObject* self, PyObject* args); + static PyObject* wrapImageCOffset(PyObject* self, PyObject* args); + static PyObject* wrapImageCShear(PyObject* self, PyObject* args); + static PyObject* wrapImageResetT(PyObject* self, PyObject* args); }; diff --git a/include/flatbufserver/FlatBufferServer.h b/include/flatbufserver/FlatBufferServer.h index ba39c647..6120b845 100644 --- a/include/flatbufserver/FlatBufferServer.h +++ b/include/flatbufserver/FlatBufferServer.h @@ -71,5 +71,7 @@ private: quint16 _port; const QJsonDocument _config; + int _pixelDecimation; + QVector _openConnections; }; diff --git a/include/grabber/osx/OsxFrameGrabber.h b/include/grabber/osx/OsxFrameGrabber.h index afb430fc..1adc3696 100644 --- a/include/grabber/osx/OsxFrameGrabber.h +++ b/include/grabber/osx/OsxFrameGrabber.h @@ -1,6 +1,6 @@ #pragma once -// OSX includes +// CoreGraphics #include // Utils includes diff --git a/include/hyperion/AuthManager.h b/include/hyperion/AuthManager.h index 790f70b6..a4b02154 100644 --- a/include/hyperion/AuthManager.h +++ b/include/hyperion/AuthManager.h @@ -23,7 +23,7 @@ class AuthManager : public QObject private: friend class HyperionDaemon; /// constructor is private, can be called from HyperionDaemon - AuthManager(QObject *parent = nullptr, bool readonlyMode = false); + AuthManager(QObject *parent = nullptr); public: struct AuthDefinition diff --git a/include/hyperion/Hyperion.h b/include/hyperion/Hyperion.h index a15eda8e..6a377205 100644 --- a/include/hyperion/Hyperion.h +++ b/include/hyperion/Hyperion.h @@ -108,8 +108,6 @@ public: /// QString getActiveDeviceType() const; - bool getReadOnlyMode() const {return _readOnlyMode; } - public slots: /// @@ -335,18 +333,9 @@ public slots: /// /// @brief Save a complete json config /// @param config The entire config object - /// @param correct If true will correct json against schema before save /// @return True on success else false /// - bool saveSettings(const QJsonObject& config, bool correct = false); - - /// - /// @brief Restore a complete json config - /// @param config The entire config object - /// @param correct If true will correct json against schema before save - /// @return True on success else false - /// - bool restoreSettings(const QJsonObject& config, bool correct = false); + QPair saveSettings(const QJsonObject& config); /// ############ /// COMPONENTREGISTER @@ -552,7 +541,7 @@ private: /// @brief Constructs the Hyperion instance, just accessible for HyperionIManager /// @param instance The instance index /// - Hyperion(quint8 instance, bool readonlyMode = false); + Hyperion(quint8 instance); /// instance index const quint8 _instIndex; @@ -615,6 +604,4 @@ private: /// Boblight instance BoblightServer* _boblightServer; #endif - - bool _readOnlyMode; }; diff --git a/include/hyperion/HyperionIManager.h b/include/hyperion/HyperionIManager.h index d7610d5b..60a17076 100644 --- a/include/hyperion/HyperionIManager.h +++ b/include/hyperion/HyperionIManager.h @@ -59,12 +59,18 @@ public slots: /// QVector getInstanceData() const; + QString getInstanceName(quint8 inst = 0); /// /// @brief Get all instance indicies of running instances /// QList getRunningInstanceIdx() const; + /// + /// @brief Get all instance indicies configured + /// + QList getInstanceIds() const; + /// /// @brief Start a Hyperion instance /// @param instance Instance index @@ -115,8 +121,6 @@ public slots: /// bool saveName(quint8 inst, const QString& name); - QString getRootPath() const { return _rootPath; } - signals: /// /// @brief Emits whenever the state of a instance changes according to enum instanceState @@ -195,9 +199,8 @@ private: friend class HyperionDaemon; /// /// @brief Construct the Manager - /// @param The root path of all userdata /// - HyperionIManager(const QString& rootPath, QObject* parent = nullptr, bool readonlyMode = false); + HyperionIManager(QObject* parent = nullptr); /// /// @brief Start all instances that are marked as enabled in db. Non blocking @@ -218,12 +221,9 @@ private: private: Logger* _log; InstanceTable* _instanceTable; - const QString _rootPath; QMap _runningInstances; + QList _startQueue; - - bool _readonlyMode; - /// All pending requests QMap _pendingRequests; }; diff --git a/include/hyperion/SettingsManager.h b/include/hyperion/SettingsManager.h index 9d525cf6..ddb922a9 100644 --- a/include/hyperion/SettingsManager.h +++ b/include/hyperion/SettingsManager.h @@ -3,14 +3,11 @@ #include #include -#include -using namespace semver; +#include // qt includes #include -const int GLOABL_INSTANCE_ID = 255; - class Hyperion; class SettingsTable; @@ -26,23 +23,21 @@ public: /// @params instance Instance index of HyperionInstanceManager /// @params parent The parent hyperion instance /// - SettingsManager(quint8 instance, QObject* parent = nullptr, bool readonlyMode = false); + SettingsManager(quint8 instance = GLOABL_INSTANCE_ID, QObject* parent = nullptr); /// - /// @brief Save a complete json configuration + /// @brief Save a complete JSON configuration /// @param config The entire config object - /// @param correct If true will correct json against schema before save - /// @return True on success else false + /// @return True on success else false, plus validation errors /// - bool saveSettings(QJsonObject config, bool correct = false); + QPair saveSettings(const QJsonObject& config); /// - /// @brief Restore a complete json configuration + /// @brief Correct a complete JSON configuration /// @param config The entire config object - /// @param correct If true will correct json against schema before save - /// @return True on success else false + /// @return True on success else false, plus correction details /// - bool restoreSettings(QJsonObject config, bool correct = false); + QPair correctSettings(QJsonObject& config); /// /// @brief get a single setting json from configuration @@ -52,10 +47,18 @@ public: QJsonDocument getSetting(settings::type type) const; /// - /// @brief get the full settings object of this instance (with global settings) + /// @brief get a single setting json from configuration + /// @param type The type as string + /// @return The requested json data as QJsonDocument + /// + QJsonDocument getSetting(const QString& type) const; + + /// + /// @brief get the selected settings objects of this instance (including global settings) /// @return The requested json /// - QJsonObject getSettings() const; + QJsonObject getSettings(const QStringList& filteredTypes = {}) const; + QJsonObject getSettings(const QVariant& instance, const QStringList& filteredTypes = {} ) const; signals: /// @@ -71,31 +74,19 @@ private: /// @param config The configuration object /// @return True when a migration has been triggered /// - bool handleConfigUpgrade(QJsonObject& config); + bool upgradeConfig(QJsonObject& config); - bool resolveConfigVersion(QJsonObject& config); - /// Logger instance Logger* _log; - /// Hyperion instance - Hyperion* _hyperion; - /// Instance number quint8 _instance; /// instance of database table interface SettingsTable* _sTable; - /// the schema - static QJsonObject schemaJson; - /// the current configuration of this instance QJsonObject _qconfig; - semver::version _configVersion; - semver::version _previousVersion; - - bool _readonlyMode; }; diff --git a/include/python/PythonInit.h b/include/python/PythonInit.h index d3d5ce47..316d8464 100644 --- a/include/python/PythonInit.h +++ b/include/python/PythonInit.h @@ -1,5 +1,9 @@ #pragma once +#undef slots +#include +#define slots Q_SLOTS + /// /// @brief Handle the PythonInit, module registers and DeInit /// @@ -10,4 +14,8 @@ private: PythonInit(); ~PythonInit(); + +#if (PY_VERSION_HEX >= 0x03080000) + void handlePythonError(PyStatus status, PyConfig& config); +#endif }; diff --git a/include/python/PythonProgram.h b/include/python/PythonProgram.h index b161dcd6..4b3d05fe 100644 --- a/include/python/PythonProgram.h +++ b/include/python/PythonProgram.h @@ -9,6 +9,8 @@ #include "Python.h" #define slots +#include + class Logger; class PythonProgram @@ -17,9 +19,15 @@ public: PythonProgram(const QString & name, Logger * log); ~PythonProgram(); + operator PyThreadState* () + { + return _tstate; + } + void execute(const QByteArray &python_code); private: + QString _name; Logger* _log; PyThreadState* _tstate; diff --git a/include/utils/ImageResampler.h b/include/utils/ImageResampler.h index 7aba7a40..4f6dab46 100644 --- a/include/utils/ImageResampler.h +++ b/include/utils/ImageResampler.h @@ -13,6 +13,7 @@ public: void setHorizontalPixelDecimation(int decimator) { _horizontalDecimation = decimator; } void setVerticalPixelDecimation(int decimator) { _verticalDecimation = decimator; } + void setPixelDecimation(int decimator) { _horizontalDecimation = decimator; _verticalDecimation = decimator;} void setCropping(int cropLeft, int cropRight, int cropTop, int cropBottom); void setVideoMode(VideoMode mode) { _videoMode = mode; } void setFlipMode(FlipMode mode) { _flipMode = mode; } diff --git a/include/utils/JsonUtils.h b/include/utils/JsonUtils.h index 10b89d90..9629525a 100644 --- a/include/utils/JsonUtils.h +++ b/include/utils/JsonUtils.h @@ -3,13 +3,14 @@ #include #include +#include #include #include #include namespace JsonUtils { /// - /// @brief read a json file and get the parsed result on success + /// @brief read a JSON file and get the parsed result on success /// @param[in] path The file path to read /// @param[out] obj Returns the parsed QJsonObject /// @param[in] log The logger of the caller to print errors @@ -17,6 +18,7 @@ namespace JsonUtils { /// @return true on success else false /// QPair readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError=false); + QPair readFile(const QString& path, QJsonValue& obj, Logger* log, bool ignError=false); /// /// @brief read a schema file and resolve $refs @@ -28,18 +30,19 @@ namespace JsonUtils { bool readSchema(const QString& path, QJsonObject& obj, Logger* log); /// - /// @brief parse a json QString and get a QJsonObject. Overloaded funtion - /// @param[in] path The file path/name just used for log messages + /// @brief parse a JSON QString and get a QJsonObject. Overloaded funtion + /// @param[in] path The file path/name context used for log messages /// @param[in] data Data to parse /// @param[out] obj Retuns the parsed QJsonObject /// @param[in] log The logger of the caller to print errors /// @return true on success else false /// QPair parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log); + QPair parse(const QString& path, const QString& data, QJsonValue& value, Logger* log); /// - /// @brief parse a json QString and get a QJsonArray. Overloaded function - /// @param[in] path The file path/name just used for log messages + /// @brief parse a JSON QString and get a QJsonArray. Overloaded function + /// @param[in] path The file path/name context used for log messages /// @param[in] data Data to parse /// @param[out] arr Retuns the parsed QJsonArray /// @param[in] log The logger of the caller to print errors @@ -48,8 +51,8 @@ namespace JsonUtils { QPair parse(const QString& path, const QString& data, QJsonArray& arr, Logger* log); /// - /// @brief parse a json QString and get a QJsonDocument - /// @param[in] path The file path/name just used for log messages + /// @brief parse a JSON QString and get a QJsonDocument + /// @param[in] path The file path/name context used for log messages /// @param[in] data Data to parse /// @param[out] doc Retuns the parsed QJsonDocument /// @param[in] log The logger of the caller to print errors @@ -58,29 +61,39 @@ namespace JsonUtils { QPair parse(const QString& path, const QString& data, QJsonDocument& doc, Logger* log); /// - /// @brief Validate json data against a schema - /// @param[in] file The path/name of json file just used for log messages - /// @param[in] json The json data + /// @brief Validate JSON data against a schema + /// @param[in] file The path/name of JSON file context used for log messages + /// @param[in] json The JSON data /// @param[in] schemaP The schema path /// @param[in] log The logger of the caller to print errors - /// @return true on success else false + /// @return true on success else false, plus validation errors /// - QPair validate(const QString& file, const QJsonObject& json, const QString& schemaPath, Logger* log); + QPair validate(const QString& file, const QJsonValue& json, const QString& schemaPath, Logger* log); /// - /// @brief Validate json data against a schema - /// @param[in] file The path/name of json file just used for log messages - /// @param[in] json The json data + /// @brief Validate JSON data against a schema + /// @param[in] file The path/name of JSON file context used for log messages + /// @param[in] json The JSON data /// @param[in] schema The schema object /// @param[in] log The logger of the caller to print errors - /// @return true on success else false + /// @return true on success else false, plus validation errors /// - QPair validate(const QString& file, const QJsonObject& json, const QJsonObject& schema, Logger* log); + QPair validate(const QString& file, const QJsonValue& json, const QJsonObject& schema, Logger* log); /// - /// @brief Write json data to file + /// @brief Validate JSON data against a schema + /// @param[in] file The path/name of JSON file context used for log messages + /// @param[in/out] json The JSON data + /// @param[in] schema The schema object + /// @param[in] log The logger of the caller to print errors + /// @return true on success else false, plus correction messages + /// + QPair correct(const QString& file, QJsonValue& json, const QJsonObject& schema, Logger* log); + + /// + /// @brief Write JSON data to file /// @param[in] filenameThe file path to write - /// @param[in] json The json data to write + /// @param[in] json The JSON data to write /// @param[in] log The logger of the caller to print errors /// @return true on success else false /// @@ -94,4 +107,15 @@ namespace JsonUtils { /// @return true on success else false /// bool resolveRefs(const QJsonObject& schema, QJsonObject& obj, Logger* log); + + + /// + /// @brief Function to convert QJsonValue to QString using QJsonDocument + /// + QString jsonValueToQString(const QJsonValue &value, QJsonDocument::JsonFormat format = QJsonDocument::Compact); + + /// + /// @brief Function to merge two QJsonObjects + /// + QJsonObject mergeJsonObjects(const QJsonObject &obj1, const QJsonObject &obj2, bool overrideObj1 = false); } diff --git a/include/utils/jsonschema/QJsonSchemaChecker.h b/include/utils/jsonschema/QJsonSchemaChecker.h index dbf70683..0ec1f877 100644 --- a/include/utils/jsonschema/QJsonSchemaChecker.h +++ b/include/utils/jsonschema/QJsonSchemaChecker.h @@ -44,7 +44,7 @@ public: /// @return The first boolean is true when the arguments is valid according to the schema. The second is true when the schema contains no errors /// @return TODO: Check the Schema in SetSchema() function and remove the QPair result /// - QPair validate(const QJsonObject& value, bool ignoreRequired = false); + QPair validate(const QJsonValue& value, bool ignoreRequired = false); /// /// @brief Auto correct a JSON structure @@ -52,7 +52,7 @@ public: /// @param ignoreRequired Ignore the "required" keyword in hyperion schema. Default is false /// @return The corrected JSON structure /// - QJsonObject getAutoCorrectedConfig(const QJsonObject& value, bool ignoreRequired = false); + QJsonValue getAutoCorrectedConfig(const QJsonValue& value, bool ignoreRequired = false); /// /// @return A list of error messages @@ -207,7 +207,7 @@ private: /// Auto correction variable QString _correct; /// The auto corrected json-configuration - QJsonObject _autoCorrected; + QJsonValue _autoCorrected; /// The current location into a json-configuration structure being checked QStringList _currentPath; /// The result messages collected during the schema verification diff --git a/include/utils/jsonschema/QJsonUtils.h b/include/utils/jsonschema/QJsonUtils.h index 98498a2b..274934c0 100644 --- a/include/utils/jsonschema/QJsonUtils.h +++ b/include/utils/jsonschema/QJsonUtils.h @@ -11,7 +11,7 @@ class QJsonUtils { public: - static void modify(QJsonObject& value, QStringList path, const QJsonValue& newValue = QJsonValue::Null, QString propertyName = "") + static void modify(QJsonValue& value, QStringList path, const QJsonValue& newValue = QJsonValue::Null, QString propertyName = "") { QJsonObject result; @@ -27,7 +27,7 @@ public: *it = current.mid(1, current.size()-1); } - if (!value.isEmpty()) + if (! (value.toObject().isEmpty() && value.toArray().isEmpty()) ) modifyValue(value, result, path, newValue, propertyName); else if (newValue != QJsonValue::Null && !propertyName.isEmpty()) result[propertyName] = newValue; diff --git a/include/utils/version.hpp b/include/utils/version.hpp index 11c3388c..4b5f6b4e 100644 --- a/include/utils/version.hpp +++ b/include/utils/version.hpp @@ -385,7 +385,7 @@ namespace semver { return -1; } - version& operator= (version& rgt) + version& operator= (const version& rgt) { if ((*this) != rgt) { @@ -404,17 +404,17 @@ namespace semver { return *this; } - friend bool operator== (version &lft, version &rgt) + friend bool operator== (const version &lft, const version &rgt) { return !(lft != rgt); } - friend bool operator!= (version &lft, version &rgt) + friend bool operator!= (const version &lft, const version &rgt) { return (lft > rgt) || (lft < rgt); } - friend bool operator> (version &lft, version &rgt) + friend bool operator> (const version &lft, const version &rgt) { // Major if (lft.getMajor() < 0 && rgt.getMajor() >= 0) @@ -522,17 +522,17 @@ namespace semver { return false; } - friend bool operator>= (version &lft, version &rgt) + friend bool operator>= (const version &lft, const version &rgt) { return (lft > rgt) || (lft == rgt); } - friend bool operator< (version &lft, version &rgt) + friend bool operator< (const version &lft, const version &rgt) { return (rgt > lft); } - friend bool operator<= (version &lft, version &rgt) + friend bool operator<= (const version &lft, const version &rgt) { return (lft < rgt) || (lft == rgt); } diff --git a/libsrc/api/API.cpp b/libsrc/api/API.cpp index 961b415c..b34ec508 100644 --- a/libsrc/api/API.cpp +++ b/libsrc/api/API.cpp @@ -391,34 +391,6 @@ QString API::saveEffect(const QJsonObject &data) } #endif -bool API::saveSettings(const QJsonObject &data) -{ - bool isSaved {true}; - if (!_adminAuthorized) - { - isSaved = false; - } - else - { - QMetaObject::invokeMethod(_hyperion, "saveSettings", Qt::DirectConnection, Q_RETURN_ARG(bool, isSaved), Q_ARG(QJsonObject, data), Q_ARG(bool, true)); - } - return isSaved; -} - -bool API::restoreSettings(const QJsonObject &data) -{ - bool isRestored {true}; - if (!_adminAuthorized) - { - isRestored = false; - } - else - { - QMetaObject::invokeMethod(_hyperion, "restoreSettings", Qt::DirectConnection, Q_RETURN_ARG(bool, isRestored), Q_ARG(QJsonObject, data), Q_ARG(bool, true)); - } - return isRestored; -} - bool API::updateHyperionPassword(const QString &password, const QString &newPassword) { bool isPwUpdated {true}; diff --git a/libsrc/api/JSONRPC_schema/schema-config.json b/libsrc/api/JSONRPC_schema/schema-config.json index 204661cf..50937242 100644 --- a/libsrc/api/JSONRPC_schema/schema-config.json +++ b/libsrc/api/JSONRPC_schema/schema-config.json @@ -10,18 +10,40 @@ "subcommand": { "type" : "string", "required" : true, - "enum" : ["getconfig","getschema","setconfig","restoreconfig","reload"] - }, - "instance" : { - "type" : "integer", - "minimum": 0, - "maximum": 255 + "enum" : ["getconfig","getconfig-old","getschema","setconfig","restoreconfig","reload"] }, "tan" : { "type" : "integer" }, + "configFilter": { + "global" : { + "types": { + "type": "array", + "required": false, + "items" : { + "type" : "string" + } + } + }, + "instances" : { + "ids" : { + "type": "array", + "required": true, + "items" : {}, + "minItems": 1 + }, + "types": { + "type": "array", + "required": false, + "items" :{ + "type" : "string" + } + } + } + }, "config": { - "type" : "object" + "required": false, + "$ref": "schema-settings-full-relaxed.json" } }, "additionalProperties": false diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index fd93cc55..52a5ef6c 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -40,6 +40,7 @@ // auth manager #include +#include #ifdef ENABLE_MDNS // mDNS discover @@ -68,6 +69,8 @@ constexpr int BEARER_TOKEN_TAG_LENGTH = sizeof(BEARER_TOKEN_TAG) - 1; const int MIN_PASSWORD_LENGTH = 8; const int APP_TOKEN_LENGTH = 36; +const char SETTINGS_UI_SCHEMA_FILE[] = ":/schema-settings-ui.json"; + const bool verbose = false; } @@ -707,7 +710,11 @@ void JsonAPI::handleConfigCommand(const QJsonObject& message, const JsonApiComma handleSchemaGetCommand(message, cmd); break; - case SubCommand::GetConfig: + case SubCommand::GetConfig: + handleConfigGetCommand(message, cmd); + break; + + case SubCommand::GetConfigOld: sendSuccessDataReply(_hyperion->getQJsonConfig(), cmd); break; @@ -732,45 +739,165 @@ void JsonAPI::handleConfigCommand(const QJsonObject& message, const JsonApiComma void JsonAPI::handleConfigSetCommand(const QJsonObject &message, const JsonApiCommand& cmd) { - if (message.contains("config")) + if (DBManager::isReadOnly()) { - QJsonObject config = message["config"].toObject(); - if (API::isHyperionEnabled()) + sendErrorReply("Database Error", {"Hyperion is running in read-only mode","Configuration updates are not possible"}, cmd); + return; + } + + QJsonObject config = message["config"].toObject(); + if (config.isEmpty()) + { + sendErrorReply("Update configuration failed", {"No configuration data provided!"}, cmd); + return; + } + + QStringList errorDetails; + + QMap instancesNewConfigs; + + const QJsonArray instances = config["instances"].toArray(); + if (!instances.isEmpty()) + { + QList configuredInstanceIds = _instanceManager->getInstanceIds(); + for (const auto &instance : instances) { - if ( API::saveSettings(config) ) { - sendSuccessReply(cmd); - } else { - sendErrorReply("Save settings failed", cmd); + QJsonObject instanceObject = instance.toObject(); + const QJsonValue idx = instanceObject["id"]; + if (idx.isDouble()) + { + quint8 instanceId = static_cast(idx.toInt()); + if (configuredInstanceIds.contains(instanceId)) + { + instancesNewConfigs.insert(instanceId,instanceObject.value("settings").toObject()); + } + else + { + errorDetails.append(QString("Given instance id '%1' does not exist. Configuration item will be ignored").arg(instanceId)); + } } } - else + } + + const QJsonObject globalSettings = config["global"].toObject().value("settings").toObject(); + if (!globalSettings.isEmpty()) + { + const QJsonObject instanceZeroConfig = instancesNewConfigs.value(0); + instancesNewConfigs.insert(0, JsonUtils::mergeJsonObjects(instanceZeroConfig, globalSettings)); + } + + QMapIterator i (instancesNewConfigs); + while (i.hasNext()) { + i.next(); + + quint8 idx = i.key(); + Hyperion* instance = HyperionIManager::getInstance()->getHyperionInstance(idx); + + QPair isSaved = instance->saveSettings(i.value()); + errorDetails.append(isSaved.second); + } + + if (!errorDetails.isEmpty()) + { + sendErrorReply("Update configuration failed", errorDetails, cmd); + return; + } + + sendSuccessReply(cmd); +} + +void JsonAPI::handleConfigGetCommand(const QJsonObject &message, const JsonApiCommand& cmd) +{ + QJsonObject settings; + QStringList errorDetails; + + QJsonObject filter = message["configFilter"].toObject(); + if (!filter.isEmpty()) + { + QStringList globalFilterTypes; + + const QJsonObject globalConfig = filter["global"].toObject(); + if (!globalConfig.isEmpty()) { - sendErrorReply("It is not possible saving a configuration while Hyperion is disabled", cmd); + const QJsonArray globalTypes = globalConfig["types"].toArray(); + for (const auto &type : globalTypes) { + if (type.isString()) { + globalFilterTypes.append(type.toString()); + } + } } + + QList instanceListFilter; + QStringList instanceFilterTypes; + + const QJsonObject instances = filter["instances"].toObject(); + if (!instances.isEmpty()) + { + QList configuredInstanceIds = _instanceManager->getInstanceIds(); + const QJsonArray instanceIds = instances["ids"].toArray(); + for (const auto &idx : instanceIds) { + if (idx.isDouble()) { + quint8 instanceId = static_cast(idx.toInt()); + if (configuredInstanceIds.contains(instanceId)) + { + instanceListFilter.append(instanceId); + } + else + { + errorDetails.append(QString("Given instance number '%1' does not exist.").arg(instanceId)); + } + } + } + + const QJsonArray instanceTypes = instances["types"].toArray(); + for (const auto &type : instanceTypes) { + if (type.isString()) { + instanceFilterTypes.append(type.toString()); + } + } + } + + settings = JsonInfo::getConfiguration(instanceListFilter, instanceFilterTypes, globalFilterTypes); + } + else + { + //Get complete configuration + settings = JsonInfo::getConfiguration(); + } + + if (!settings.empty()) + { + sendSuccessDataReplyWithError(settings, cmd, errorDetails); + } + else + { + sendErrorReply("Generating full config failed", cmd); } } void JsonAPI::handleConfigRestoreCommand(const QJsonObject &message, const JsonApiCommand& cmd) { - if (message.contains("config")) + QJsonObject config = message["config"].toObject(); + if (API::isHyperionEnabled()) { - QJsonObject config = message["config"].toObject(); - if (API::isHyperionEnabled()) + DBConfigManager configManager; + QPair result = configManager.updateConfiguration(config, false); + if (result.first) { - if ( API::restoreSettings(config) ) - { - sendSuccessReply(cmd); - } - else - { - sendErrorReply("Restore settings failed", cmd); - } + QString infoMsg {"Restarting after importing configuration successfully."}; + sendSuccessDataReply(infoMsg, cmd); + Info(_log, "%s", QSTRING_CSTR(infoMsg)); + emit signalEvent(Event::Restart); } else { - sendErrorReply("Restoring configuration while Hyperion is disabled is not possible", cmd); + sendErrorReply("Restore configuration failed", result.second, cmd); } } + else + { + sendErrorReply("Restoring configuration while Hyperion is disabled is not possible", cmd); + } } void JsonAPI::handleSchemaGetCommand(const QJsonObject& /*message*/, const JsonApiCommand& cmd) @@ -784,7 +911,7 @@ void JsonAPI::handleSchemaGetCommand(const QJsonObject& /*message*/, const JsonA Q_INIT_RESOURCE(resource); // read the hyperion json schema from the resource - QString schemaFile = ":/hyperion-schema"; + QString schemaFile = SETTINGS_UI_SCHEMA_FILE; try { @@ -800,6 +927,21 @@ void JsonAPI::handleSchemaGetCommand(const QJsonObject& /*message*/, const JsonA alldevices = LedDeviceWrapper::getLedDeviceSchemas(); properties.insert("alldevices", alldevices); + // Add infor about the type of setting elements + QJsonObject settingTypes; + QJsonArray globalSettingTypes; + for (const QString &type : SettingsTable().getGlobalSettingTypes()) { + globalSettingTypes.append(type); + } + settingTypes.insert("globalProperties", globalSettingTypes); + + QJsonArray instanceSettingTypes; + for (const QString &type : SettingsTable().getInstanceSettingTypes()) { + instanceSettingTypes.append(type); + } + settingTypes.insert("instanceProperties", instanceSettingTypes); + properties.insert("propertiesTypes", settingTypes); + #if defined(ENABLE_EFFECTENGINE) // collect all available effect schemas QJsonArray schemaList; @@ -1166,7 +1308,7 @@ void JsonAPI::handleInstanceCommand(const QJsonObject &message, const JsonApiCom { QString replyMsg; - const quint8 &inst = static_cast(message["instance"].toInt()); + const quint8 inst = static_cast(message["instance"].toInt()); const QString &name = message["name"].toString(); switch (cmd.subCommand) { @@ -1201,7 +1343,6 @@ void JsonAPI::handleInstanceCommand(const QJsonObject &message, const JsonApiCom break; case SubCommand::DeleteInstance: - handleConfigRestoreCommand(message, cmd); if (API::deleteInstance(inst, replyMsg)) { sendSuccessReply(cmd); @@ -1223,7 +1364,7 @@ void JsonAPI::handleInstanceCommand(const QJsonObject &message, const JsonApiCom if (cmd.subCommand == SubCommand::CreateInstance) { replyMsg = API::createInstance(name); } else { - replyMsg = setInstanceName(inst, name); + replyMsg = API::setInstanceName(inst, name); } if (replyMsg.isEmpty()) { diff --git a/libsrc/api/JsonInfo.cpp b/libsrc/api/JsonInfo.cpp index 191179dc..baab3862 100644 --- a/libsrc/api/JsonInfo.cpp +++ b/libsrc/api/JsonInfo.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -500,8 +501,8 @@ QJsonObject JsonInfo::getSystemInfo(const Hyperion* hyperion) hyperionInfo["gitremote"] = QString(HYPERION_GIT_REMOTE); hyperionInfo["time"] = QString(__DATE__ " " __TIME__); hyperionInfo["id"] = AuthManager::getInstance()->getID(); - hyperionInfo["rootPath"] = HyperionIManager::getInstance()->getRootPath(); - hyperionInfo["readOnlyMode"] = hyperion->getReadOnlyMode(); + hyperionInfo["configDatabaseFile"] = DBManager::getFileInfo().absoluteFilePath(); + hyperionInfo["readOnlyMode"] = DBManager::isReadOnly(); QCoreApplication* app = QCoreApplication::instance(); hyperionInfo["isGuiMode"] = qobject_cast(app) != nullptr; @@ -624,3 +625,9 @@ QJsonArray JsonInfo::discoverScreenInputs(const QJsonObject& params) const return screenInputs; } + +QJsonObject JsonInfo::getConfiguration(const QList& instancesfilter, const QStringList& instanceFilteredTypes, const QStringList& globalFilterTypes ) +{ + DBConfigManager configManager; + return configManager.getConfiguration(instancesfilter, instanceFilteredTypes, globalFilterTypes ); +} diff --git a/libsrc/db/AuthTable.cpp b/libsrc/db/AuthTable.cpp new file mode 100644 index 00000000..32ce8e55 --- /dev/null +++ b/libsrc/db/AuthTable.cpp @@ -0,0 +1,176 @@ + +// hyperion +#include +#include + +// qt +#include +#include + +/// construct wrapper with auth table +AuthTable::AuthTable(QObject* parent) + : DBManager(parent) +{ + // init Auth table + setTable("auth"); + // create table columns + createTable(QStringList()<<"user TEXT"<<"password BLOB"<<"token BLOB"<<"salt BLOB"<<"comment TEXT"<<"id TEXT"<<"created_at TEXT"<<"last_use TEXT"); +}; + +bool AuthTable::createUser(const QString& user, const QString& password) +{ + // new salt + QByteArray salt = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex(); + QVariantMap map; + map["user"] = user; + map["salt"] = salt; + map["password"] = hashPasswordWithSalt(password,salt); + map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + return createRecord({{"user",user}}, map); +} + +bool AuthTable::userExist(const QString& user) +{ + return recordExists({{"user",user}}); +} + +bool AuthTable::isUserAuthorized(const QString& user, const QString& password) +{ + if(userExist(user) && (calcPasswordHashOfUser(user, password) == getPasswordHashOfUser(user))) + { + updateUserUsed(user); + return true; + } + return false; +} + +bool AuthTable::isUserTokenAuthorized(const QString& usr, const QString& token) +{ + if(getUserToken(usr) == token.toUtf8()) + { + updateUserUsed(usr); + return true; + } + return false; +} + +bool AuthTable::setUserToken(const QString& user) +{ + QVariantMap map; + map["token"] = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex(); + + return updateRecord({{"user",user}}, map); +} + +const QByteArray AuthTable::getUserToken(const QString& user) +{ + QVariantMap results; + getRecord({{"user",user}}, results, QStringList()<<"token"); + + return results["token"].toByteArray(); +} + +bool AuthTable::updateUserPassword(const QString& user, const QString& newPassword) +{ + QVariantMap map; + map["password"] = calcPasswordHashOfUser(user, newPassword); + + return updateRecord({{"user",user}}, map); +} + +bool AuthTable::resetHyperionUser() +{ + QVariantMap map; + map["password"] = calcPasswordHashOfUser(hyperion::DEFAULT_USER, hyperion::DEFAULT_PASSWORD); + + return updateRecord({{"user", hyperion::DEFAULT_USER}}, map); +} + +void AuthTable::updateUserUsed(const QString& user) +{ + QVariantMap map; + map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + updateRecord({{"user",user}}, map); +} + +bool AuthTable::tokenExist(const QString& token) +{ + QVariantMap map; + map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + VectorPair cond; + cond.append(CPair("token", hashToken(token))); + if(recordExists(cond)) + { + // update it + createRecord(cond,map); + return true; + } + return false; +} + +bool AuthTable::createToken(const QString& token, const QString& comment, const QString& identifier) +{ + QVariantMap map; + map["comment"] = comment; + map["id"] = identifierExist(identifier) ? QUuid::createUuid().toString().remove("{").remove("}").left(5) : identifier; + map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + return createRecord({{"token", hashToken(token)}}, map); +} + +bool AuthTable::deleteToken(const QString& identifier) +{ + return deleteRecord({{"id", identifier}}); +} + +bool AuthTable::renameToken(const QString &identifier, const QString &comment) +{ + QVariantMap map; + map["comment"] = comment; + + return updateRecord({{"id", identifier}}, map); +} + +const QVector AuthTable::getTokenList() +{ + QVector results; + getRecords(results, QStringList() << "comment" << "id" << "last_use"); + + return results; +} + +bool AuthTable::identifierExist(const QString& identifier) +{ + return recordExists({{"id", identifier}}); +} + +const QByteArray AuthTable::getPasswordHashOfUser(const QString& user) +{ + QVariantMap results; + getRecord({{"user",user}}, results, QStringList()<<"password"); + + return results["password"].toByteArray(); +} + +const QByteArray AuthTable::calcPasswordHashOfUser(const QString& user, const QString& password) +{ + // get salt + QVariantMap results; + getRecord({{"user",user}}, results, QStringList()<<"salt"); + + // calc + return hashPasswordWithSalt(password,results["salt"].toByteArray()); +} + +const QByteArray AuthTable::hashPasswordWithSalt(const QString& password, const QByteArray& salt) +{ + return QCryptographicHash::hash(password.toUtf8().append(salt), QCryptographicHash::Sha512).toHex(); +} + +const QByteArray AuthTable::hashToken(const QString& token) +{ + return QCryptographicHash::hash(token.toUtf8(), QCryptographicHash::Sha512).toHex(); +} diff --git a/libsrc/db/CMakeLists.txt b/libsrc/db/CMakeLists.txt index 1beb3fe5..8b83df59 100644 --- a/libsrc/db/CMakeLists.txt +++ b/libsrc/db/CMakeLists.txt @@ -1,10 +1,18 @@ add_library(database ${CMAKE_SOURCE_DIR}/include/db/AuthTable.h ${CMAKE_SOURCE_DIR}/include/db/DBManager.h + ${CMAKE_SOURCE_DIR}/include/db/DBConfigManager.h + ${CMAKE_SOURCE_DIR}/include/db/DBMigrationManager.h ${CMAKE_SOURCE_DIR}/include/db/InstanceTable.h ${CMAKE_SOURCE_DIR}/include/db/MetaTable.h ${CMAKE_SOURCE_DIR}/include/db/SettingsTable.h + ${CMAKE_SOURCE_DIR}/libsrc/db/AuthTable.cpp ${CMAKE_SOURCE_DIR}/libsrc/db/DBManager.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/DBConfigManager.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/DBMigrationManager.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/InstanceTable.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/MetaTable.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/SettingsTable.cpp ) target_link_libraries(database diff --git a/libsrc/db/DBConfigManager.cpp b/libsrc/db/DBConfigManager.cpp new file mode 100644 index 00000000..24f22b2e --- /dev/null +++ b/libsrc/db/DBConfigManager.cpp @@ -0,0 +1,382 @@ +#include + +#include +#include "db/SettingsTable.h" +#include + +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace { +const char SETTINGS_FULL_SCHEMA_FILE[] = ":/schema-settings-full.json"; +} + +DBConfigManager::DBConfigManager(QObject* parent) + : DBManager(parent) +{ +} + +QPair DBConfigManager::importJson(const QString& configFile) +{ + Info(_log,"Import configuration file '%s'", QSTRING_CSTR(configFile)); + + QJsonObject config; + QPair result = JsonUtils::readFile(configFile, config, _log, false); + + if (!result.first) + { + QString errorText = QString("Import configuration file '%1' failed!").arg(configFile); + result.second.prepend(errorText); + Error(_log, "%s", QSTRING_CSTR(errorText)); + return result; + } + + DBMigrationManager migtrationManger; + migtrationManger.migrateSettings(config); + + return updateConfiguration(config, true); +} + +bool DBConfigManager::exportJson(const QString& path) const +{ + bool isExported {false}; + + QDir exportPath{path}; + if (path.isEmpty()) + { + exportPath.setPath(getDataDirectory().absoluteFilePath("archive")); + } + + QString jsonFile; + if (QDir().mkpath(exportPath.absolutePath())) + { + const QJsonObject configurtion = getConfiguration(); + if (!configurtion.isEmpty()) + { + const QJsonObject generalSettings = configurtion.value("global").toObject().value("settings").toObject().value("general").toObject(); + const QString configVersion = generalSettings.value("configVersion").toString(); + + jsonFile = exportPath.absoluteFilePath(QString("HyperionBackup_%1_v%2.json").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_hh:mm:ss:zzz"), configVersion )); + if (FileUtils::writeFile(jsonFile, QJsonDocument(configurtion).toJson(QJsonDocument::Indented), _log)) + { + isExported = true; + } + } + } + + if (isExported) + { + Info(_log, "Successfully exported configuration to '%s'", QSTRING_CSTR(jsonFile)); + } + else + { + Error(_log, "Failed to export configuration to '%s'", QSTRING_CSTR(jsonFile)); + } + + return isExported; +} + +QPair DBConfigManager::validateConfiguration() +{ + QJsonObject config = getConfiguration(); + return validateConfiguration(config, false); +} + +QPair DBConfigManager::validateConfiguration(QJsonObject& config, bool doCorrections) +{ + Info(_log, "Validate configuration%s", doCorrections ? " and apply corrections, if required" : ""); + + QStringList errorList; + if (config.isEmpty()) + { + QString errorText {"No configuration data provided!"}; + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); + return qMakePair (false, errorList ); + } + + QJsonObject schema = QJsonFactory::readSchema(SETTINGS_FULL_SCHEMA_FILE); + + bool wasCorrected {false}; + if (doCorrections) + { + QJsonValue configValue(config); + QPair correctionResult = JsonUtils::correct(__FUNCTION__, configValue, schema, _log); + + wasCorrected = correctionResult.first; + if (wasCorrected) + { + config = configValue.toObject(); + } + } + else + { + QPair validationResult = JsonUtils::validate(__FUNCTION__, config, schema, _log); + if (!validationResult.first) + { + Error(_log, "Configuration has errors!"); + return qMakePair (false, validationResult.second ); + } + } + + Info(_log, "Configuration is valid%s", wasCorrected ? ", but had to be corrected" : ""); + return qMakePair (true, errorList ); +} + +QPair DBConfigManager::updateConfiguration() +{ + QJsonObject config = getConfiguration(); + return updateConfiguration(config, true); +} + + +QPair DBConfigManager::addMissingDefaults() +{ + Debug(_log, "Add default settings for missing configuration items"); + + QStringList errorList; + + SettingsTable globalSettingsTable; + QPair result = globalSettingsTable.addMissingDefaults(); + errorList.append(result.second); + + InstanceTable instanceTable; + + //Ensure that first instance as default one exists + instanceTable.createDefaultInstance(); + + const QList instances = instanceTable.getAllInstanceIDs(); + for (const auto &instanceIdx : instances) + { + SettingsTable instanceSettingsTable(instanceIdx); + result = instanceSettingsTable.addMissingDefaults(); + errorList.append(result.second); + } + + if(errorList.isEmpty()) + { + Debug(_log, "Successfully defaulted settings for missing configuration items"); + } + + return qMakePair (errorList.isEmpty(), errorList ); +} + +QPair DBConfigManager::updateConfiguration(QJsonObject& config, bool doCorrections) +{ + Info(_log, "Update configuration database"); + + QPair validationResult = validateConfiguration(config, doCorrections); + if (!validationResult.first) + { + return validationResult; + } + + Info(_log, "Create backup of current configuration"); + if (!exportJson()) + { + Warning(_log, "Backup of current configuration failed"); + } + + QStringList errorList; + QSqlDatabase idb = getDB(); + + if (!startTransaction(idb, errorList)) + { + return qMakePair(false, errorList); + } + + // Clear existing tables and import the new configuration. + bool errorOccurred = false; + if (!deleteTable("instances") && deleteTable("settings")) + { + errorOccurred = true; + logErrorAndAppend("Failed to clear tables before import", errorList); + } + else + { + errorOccurred = !importGlobalSettings(config, errorList) || !importInstances(config, errorList); + } + + // Rollback if any error occurred during the import process. + if (errorOccurred) + { + if (!rollbackTransaction(idb, errorList)) + { + return qMakePair(false, errorList); + } + } + + commiTransaction(idb, errorList); + + if (errorList.isEmpty()) + { + Info(_log, "Successfully imported new configuration"); + } + + return qMakePair(errorList.isEmpty(), errorList); +} + +// Function to import global settings +bool DBConfigManager::importGlobalSettings(const QJsonObject& config, QStringList& errorList) +{ + SettingsTable settingsTableGlobal; + const QJsonObject globalConfig = config.value("global").toObject(); + const QJsonObject globalSettings = globalConfig.value("settings").toObject(); + + bool errorOccurred = false; + for (QJsonObject::const_iterator it = globalSettings.constBegin(); it != globalSettings.constEnd(); ++it) + { + if (!settingsTableGlobal.createSettingsRecord(it.key(), JsonUtils::jsonValueToQString(it.value()))) + { + errorOccurred = true; + logErrorAndAppend("Failed to import global setting", errorList); + } + } + + return !errorOccurred; +} + +// Function to import instances +bool DBConfigManager::importInstances(const QJsonObject& config, QStringList& errorList) +{ + InstanceTable instanceTable; + const QJsonArray instancesConfig = config.value("instances").toArray(); + + bool errorOccurred = false; + quint8 instanceIdx = 0; + for (const auto& instanceItem : instancesConfig) + { + if (!importInstance(instanceTable, instanceItem.toObject(), instanceIdx, errorList)) + { + errorOccurred = true; + } + ++instanceIdx; + } + + return !errorOccurred; +} + +// Function to import a single instance +bool DBConfigManager::importInstance(InstanceTable& instanceTable, const QJsonObject& instanceConfig, quint8 instanceIdx, QStringList& errorList) +{ + QString instanceName = instanceConfig.value("name").toString(QString("Instance %1").arg(instanceIdx)); + bool isInstanceEnabled = instanceConfig.value("enabled").toBool(true); + + if (instanceIdx == 0) + { + isInstanceEnabled = true; // The first instance must be enabled. + } + + if (!instanceTable.createInstance(instanceName, instanceIdx) || + !instanceTable.setEnable(instanceIdx, isInstanceEnabled)) + { + logErrorAndAppend("Failed to import instance", errorList); + return false; + } + + SettingsTable settingsTableInstance(instanceIdx); + const QJsonObject instanceSettings = instanceConfig.value("settings").toObject(); + return importInstanceSettings(settingsTableInstance, instanceSettings, errorList); +} + +// Function to import instance settings +bool DBConfigManager::importInstanceSettings(SettingsTable& settingsTable, const QJsonObject& instanceSettings, QStringList& errorList) +{ + bool errorOccurred = false; + for (QJsonObject::const_iterator it = instanceSettings.constBegin(); it != instanceSettings.constEnd(); ++it) + { + if (!settingsTable.createSettingsRecord(it.key(), JsonUtils::jsonValueToQString(it.value()))) + { + errorOccurred = true; + logErrorAndAppend("Failed to import instance setting", errorList); + } + } + + return !errorOccurred; +} + +QJsonObject DBConfigManager::getConfiguration(const QList& instancesFilter, const QStringList& instanceFilteredTypes, const QStringList& globalFilterTypes ) const +{ + QSqlDatabase idb = getDB(); + + if (!startTransaction(idb)) + { + return {}; + } + + InstanceTable instanceTable; + SettingsTable settingsTable; + + QJsonObject config; + + QJsonObject globalConfig; + MetaTable metaTable; + globalConfig.insert("uuid", metaTable.getUUID()); + globalConfig.insert("settings", settingsTable.getSettings(globalFilterTypes)); + config.insert("global", globalConfig); + + QList instances {instancesFilter}; + if (instances.isEmpty()) + { + instances = instanceTable.getAllInstanceIDs(); + } + + QList sortedInstances = instances; + std::sort(sortedInstances.begin(), sortedInstances.end()); + + QJsonArray instanceIdList; + QJsonArray configInstanceList; + for (const quint8 instanceIdx : sortedInstances) + { + QJsonObject instanceConfig; + instanceConfig.insert("id",instanceIdx); + instanceConfig.insert("name", instanceTable.getNamebyIndex(instanceIdx)); + instanceConfig.insert("enabled", instanceTable.isEnabled(instanceIdx)); + instanceConfig.insert("settings", settingsTable.getSettings(static_cast(instanceIdx), instanceFilteredTypes)); + configInstanceList.append(instanceConfig); + + instanceIdList.append(instanceIdx); + } + + config.insert("instanceIds", instanceIdList); + config.insert("instances", configInstanceList); + + if (!commiTransaction(idb)) + { + return {}; + } + + return config; +} + +QPair DBConfigManager::migrateConfiguration() +{ + Info(_log, "Check, if configuration database is required to be migrated"); + + DBMigrationManager migtrationManger; + if (migtrationManger.isMigrationRequired()) + { + QJsonObject config = getConfiguration(); + + if (migtrationManger.migrateSettings(config)) + { + return updateConfiguration(config, true); + } + } + + Info(_log, "Database migration is not required"); + return qMakePair (true, QStringList{} ); +} + diff --git a/libsrc/db/DBManager.cpp b/libsrc/db/DBManager.cpp index f4494967..6163e2f5 100644 --- a/libsrc/db/DBManager.cpp +++ b/libsrc/db/DBManager.cpp @@ -1,38 +1,48 @@ +#include "utils/settings.h" #include #include #include #include #include -#include #include #include #include +#include #ifdef _WIN32 #include #endif -// not in header because of linking -static QString _rootPath; -static QThreadStorage _databasePool; +#define NO_SQLQUERY_LOGGING + +// Constants +namespace { + const char DATABASE_DIRECTORYNAME[] = "db"; + const char DATABASE_FILENAME[] = "hyperion.db"; + +} //End of constants + + +QDir DBManager::_dataDirectory; +QDir DBManager::_databaseDirectory; +QFileInfo DBManager::_databaseFile; +QThreadStorage DBManager::_databasePool; +bool DBManager::_isReadOnly {false}; DBManager::DBManager(QObject* parent) : QObject(parent) , _log(Logger::getInstance("DB")) - , _readonlyMode (false) { } -DBManager::~DBManager() +void DBManager::initializeDatabase(const QDir& dataDirectory, bool isReadOnly) { -} - -void DBManager::setRootPath(const QString& rootPath) -{ - _rootPath = rootPath; - // create directory - QDir().mkpath(_rootPath+"/db"); + _dataDirectory = dataDirectory; + _databaseDirectory.setPath(_dataDirectory.absoluteFilePath(DATABASE_DIRECTORYNAME)); + QDir().mkpath(_databaseDirectory.absolutePath()); + _databaseFile.setFile(_databaseDirectory,DATABASE_FILENAME); + _isReadOnly = isReadOnly; } void DBManager::setTable(const QString& table) @@ -43,38 +53,42 @@ void DBManager::setTable(const QString& table) QSqlDatabase DBManager::getDB() const { if(_databasePool.hasLocalData()) - return _databasePool.localData(); - else { - auto db = QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()); - _databasePool.setLocalData(db); - db.setDatabaseName(_rootPath+"/db/"+_dbn+".db"); - if(!db.open()) + return _databasePool.localData(); + } + auto database = QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()); + + if (isReadOnly()) { - Error(_log, "%s", QSTRING_CSTR(db.lastError().text())); + database.setConnectOptions("QSQLITE_OPEN_READONLY"); + } + +#ifdef SQLQUERY_LOGGING + Debug(Logger::getInstance("DB"), "Database is opened in %s mode", _isReadOnly ? "read-only" : "read/write"); +#endif + + _databasePool.setLocalData(database); + database.setDatabaseName(_databaseFile.absoluteFilePath()); + if(!database.open()) + { + Error(_log, "%s", QSTRING_CSTR(database.lastError().text())); throw std::runtime_error("Failed to open database connection!"); } - return db; - } + + return database; } bool DBManager::createRecord(const VectorPair& conditions, const QVariantMap& columns) const { - if ( _readonlyMode ) - { - return false; - } - if(recordExists(conditions)) { // if there is no column data, return if(columns.isEmpty()) + { return true; + } - if(!updateRecord(conditions, columns)) - return false; - - return true; + return updateRecord(conditions, columns); } QSqlDatabase idb = getDB(); @@ -84,14 +98,15 @@ bool DBManager::createRecord(const VectorPair& conditions, const QVariantMap& co QVariantList cValues; QStringList prep; QStringList placeh; + // prep merge columns & condition - QVariantMap::const_iterator i = columns.constBegin(); - while (i != columns.constEnd()) { - prep.append(i.key()); - cValues += i.value(); + QVariantMap::const_iterator columnIter = columns.constBegin(); + while (columnIter != columns.constEnd()) { + prep.append(columnIter.key()); + cValues += columnIter.value(); placeh.append("?"); - ++i; + ++columnIter; } for(const auto& pair : conditions) { @@ -101,21 +116,19 @@ bool DBManager::createRecord(const VectorPair& conditions, const QVariantMap& co cValues << pair.second; placeh.append("?"); } - query.prepare(QString("INSERT INTO %1 ( %2 ) VALUES ( %3 )").arg(_table,prep.join(", ")).arg(placeh.join(", "))); + query.prepare(QString("INSERT INTO %1 ( %2 ) VALUES ( %3 )").arg(_table,prep.join(", "), placeh.join(", "))); // add column & condition values - doAddBindValue(query, cValues); - if(!query.exec()) - { - Error(_log, "Failed to create record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prep.join(", ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - return true; + addBindValues(query, cValues); + + return executeQuery(query); } bool DBManager::recordExists(const VectorPair& conditions) const { if(conditions.isEmpty()) + { return false; + } QSqlDatabase idb = getDB(); QSqlQuery query(idb); @@ -127,33 +140,66 @@ bool DBManager::recordExists(const VectorPair& conditions) const for(const auto& pair : conditions) { - prepCond << pair.first+"=?"; + prepCond << pair.first+"= ?"; bindVal << pair.second; } query.prepare(QString("SELECT * FROM %1 %2").arg(_table,prepCond.join(" "))); - doAddBindValue(query, bindVal); - if(!query.exec()) + addBindValues(query, bindVal); + + if (!executeQuery(query)) { - Error(_log, "Failed recordExists(): '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); return false; } int entry = 0; - while (query.next()) { + while (query.next()) + { entry++; } - if(entry) - return true; + return entry > 0; +} - return false; +bool DBManager::recordsNotExisting(const QVariantList& testValues,const QString& column, QStringList& nonExistingRecs, const QString& condition ) const +{ + QSqlDatabase idb = getDB(); + + QSqlQuery query(idb); + query.setForwardOnly(true); + + // prep conditions + QString prepCond; + if(!condition.isEmpty()) + { + prepCond = QString("WHERE %1").arg(condition); + } + + QVector valueItem(testValues.size(), "(?)"); + QString values = QStringList::fromVector(valueItem).join(","); + query.prepare( + QString("SELECT v.[column1] [%1] FROM ( VALUES %2 ) [v] WHERE %1 NOT IN ( SELECT %1 from settings %3 )") + .arg(column,values, prepCond) + ); + + addBindValues(query, testValues); + + if (!executeQuery(query)) + { + return false; + } + + while (query.next()) { + nonExistingRecs << query.value(0).toString(); + } + + return true; } bool DBManager::updateRecord(const VectorPair& conditions, const QVariantMap& columns) const { - if ( _readonlyMode ) + if (isReadOnly()) { - return false; + return true; } QSqlDatabase idb = getDB(); @@ -164,88 +210,75 @@ bool DBManager::updateRecord(const VectorPair& conditions, const QVariantMap& co QStringList prep; // prepare columns valus - QVariantMap::const_iterator i = columns.constBegin(); - while (i != columns.constEnd()) { - prep += i.key()+"=?"; - values += i.value(); + QVariantMap::const_iterator columnIter = columns.constBegin(); + while (columnIter != columns.constEnd()) { + prep += columnIter.key()+"= ?"; + values += columnIter.value(); - ++i; + ++columnIter; } // prepare condition values QStringList prepCond; QVariantList prepBindVal; - if(!conditions.isEmpty()) + if(!conditions.isEmpty()) { prepCond << "WHERE"; + } for(const auto& pair : conditions) { - prepCond << pair.first+"=?"; + prepCond << pair.first+"= ?"; prepBindVal << pair.second; } - query.prepare(QString("UPDATE %1 SET %2 %3").arg(_table,prep.join(", ")).arg(prepCond.join(" "))); + query.prepare(QString("UPDATE %1 SET %2 %3").arg(_table,prep.join(", "), prepCond.join(" "))); // add column values - doAddBindValue(query, values); + addBindValues(query, values); // add condition values - doAddBindValue(query, prepBindVal); - if(!query.exec()) - { - Error(_log, "Failed to update record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - return true; + addBindValues(query, prepBindVal); + + return executeQuery(query); } bool DBManager::getRecord(const VectorPair& conditions, QVariantMap& results, const QStringList& tColumns, const QStringList& tOrder) const { - QSqlDatabase idb = getDB(); - QSqlQuery query(idb); - query.setForwardOnly(true); - - QString sColumns("*"); - if(!tColumns.isEmpty()) - sColumns = tColumns.join(", "); - - QString sOrder(""); - if(!tOrder.isEmpty()) - { - sOrder = " ORDER BY "; - sOrder.append(tOrder.join(", ")); + QVector resultVector{}; + bool success = getRecords(conditions, resultVector, tColumns, tOrder); + if (success && !resultVector.isEmpty()) { + results = resultVector.first(); } - // prep conditions - QStringList prepCond; - QVariantList bindVal; - if(!conditions.isEmpty()) - prepCond << " WHERE"; - - for(const auto& pair : conditions) - { - prepCond << pair.first+"=?"; - bindVal << pair.second; - } - query.prepare(QString("SELECT %1 FROM %2%3%4").arg(sColumns,_table).arg(prepCond.join(" ")).arg(sOrder)); - doAddBindValue(query, bindVal); - - if(!query.exec()) - { - Error(_log, "Failed to get record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - - // go to first row - query.next(); - - QSqlRecord rec = query.record(); - for(int i = 0; i& results, const QStringList& tColumns, const QStringList& tOrder) const +{ + return getRecords({}, results, tColumns, tOrder); +} + +bool DBManager::getRecords(const VectorPair& conditions, QVector& results, const QStringList& tColumns, const QStringList& tOrder) const +{ + // prep conditions + QStringList conditionList; + QVariantList bindValues; + + for(const auto& pair : conditions) + { + conditionList << pair.first; + if (pair.second.isNull()) + { + conditionList << "IS NULL"; + } + else + { + conditionList << "= ?"; + bindValues << pair.second; + } + } + + return getRecords(conditionList.join((" ")), bindValues, results, tColumns, tOrder); +} + +bool DBManager::getRecords(const QString& condition, const QVariantList& bindValues, QVector& results, const QStringList& tColumns, const QStringList& tOrder) const { QSqlDatabase idb = getDB(); QSqlQuery query(idb); @@ -253,20 +286,28 @@ bool DBManager::getRecords(QVector& results, const QStringList& tCo QString sColumns("*"); if(!tColumns.isEmpty()) + { sColumns = tColumns.join(", "); + } QString sOrder(""); if(!tOrder.isEmpty()) { - sOrder = " ORDER BY "; + sOrder = "ORDER BY "; sOrder.append(tOrder.join(", ")); } - query.prepare(QString("SELECT %1 FROM %2%3").arg(sColumns,_table,sOrder)); - - if(!query.exec()) + // prep conditions + QString prepCond; + if(!condition.isEmpty()) + { + prepCond = QString("WHERE %1").arg(condition); + } + + query.prepare(QString("SELECT %1 FROM %2 %3 %4").arg(sColumns,_table, prepCond, sOrder)); + addBindValues(query, bindValues); + if (!executeQuery(query)) { - Error(_log, "Failed to get records: '%s' in table: '%s' Error: %s", QSTRING_CSTR(sColumns), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); return false; } @@ -285,12 +326,11 @@ bool DBManager::getRecords(QVector& results, const QStringList& tCo return true; } - bool DBManager::deleteRecord(const VectorPair& conditions) const { - if ( _readonlyMode ) + if (_isReadOnly) { - return false; + return true; } if(conditions.isEmpty()) @@ -310,29 +350,20 @@ bool DBManager::deleteRecord(const VectorPair& conditions) const for(const auto& pair : conditions) { - prepCond << pair.first+"=?"; + prepCond << pair.first+"= ?"; bindValues << pair.second; } query.prepare(QString("DELETE FROM %1 %2").arg(_table,prepCond.join(" "))); - doAddBindValue(query, bindValues); - if(!query.exec()) - { - Error(_log, "Failed to delete record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - return true; + addBindValues(query, bindValues); + + return executeQuery(query); } return false; } bool DBManager::createTable(QStringList& columns) const { - if ( _readonlyMode ) - { - return false; - } - if(columns.isEmpty()) { Error(_log,"Empty tables aren't supported!"); @@ -347,9 +378,9 @@ bool DBManager::createTable(QStringList& columns) const // empty tables aren't supported by sqlite, add one column QString tcolumn = columns.takeFirst(); // default CURRENT_TIMESTAMP is not supported by ALTER TABLE - if(!query.exec(QString("CREATE TABLE %1 ( %2 )").arg(_table,tcolumn))) + query.prepare(QString("CREATE TABLE %1 ( %2 )").arg(_table,tcolumn)); + if (!executeQuery(query)) { - Error(_log, "Failed to create table: '%s' Error: %s", QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); return false; } } @@ -358,8 +389,8 @@ bool DBManager::createTable(QStringList& columns) const int err = 0; for(const auto& column : columns) { - QStringList id = column.split(' '); - if(rec.indexOf(id.at(0)) == -1) + QStringList columName = column.split(' '); + if(rec.indexOf(columName.at(0)) == -1) { if(!createColumn(column)) { @@ -367,79 +398,168 @@ bool DBManager::createTable(QStringList& columns) const } } } - if(err) - return false; - - return true; + return err == 0; } bool DBManager::createColumn(const QString& column) const { - if ( _readonlyMode ) - { - return false; - } - QSqlDatabase idb = getDB(); QSqlQuery query(idb); - if(!query.exec(QString("ALTER TABLE %1 ADD COLUMN %2").arg(_table,column))) - { - Error(_log, "Failed to create column: '%s' in table: '%s' Error: %s", QSTRING_CSTR(column), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - return true; + + query.prepare(QString("ALTER TABLE %1 ADD COLUMN %2").arg(_table,column)); + return executeQuery(query); } bool DBManager::tableExists(const QString& table) const { QSqlDatabase idb = getDB(); QStringList tables = idb.tables(); - if(tables.contains(table)) - return true; - return false; + return tables.contains(table); } bool DBManager::deleteTable(const QString& table) const { - if ( _readonlyMode ) - { - return false; - } - if(tableExists(table)) { QSqlDatabase idb = getDB(); QSqlQuery query(idb); - if(!query.exec(QString("DROP TABLE %1").arg(table))) - { - Error(_log, "Failed to delete table: '%s' Error: %s", QSTRING_CSTR(table), QSTRING_CSTR(idb.lastError().text())); - return false; - } + + query.prepare(QString("DROP TABLE %1").arg(table)); + return executeQuery(query); } return true; } -void DBManager::doAddBindValue(QSqlQuery& query, const QVariantList& variants) const +void DBManager::addBindValues(QSqlQuery& query, const QVariantList& bindValues) const { - for(const auto& variant : variants) + if (!bindValues.isEmpty()) { - auto t = variant.userType(); - switch(t) + for(const auto& value : bindValues) { - case QMetaType::UInt: - case QMetaType::Int: - case QMetaType::Bool: - query.addBindValue(variant.toInt()); - break; - case QMetaType::Double: - query.addBindValue(variant.toFloat()); - break; - case QMetaType::QByteArray: - query.addBindValue(variant.toByteArray()); - break; - default: - query.addBindValue(variant.toString()); - break; + query.addBindValue(value); } } } + +QString DBManager::constructExecutedQuery(const QSqlQuery& query) const +{ + QString executedQuery = query.executedQuery(); + + // Check if the query uses positional placeholders + if (executedQuery.contains('?')) { +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + QVariantList boundValues = query.boundValues(); // Get bound values as a list +#else + QVariantMap boundValues = query.boundValues(); // Get bound values as a list +#endif + // Iterate through the bound values and replace placeholders + for (const QVariant &value : boundValues) { + // Replace the first occurrence of '?' with the actual value + QString valueStr; + if (value.canConvert()) + { + valueStr = value.toString(); + } + else + { + valueStr = "Unkown"; + } + executedQuery.replace(executedQuery.indexOf('?'), 1, valueStr); + } + } + return executedQuery; +} + +bool DBManager::executeQuery(QSqlQuery& query) const +{ + if( !query.exec()) + { + QString finalQuery = constructExecutedQuery(query); + QString errorText = query.lastError().text(); + + Debug(_log, "Database Error: '%s', SqlQuery: '%s'", QSTRING_CSTR(errorText), QSTRING_CSTR(finalQuery)); + Error(_log, "Database Error: '%s'", QSTRING_CSTR(errorText)); + + return false; + } + +#ifdef SQLQUERY_LOGGING + QString finalQuery = constructExecutedQuery(query); + Debug(_log, "SqlQuery executed: '%s'", QSTRING_CSTR(finalQuery)); +#endif + + return true; +} + +bool DBManager::startTransaction(QSqlDatabase& idb) const +{ + if (!idb.transaction()) + { + QString errorText = QString("Could not create a database transaction. Error: %1").arg(idb.lastError().text()); + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + return false; + } + return true; +} + +bool DBManager::startTransaction(QSqlDatabase& idb, QStringList& errorList) +{ + if (!idb.transaction()) + { + QString errorText = QString("Could not create a database transaction. Error: %1").arg(idb.lastError().text()); + logErrorAndAppend(errorText, errorList); + return false; + } + return true; +} + +bool DBManager::commiTransaction(QSqlDatabase& idb) const +{ + if (!idb.commit()) + { + QString errorText = QString("Could not finalize the database changes. Error: %1").arg(idb.lastError().text()); + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + return false; + } + return true; +} + +bool DBManager::commiTransaction(QSqlDatabase& idb, QStringList& errorList) +{ + if (!idb.commit()) + { + QString errorText = QString("Could not finalize the database changes. Error: %1").arg(idb.lastError().text()); + logErrorAndAppend(errorText, errorList); + return false; + } + return true; +} + +bool DBManager::rollbackTransaction(QSqlDatabase& idb) const +{ + if (!idb.rollback()) + { + QString errorText = QString("Could not rollback the database transaction. Error: %1").arg(idb.lastError().text()); + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + return false; + } + return true; +} + +bool DBManager::rollbackTransaction(QSqlDatabase& idb, QStringList& errorList) +{ + if (!idb.rollback()) + { + QString errorText = QString("Could not rollback the database transaction. Error: %1").arg(idb.lastError().text()); + logErrorAndAppend(errorText, errorList); + return false; + } + return true; +} + +// Function to log error and append it to the error list +void DBManager::logErrorAndAppend(const QString& errorText, QStringList& errorList) +{ + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); +} diff --git a/libsrc/db/DBMigrationManager.cpp b/libsrc/db/DBMigrationManager.cpp new file mode 100644 index 00000000..c2c92e3f --- /dev/null +++ b/libsrc/db/DBMigrationManager.cpp @@ -0,0 +1,767 @@ +#include + +#include "db/SettingsTable.h" +#include +#include + +#include + +#include + +DBMigrationManager::DBMigrationManager(QObject *parent) + : DBManager{parent} +{ +} + +bool DBMigrationManager::isMigrationRequired() +{ + bool isNewRelease = false; + + SettingsTable settingsTableGlobal; + + if (settingsTableGlobal.resolveConfigVersion()) + { + semver::version BUILD_VERSION(HYPERION_VERSION); + + if (!BUILD_VERSION.isValid()) + { + Error(_log, "Current Hyperion version [%s] is invalid. Exiting...", BUILD_VERSION.getVersion().c_str()); + exit(1); + } + + const semver::version& currentVersion = settingsTableGlobal.getConfigVersion(); + if (currentVersion > BUILD_VERSION) + { + Error(_log, "Database version [%s] is greater than current Hyperion version [%s]. Exiting...", currentVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str()); + exit(1); + } + + if (currentVersion < BUILD_VERSION) + { + isNewRelease = true; + } + } + return isNewRelease; +} + +bool DBMigrationManager::migrateSettings(QJsonObject& config) +{ + bool migrated = false; + semver::version BUILD_VERSION(HYPERION_VERSION); + + SettingsTable settingsTableGlobal; + QJsonObject generalConfig = config.value("global").toObject().value("settings").toObject().value("general").toObject(); + + if (settingsTableGlobal.resolveConfigVersion(generalConfig)) + { + semver::version currentVersion = settingsTableGlobal.getConfigVersion(); + + if (currentVersion < BUILD_VERSION) + { + Info(_log, "Migration from current version [%s] to new version [%s] started", currentVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str()); + + // Extract, modify, and reinsert the global settings + QJsonObject globalSettings = config.value("global").toObject().value("settings").toObject(); + upgradeGlobalSettings(currentVersion, globalSettings); + + QJsonObject globalConfig = config.value("global").toObject(); + globalConfig.insert("settings", globalSettings); + config.insert("global", globalConfig); + + // Update each instance directly within the config + QJsonArray instancesConfig = config.value("instances").toArray(); + for (int i = 0; i < instancesConfig.size(); ++i) + { + QJsonObject instanceConfig = instancesConfig[i].toObject(); + QJsonObject instanceSettings = instanceConfig.value("settings").toObject(); + + upgradeInstanceSettings(currentVersion, static_cast(i), instanceSettings); + + // Reinsert the modified instance settings back into the instanceConfig + instanceConfig.insert("settings", instanceSettings); + instancesConfig.replace(i, instanceConfig); + } + config.insert("instances", instancesConfig); + + Info(_log, "Migration from current version [%s] to new version [%s] finished", currentVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str()); + migrated = true; + } + } + + return migrated; +} + +bool DBMigrationManager::upgradeGlobalSettings(const semver::version& currentVersion, QJsonObject& config) +{ + bool migrated = false; + + semver::version migratedVersion = currentVersion; + + //Migration step for versions < alpha 9 + upgradeGlobalSettings_alpha_9(migratedVersion, config); + //Migration step for versions < 2.0.12 + upgradeGlobalSettings_2_0_12(migratedVersion, config); + //Migration step for versions < 2.0.16 + upgradeGlobalSettings_2_0_16(migratedVersion, config); + //Migration step for versions < 2.0.17 + upgradeGlobalSettings_2_1_0(migratedVersion, config); + + // Set the daqtabase version to the current build version + QJsonObject generalConfig = config["general"].toObject(); + // Update the configVersion if necessary + if (generalConfig["configVersion"].toString() != HYPERION_VERSION) { + generalConfig["configVersion"] = HYPERION_VERSION; + migrated = true; + } + // Re-insert the modified "general" object back into the config + config["general"] = generalConfig; + + return migrated; +} + +bool DBMigrationManager::upgradeInstanceSettings(const semver::version& currentVersion, quint8 instance, QJsonObject& config) +{ + bool migrated = false; + semver::version migratedVersion = currentVersion; + + //Migration step for versions < alpha 9 + upgradeInstanceSettings_alpha_9(migratedVersion, instance, config); + //Migration step for versions < 2.0.12 + upgradeInstanceSettings_2_0_12(migratedVersion, instance, config); + //Migration step for versions < 2.0.13 + upgradeInstanceSettings_2_0_13(migratedVersion, instance, config); + //Migration step for versions < 2.0.16 + upgradeInstanceSettings_2_0_16(migratedVersion, instance, config); + + return migrated; +} + +bool DBMigrationManager::upgradeGlobalSettings_alpha_9(semver::version& currentVersion, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.0-alpha.9" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + if (config.contains("grabberV4L2")) + { + QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject(); + + if (newGrabberV4L2Config.contains("encoding_format")) + { + newGrabberV4L2Config.remove("encoding_format"); + newGrabberV4L2Config["grabberV4L2"] = newGrabberV4L2Config; + migrated = true; + } + + //Add new element enable + if (!newGrabberV4L2Config.contains("enable")) + { + newGrabberV4L2Config["enable"] = false; + migrated = true; + } + config["grabberV4L2"] = newGrabberV4L2Config; + Debug(_log, "GrabberV4L2 records migrated"); + } + + if (config.contains("grabberAudio")) + { + QJsonObject newGrabberAudioConfig = config["grabberAudio"].toObject(); + + //Add new element enable + if (!newGrabberAudioConfig.contains("enable")) + { + newGrabberAudioConfig["enable"] = false; + migrated = true; + } + config["grabberAudio"] = newGrabberAudioConfig; + Debug(_log, "GrabberAudio records migrated"); + } + + if (config.contains("framegrabber")) + { + QJsonObject newFramegrabberConfig = config["framegrabber"].toObject(); + + //Align element namings with grabberV4L2 + //Rename element type -> device + if (newFramegrabberConfig.contains("type")) + { + newFramegrabberConfig["device"] = newFramegrabberConfig["type"].toString(); + newFramegrabberConfig.remove("type"); + migrated = true; + } + //Rename element frequency_Hz -> fps + if (newFramegrabberConfig.contains("frequency_Hz")) + { + newFramegrabberConfig["fps"] = newFramegrabberConfig["frequency_Hz"].toInt(25); + newFramegrabberConfig.remove("frequency_Hz"); + migrated = true; + } + + //Rename element display -> input + if (newFramegrabberConfig.contains("display")) + { + newFramegrabberConfig["input"] = newFramegrabberConfig["display"]; + newFramegrabberConfig.remove("display"); + migrated = true; + } + + //Add new element enable + if (!newFramegrabberConfig.contains("enable")) + { + newFramegrabberConfig["enable"] = false; + migrated = true; + } + + config["framegrabber"] = newFramegrabberConfig; + Debug(_log, "Framegrabber records migrated"); + } + } + + return migrated; +} + +bool DBMigrationManager::upgradeGlobalSettings_2_0_12(semver::version& currentVersion, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.12" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + // Have Hostname/IP-address separate from port for Forwarder + if (config.contains("forwarder")) + { + QJsonObject newForwarderConfig = config["forwarder"].toObject(); + + QJsonArray json; + if (newForwarderConfig.contains("json")) + { + const QJsonArray oldJson = newForwarderConfig["json"].toArray(); + QJsonObject newJsonConfig; + + for (const QJsonValue& value : oldJson) + { + if (value.isString()) + { + QString oldHost = value.toString(); + // Resolve hostname and port + QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts); + QString host = addressparts[0]; + + if (host != "127.0.0.1") + { + newJsonConfig["host"] = host; + + if (addressparts.size() > 1) + { + newJsonConfig["port"] = addressparts[1].toInt(); + } + else + { + newJsonConfig["port"] = 19444; + } + newJsonConfig["name"] = host; + + json.append(newJsonConfig); + migrated = true; + } + } + } + + if (!json.isEmpty()) + { + newForwarderConfig["jsonapi"] = json; + } + newForwarderConfig.remove("json"); + migrated = true; + } + + QJsonArray flatbuffer; + if (newForwarderConfig.contains("flat")) + { + const QJsonArray oldFlatbuffer = newForwarderConfig["flat"].toArray(); + QJsonObject newFlattbufferConfig; + + for (const QJsonValue& value : oldFlatbuffer) + { + if (value.isString()) + { + QString oldHost = value.toString(); + // Resolve hostname and port + QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts); + QString host = addressparts[0]; + + if (host != "127.0.0.1") + { + newFlattbufferConfig["host"] = host; + + if (addressparts.size() > 1) + { + newFlattbufferConfig["port"] = addressparts[1].toInt(); + } + else + { + newFlattbufferConfig["port"] = 19400; + } + newFlattbufferConfig["name"] = host; + + flatbuffer.append(newFlattbufferConfig); + } + } + + if (!flatbuffer.isEmpty()) + { + newForwarderConfig["flatbuffer"] = flatbuffer; + } + newForwarderConfig.remove("flat"); + migrated = true; + } + } + + if (json.isEmpty() && flatbuffer.isEmpty()) + { + newForwarderConfig["enable"] = false; + } + + if (migrated) + { + config["forwarder"] = newForwarderConfig; + Debug(_log, "Forwarder records migrated"); + currentVersion = targetVersion; + } + } + } + + return migrated; +} + +bool DBMigrationManager::upgradeGlobalSettings_2_0_16(semver::version& currentVersion, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.16" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + if (config.contains("cecEvents")) + { + bool isCECEnabled {false}; + if (config.contains("grabberV4L2")) + { + QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject(); + if (newGrabberV4L2Config.contains("cecDetection")) + { + isCECEnabled = newGrabberV4L2Config.value("cecDetection").toBool(false); + newGrabberV4L2Config.remove("cecDetection"); + config["grabberV4L2"] = newGrabberV4L2Config; + + QJsonObject newGCecEventsConfig = config["cecEvents"].toObject(); + newGCecEventsConfig["enable"] = isCECEnabled; + if (!newGCecEventsConfig.contains("actions")) + { + QJsonObject action1 + { + {"action", "Suspend"}, + {"event", "standby"} + }; + QJsonObject action2 + { + {"action", "Resume"}, + {"event", "set stream path"} + }; + + QJsonArray actions { action1, action2 }; + newGCecEventsConfig.insert("actions",actions); + } + config["cecEvents"] = newGCecEventsConfig; + + migrated = true; + Debug(_log, "CEC configuration records migrated"); + } + } + } + } + + return migrated; +} + +bool DBMigrationManager::upgradeGlobalSettings_2_1_0(semver::version& currentVersion, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.17-beta.2" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + if (config.contains("general")) + { + QJsonObject newGeneralConfig = config["general"].toObject(); + newGeneralConfig.remove("previousVersion"); + config.insert("general", newGeneralConfig); + + Debug(_log, "General settings migrated"); + migrated = true; + } + + if (config.contains("network")) + { + QJsonObject newNetworkConfig = config["network"].toObject(); + newNetworkConfig.remove("apiAuth"); + newNetworkConfig.remove("localAdminAuth"); + config.insert("network", newNetworkConfig); + + Debug(_log, "Network settings migrated"); + migrated = true; + } + + } + + //Remove wrong instance 255 configuration records, created by the global instance #255 + SettingsTable globalSettingsTable(255); + globalSettingsTable.deleteInstance(); + migrated = true; + + return migrated; +} + +bool DBMigrationManager::upgradeInstanceSettings_alpha_9(semver::version& currentVersion, quint8 instance, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.0-alpha.9" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + // LED LAYOUT UPGRADE + // from { hscan: { minimum: 0.2, maximum: 0.3 }, vscan: { minimum: 0.2, maximum: 0.3 } } + // from { h: { min: 0.2, max: 0.3 }, v: { min: 0.2, max: 0.3 } } + // to { hmin: 0.2, hmax: 0.3, vmin: 0.2, vmax: 0.3} + if (config.contains("leds")) + { + const QJsonArray ledarr = config["leds"].toArray(); + const QJsonObject firstLed = ledarr[0].toObject(); + + if (firstLed.contains("hscan") || firstLed.contains("h")) + { + const bool whscan = firstLed.contains("hscan"); + QJsonArray newLedarr; + + for (const auto& entry : ledarr) + { + const QJsonObject led = entry.toObject(); + QJsonObject hscan; + QJsonObject vscan; + QJsonValue hmin; + QJsonValue hmax; + QJsonValue vmin; + QJsonValue vmax; + QJsonObject nL; + + if (whscan) + { + hscan = led["hscan"].toObject(); + vscan = led["vscan"].toObject(); + hmin = hscan["minimum"]; + hmax = hscan["maximum"]; + vmin = vscan["minimum"]; + vmax = vscan["maximum"]; + } + else + { + hscan = led["h"].toObject(); + vscan = led["v"].toObject(); + hmin = hscan["min"]; + hmax = hscan["max"]; + vmin = vscan["min"]; + vmax = vscan["max"]; + } + // append to led object + nL["hmin"] = hmin; + nL["hmax"] = hmax; + nL["vmin"] = vmin; + nL["vmax"] = vmax; + newLedarr.append(nL); + } + // replace + config["leds"] = newLedarr; + migrated = true; + Info(_log, "Instance [%u]: LED Layout migrated", instance); + } + } + + if (config.contains("ledConfig")) + { + QJsonObject oldLedConfig = config["ledConfig"].toObject(); + if (!oldLedConfig.contains("classic")) + { + QJsonObject newLedConfig; + newLedConfig.insert("classic", oldLedConfig); + QJsonObject defaultMatrixConfig{ {"ledshoriz", 1} + ,{"ledsvert", 1} + ,{"cabling","snake"} + ,{"start","top-left"} + }; + newLedConfig.insert("matrix", defaultMatrixConfig); + + config["ledConfig"] = newLedConfig; + migrated = true; + Info(_log, "Instance [%u]: LED-Config migrated", instance); + } + } + + // LED Hardware count is leading for versions after alpha 9 + // Setting Hardware LED count to number of LEDs configured via layout, if layout number is greater than number of hardware LEDs + if (config.contains("device")) + { + QJsonObject newDeviceConfig = config["device"].toObject(); + + if (newDeviceConfig.contains("hardwareLedCount")) + { + int hwLedcount = newDeviceConfig["hardwareLedCount"].toInt(); + if (config.contains("leds")) + { + const QJsonArray ledarr = config["leds"].toArray(); + int layoutLedCount = ledarr.size(); + + if (hwLedcount < layoutLedCount) + { + Warning(_log, "Instance [%u]: HwLedCount/Layout mismatch! Setting Hardware LED count to number of LEDs configured via layout", instance); + hwLedcount = layoutLedCount; + newDeviceConfig["hardwareLedCount"] = hwLedcount; + migrated = true; + } + } + } + + if (newDeviceConfig.contains("type")) + { + QString type = newDeviceConfig["type"].toString(); + if (type == "atmoorb" || type == "fadecandy" || type == "philipshue") + { + if (newDeviceConfig.contains("output")) + { + newDeviceConfig["host"] = newDeviceConfig["output"].toString(); + newDeviceConfig.remove("output"); + migrated = true; + } + } + } + + if (migrated) + { + config["device"] = newDeviceConfig; + Debug(_log, "LED-Device records migrated"); + } + } + } + + return migrated; +} + +bool DBMigrationManager::upgradeInstanceSettings_2_0_12(semver::version& currentVersion, quint8 instance, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.12" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + // Have Hostname/IP-address separate from port for LED-Devices + if (config.contains("device")) + { + QJsonObject newDeviceConfig = config["device"].toObject(); + + if (newDeviceConfig.contains("host")) + { + QString oldHost = newDeviceConfig["host"].toString(); + + // Resolve hostname and port + QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts); + + newDeviceConfig["host"] = addressparts[0]; + + if (addressparts.size() > 1) + { + if (!newDeviceConfig.contains("port")) + { + newDeviceConfig["port"] = addressparts[1].toInt(); + } + migrated = true; + } + } + + if (newDeviceConfig.contains("type")) + { + QString type = newDeviceConfig["type"].toString(); + if (type == "apa102") + { + if (newDeviceConfig.contains("colorOrder")) + { + QString colorOrder = newDeviceConfig["colorOrder"].toString(); + if (colorOrder == "bgr") + { + newDeviceConfig["colorOrder"] = "rgb"; + migrated = true; + } + } + } + } + + if (migrated) + { + config["device"] = newDeviceConfig; + Debug(_log, "LED-Device records migrated"); + } + } + + } + + return migrated; +} + +bool DBMigrationManager::upgradeInstanceSettings_2_0_13(semver::version& currentVersion, quint8 instance, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.13" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + // Have Hostname/IP-address separate from port for LED-Devices + if (config.contains("device")) + { + QJsonObject newDeviceConfig = config["device"].toObject(); + + if (newDeviceConfig.contains("type")) + { + QString type = newDeviceConfig["type"].toString(); + + const QStringList serialDevices{ "adalight", "dmx", "atmo", "sedu", "tpm2", "karate" }; + if (serialDevices.contains(type)) + { + if (!newDeviceConfig.contains("rateList")) + { + newDeviceConfig["rateList"] = "CUSTOM"; + migrated = true; + } + } + + if (type == "adalight") + { + if (newDeviceConfig.contains("lightberry_apa102_mode")) + { + bool lightberry_apa102_mode = newDeviceConfig["lightberry_apa102_mode"].toBool(); + if (lightberry_apa102_mode) + { + newDeviceConfig["streamProtocol"] = "1"; + } + else + { + newDeviceConfig["streamProtocol"] = "0"; + } + newDeviceConfig.remove("lightberry_apa102_mode"); + migrated = true; + } + } + } + + if (migrated) + { + config["device"] = newDeviceConfig; + Debug(_log, "LED-Device records migrated"); + } + } + } + + return migrated; +} + +bool DBMigrationManager::upgradeInstanceSettings_2_0_16(semver::version& currentVersion, quint8 instance, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.16" }; + + if (currentVersion >= targetVersion) return migrated; + + Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + if (config.contains("device")) + { + QJsonObject newDeviceConfig = config["device"].toObject(); + + auto convertIntToString = [&](const QString& key) { + if (newDeviceConfig.contains(key) && newDeviceConfig[key].isDouble()) { + int value = newDeviceConfig[key].toInt(); + newDeviceConfig[key] = QString::number(value); + migrated = true; + } + }; + + if (newDeviceConfig.contains("type")) + { + QString type = newDeviceConfig["type"].toString(); + + if (type == "philipshue") + { + convertIntToString("groupId"); + + if (newDeviceConfig.contains("lightIds")) + { + QJsonArray lightIds = newDeviceConfig["lightIds"].toArray(); + for (int i = 0; i < lightIds.size(); ++i) + { + if (lightIds[i].isDouble()) + { + lightIds[i] = QString::number(lightIds[i].toInt()); + migrated = true; + } + } + newDeviceConfig["lightIds"] = lightIds; + } + } + else if (type == "nanoleaf") + { + const auto updatePanelOrder = [&](const QString& key, const QString& zeroStr, const QString& oneStr) { + if (newDeviceConfig.contains(key)) + { + int order = newDeviceConfig[key].isDouble() ? newDeviceConfig[key].toInt() : newDeviceConfig[key].toString().toInt(); + newDeviceConfig[key] = (order == 0) ? zeroStr : oneStr; + migrated = true; + } + }; + + newDeviceConfig.remove("panelStartPos"); + migrated = true; + + updatePanelOrder("panelOrderTopDown", "top2down", "bottom2up"); + updatePanelOrder("panelOrderLeftRight", "left2right", "right2left"); + } + } + + if (migrated) + { + config["device"] = newDeviceConfig; + Debug(_log, "LED-Device records migrated"); + } + } + + return migrated; +} + diff --git a/libsrc/db/InstanceTable.cpp b/libsrc/db/InstanceTable.cpp new file mode 100644 index 00000000..702e124a --- /dev/null +++ b/libsrc/db/InstanceTable.cpp @@ -0,0 +1,142 @@ + +// db +#include +#include + +// qt +#include + +InstanceTable::InstanceTable(QObject* parent) + : DBManager(parent) +{ + // Init instance table + setTable("instances"); + createTable(QStringList()<<"instance INTEGER"<<"friendly_name TEXT"<<"enabled INTEGER DEFAULT 0"<<"last_use TEXT"); +} + +bool InstanceTable::createInstance(const QString& name, quint8& inst) +{ + // check duplicate + if(!recordExists({{"friendly_name", name}})) + { + QList instanceList = getAllInstanceIDs(false); + + inst = 0; + while (instanceList.contains(inst)) + { + ++inst; + } + + // create + QVariantMap data; + data["friendly_name"] = name; + data["instance"] = inst; + return createRecord({}, data); + } + + return false; +} + +bool InstanceTable::deleteInstance(quint8 inst) +{ + Debug(_log,""); + if(deleteRecord({{"instance",inst}})) + { + // delete settings entries + SettingsTable settingsTable(inst); + settingsTable.deleteInstance(); + return true; + } + return false; +} + +bool InstanceTable::saveName(quint8 inst, const QString& name) +{ + // check duplicate + if(!recordExists({{"friendly_name", name}})) + { + if(instanceExist(inst)) + { + return updateRecord({{"instance",inst}}, {{"friendly_name", name}}); + } + } + return false; +} + +QVector InstanceTable::getAllInstances(bool onlyEnabled) +{ + QVector results; + + VectorPair onlyEnabledCondition {}; + if (onlyEnabled) + { + onlyEnabledCondition = {{"enabled", true}}; + } + getRecords(onlyEnabledCondition, results, {}, {"instance ASC"}); + return results; +} + +QList InstanceTable::getAllInstanceIDs (bool onlyEnabled) +{ + QVector instanceList = getAllInstances(onlyEnabled); + QList instanceIds; + for (const QVariantMap &idx : std::as_const(instanceList)) + { + instanceIds.append(static_cast(idx.value("instance").toInt())); + } + + return instanceIds; +} + +bool InstanceTable::instanceExist(quint8 inst) +{ + return recordExists({{"instance",inst}}); +} + +QString InstanceTable::getNamebyIndex(quint8 index) +{ + QVariantMap results; + getRecord({{"instance", index}}, results, {"friendly_name"}); + + QString name = results["friendly_name"].toString(); + return name.isEmpty() ? "NOT FOUND" : name; +} + +bool InstanceTable::setLastUse(quint8 inst) +{ + return updateRecord({{"instance", inst}}, {{"last_use", QDateTime::currentDateTimeUtc().toString(Qt::ISODate)}}); +} + +bool InstanceTable::setEnable(quint8 inst, bool newState) +{ + return updateRecord({{"instance", inst}}, {{"enabled", newState}}); +} + +bool InstanceTable::isEnabled(quint8 inst) +{ + QVariantMap results; + getRecord({{"instance", inst}}, results); + + return results["enabled"].toBool(); +} + +void InstanceTable::createDefaultInstance() +{ + if(instanceExist(0)) + { + setEnable(0, true); + } + else + { + if(createRecord({{"instance", 0}}, {{"friendly_name", "First LED Hardware instance"}})) + { + setEnable(0, true); + } + else + { + throw std::runtime_error("Failed to create Hyperion root instance in db! This should never be the case..."); + } + } +} + + diff --git a/libsrc/db/MetaTable.cpp b/libsrc/db/MetaTable.cpp new file mode 100644 index 00000000..1ee723e6 --- /dev/null +++ b/libsrc/db/MetaTable.cpp @@ -0,0 +1,47 @@ + +#include + +// qt +#include +#include +#include +#include + +MetaTable::MetaTable(QObject* parent) + : DBManager(parent) +{ + setTable("meta"); + createTable(QStringList()<<"uuid TEXT"<<"created_at TEXT"); +}; + +QString MetaTable::getUUID() const +{ + QVector results; + getRecords(results, QStringList() << "uuid"); + + for(const auto & entry : std::as_const(results)) + { + if(!entry["uuid"].toString().isEmpty()) + { + return entry["uuid"].toString(); + } + } + + // create new uuidv5 based on net adapter MAC, save to db and return + QString hash; + foreach(QNetworkInterface interface, QNetworkInterface::allInterfaces()) + { + if (!(interface.flags() & QNetworkInterface::IsLoopBack)) + { + hash = QCryptographicHash::hash(interface.hardwareAddress().toLocal8Bit(),QCryptographicHash::Sha1).toHex(); + break; + } + } + const QString newUuid = QUuid::createUuidV5(QUuid(), hash).toString().mid(1, 36); + QVariantMap map; + map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + createRecord({{"uuid",newUuid}}, map); + + return newUuid; +} diff --git a/libsrc/db/SettingsTable.cpp b/libsrc/db/SettingsTable.cpp new file mode 100644 index 00000000..573524da --- /dev/null +++ b/libsrc/db/SettingsTable.cpp @@ -0,0 +1,389 @@ +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace { +const char DEFAULT_INSTANCE_SETTINGS_SCHEMA_FILE[] = ":/schema-settings-instance.json"; +const char GLOBAL_SETTINGS_SCHEMA_FILE[] = ":/schema-settings-global.json"; +const char DEFAULT_SETTINGS_SCHEMA_FILE[] = ":/schema-settings-default.json"; +const char DEFAULT_SETTINGS_CONFIGURATION_FILE[] = ":/hyperion_default.settings"; +} + +QVector SettingsTable::globalSettingTypes; +bool SettingsTable::areGlobalSettingTypesInitialised = false; + +QVector SettingsTable::instanceSettingTypes; +bool SettingsTable::areInstanceSettingTypesInitialised = false; + +QJsonObject SettingsTable::defaultSettings; +bool SettingsTable::areDefaultSettingsInitialised = false; + + +SettingsTable::SettingsTable(quint8 instance, QObject* parent) + : DBManager(parent) + , _instance(instance) + , _configVersion(DEFAULT_CONFIG_VERSION) +{ + setTable("settings"); + // create table columns + createTable(QStringList()<<"type TEXT"<<"config TEXT"<<"hyperion_inst INTEGER"<<"updated_at TEXT"); +} + +const QVector& SettingsTable::getGlobalSettingTypes() const +{ + if (!areGlobalSettingTypesInitialised) { + globalSettingTypes = initializeGlobalSettingTypes(); + areGlobalSettingTypesInitialised = true; + } + return globalSettingTypes; +} + +QVector SettingsTable::initializeGlobalSettingTypes() const +{ + QJsonObject schemaJson; + try + { + schemaJson = QJsonFactory::readSchema(GLOBAL_SETTINGS_SCHEMA_FILE); + } + catch (const std::runtime_error& error) + { + throw std::runtime_error(error.what()); + } + + const QVector types = schemaJson.value("properties").toObject().keys().toVector(); + return types; +} + +bool SettingsTable::isGlobalSettingType(const QString& type) const { + return getGlobalSettingTypes().contains(type); +} + +bool SettingsTable::isInstanceSettingType(const QString& type) const { + return getInstanceSettingTypes().contains(type); +} + +const QVector& SettingsTable::getInstanceSettingTypes() const +{ + if (!areInstanceSettingTypesInitialised) { + instanceSettingTypes = initializeInstanceSettingTypes(); + areInstanceSettingTypesInitialised = true; + } + return instanceSettingTypes; +} + +QVector SettingsTable::initializeInstanceSettingTypes() const +{ + QJsonObject schemaJson; + try + { + schemaJson = QJsonFactory::readSchema(DEFAULT_INSTANCE_SETTINGS_SCHEMA_FILE); + } + catch (const std::runtime_error& error) + { + throw std::runtime_error(error.what()); + } + + const QVector types = schemaJson.value("properties").toObject().keys().toVector(); + return types; +} + +const QJsonObject& SettingsTable::getDefaultSettings() const +{ + if (!areDefaultSettingsInitialised) { + defaultSettings = initializeDefaultSettings(); + areDefaultSettingsInitialised = true; + } + return defaultSettings; +} + +QJsonObject SettingsTable::initializeDefaultSettings() const +{ + QJsonObject defaultConfig; + if ( QJsonFactory::load(DEFAULT_SETTINGS_SCHEMA_FILE, DEFAULT_SETTINGS_CONFIGURATION_FILE, defaultConfig) < 0) + { + Error(_log,"Failed to read default config"); + } + + return defaultConfig; +} + +bool SettingsTable::createSettingsRecord(const QString& type, const QString& config) const +{ + QVariantMap map; + map["config"] = config; + map["updated_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isGlobalSettingType(type)) + { + cond.append(CPair("AND hyperion_inst",_instance)); + } + return createRecord(cond, map); +} + +bool SettingsTable::recordExist(const QString& type) const +{ + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isGlobalSettingType(type)) + { + cond.append(CPair("AND hyperion_inst",_instance)); + } + return recordExists(cond); +} + +QJsonDocument SettingsTable::getSettingsRecord(const QString& type) const +{ + QVariantMap results; + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isGlobalSettingType(type)) + { + cond.append(CPair("AND hyperion_inst",_instance)); + } + getRecord(cond, results, QStringList("config")); + return QJsonDocument::fromJson(results["config"].toByteArray()); +} + +QString SettingsTable::getSettingsRecordString(const QString& type) const +{ + QVariantMap results; + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isGlobalSettingType(type)) + { + cond.append(CPair("AND hyperion_inst",_instance)); + } + getRecord(cond, results, QStringList("config")); + return results["config"].toString(); +} + +QJsonObject SettingsTable::getSettings(const QStringList& filteredTypes ) const +{ + return getSettings(_instance, filteredTypes); +} + +QJsonObject SettingsTable::getSettings(const QVariant& instance, const QStringList& filteredTypes ) const +{ + QJsonObject settingsObject; + QStringList settingsKeys({ "type", "config" }); + QString settingsCondition; + QVariantList conditionValues; + + if (instance.isNull() || instance == GLOABL_INSTANCE_ID ) + { + settingsCondition = "hyperion_inst IS NULL"; + } + else + { + settingsCondition = "hyperion_inst = ?"; + conditionValues.append(instance); + } + + if (!filteredTypes.isEmpty()) + { + QStringList seletedSettingTypes; + for (const auto &type : filteredTypes) { + seletedSettingTypes << QString("%1=?").arg("type"); + conditionValues.append(type); + } + settingsCondition += QString (" AND (%1)").arg(seletedSettingTypes.join(" OR ")); + } + + QVector settingsList; + if (getRecords(settingsCondition, conditionValues, settingsList, settingsKeys)) + { + for (const QVariantMap &setting : std::as_const(settingsList)) + { + QString type = setting.value("type").toString(); + QByteArray configObject = setting.value("config").toByteArray(); + QJsonDocument jsonDoc = QJsonDocument::fromJson(configObject); + + if (!jsonDoc.isNull()) + { + QJsonValue config; + + if (jsonDoc.isArray()) + { + config = jsonDoc.array(); + } + else if (jsonDoc.isObject()) + { + config = jsonDoc.object(); + } + settingsObject.insert(type, config); + } + } + } + return settingsObject; +} + +QStringList SettingsTable::nonExtingTypes() const +{ + QStringList testTypes; + QString condition {"hyperion_inst"}; + if(_instance == GLOABL_INSTANCE_ID) + { + condition += " IS NULL"; + testTypes = getGlobalSettingTypes().toList(); + } + else + { + condition += QString(" = %1").arg(_instance); + testTypes = getInstanceSettingTypes().toList(); + } + + QVariantList testTypesList; + testTypesList.reserve(testTypes.size()); + + for (const QString &str : std::as_const(testTypes)) { + testTypesList.append(QVariant(str)); + } + + QStringList nonExistingRecs; + recordsNotExisting(testTypesList, "type", nonExistingRecs, condition ); + + return nonExistingRecs; +} + +QPair SettingsTable::addMissingDefaults() +{ + QStringList errorList; + + QJsonObject defaultSettings; + if (_instance == GLOABL_INSTANCE_ID) + { + defaultSettings = getDefaultSettings().value("global").toObject(); + } + else + { + defaultSettings = getDefaultSettings().value("instance").toObject(); + } + + const QStringList missingTypes = nonExtingTypes(); + if (missingTypes.empty()) + { + Debug(_log, "Instance [%u]: No missing configuration items identified", _instance); + return qMakePair (true, errorList ); + } + + QSqlDatabase idb = getDB(); + if (!startTransaction(idb, errorList)) + { + return qMakePair(false, errorList); + } + + + bool errorOccured {false}; + + Info(_log, "Instance [%u]: Add default settings for %d missing configuration items",_instance, missingTypes.size()); + for (const auto &missingType: missingTypes) + { + if (!createSettingsRecord(missingType, JsonUtils::jsonValueToQString(defaultSettings.value(missingType)))) + { + errorOccured = true; + } + } + + if (errorOccured) + { + QString errorText = "Errors occured while adding missing settings to instance configuration items"; + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); + + if (!idb.rollback()) + { + errorText = QString("Could not create a database transaction. Error: %1").arg(idb.lastError().text()); + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); + } + } + + commiTransaction(idb, errorList); + + if(errorList.isEmpty()) + { + Debug(_log, "Instance [%u]: Successfully defaulted settings for %d missing configuration items", _instance, missingTypes.size()); + } + + return qMakePair (errorList.isEmpty(), errorList ); +} + +void SettingsTable::deleteInstance() const +{ + deleteRecord({{"hyperion_inst",_instance}}); +} + +QString SettingsTable::fixVersion(const QString& version) +{ + QString newVersion; + + // Use a static QRegularExpression to avoid re-creating it every time + static const QRegularExpression regEx( + "(\\d+\\.\\d+\\.\\d+-?[a-zA-Z-\\d]*\\.?[\\d]*)", + QRegularExpression::CaseInsensitiveOption | QRegularExpression::MultilineOption + ); + + // Try fixing version number, remove dot-separated pre-release identifiers not supported + QRegularExpressionMatch match = regEx.match(version); + + if (match.hasMatch()) + { + newVersion = match.captured(1); + } + + return newVersion; +} + +bool SettingsTable::resolveConfigVersion() +{ + QJsonObject generalConfig = getSettingsRecord({"general"}).object(); + return resolveConfigVersion(generalConfig); +} + +bool SettingsTable::resolveConfigVersion(QJsonObject generalConfig) +{ + bool isValid = false; + + QString configVersion = generalConfig["configVersion"].toString(); + if (!configVersion.isEmpty()) + { + isValid = _configVersion.setVersion(configVersion.toStdString()); + if (!isValid) + { + isValid = _configVersion.setVersion(fixVersion(configVersion).toStdString()); + if (isValid) + { + Info(_log, "Invalid config version [%s] fixed. Updated to [%s]", QSTRING_CSTR(configVersion), _configVersion.getVersion().c_str()); + } + } + } + else + { + isValid = true; + } + + return isValid; +} + +QString SettingsTable::getConfigVersionString() +{ + return _configVersion.getVersion().data(); +} + +semver::version SettingsTable::getConfigVersion() +{ + return _configVersion; +} diff --git a/libsrc/effectengine/Effect.cpp b/libsrc/effectengine/Effect.cpp index a03ed570..c01f2643 100644 --- a/libsrc/effectengine/Effect.cpp +++ b/libsrc/effectengine/Effect.cpp @@ -13,7 +13,7 @@ // python utils #include -Effect::Effect(Hyperion *hyperion, int priority, int timeout, const QString &script, const QString &name, const QJsonObject &args, const QString &imageData) +Effect::Effect(Hyperion* hyperion, int priority, int timeout, const QString& script, const QString& name, const QJsonObject& args, const QString& imageData) : QThread() , _hyperion(hyperion) , _priority(priority) @@ -26,7 +26,7 @@ Effect::Effect(Hyperion *hyperion, int priority, int timeout, const QString &scr , _endTime(-1) , _interupt(false) , _imageSize(hyperion->getLedGridSize()) - , _image(_imageSize,QImage::Format_ARGB32_Premultiplied) + , _image(_imageSize, QImage::Format_ARGB32_Premultiplied) { _colors.resize(_hyperion->getLedCount()); _colors.fill(ColorRgb::BLACK); @@ -61,41 +61,81 @@ int Effect::getRemaining() const if (timeout >= 0) { - timeout = static_cast( _endTime - QDateTime::currentMSecsSinceEpoch()); + timeout = static_cast(_endTime - QDateTime::currentMSecsSinceEpoch()); } return timeout; } -void Effect::setModuleParameters() +bool Effect::setModuleParameters() { // import the buildtin Hyperion module - PyObject * module = PyImport_ImportModule("hyperion"); + PyObject* module = PyImport_ImportModule("hyperion"); - // add a capsule containing 'this' to the module to be able to retrieve the effect from the callback function - PyModule_AddObject(module, "__effectObj", PyCapsule_New((void*)this, "hyperion.__effectObj", nullptr)); + if (module == nullptr) { + PyErr_Print(); // Print error if module import fails + return false; + } - // add ledCount variable to the interpreter + // Add a capsule containing 'this' to the module for callback access + PyObject* capsule = PyCapsule_New((void*)this, "hyperion.__effectObj", nullptr); + if (capsule == nullptr || PyModule_AddObject(module, "__effectObj", capsule) < 0) { + PyErr_Print(); // Print error if adding capsule fails + Py_XDECREF(module); // Clean up if capsule addition fails + Py_XDECREF(capsule); + return false; + } + + // Add ledCount variable to the interpreter int ledCount = 0; QMetaObject::invokeMethod(_hyperion, "getLedCount", Qt::BlockingQueuedConnection, Q_RETURN_ARG(int, ledCount)); - PyObject_SetAttrString(module, "ledCount", Py_BuildValue("i", ledCount)); + PyObject* ledCountObj = Py_BuildValue("i", ledCount); + if (PyObject_SetAttrString(module, "ledCount", ledCountObj) < 0) { + PyErr_Print(); // Print error if setting attribute fails + } + Py_XDECREF(ledCountObj); - // add minimumWriteTime variable to the interpreter + // Add minimumWriteTime variable to the interpreter int latchTime = 0; QMetaObject::invokeMethod(_hyperion, "getLatchTime", Qt::BlockingQueuedConnection, Q_RETURN_ARG(int, latchTime)); - PyObject_SetAttrString(module, "latchTime", Py_BuildValue("i", latchTime)); + PyObject* latchTimeObj = Py_BuildValue("i", latchTime); + if (PyObject_SetAttrString(module, "latchTime", latchTimeObj) < 0) { + PyErr_Print(); // Print error if setting attribute fails + } + Py_XDECREF(latchTimeObj); - // add a args variable to the interpreter - PyObject_SetAttrString(module, "args", EffectModule::json2python(_args)); + // Add args variable to the interpreter + PyObject* argsObj = EffectModule::json2python(_args); + if (PyObject_SetAttrString(module, "args", argsObj) < 0) { + PyErr_Print(); // Print error if setting attribute fails + } + Py_XDECREF(argsObj); - // decref the module + // Decrement module reference Py_XDECREF(module); + + return true; } void Effect::run() { PythonProgram program(_name, _log); - setModuleParameters(); +#if (PY_VERSION_HEX < 0x030C0000) + PyThreadState* prev_thread_state = PyThreadState_Swap(program); +#endif + + if (!setModuleParameters()) + { + Error(_log, "Failed to set Module parameters. Effect will not be executed."); +#if (PY_VERSION_HEX < 0x030C0000) + PyThreadState_Swap(prev_thread_state); +#endif + return; + } + +#if (PY_VERSION_HEX < 0x030C0000) + PyThreadState_Swap(prev_thread_state); +#endif // Set the end time if applicable if (_timeout > 0) @@ -104,7 +144,7 @@ void Effect::run() } // Run the effect script - QFile file (_script); + QFile file(_script); if (file.open(QIODevice::ReadOnly)) { program.execute(file.readAll()); diff --git a/libsrc/effectengine/EffectFileHandler.cpp b/libsrc/effectengine/EffectFileHandler.cpp index 734c4da2..5d441b7d 100644 --- a/libsrc/effectengine/EffectFileHandler.cpp +++ b/libsrc/effectengine/EffectFileHandler.cpp @@ -103,7 +103,7 @@ QString EffectFileHandler::saveEffect(const QJsonObject& message) if (it != effectsSchemas.end()) { - if (!JsonUtils::validate("EffectFileHandler", message["args"].toObject(), it->schemaFile, _log).first) + if (!JsonUtils::validate("EffectFileHandler", message["args"], it->schemaFile, _log).first) { return "Error during arg validation against schema, please consult the Hyperion Log"; } diff --git a/libsrc/effectengine/EffectModule.cpp b/libsrc/effectengine/EffectModule.cpp index bfa4a4c4..d3ce4a49 100644 --- a/libsrc/effectengine/EffectModule.cpp +++ b/libsrc/effectengine/EffectModule.cpp @@ -17,25 +17,63 @@ #include #include +// Define a struct for per-interpreter state +typedef struct { +} hyperion_module_state; + +// Macro to access the module state +#define GET_HYPERION_STATE(module) ((hyperion_module_state*)PyModule_GetState(module)) + // Get the effect from the capsule #define getEffect() static_cast((Effect*)PyCapsule_Import("hyperion.__effectObj", 0)) -// create the hyperion module -struct PyModuleDef EffectModule::moduleDef = { - PyModuleDef_HEAD_INIT, - "hyperion", /* m_name */ - "Hyperion module", /* m_doc */ - -1, /* m_size */ - EffectModule::effectMethods, /* m_methods */ - NULL, /* m_reload */ - NULL, /* m_traverse */ - NULL, /* m_clear */ - NULL, /* m_free */ +// Module execution function for multi-phase init +static int hyperion_exec(PyObject* module) { + // Initialize per-interpreter state + hyperion_module_state* state = GET_HYPERION_STATE(module); + if (state == NULL) + { + return -1; + } + return 0; +} + +// Module deallocation function to clean up per-interpreter state +static void hyperion_free(void* /* module */) +{ + // No specific cleanup required in this example +} + +static PyModuleDef_Slot hyperion_slots[] = { + {Py_mod_exec, reinterpret_cast(hyperion_exec)}, +#if (PY_VERSION_HEX >= 0x030C0000) + {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, +#endif + {0, NULL} }; -PyObject* EffectModule::PyInit_hyperion() +// Module definition with multi-phase and per-interpreter state +static struct PyModuleDef hyperion_module = { + PyModuleDef_HEAD_INIT, + "hyperion", // Module name + "Hyperion module", // Module docstring + sizeof(hyperion_module_state), // Size of per-interpreter state + EffectModule::effectMethods, // Methods array + NULL, // Slots array (will be added in PyInit_hyperion) + NULL, // Traverse function (optional) + NULL, // Clear function (optional) + hyperion_free // Free function +}; + +// initialize function for the hyperion module +PyMODINIT_FUNC PyInit_hyperion(void) { - return PyModule_Create(&moduleDef); + + // assign slots to the module definition + hyperion_module.m_slots = hyperion_slots; + + // return a new module definition instance + return PyModuleDef_Init(&hyperion_module); } void EffectModule::registerHyperionExtensionModule() @@ -43,54 +81,77 @@ void EffectModule::registerHyperionExtensionModule() PyImport_AppendInittab("hyperion", &PyInit_hyperion); } -PyObject *EffectModule::json2python(const QJsonValue &jsonData) +PyObject* EffectModule::json2python(const QJsonValue& jsonData) { switch (jsonData.type()) { - case QJsonValue::Null: - Py_RETURN_NONE; - case QJsonValue::Undefined: - Py_RETURN_NOTIMPLEMENTED; - case QJsonValue::Double: + case QJsonValue::Null: + Py_RETURN_NONE; + + case QJsonValue::Undefined: + Py_RETURN_NOTIMPLEMENTED; + + case QJsonValue::Double: + { + double value = jsonData.toDouble(); + if (value == static_cast(value)) // If no fractional part, value is equal to its integer representation { - double doubleIntegratlPart; - double doubleFractionalPart = std::modf(jsonData.toDouble(), &doubleIntegratlPart); - if (doubleFractionalPart > std::numeric_limits::epsilon()) - { - return Py_BuildValue("d", jsonData.toDouble()); - } - return Py_BuildValue("i", jsonData.toInt()); + return Py_BuildValue("i", static_cast(value)); } - case QJsonValue::Bool: - return Py_BuildValue("i", jsonData.toBool() ? 1 : 0); - case QJsonValue::String: - return Py_BuildValue("s", jsonData.toString().toUtf8().constData()); - case QJsonValue::Object: + return Py_BuildValue("d", value); + } + + case QJsonValue::Bool: + return PyBool_FromLong(jsonData.toBool() ? 1 : 0); + + case QJsonValue::String: + return PyUnicode_FromString(jsonData.toString().toUtf8().constData()); + + case QJsonValue::Array: + { + QJsonArray arrayData = jsonData.toArray(); + PyObject* list = PyList_New(arrayData.size()); + int index = 0; + for (QJsonArray::iterator i = arrayData.begin(); i != arrayData.end(); ++i, ++index) { - PyObject * dict= PyDict_New(); - QJsonObject objectData = jsonData.toObject(); - for (QJsonObject::iterator i = objectData.begin(); i != objectData.end(); ++i) - { - PyObject * obj = json2python(*i); - PyDict_SetItemString(dict, i.key().toStdString().c_str(), obj); - Py_XDECREF(obj); - } - return dict; + PyObject* obj = json2python(*i); + Py_INCREF(obj); + PyList_SetItem(list, index, obj); + Py_XDECREF(obj); } - case QJsonValue::Array: - { - QJsonArray arrayData = jsonData.toArray(); - PyObject * list = PyList_New(arrayData.size()); - int index = 0; - for (QJsonArray::iterator i = arrayData.begin(); i != arrayData.end(); ++i, ++index) - { - PyObject * obj = json2python(*i); - Py_INCREF(obj); - PyList_SetItem(list, index, obj); - Py_XDECREF(obj); + return list; + } + + case QJsonValue::Object: { + // Python's dict + QJsonObject jsonObject = jsonData.toObject(); + PyObject* pyDict = PyDict_New(); + for (auto it = jsonObject.begin(); it != jsonObject.end(); ++it) { + // Convert key + PyObject* pyKey = PyUnicode_FromString(it.key().toUtf8().constData()); + if (!pyKey) { + Py_XDECREF(pyDict); + return nullptr; // Error occurred, return null } - return list; + // Convert value + PyObject* pyValue = json2python(it.value()); + if (!pyValue) { + Py_XDECREF(pyKey); + Py_XDECREF(pyDict); + return nullptr; // Error occurred, return null + } + // Add to dictionary + PyDict_SetItem(pyDict, pyKey, pyValue); + Py_XDECREF(pyKey); + Py_XDECREF(pyValue); } + return pyDict; + } + + default: + // Unsupported type + PyErr_SetString(PyExc_TypeError, "Unsupported QJsonValue type."); + return nullptr; } assert(false); @@ -126,7 +187,7 @@ PyMethodDef EffectModule::effectMethods[] = { {NULL, NULL, 0, NULL} }; -PyObject* EffectModule::wrapSetColor(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapSetColor(PyObject* self, PyObject* args) { // check the number of arguments int argCount = PyTuple_Size(args); @@ -138,7 +199,7 @@ PyObject* EffectModule::wrapSetColor(PyObject *self, PyObject *args) { getEffect()->_colors.fill(color); QVector _cQV = getEffect()->_colors; - emit getEffect()->setInput(getEffect()->_priority, std::vector( _cQV.begin(), _cQV.end() ), getEffect()->getRemaining(), false); + emit getEffect()->setInput(getEffect()->_priority, std::vector(_cQV.begin(), _cQV.end()), getEffect()->getRemaining(), false); Py_RETURN_NONE; } return nullptr; @@ -146,7 +207,7 @@ PyObject* EffectModule::wrapSetColor(PyObject *self, PyObject *args) else if (argCount == 1) { // bytearray of values - PyObject * bytearray = nullptr; + PyObject* bytearray = nullptr; if (PyArg_ParseTuple(args, "O", &bytearray)) { if (PyByteArray_Check(bytearray)) @@ -154,10 +215,10 @@ PyObject* EffectModule::wrapSetColor(PyObject *self, PyObject *args) size_t length = PyByteArray_Size(bytearray); if (length == 3 * static_cast(getEffect()->_hyperion->getLedCount())) { - char * data = PyByteArray_AS_STRING(bytearray); + char* data = PyByteArray_AS_STRING(bytearray); memcpy(getEffect()->_colors.data(), data, length); QVector _cQV = getEffect()->_colors; - emit getEffect()->setInput(getEffect()->_priority, std::vector( _cQV.begin(), _cQV.end() ), getEffect()->getRemaining(), false); + emit getEffect()->setInput(getEffect()->_priority, std::vector(_cQV.begin(), _cQV.end()), getEffect()->getRemaining(), false); Py_RETURN_NONE; } else @@ -184,12 +245,12 @@ PyObject* EffectModule::wrapSetColor(PyObject *self, PyObject *args) } } -PyObject* EffectModule::wrapSetImage(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapSetImage(PyObject* self, PyObject* args) { // bytearray of values int width = 0; int height = 0; - PyObject * bytearray = nullptr; + PyObject* bytearray = nullptr; if (PyArg_ParseTuple(args, "iiO", &width, &height, &bytearray)) { if (PyByteArray_Check(bytearray)) @@ -198,7 +259,7 @@ PyObject* EffectModule::wrapSetImage(PyObject *self, PyObject *args) if (length == 3 * width * height) { Image image(width, height); - char * data = PyByteArray_AS_STRING(bytearray); + char* data = PyByteArray_AS_STRING(bytearray); memcpy(image.memptr(), data, length); emit getEffect()->setInputImage(getEffect()->_priority, image, getEffect()->getRemaining(), false); Py_RETURN_NONE; @@ -225,11 +286,11 @@ PyObject* EffectModule::wrapSetImage(PyObject *self, PyObject *args) return nullptr; } -PyObject* EffectModule::wrapGetImage(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapGetImage(PyObject* self, PyObject* args) { QBuffer buffer; QImageReader reader; - char *source; + char* source; int cropLeft = 0, cropTop = 0, cropRight = 0, cropBottom = 0; int grayscale = false; @@ -237,7 +298,7 @@ PyObject* EffectModule::wrapGetImage(PyObject *self, PyObject *args) { Q_INIT_RESOURCE(EffectEngine); - if(!PyArg_ParseTuple(args, "s|iiiip", &source, &cropLeft, &cropTop, &cropRight, &cropBottom, &grayscale)) + if (!PyArg_ParseTuple(args, "s|iiiip", &source, &cropLeft, &cropTop, &cropRight, &cropBottom, &grayscale)) { PyErr_SetString(PyExc_TypeError, "String required"); return nullptr; @@ -246,8 +307,8 @@ PyObject* EffectModule::wrapGetImage(PyObject *self, PyObject *args) const QUrl url = QUrl(source); if (url.isValid()) { - QNetworkAccessManager *networkManager = new QNetworkAccessManager(); - QNetworkReply * networkReply = networkManager->get(QNetworkRequest(url)); + QNetworkAccessManager* networkManager = new QNetworkAccessManager(); + QNetworkReply* networkReply = networkManager->get(QNetworkRequest(url)); QEventLoop eventLoop; connect(networkReply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); @@ -262,14 +323,14 @@ PyObject* EffectModule::wrapGetImage(PyObject *self, PyObject *args) } delete networkReply; - delete networkManager; + delete networkManager; } else { QString file = QString::fromUtf8(source); - if (file.mid(0, 1) == ":") - file = ":/effects/"+file.mid(1); + if (file.mid(0, 1) == ":") + file = ":/effects/" + file.mid(1); reader.setDecideFormatFromContent(true); reader.setFileName(file); @@ -286,7 +347,7 @@ PyObject* EffectModule::wrapGetImage(PyObject *self, PyObject *args) if (reader.canRead()) { - PyObject *result = PyList_New(reader.imageCount()); + PyObject* result = PyList_New(reader.imageCount()); for (int i = 0; i < reader.imageCount(); ++i) { @@ -312,18 +373,18 @@ PyObject* EffectModule::wrapGetImage(PyObject *self, PyObject *args) } QByteArray binaryImage; - for (int i = 0; i(qimage.scanLine(i)); - const QRgb *end = scanline + qimage.width(); + const QRgb* scanline = reinterpret_cast(qimage.scanLine(i)); + const QRgb* end = scanline + qimage.width(); for (; scanline != end; scanline++) { - binaryImage.append(!grayscale ? (char) qRed(scanline[0]) : (char) qGray(scanline[0])); - binaryImage.append(!grayscale ? (char) qGreen(scanline[1]) : (char) qGray(scanline[1])); - binaryImage.append(!grayscale ? (char) qBlue(scanline[2]) : (char) qGray(scanline[2])); + binaryImage.append(!grayscale ? (char)qRed(scanline[0]) : (char)qGray(scanline[0])); + binaryImage.append(!grayscale ? (char)qGreen(scanline[1]) : (char)qGray(scanline[1])); + binaryImage.append(!grayscale ? (char)qBlue(scanline[2]) : (char)qGray(scanline[2])); } } - PyList_SET_ITEM(result, i, Py_BuildValue("{s:i,s:i,s:O}", "imageWidth", width, "imageHeight", height, "imageData", PyByteArray_FromStringAndSize(binaryImage.constData(),binaryImage.size()))); + PyList_SET_ITEM(result, i, Py_BuildValue("{s:i,s:i,s:O}", "imageWidth", width, "imageHeight", height, "imageData", PyByteArray_FromStringAndSize(binaryImage.constData(), binaryImage.size()))); } else { @@ -341,13 +402,13 @@ PyObject* EffectModule::wrapGetImage(PyObject *self, PyObject *args) } } -PyObject* EffectModule::wrapAbort(PyObject *self, PyObject *) +PyObject* EffectModule::wrapAbort(PyObject* self, PyObject*) { return Py_BuildValue("i", getEffect()->isInterruptionRequested() ? 1 : 0); } -PyObject* EffectModule::wrapImageShow(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageShow(PyObject* self, PyObject* args) { int argCount = PyTuple_Size(args); int imgId = -1; @@ -357,27 +418,27 @@ PyObject* EffectModule::wrapImageShow(PyObject *self, PyObject *args) argsOk = true; } - if ( ! argsOk || (imgId>-1 && imgId >= getEffect()->_imageStack.size())) + if (!argsOk || (imgId > -1 && imgId >= getEffect()->_imageStack.size())) { return nullptr; } - QImage * qimage = (imgId<0) ? &(getEffect()->_image) : &(getEffect()->_imageStack[imgId]); + QImage* qimage = (imgId < 0) ? &(getEffect()->_image) : &(getEffect()->_imageStack[imgId]); int width = qimage->width(); int height = qimage->height(); Image image(width, height); QByteArray binaryImage; - for (int i = 0; i(qimage->scanLine(i)); - for (int j = 0; j< width; ++j) + const QRgb* scanline = reinterpret_cast(qimage->scanLine(i)); + for (int j = 0; j < width; ++j) { - binaryImage.append((char) qRed(scanline[j])); - binaryImage.append((char) qGreen(scanline[j])); - binaryImage.append((char) qBlue(scanline[j])); + binaryImage.append((char)qRed(scanline[j])); + binaryImage.append((char)qGreen(scanline[j])); + binaryImage.append((char)qBlue(scanline[j])); } } @@ -387,27 +448,27 @@ PyObject* EffectModule::wrapImageShow(PyObject *self, PyObject *args) return Py_BuildValue(""); } -PyObject* EffectModule::wrapImageLinearGradient(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageLinearGradient(PyObject* self, PyObject* args) { int argCount = PyTuple_Size(args); - PyObject * bytearray = nullptr; + PyObject* bytearray = nullptr; int startRX = 0; int startRY = 0; int startX = 0; int startY = 0; int width = getEffect()->_imageSize.width(); - int endX {width}; + int endX{ width }; int height = getEffect()->_imageSize.height(); - int endY {height}; + int endY{ height }; int spread = 0; bool argsOK = false; - if ( argCount == 10 && PyArg_ParseTuple(args, "iiiiiiiiOi", &startRX, &startRY, &width, &height, &startX, &startY, &endX, &endY, &bytearray, &spread) ) + if (argCount == 10 && PyArg_ParseTuple(args, "iiiiiiiiOi", &startRX, &startRY, &width, &height, &startX, &startY, &endX, &endY, &bytearray, &spread)) { argsOK = true; } - if ( argCount == 6 && PyArg_ParseTuple(args, "iiiiOi", &startX, &startY, &endX, &endY, &bytearray, &spread) ) + if (argCount == 6 && PyArg_ParseTuple(args, "iiiiOi", &startX, &startY, &endX, &endY, &bytearray, &spread)) { argsOK = true; } @@ -420,20 +481,20 @@ PyObject* EffectModule::wrapImageLinearGradient(PyObject *self, PyObject *args) const unsigned arrayItemLength = 5; if (length % arrayItemLength == 0) { - QRect myQRect(startRX,startRY,width,height); - QLinearGradient gradient(QPoint(startX,startY), QPoint(endX,endY)); - char * data = PyByteArray_AS_STRING(bytearray); + QRect myQRect(startRX, startRY, width, height); + QLinearGradient gradient(QPoint(startX, startY), QPoint(endX, endY)); + char* data = PyByteArray_AS_STRING(bytearray); - for (int idx=0; idx(spread)); @@ -456,29 +517,29 @@ PyObject* EffectModule::wrapImageLinearGradient(PyObject *self, PyObject *args) return nullptr; } -PyObject* EffectModule::wrapImageConicalGradient(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageConicalGradient(PyObject* self, PyObject* args) { int argCount = PyTuple_Size(args); - PyObject * bytearray = nullptr; + PyObject* bytearray = nullptr; int centerX = 0; int centerY = 0; int angle = 0; int startX = 0; int startY = 0; - int width = getEffect()->_imageSize.width(); + int width = getEffect()->_imageSize.width(); int height = getEffect()->_imageSize.height(); bool argsOK = false; - if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiiO", &startX, &startY, &width, &height, ¢erX, ¢erY, &angle, &bytearray) ) + if (argCount == 8 && PyArg_ParseTuple(args, "iiiiiiiO", &startX, &startY, &width, &height, ¢erX, ¢erY, &angle, &bytearray)) { argsOK = true; } - if ( argCount == 4 && PyArg_ParseTuple(args, "iiiO", ¢erX, ¢erY, &angle, &bytearray) ) + if (argCount == 4 && PyArg_ParseTuple(args, "iiiO", ¢erX, ¢erY, &angle, &bytearray)) { argsOK = true; } - angle = qMax(qMin(angle,360),0); + angle = qMax(qMin(angle, 360), 0); if (argsOK) { @@ -488,20 +549,20 @@ PyObject* EffectModule::wrapImageConicalGradient(PyObject *self, PyObject *args) const unsigned arrayItemLength = 5; if (length % arrayItemLength == 0) { - QRect myQRect(startX,startY,width,height); - QConicalGradient gradient(QPoint(centerX,centerY), angle ); - char * data = PyByteArray_AS_STRING(bytearray); + QRect myQRect(startX, startY, width, height); + QConicalGradient gradient(QPoint(centerX, centerY), angle); + char* data = PyByteArray_AS_STRING(bytearray); - for (int idx=0; idx_painter->fillRect(myQRect, gradient); @@ -524,44 +585,44 @@ PyObject* EffectModule::wrapImageConicalGradient(PyObject *self, PyObject *args) } -PyObject* EffectModule::wrapImageRadialGradient(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageRadialGradient(PyObject* self, PyObject* args) { int argCount = PyTuple_Size(args); - PyObject * bytearray = nullptr; + PyObject* bytearray = nullptr; int centerX = 0; int centerY = 0; int radius = 0; int focalX = 0; int focalY = 0; - int focalRadius =0; + int focalRadius = 0; int spread = 0; int startX = 0; int startY = 0; - int width = getEffect()->_imageSize.width(); + int width = getEffect()->_imageSize.width(); int height = getEffect()->_imageSize.height(); bool argsOK = false; - if ( argCount == 12 && PyArg_ParseTuple(args, "iiiiiiiiiiOi", &startX, &startY, &width, &height, ¢erX, ¢erY, &radius, &focalX, &focalY, &focalRadius, &bytearray, &spread) ) - { - argsOK = true; - } - if ( argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiOi", &startX, &startY, &width, &height, ¢erX, ¢erY, &radius, &bytearray, &spread) ) - { - argsOK = true; - focalX = centerX; - focalY = centerY; - focalRadius = radius; - } - if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiOi", ¢erX, ¢erY, &radius, &focalX, &focalY, &focalRadius, &bytearray, &spread) ) + if (argCount == 12 && PyArg_ParseTuple(args, "iiiiiiiiiiOi", &startX, &startY, &width, &height, ¢erX, ¢erY, &radius, &focalX, &focalY, &focalRadius, &bytearray, &spread)) { argsOK = true; } - if ( argCount == 5 && PyArg_ParseTuple(args, "iiiOi", ¢erX, ¢erY, &radius, &bytearray, &spread) ) + if (argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiOi", &startX, &startY, &width, &height, ¢erX, ¢erY, &radius, &bytearray, &spread)) { - argsOK = true; - focalX = centerX; - focalY = centerY; + argsOK = true; + focalX = centerX; + focalY = centerY; + focalRadius = radius; + } + if (argCount == 8 && PyArg_ParseTuple(args, "iiiiiiOi", ¢erX, ¢erY, &radius, &focalX, &focalY, &focalRadius, &bytearray, &spread)) + { + argsOK = true; + } + if (argCount == 5 && PyArg_ParseTuple(args, "iiiOi", ¢erX, ¢erY, &radius, &bytearray, &spread)) + { + argsOK = true; + focalX = centerX; + focalY = centerY; focalRadius = radius; } @@ -573,19 +634,19 @@ PyObject* EffectModule::wrapImageRadialGradient(PyObject *self, PyObject *args) if (length % 4 == 0) { - QRect myQRect(startX,startY,width,height); - QRadialGradient gradient(QPoint(centerX,centerY), qMax(radius,0) ); - char * data = PyByteArray_AS_STRING(bytearray); + QRect myQRect(startX, startY, width, height); + QRadialGradient gradient(QPoint(centerX, centerY), qMax(radius, 0)); + char* data = PyByteArray_AS_STRING(bytearray); - for (int idx=0; idx(spread)); @@ -608,9 +669,9 @@ PyObject* EffectModule::wrapImageRadialGradient(PyObject *self, PyObject *args) return nullptr; } -PyObject* EffectModule::wrapImageDrawPolygon(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageDrawPolygon(PyObject* self, PyObject* args) { - PyObject * bytearray = nullptr; + PyObject* bytearray = nullptr; int argCount = PyTuple_Size(args); int r = 0; @@ -620,11 +681,11 @@ PyObject* EffectModule::wrapImageDrawPolygon(PyObject *self, PyObject *args) bool argsOK = false; - if ( argCount == 5 && PyArg_ParseTuple(args, "Oiiii", &bytearray, &r, &g, &b, &a) ) + if (argCount == 5 && PyArg_ParseTuple(args, "Oiiii", &bytearray, &r, &g, &b, &a)) { argsOK = true; } - if ( argCount == 4 && PyArg_ParseTuple(args, "Oiii", &bytearray, &r, &g, &b) ) + if (argCount == 4 && PyArg_ParseTuple(args, "Oiii", &bytearray, &r, &g, &b)) { argsOK = true; } @@ -637,18 +698,18 @@ PyObject* EffectModule::wrapImageDrawPolygon(PyObject *self, PyObject *args) if (length % 2 == 0) { QVector points; - char * data = PyByteArray_AS_STRING(bytearray); + char* data = PyByteArray_AS_STRING(bytearray); - for (int idx=0; idx_painter; + QPainter* painter = getEffect()->_painter; QPen oldPen = painter->pen(); - QPen newPen(QColor(r,g,b,a)); + QPen newPen(QColor(r, g, b, a)); painter->setPen(newPen); - painter->setBrush(QBrush(QColor(r,g,b,a), Qt::SolidPattern)); + painter->setBrush(QBrush(QColor(r, g, b, a), Qt::SolidPattern)); painter->drawPolygon(points); painter->setPen(oldPen); Py_RETURN_NONE; @@ -668,9 +729,9 @@ PyObject* EffectModule::wrapImageDrawPolygon(PyObject *self, PyObject *args) return nullptr; } -PyObject* EffectModule::wrapImageDrawPie(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageDrawPie(PyObject* self, PyObject* args) { - PyObject * bytearray = nullptr; + PyObject* bytearray = nullptr; QString brush; int argCount = PyTuple_Size(args); @@ -686,30 +747,30 @@ PyObject* EffectModule::wrapImageDrawPie(PyObject *self, PyObject *args) bool argsOK = false; - if ( argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiii", ¢erX, ¢erY, &radius, &startAngle, &spanAngle, &r, &g, &b, &a) ) + if (argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiii", ¢erX, ¢erY, &radius, &startAngle, &spanAngle, &r, &g, &b, &a)) { argsOK = true; } - if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", ¢erX, ¢erY, &radius, &startAngle, &spanAngle, &r, &g, &b) ) + if (argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", ¢erX, ¢erY, &radius, &startAngle, &spanAngle, &r, &g, &b)) { argsOK = true; } - if ( argCount == 7 && PyArg_ParseTuple(args, "iiiiisO", ¢erX, ¢erY, &radius, &startAngle, &spanAngle, &brush, &bytearray) ) + if (argCount == 7 && PyArg_ParseTuple(args, "iiiiisO", ¢erX, ¢erY, &radius, &startAngle, &spanAngle, &brush, &bytearray)) { argsOK = true; } - if ( argCount == 5 && PyArg_ParseTuple(args, "iiisO", ¢erX, ¢erY, &radius, &brush, &bytearray) ) + if (argCount == 5 && PyArg_ParseTuple(args, "iiisO", ¢erX, ¢erY, &radius, &brush, &bytearray)) { argsOK = true; } if (argsOK) { - QPainter * painter = getEffect()->_painter; - startAngle = qMax(qMin(startAngle,360),0); - spanAngle = qMax(qMin(spanAngle,360),-360); + QPainter* painter = getEffect()->_painter; + startAngle = qMax(qMin(startAngle, 360), 0); + spanAngle = qMax(qMin(spanAngle, 360), -360); - if( argCount == 7 || argCount == 5 ) + if (argCount == 7 || argCount == 5) { a = 0; if (PyByteArray_Check(bytearray)) @@ -718,21 +779,21 @@ PyObject* EffectModule::wrapImageDrawPie(PyObject *self, PyObject *args) if (length % 5 == 0) { - QConicalGradient gradient(QPoint(centerX,centerY), startAngle); + QConicalGradient gradient(QPoint(centerX, centerY), startAngle); - char * data = PyByteArray_AS_STRING(bytearray); + char* data = PyByteArray_AS_STRING(bytearray); - for (int idx=0; idxsetBrush(gradient); @@ -752,10 +813,10 @@ PyObject* EffectModule::wrapImageDrawPie(PyObject *self, PyObject *args) } else { - painter->setBrush(QBrush(QColor(r,g,b,a), Qt::SolidPattern)); + painter->setBrush(QBrush(QColor(r, g, b, a), Qt::SolidPattern)); } QPen oldPen = painter->pen(); - QPen newPen(QColor(r,g,b,a)); + QPen newPen(QColor(r, g, b, a)); painter->setPen(newPen); painter->drawPie(centerX - radius, centerY - radius, centerX + radius, centerY + radius, startAngle * 16, spanAngle * 16); painter->setPen(oldPen); @@ -764,7 +825,7 @@ PyObject* EffectModule::wrapImageDrawPie(PyObject *self, PyObject *args) return nullptr; } -PyObject* EffectModule::wrapImageSolidFill(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageSolidFill(PyObject* self, PyObject* args) { int argCount = PyTuple_Size(args); int r = 0; @@ -773,39 +834,39 @@ PyObject* EffectModule::wrapImageSolidFill(PyObject *self, PyObject *args) int a = 255; int startX = 0; int startY = 0; - int width = getEffect()->_imageSize.width(); + int width = getEffect()->_imageSize.width(); int height = getEffect()->_imageSize.height(); bool argsOK = false; - if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", &startX, &startY, &width, &height, &r, &g, &b, &a) ) + if (argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", &startX, &startY, &width, &height, &r, &g, &b, &a)) { argsOK = true; } - if ( argCount == 7 && PyArg_ParseTuple(args, "iiiiiii", &startX, &startY, &width, &height, &r, &g, &b) ) + if (argCount == 7 && PyArg_ParseTuple(args, "iiiiiii", &startX, &startY, &width, &height, &r, &g, &b)) { argsOK = true; } - if ( argCount == 4 && PyArg_ParseTuple(args, "iiii",&r, &g, &b, &a) ) + if (argCount == 4 && PyArg_ParseTuple(args, "iiii", &r, &g, &b, &a)) { argsOK = true; } - if ( argCount == 3 && PyArg_ParseTuple(args, "iii",&r, &g, &b) ) + if (argCount == 3 && PyArg_ParseTuple(args, "iii", &r, &g, &b)) { argsOK = true; } if (argsOK) { - QRect myQRect(startX,startY,width,height); - getEffect()->_painter->fillRect(myQRect, QColor(r,g,b,a)); + QRect myQRect(startX, startY, width, height); + getEffect()->_painter->fillRect(myQRect, QColor(r, g, b, a)); Py_RETURN_NONE; } return nullptr; } -PyObject* EffectModule::wrapImageDrawLine(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageDrawLine(PyObject* self, PyObject* args) { int argCount = PyTuple_Size(args); int r = 0; @@ -814,27 +875,27 @@ PyObject* EffectModule::wrapImageDrawLine(PyObject *self, PyObject *args) int a = 255; int startX = 0; int startY = 0; - int thick = 1; - int endX = getEffect()->_imageSize.width(); - int endY = getEffect()->_imageSize.height(); + int thick = 1; + int endX = getEffect()->_imageSize.width(); + int endY = getEffect()->_imageSize.height(); bool argsOK = false; - if ( argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiii", &startX, &startY, &endX, &endY, &thick, &r, &g, &b, &a) ) + if (argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiii", &startX, &startY, &endX, &endY, &thick, &r, &g, &b, &a)) { argsOK = true; } - if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", &startX, &startY, &endX, &endY, &thick, &r, &g, &b) ) + if (argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", &startX, &startY, &endX, &endY, &thick, &r, &g, &b)) { argsOK = true; } if (argsOK) { - QPainter * painter = getEffect()->_painter; + QPainter* painter = getEffect()->_painter; QRect myQRect(startX, startY, endX, endY); QPen oldPen = painter->pen(); - QPen newPen(QColor(r,g,b,a)); + QPen newPen(QColor(r, g, b, a)); newPen.setWidth(thick); painter->setPen(newPen); painter->drawLine(startX, startY, endX, endY); @@ -845,7 +906,7 @@ PyObject* EffectModule::wrapImageDrawLine(PyObject *self, PyObject *args) return nullptr; } -PyObject* EffectModule::wrapImageDrawPoint(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageDrawPoint(PyObject* self, PyObject* args) { int argCount = PyTuple_Size(args); int r = 0; @@ -854,24 +915,24 @@ PyObject* EffectModule::wrapImageDrawPoint(PyObject *self, PyObject *args) int x = 0; int y = 0; int a = 255; - int thick = 1; + int thick = 1; bool argsOK = false; - if ( argCount == 7 && PyArg_ParseTuple(args, "iiiiiii", &x, &y, &thick, &r, &g, &b, &a) ) + if (argCount == 7 && PyArg_ParseTuple(args, "iiiiiii", &x, &y, &thick, &r, &g, &b, &a)) { argsOK = true; } - if ( argCount == 6 && PyArg_ParseTuple(args, "iiiiii", &x, &y, &thick, &r, &g, &b) ) + if (argCount == 6 && PyArg_ParseTuple(args, "iiiiii", &x, &y, &thick, &r, &g, &b)) { argsOK = true; } if (argsOK) { - QPainter * painter = getEffect()->_painter; + QPainter* painter = getEffect()->_painter; QPen oldPen = painter->pen(); - QPen newPen(QColor(r,g,b,a)); + QPen newPen(QColor(r, g, b, a)); newPen.setWidth(thick); painter->setPen(newPen); painter->drawPoint(x, y); @@ -882,7 +943,7 @@ PyObject* EffectModule::wrapImageDrawPoint(PyObject *self, PyObject *args) return nullptr; } -PyObject* EffectModule::wrapImageDrawRect(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageDrawRect(PyObject* self, PyObject* args) { int argCount = PyTuple_Size(args); int r = 0; @@ -891,27 +952,27 @@ PyObject* EffectModule::wrapImageDrawRect(PyObject *self, PyObject *args) int a = 255; int startX = 0; int startY = 0; - int thick = 1; - int width = getEffect()->_imageSize.width(); - int height = getEffect()->_imageSize.height(); + int thick = 1; + int width = getEffect()->_imageSize.width(); + int height = getEffect()->_imageSize.height(); bool argsOK = false; - if ( argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiii", &startX, &startY, &width, &height, &thick, &r, &g, &b, &a) ) + if (argCount == 9 && PyArg_ParseTuple(args, "iiiiiiiii", &startX, &startY, &width, &height, &thick, &r, &g, &b, &a)) { argsOK = true; } - if ( argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", &startX, &startY, &width, &height, &thick, &r, &g, &b) ) + if (argCount == 8 && PyArg_ParseTuple(args, "iiiiiiii", &startX, &startY, &width, &height, &thick, &r, &g, &b)) { argsOK = true; } if (argsOK) { - QPainter * painter = getEffect()->_painter; - QRect myQRect(startX,startY,width,height); + QPainter* painter = getEffect()->_painter; + QRect myQRect(startX, startY, width, height); QPen oldPen = painter->pen(); - QPen newPen(QColor(r,g,b,a)); + QPen newPen(QColor(r, g, b, a)); newPen.setWidth(thick); painter->setPen(newPen); painter->drawRect(startX, startY, width, height); @@ -923,7 +984,7 @@ PyObject* EffectModule::wrapImageDrawRect(PyObject *self, PyObject *args) } -PyObject* EffectModule::wrapImageSetPixel(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageSetPixel(PyObject* self, PyObject* args) { int argCount = PyTuple_Size(args); int r = 0; @@ -932,9 +993,9 @@ PyObject* EffectModule::wrapImageSetPixel(PyObject *self, PyObject *args) int x = 0; int y = 0; - if ( argCount == 5 && PyArg_ParseTuple(args, "iiiii", &x, &y, &r, &g, &b ) ) + if (argCount == 5 && PyArg_ParseTuple(args, "iiiii", &x, &y, &r, &g, &b)) { - getEffect()->_image.setPixel(x,y,qRgb(r,g,b)); + getEffect()->_image.setPixel(x, y, qRgb(r, g, b)); Py_RETURN_NONE; } @@ -942,43 +1003,43 @@ PyObject* EffectModule::wrapImageSetPixel(PyObject *self, PyObject *args) } -PyObject* EffectModule::wrapImageGetPixel(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageGetPixel(PyObject* self, PyObject* args) { int argCount = PyTuple_Size(args); int x = 0; int y = 0; - if ( argCount == 2 && PyArg_ParseTuple(args, "ii", &x, &y) ) + if (argCount == 2 && PyArg_ParseTuple(args, "ii", &x, &y)) { - QRgb rgb = getEffect()->_image.pixel(x,y); - return Py_BuildValue("iii",qRed(rgb),qGreen(rgb),qBlue(rgb)); + QRgb rgb = getEffect()->_image.pixel(x, y); + return Py_BuildValue("iii", qRed(rgb), qGreen(rgb), qBlue(rgb)); } return nullptr; } -PyObject* EffectModule::wrapImageSave(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageSave(PyObject* self, PyObject* args) { QImage img(getEffect()->_image.copy()); getEffect()->_imageStack.append(img); - return Py_BuildValue("i", getEffect()->_imageStack.size()-1); + return Py_BuildValue("i", getEffect()->_imageStack.size() - 1); } -PyObject* EffectModule::wrapImageMinSize(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageMinSize(PyObject* self, PyObject* args) { int argCount = PyTuple_Size(args); int w = 0; int h = 0; - int width = getEffect()->_imageSize.width(); - int height = getEffect()->_imageSize.height(); + int width = getEffect()->_imageSize.width(); + int height = getEffect()->_imageSize.height(); - if ( argCount == 2 && PyArg_ParseTuple(args, "ii", &w, &h) ) + if (argCount == 2 && PyArg_ParseTuple(args, "ii", &w, &h)) { - if (width_painter; - getEffect()->_image = getEffect()->_image.scaled(qMax(width,w),qMax(height,h), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + getEffect()->_image = getEffect()->_image.scaled(qMax(width, w), qMax(height, h), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); getEffect()->_imageSize = getEffect()->_image.size(); getEffect()->_painter = new QPainter(&(getEffect()->_image)); } @@ -987,60 +1048,60 @@ PyObject* EffectModule::wrapImageMinSize(PyObject *self, PyObject *args) return nullptr; } -PyObject* EffectModule::wrapImageWidth(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageWidth(PyObject* self, PyObject* args) { return Py_BuildValue("i", getEffect()->_imageSize.width()); } -PyObject* EffectModule::wrapImageHeight(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageHeight(PyObject* self, PyObject* args) { return Py_BuildValue("i", getEffect()->_imageSize.height()); } -PyObject* EffectModule::wrapImageCRotate(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageCRotate(PyObject* self, PyObject* args) { int argCount = PyTuple_Size(args); int angle; - if ( argCount == 1 && PyArg_ParseTuple(args, "i", &angle ) ) + if (argCount == 1 && PyArg_ParseTuple(args, "i", &angle)) { - angle = qMax(qMin(angle,360),0); + angle = qMax(qMin(angle, 360), 0); getEffect()->_painter->rotate(angle); Py_RETURN_NONE; } return nullptr; } -PyObject* EffectModule::wrapImageCOffset(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageCOffset(PyObject* self, PyObject* args) { int offsetX = 0; int offsetY = 0; int argCount = PyTuple_Size(args); - if ( argCount == 2 ) + if (argCount == 2) { - PyArg_ParseTuple(args, "ii", &offsetX, &offsetY ); + PyArg_ParseTuple(args, "ii", &offsetX, &offsetY); } - getEffect()->_painter->translate(QPoint(offsetX,offsetY)); + getEffect()->_painter->translate(QPoint(offsetX, offsetY)); Py_RETURN_NONE; } -PyObject* EffectModule::wrapImageCShear(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageCShear(PyObject* self, PyObject* args) { int sh = 0; int sv = 0; int argCount = PyTuple_Size(args); - if ( argCount == 2 && PyArg_ParseTuple(args, "ii", &sh, &sv )) + if (argCount == 2 && PyArg_ParseTuple(args, "ii", &sh, &sv)) { - getEffect()->_painter->shear(sh,sv); + getEffect()->_painter->shear(sh, sv); Py_RETURN_NONE; } return nullptr; } -PyObject* EffectModule::wrapImageResetT(PyObject *self, PyObject *args) +PyObject* EffectModule::wrapImageResetT(PyObject* self, PyObject* args) { getEffect()->_painter->resetTransform(); Py_RETURN_NONE; diff --git a/libsrc/flatbufserver/FlatBufferClient.cpp b/libsrc/flatbufserver/FlatBufferClient.cpp index 8df752ca..f1659585 100644 --- a/libsrc/flatbufserver/FlatBufferClient.cpp +++ b/libsrc/flatbufserver/FlatBufferClient.cpp @@ -1,4 +1,5 @@ #include "FlatBufferClient.h" +#include // qt #include @@ -15,6 +16,8 @@ FlatBufferClient::FlatBufferClient(QTcpSocket* socket, int timeout, QObject *par , _timeout(timeout * 1000) , _priority() { + _imageResampler.setPixelDecimation(1); + // timer setup _timeoutTimer->setSingleShot(true); _timeoutTimer->setInterval(_timeout); @@ -25,6 +28,11 @@ FlatBufferClient::FlatBufferClient(QTcpSocket* socket, int timeout, QObject *par connect(_socket, &QTcpSocket::disconnected, this, &FlatBufferClient::disconnected); } +void FlatBufferClient::setPixelDecimation(int decimator) +{ + _imageResampler.setPixelDecimation(decimator); +} + void FlatBufferClient::readyRead() { _timeoutTimer->start(); @@ -141,55 +149,71 @@ void FlatBufferClient::handleRegisterCommand(const hyperionnet::Register *regReq void FlatBufferClient::handleImageCommand(const hyperionnet::Image *image) { + Image imageRGB; + // extract parameters int duration = image->duration(); - const void* reqPtr; if ((reqPtr = image->data_as_RawImage()) != nullptr) { - const auto *img = static_cast(reqPtr); - const auto & imageData = img->data(); - const int width = img->width(); - const int height = img->height(); + const auto* img = static_cast(reqPtr); - if (width <= 0 || height <= 0) + hyperionnet::RawImageT rawImageNative; + img->UnPackTo(&rawImageNative); + + const int width = rawImageNative.width; + const int height = rawImageNative.height; + + if (width <= 0 || height <= 0 || rawImageNative.data.empty()) { - sendErrorReply("Size of image data does not match with the width and height"); + sendErrorReply("Invalid width and/or height or no raw image data provided"); return; } // check consistency of the size of the received data - int channelCount = (int)imageData->size()/(width*height); - if (channelCount != 3 && channelCount != 4) + int bytesPerPixel = rawImageNative.data.size() / (width * height); + if (bytesPerPixel != 3 && bytesPerPixel != 4) { sendErrorReply("Size of image data does not match with the width and height"); return; } - // create ImageRgb - Image imageRGB(width, height); - if (channelCount == 3) - { - memmove(imageRGB.memptr(), imageData->data(), imageData->size()); - } - - if (channelCount == 4) - { - for (int source=0, destination=0; source < width * height * static_cast(sizeof(ColorRgb)); source+=sizeof(ColorRgb), destination+=sizeof(ColorRgba)) - { - memmove((uint8_t*)imageRGB.memptr() + source, imageData->data() + destination, sizeof(ColorRgb)); - } - } - - emit setGlobalInputImage(_priority, imageRGB, duration); - emit setBufferImage("FlatBuffer", imageRGB); + imageRGB.resize(width, height); + processRawImage(rawImageNative, bytesPerPixel, _imageResampler, imageRGB); } + else if ((reqPtr = image->data_as_NV12Image()) != nullptr) + { + const auto* img = static_cast(reqPtr); + + hyperionnet::NV12ImageT nv12ImageNative; + img->UnPackTo(&nv12ImageNative); + + const int width = nv12ImageNative.width; + const int height = nv12ImageNative.height; + + if (width <= 0 || height <= 0 || nv12ImageNative.data_y.empty() || nv12ImageNative.data_uv.empty()) + { + sendErrorReply("Invalid width and/or height or no complete NV12 image data provided"); + return; + } + + imageRGB.resize(width, height); + processNV12Image(nv12ImageNative, _imageResampler, imageRGB); + + } + else + { + sendErrorReply("No or unknown image data provided"); + return; + } + + emit setGlobalInputImage(_priority, imageRGB, duration); + emit setBufferImage("FlatBuffer", imageRGB); // send reply sendSuccessReply(); } - void FlatBufferClient::handleClearCommand(const hyperionnet::Clear *clear) { // extract parameters @@ -242,3 +266,50 @@ void FlatBufferClient::sendErrorReply(const std::string &error) _builder.Clear(); } + +inline void FlatBufferClient::processRawImage(const hyperionnet::RawImageT& raw_image, int bytesPerPixel, ImageResampler& resampler, Image& outputImage) { + + int width = raw_image.width; + int height = raw_image.height; + + int lineLength = width * bytesPerPixel; + PixelFormat pixelFormat = (bytesPerPixel == 4) ? PixelFormat::RGB32 : PixelFormat::RGB24; + + // Process the image + resampler.processImage( + raw_image.data.data(), // Raw RGB/RGBA buffer + width, // Image width + height, // Image height + lineLength, // Line length + pixelFormat, // Pixel format (RGB24/RGB32) + outputImage // Output image + ); +} + +inline void FlatBufferClient::processNV12Image(const hyperionnet::NV12ImageT& nv12_image, ImageResampler& resampler, Image& outputImage) { + // Combine data_y and data_uv into a single buffer + int width = nv12_image.width; + int height = nv12_image.height; + + size_t y_size = nv12_image.data_y.size(); + size_t uv_size = nv12_image.data_uv.size(); + std::vector combined_buffer(y_size + uv_size); + + std::memcpy(combined_buffer.data(), nv12_image.data_y.data(), y_size); + std::memcpy(combined_buffer.data() + y_size, nv12_image.data_uv.data(), uv_size); + + // Determine line length (stride_y) + int lineLength = nv12_image.stride_y > 0 ? nv12_image.stride_y : width; + + PixelFormat pixelFormat = PixelFormat::NV12; + + // Process the image + resampler.processImage( + combined_buffer.data(), // Combined NV12 buffer + width, // Image width + height, // Image height + lineLength, // Line length for Y plane + pixelFormat, // Pixel format (NV12) + outputImage // Output image + ); +} \ No newline at end of file diff --git a/libsrc/flatbufserver/FlatBufferClient.h b/libsrc/flatbufserver/FlatBufferClient.h index 5776df54..24eca77b 100644 --- a/libsrc/flatbufserver/FlatBufferClient.h +++ b/libsrc/flatbufserver/FlatBufferClient.h @@ -6,6 +6,7 @@ #include #include #include +#include "utils/ImageResampler.h" // flatbuffer FBS #include "hyperion_reply_generated.h" @@ -33,6 +34,8 @@ public: /// explicit FlatBufferClient(QTcpSocket* socket, int timeout, QObject *parent = nullptr); + void setPixelDecimation(int decimator); + signals: /// /// @brief forward register data to HyperionDaemon @@ -138,6 +141,9 @@ private: /// void sendErrorReply(const std::string & error); + void processRawImage(const hyperionnet::RawImageT& raw_image, int bytesPerPixel, ImageResampler& resampler, Image& outputImage); + void processNV12Image(const hyperionnet::NV12ImageT& nv12_image, ImageResampler& resampler, Image& outputImage); + private: Logger *_log; QTcpSocket *_socket; @@ -148,6 +154,8 @@ private: QByteArray _receiveBuffer; + ImageResampler _imageResampler; + // Flatbuffers builder flatbuffers::FlatBufferBuilder _builder; }; diff --git a/libsrc/flatbufserver/FlatBufferServer.cpp b/libsrc/flatbufserver/FlatBufferServer.cpp index 33e015a6..cbddf78e 100644 --- a/libsrc/flatbufserver/FlatBufferServer.cpp +++ b/libsrc/flatbufserver/FlatBufferServer.cpp @@ -62,6 +62,12 @@ void FlatBufferServer::handleSettingsUpdate(settings::type type, const QJsonDocu _timeout = obj["timeout"].toInt(5000); // enable check obj["enable"].toBool(true) ? startServer() : stopServer(); + + _pixelDecimation = obj["pixelDecimation"].toInt(1); + for (const auto& client : _openConnections) + { + client->setPixelDecimation(_pixelDecimation); + } } } @@ -75,6 +81,9 @@ void FlatBufferServer::newConnection() { Debug(_log, "New connection from %s", QSTRING_CSTR(socket->peerAddress().toString())); FlatBufferClient *client = new FlatBufferClient(socket, _timeout, this); + + client->setPixelDecimation(_pixelDecimation); + // internal connect(client, &FlatBufferClient::clientDisconnected, this, &FlatBufferServer::clientDisconnected); connect(client, &FlatBufferClient::registerGlobalInput, GlobalSignals::getInstance(), &GlobalSignals::registerGlobalInput); diff --git a/libsrc/flatbufserver/hyperion_request.fbs b/libsrc/flatbufserver/hyperion_request.fbs index 5a076b65..06bd54a2 100644 --- a/libsrc/flatbufserver/hyperion_request.fbs +++ b/libsrc/flatbufserver/hyperion_request.fbs @@ -12,9 +12,17 @@ table RawImage { height:int = -1; } -union ImageType {RawImage} +table NV12Image { + data_y:[ubyte]; + data_uv:[ubyte]; + width:int; + height:int; + stride_y:int = 0; + stride_uv:int = 0; +} + +union ImageType {RawImage, NV12Image} -// Either RGB or RGBA data can be transferred table Image { data:ImageType (required); duration:int = -1; diff --git a/libsrc/grabber/osx/CMakeLists.txt b/libsrc/grabber/osx/CMakeLists.txt index 7d18e8d3..a5f01acb 100644 --- a/libsrc/grabber/osx/CMakeLists.txt +++ b/libsrc/grabber/osx/CMakeLists.txt @@ -1,10 +1,28 @@ add_library(osx-grabber ${CMAKE_SOURCE_DIR}/include/grabber/osx/OsxFrameGrabber.h ${CMAKE_SOURCE_DIR}/include/grabber/osx/OsxWrapper.h - ${CMAKE_SOURCE_DIR}/libsrc/grabber/osx/OsxFrameGrabber.cpp + ${CMAKE_SOURCE_DIR}/libsrc/grabber/osx/OsxFrameGrabber.mm ${CMAKE_SOURCE_DIR}/libsrc/grabber/osx/OsxWrapper.cpp ) target_link_libraries(osx-grabber hyperion + "$" ) + +file(WRITE ${CMAKE_BINARY_DIR}/tmp/SDK15Available.c + "#include + #if __MAC_OS_X_VERSION_MAX_ALLOWED < 150000 + #error __MAC_OS_X_VERSION_MAX_ALLOWED < 150000 + #endif + int main(int argc, char** argv) + { + return 0; + }" +) + +try_compile(SDK_15_AVAILABLE ${CMAKE_BINARY_DIR} SOURCES ${CMAKE_BINARY_DIR}/tmp/SDK15Available.c) +if(SDK_15_AVAILABLE) + target_compile_definitions(osx-grabber PRIVATE SDK_15_AVAILABLE) + target_link_libraries(osx-grabber "$") +endif() diff --git a/libsrc/grabber/osx/OsxFrameGrabber.cpp b/libsrc/grabber/osx/OsxFrameGrabber.mm similarity index 56% rename from libsrc/grabber/osx/OsxFrameGrabber.cpp rename to libsrc/grabber/osx/OsxFrameGrabber.mm index 916402c2..ef537c16 100644 --- a/libsrc/grabber/osx/OsxFrameGrabber.cpp +++ b/libsrc/grabber/osx/OsxFrameGrabber.mm @@ -1,10 +1,16 @@ // STL includes #include #include +#include -// Local includes +// Header #include +// ScreenCaptureKit +#if defined(SDK_15_AVAILABLE) +#include +#endif + //Qt #include #include @@ -15,9 +21,70 @@ namespace { const bool verbose = false; } //End of constants +#if defined(SDK_15_AVAILABLE) + static CGImageRef capture15(CGDirectDisplayID id, CGRect diIntersectDisplayLocal) + { + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block CGImageRef image1 = nil; + [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent* content, NSError* error) + { + @autoreleasepool + { + if (error || !content) + { + dispatch_semaphore_signal(semaphore); + return; + } + + SCDisplay* target = nil; + for (SCDisplay *display in content.displays) + { + if (display.displayID == id) + { + target = display; + break; + } + } + if (!target) + { + dispatch_semaphore_signal(semaphore); + return; + } + + SCContentFilter* filter = [[SCContentFilter alloc] initWithDisplay:target excludingWindows:@[]]; + SCStreamConfiguration* config = [[SCStreamConfiguration alloc] init]; + config.queueDepth = 5; + config.sourceRect = diIntersectDisplayLocal; + config.scalesToFit = false; + config.captureResolution = SCCaptureResolutionBest; + + CGDisplayModeRef modeRef = CGDisplayCopyDisplayMode(id); + double sysScale = CGDisplayModeGetPixelWidth(modeRef) / CGDisplayModeGetWidth(modeRef); + config.width = diIntersectDisplayLocal.size.width * sysScale; + config.height = diIntersectDisplayLocal.size.height * sysScale; + + [SCScreenshotManager captureImageWithFilter:filter + configuration:config + completionHandler:^(CGImageRef img, NSError* error) + { + if (!error) + { + image1 = CGImageCreateCopyWithColorSpace(img, CGColorSpaceCreateDeviceRGB()); + } + dispatch_semaphore_signal(semaphore); + }]; + } + }]; + + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + dispatch_release(semaphore); + return image1; + } +#endif + OsxFrameGrabber::OsxFrameGrabber(int display) : Grabber("GRABBER-OSX") - , _screenIndex(display) + , _screenIndex(display) { _isEnabled = false; _useImageResampler = true; @@ -31,6 +98,15 @@ bool OsxFrameGrabber::setupDisplay() { bool rc (false); +#if defined(SDK_15_AVAILABLE) + if (!CGPreflightScreenCaptureAccess()) + { + if(!CGRequestScreenCaptureAccess()) + Error(_log, "Screen capture permission required to start the grabber"); + return false; + } +#endif + rc = setDisplayIndex(_screenIndex); return rc; @@ -41,39 +117,38 @@ int OsxFrameGrabber::grabFrame(Image & image) int rc = 0; if (_isEnabled && !_isDeviceInError) { - CGImageRef dispImage; - CFDataRef imgData; - unsigned char * pImgData; - unsigned dspWidth; - unsigned dspHeight; - dispImage = CGDisplayCreateImage(_display); + #if defined(SDK_15_AVAILABLE) + dispImage = capture15(_display, CGDisplayBounds(_display)); + #else + dispImage = CGDisplayCreateImageForRect(_display, CGDisplayBounds(_display)); + #endif // display lost, use main if (dispImage == nullptr && _display != 0) { - dispImage = CGDisplayCreateImage(kCGDirectMainDisplay); - // no displays connected, return - if (dispImage == nullptr) - { - Error(_log, "No display connected..."); - return -1; - } + #if defined(SDK_15_AVAILABLE) + dispImage = capture15(kCGDirectMainDisplay, CGDisplayBounds(kCGDirectMainDisplay)); + #else + dispImage = CGDisplayCreateImageForRect(kCGDirectMainDisplay, CGDisplayBounds(kCGDirectMainDisplay)); + #endif } - imgData = CGDataProviderCopyData(CGImageGetDataProvider(dispImage)); - pImgData = (unsigned char*) CFDataGetBytePtr(imgData); - dspWidth = CGImageGetWidth(dispImage); - dspHeight = CGImageGetHeight(dispImage); - _imageResampler.processImage( pImgData, - static_cast(dspWidth), - static_cast(dspHeight), - static_cast(CGImageGetBytesPerRow(dispImage)), - PixelFormat::BGR32, - image); + // no displays connected, return + if (dispImage == nullptr) + { + Error(_log, "No display connected..."); + return -1; + } + + CFDataRef imgData = CGDataProviderCopyData(CGImageGetDataProvider(dispImage)); + if (imgData != nullptr) + { + _imageResampler.processImage((uint8_t *)CFDataGetBytePtr(imgData), static_cast(CGImageGetWidth(dispImage)), static_cast(CGImageGetHeight(dispImage)), static_cast(CGImageGetBytesPerRow(dispImage)), PixelFormat::BGR32, image); + CFRelease(imgData); + } - CFRelease(imgData); CGImageRelease(dispImage); } @@ -108,7 +183,12 @@ bool OsxFrameGrabber::setDisplayIndex(int index) { _display = activeDspys[_screenIndex]; - image = CGDisplayCreateImage(_display); + #if defined(SDK_15_AVAILABLE) + image = capture15(_display, CGDisplayBounds(_display)); + #else + image = CGDisplayCreateImageForRect(_display, CGDisplayBounds(_display)); + #endif + if(image == nullptr) { setEnabled(false); diff --git a/libsrc/hyperion/AuthManager.cpp b/libsrc/hyperion/AuthManager.cpp index 4e60807e..a89ab8d6 100644 --- a/libsrc/hyperion/AuthManager.cpp +++ b/libsrc/hyperion/AuthManager.cpp @@ -7,14 +7,15 @@ // qt #include #include +#include +#include AuthManager *AuthManager::manager = nullptr; -AuthManager::AuthManager(QObject *parent, bool readonlyMode) +AuthManager::AuthManager(QObject *parent) : QObject(parent) - , _authTable(new AuthTable("", this, readonlyMode)) - , _metaTable(new MetaTable(this, readonlyMode)) - , _pendingRequests() + , _authTable(new AuthTable(this)) + , _metaTable(new MetaTable(this)) , _timer(new QTimer(this)) , _authBlockTimer(new QTimer(this)) { @@ -209,7 +210,7 @@ QVector AuthManager::getPendingRequests() const bool AuthManager::renameToken(const QString &id, const QString &comment) { - if (_authTable->idExist(id)) + if (_authTable->identifierExist(id)) { if (_authTable->renameToken(id, comment)) { @@ -222,7 +223,7 @@ bool AuthManager::renameToken(const QString &id, const QString &comment) bool AuthManager::deleteToken(const QString &id) { - if (_authTable->idExist(id)) + if (_authTable->identifierExist(id)) { if (_authTable->deleteToken(id)) { diff --git a/libsrc/hyperion/Hyperion.cpp b/libsrc/hyperion/Hyperion.cpp index de719578..db970314 100644 --- a/libsrc/hyperion/Hyperion.cpp +++ b/libsrc/hyperion/Hyperion.cpp @@ -6,6 +6,7 @@ #include #include #include +#include // hyperion include #include @@ -21,6 +22,7 @@ #include #include #include +#include // LedDevice includes #include @@ -47,10 +49,10 @@ #include #endif -Hyperion::Hyperion(quint8 instance, bool readonlyMode) +Hyperion::Hyperion(quint8 instance) : QObject() , _instIndex(instance) - , _settingsManager(new SettingsManager(instance, this, readonlyMode)) + , _settingsManager(new SettingsManager(instance, this)) , _componentRegister(nullptr) , _ledString(LedString::createLedString(getSetting(settings::LEDS).array(), hyperion::createColorOrder(getSetting(settings::DEVICE).object()))) , _imageProcessor(nullptr) @@ -73,7 +75,6 @@ Hyperion::Hyperion(quint8 instance, bool readonlyMode) #if defined(ENABLE_BOBLIGHT_SERVER) , _boblightServer(nullptr) #endif - , _readOnlyMode(readonlyMode) { qRegisterMetaType("ComponentList"); @@ -320,14 +321,17 @@ QJsonDocument Hyperion::getSetting(settings::type type) const return _settingsManager->getSetting(type); } -bool Hyperion::saveSettings(const QJsonObject& config, bool correct) +// TODO: Remove function, if UI is able to handle full configuration +QJsonObject Hyperion::getQJsonConfig() const { - return _settingsManager->saveSettings(config, correct); + const QJsonObject instanceConfig = _settingsManager->getSettings(); + const QJsonObject globalConfig = _settingsManager->getSettings({},QStringList()); + return JsonUtils::mergeJsonObjects(instanceConfig, globalConfig); } -bool Hyperion::restoreSettings(const QJsonObject& config, bool correct) +QPair Hyperion::saveSettings(const QJsonObject& config) { - return _settingsManager->restoreSettings(config, correct); + return _settingsManager->saveSettings(config); } int Hyperion::getLatchTime() const @@ -597,11 +601,6 @@ int Hyperion::setEffect(const QString &effectName, const QJsonObject &args, int } #endif -QJsonObject Hyperion::getQJsonConfig() const -{ - return _settingsManager->getSettings(); -} - void Hyperion::setLedMappingType(int mappingType) { if(mappingType != _imageProcessor->getUserLedMappingType()) diff --git a/libsrc/hyperion/HyperionIManager.cpp b/libsrc/hyperion/HyperionIManager.cpp index 268cbf75..254d1bf0 100644 --- a/libsrc/hyperion/HyperionIManager.cpp +++ b/libsrc/hyperion/HyperionIManager.cpp @@ -9,15 +9,14 @@ HyperionIManager* HyperionIManager::HIMinstance; -HyperionIManager::HyperionIManager(const QString& rootPath, QObject* parent, bool readonlyMode) +HyperionIManager::HyperionIManager(QObject* parent) : QObject(parent) , _log(Logger::getInstance("HYPERION-INSTMGR")) - , _instanceTable( new InstanceTable(rootPath, this, readonlyMode) ) - , _rootPath( rootPath ) - , _readonlyMode(readonlyMode) + , _instanceTable( new InstanceTable()) { HIMinstance = this; qRegisterMetaType("InstanceState"); + _instanceTable->createDefaultInstance(); } Hyperion* HyperionIManager::getHyperionInstance(quint8 instance) @@ -45,14 +44,32 @@ QVector HyperionIManager::getInstanceData() const return instances; } +QString HyperionIManager::getInstanceName(quint8 inst) +{ + return _instanceTable->getNamebyIndex(inst); +} + QList HyperionIManager::getRunningInstanceIdx() const { return _runningInstances.keys(); } +QList HyperionIManager::getInstanceIds() const +{ + return _instanceTable->getAllInstanceIDs(); +} + + void HyperionIManager::startAll() { - for(const auto & entry : _instanceTable->getAllInstances(true)) + const QVector instances = _instanceTable->getAllInstances(true); + if (instances.isEmpty()) + { + Error(_log, "No enabled instances found to be started"); + return; + } + + for(const auto & entry : instances) { startInstance(entry["instance"].toInt()); } @@ -62,7 +79,7 @@ void HyperionIManager::stopAll() { // copy the instances due to loop corruption, even with .erase() return next iter QMap instCopy = _runningInstances; - for(const auto instance : instCopy) + for(auto *const instance : instCopy) { instance->stop(); } @@ -131,7 +148,7 @@ bool HyperionIManager::startInstance(quint8 inst, bool block, QObject* caller, i { QThread* hyperionThread = new QThread(); hyperionThread->setObjectName("HyperionThread"); - Hyperion* hyperion = new Hyperion(inst, _readonlyMode); + Hyperion* hyperion = new Hyperion(inst); hyperion->moveToThread(hyperionThread); // setup thread management connect(hyperionThread, &QThread::started, hyperion, &Hyperion::start); @@ -156,7 +173,7 @@ bool HyperionIManager::startInstance(quint8 inst, bool block, QObject* caller, i if(block) { - while(!hyperionThread->isRunning()){}; + while(!hyperionThread->isRunning()){} } if (!_pendingRequests.contains(inst) && caller != nullptr) @@ -203,10 +220,10 @@ bool HyperionIManager::stopInstance(quint8 inst) bool HyperionIManager::createInstance(const QString& name, bool start) { - quint8 inst; + quint8 inst = 0; if(_instanceTable->createInstance(name, inst)) { - Info(_log,"New Hyperion instance created with name '%s'",QSTRING_CSTR(name)); + Info(_log,"New Hyperion instance [%d] created with name '%s'", inst, QSTRING_CSTR(name)); emit instanceStateChanged(InstanceState::H_CREATED, inst, name); emit change(); @@ -221,7 +238,9 @@ bool HyperionIManager::deleteInstance(quint8 inst) { // inst 0 can't be deleted if(!isInstAllowed(inst)) + { return false; + } // stop it if required as blocking and wait stopInstance(inst); diff --git a/libsrc/hyperion/SettingsManager.cpp b/libsrc/hyperion/SettingsManager.cpp index cd5e8727..eb37308f 100644 --- a/libsrc/hyperion/SettingsManager.cpp +++ b/libsrc/hyperion/SettingsManager.cpp @@ -1,939 +1,59 @@ // proj #include -// util -#include -#include - #include -#include "HyperionConfig.h" - -// json schema process -#include -#include - -// write config to filesystem #include +#include -#include +#include using namespace semver; -// Constants -namespace { - const char DEFAULT_VERSION[] = "2.0.0-alpha.8"; -} //End of constants - -QJsonObject SettingsManager::schemaJson; - -SettingsManager::SettingsManager(quint8 instance, QObject* parent, bool readonlyMode) +SettingsManager::SettingsManager(quint8 instance, QObject* parent) : QObject(parent) , _log(Logger::getInstance("SETTINGSMGR", "I" + QString::number(instance))) , _instance(instance) , _sTable(new SettingsTable(instance, this)) - , _configVersion(DEFAULT_VERSION) - , _previousVersion(DEFAULT_VERSION) - , _readonlyMode(readonlyMode) { - _sTable->setReadonlyMode(_readonlyMode); - // get schema - if (schemaJson.isEmpty()) - { - Q_INIT_RESOURCE(resource); - try - { - schemaJson = QJsonFactory::readSchema(":/hyperion-schema"); - } - catch (const std::runtime_error& error) - { - throw std::runtime_error(error.what()); - } - } - - // get default config - QJsonObject defaultConfig; - if (!JsonUtils::readFile(":/hyperion_default.config", defaultConfig, _log).first) - { - throw std::runtime_error("Failed to read default config"); - } - - // transform json to string lists - const QStringList keyList = defaultConfig.keys(); - QStringList defValueList; - for (const auto& key : keyList) - { - if (defaultConfig[key].isObject()) - { - defValueList << QString(QJsonDocument(defaultConfig[key].toObject()).toJson(QJsonDocument::Compact)); - } - else if (defaultConfig[key].isArray()) - { - defValueList << QString(QJsonDocument(defaultConfig[key].toArray()).toJson(QJsonDocument::Compact)); - } - } - - // fill database with default data if required - for (const auto& key : keyList) - { - QString val = defValueList.takeFirst(); - // prevent overwrite - if (!_sTable->recordExist(key)) - { - _sTable->createSettingsRecord(key, val); - } - } - - // need to validate all data in database construct the entire data object - // TODO refactor schemaChecker to accept QJsonArray in validate(); QJsonDocument container? To validate them per entry... - QJsonObject dbConfig; - for (const auto& key : keyList) - { - QJsonDocument doc = _sTable->getSettingsRecord(key); - if (doc.isArray()) - { - dbConfig[key] = doc.array(); - } - else - { - dbConfig[key] = doc.object(); - } - } - - //Check, if database requires migration - bool isNewRelease = false; - // Use instance independent SettingsManager to track migration status - if (_instance == GLOABL_INSTANCE_ID) - { - if (resolveConfigVersion(dbConfig)) - { - QJsonObject newGeneralConfig = dbConfig["general"].toObject(); - - semver::version BUILD_VERSION(HYPERION_VERSION); - - if (!BUILD_VERSION.isValid()) - { - Error(_log, "Current Hyperion version [%s] is invalid. Exiting...", BUILD_VERSION.getVersion().c_str()); - exit(1); - } - - if (_configVersion > BUILD_VERSION) - { - Error(_log, "Database version [%s] is greater than current Hyperion version [%s]", _configVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str()); - // TODO: Remove version checking and Settingsmanager from components' constructor to be able to stop hyperion. - } - else - { - if (_previousVersion < BUILD_VERSION) - { - if (_configVersion == BUILD_VERSION) - { - newGeneralConfig["previousVersion"] = BUILD_VERSION.getVersion().c_str(); - dbConfig["general"] = newGeneralConfig; - isNewRelease = true; - Info(_log, "Migration completed to version [%s]", BUILD_VERSION.getVersion().c_str()); - } - else - { - Info(_log, "Migration from current version [%s] to new version [%s] started", _previousVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str()); - - newGeneralConfig["previousVersion"] = _configVersion.getVersion().c_str(); - newGeneralConfig["configVersion"] = BUILD_VERSION.getVersion().c_str(); - dbConfig["general"] = newGeneralConfig; - isNewRelease = true; - } - } - } - } - } - - // possible data upgrade steps to prevent data loss - bool migrated = handleConfigUpgrade(dbConfig); - if (isNewRelease || migrated) - { - saveSettings(dbConfig, true); - } - - // validate full dbconfig against schema, on error we need to rewrite entire table - QJsonSchemaChecker schemaChecker; - schemaChecker.setSchema(schemaJson); - QPair valid = schemaChecker.validate(dbConfig); - // check if our main schema syntax is IO - if (!valid.second) - { - for (auto& schemaError : schemaChecker.getMessages()) - { - Error(_log, "Schema Syntax Error: %s", QSTRING_CSTR(schemaError)); - } - throw std::runtime_error("The config schema has invalid syntax. This should never happen! Go fix it!"); - } - if (!valid.first) - { - Info(_log, "Table upgrade required..."); - dbConfig = schemaChecker.getAutoCorrectedConfig(dbConfig); - - for (auto& schemaError : schemaChecker.getMessages()) - { - Warning(_log, "Config Fix: %s", QSTRING_CSTR(schemaError)); - } - - saveSettings(dbConfig, true); - } - else - { - _qconfig = dbConfig; - } - - Debug(_log, "Settings database initialized"); + _sTable->addMissingDefaults(); } QJsonDocument SettingsManager::getSetting(settings::type type) const { - return _sTable->getSettingsRecord(settings::typeToString(type)); + return getSetting(settings::typeToString(type)); } -QJsonObject SettingsManager::getSettings() const +QJsonDocument SettingsManager::getSetting(const QString& type) const { - QJsonObject config; - for (const auto& key : _qconfig.keys()) - { - //Read all records from database to ensure that global settings are read across instances - QJsonDocument doc = _sTable->getSettingsRecord(key); - if (doc.isArray()) - { - config.insert(key, doc.array()); - } - else - { - config.insert(key, doc.object()); - } - } - return config; + return _sTable->getSettingsRecord(type); } -bool SettingsManager::restoreSettings(QJsonObject config, bool correct) +QJsonObject SettingsManager::getSettings(const QStringList& filteredTypes ) const { - // optional data upgrades e.g. imported legacy/older configs - handleConfigUpgrade(config); - return saveSettings(config, correct); + return _sTable->getSettings(filteredTypes); } -bool SettingsManager::saveSettings(QJsonObject config, bool correct) + QJsonObject SettingsManager::getSettings(const QVariant& instance, const QStringList& filteredTypes ) const { - // we need to validate data against schema - QJsonSchemaChecker schemaChecker; - schemaChecker.setSchema(schemaJson); - if (!schemaChecker.validate(config).first) + return _sTable->getSettings(instance, filteredTypes); +} + +QPair SettingsManager::saveSettings(const QJsonObject& config) +{ + QStringList errorList; + for (auto &key : config.keys()) { - if (!correct) - { - Error(_log, "Failed to save configuration, errors during validation"); - return false; - } - Warning(_log, "Fixing json data!"); - config = schemaChecker.getAutoCorrectedConfig(config); - - for (const auto& schemaError : schemaChecker.getMessages()) - { - Warning(_log, "Config Fix: %s", QSTRING_CSTR(schemaError)); - } - } - - // store the new config - _qconfig = config; - - // extract keys and data - const QStringList keyList = config.keys(); - QStringList newValueList; - for (const auto& key : keyList) - { - if (config[key].isObject()) - { - newValueList << QString(QJsonDocument(config[key].toObject()).toJson(QJsonDocument::Compact)); - } - else if (config[key].isArray()) - { - newValueList << QString(QJsonDocument(config[key].toArray()).toJson(QJsonDocument::Compact)); - } - } - - bool rc = true; - // compare database data with new data to emit/save changes accordingly - for (const auto& key : keyList) - { - QString data = newValueList.takeFirst(); + const QJsonValue configItem = config.value(key); + const QString data = JsonUtils::jsonValueToQString(configItem); if (_sTable->getSettingsRecordString(key) != data) { if (!_sTable->createSettingsRecord(key, data)) { - rc = false; - } - else - { - QJsonParseError error; - QJsonDocument jsonDocument = QJsonDocument::fromJson(data.toUtf8(), &error); - if (error.error != QJsonParseError::NoError) { - Error(_log, "Error parsing JSON: %s", QSTRING_CSTR(error.errorString())); - rc = false; - } - else { - emit settingsChanged(settings::stringToType(key), jsonDocument); - } + errorList.append(QString("Failed to save configuration item: %1").arg(key)); + return qMakePair (false, errorList); } + emit settingsChanged(settings::stringToType(key), QJsonDocument::fromVariant(configItem.toVariant())); } } - return rc; -} - -inline QString fixVersion(const QString& version) -{ - QString newVersion; - //Try fixing version number, remove dot separated pre-release identifiers not supported - QRegularExpression regEx("(\\d+\\.\\d+\\.\\d+-?[a-zA-Z-\\d]*\\.?[\\d]*)", QRegularExpression::CaseInsensitiveOption | QRegularExpression::MultilineOption); - QRegularExpressionMatch match; - - match = regEx.match(version); - if (match.hasMatch()) - { - newVersion = match.captured(1); - } - return newVersion; -} - -bool SettingsManager::resolveConfigVersion(QJsonObject& config) -{ - bool isValid = false; - if (config.contains("general")) - { - QJsonObject generalConfig = config["general"].toObject(); - QString configVersion = generalConfig["configVersion"].toString(); - QString previousVersion = generalConfig["previousVersion"].toString(); - - if (!configVersion.isEmpty()) - { - isValid = _configVersion.setVersion(configVersion.toStdString()); - if (!isValid) - { - isValid = _configVersion.setVersion(fixVersion(configVersion).toStdString()); - if (isValid) - { - Info(_log, "Invalid config version [%s] fixed. Updated to [%s]", QSTRING_CSTR(configVersion), _configVersion.getVersion().c_str()); - } - } - } - else - { - isValid = true; - } - - if (!previousVersion.isEmpty() && isValid) - { - isValid = _previousVersion.setVersion(previousVersion.toStdString()); - if (!isValid) - { - isValid = _previousVersion.setVersion(fixVersion(previousVersion).toStdString()); - if (isValid) - { - Info(_log, "Invalid previous version [%s] fixed. Updated to [%s]", QSTRING_CSTR(previousVersion), _previousVersion.getVersion().c_str()); - } - } - } - else - { - _previousVersion.setVersion(DEFAULT_VERSION); - isValid = true; - } - } - return isValid; -} - -bool SettingsManager::handleConfigUpgrade(QJsonObject& config) -{ - bool migrated = false; - - //Only migrate, if valid versions are available - if (!resolveConfigVersion(config)) - { - Warning(_log, "Invalid version information found in configuration. No database migration executed."); - } - else - { - //Do only migrate, if configuration is not up to date - if (_previousVersion < _configVersion) - { - //Migration steps for versions <= alpha 9 - semver::version targetVersion{ "2.0.0-alpha.9" }; - if (_previousVersion <= targetVersion) - { - Info(_log, "Instance [%u]: Migrate from version [%s] to version [%s] or later", _instance, _previousVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); - - // LED LAYOUT UPGRADE - // from { hscan: { minimum: 0.2, maximum: 0.3 }, vscan: { minimum: 0.2, maximum: 0.3 } } - // from { h: { min: 0.2, max: 0.3 }, v: { min: 0.2, max: 0.3 } } - // to { hmin: 0.2, hmax: 0.3, vmin: 0.2, vmax: 0.3} - if (config.contains("leds")) - { - const QJsonArray ledarr = config["leds"].toArray(); - const QJsonObject led = ledarr[0].toObject(); - - if (led.contains("hscan") || led.contains("h")) - { - const bool whscan = led.contains("hscan"); - QJsonArray newLedarr; - - for (const auto& entry : ledarr) - { - const QJsonObject led = entry.toObject(); - QJsonObject hscan; - QJsonObject vscan; - QJsonValue hmin; - QJsonValue hmax; - QJsonValue vmin; - QJsonValue vmax; - QJsonObject nL; - - if (whscan) - { - hscan = led["hscan"].toObject(); - vscan = led["vscan"].toObject(); - hmin = hscan["minimum"]; - hmax = hscan["maximum"]; - vmin = vscan["minimum"]; - vmax = vscan["maximum"]; - } - else - { - hscan = led["h"].toObject(); - vscan = led["v"].toObject(); - hmin = hscan["min"]; - hmax = hscan["max"]; - vmin = vscan["min"]; - vmax = vscan["max"]; - } - // append to led object - nL["hmin"] = hmin; - nL["hmax"] = hmax; - nL["vmin"] = vmin; - nL["vmax"] = vmax; - newLedarr.append(nL); - } - // replace - config["leds"] = newLedarr; - migrated = true; - Info(_log, "Instance [%u]: LED Layout migrated", _instance); - } - } - - if (config.contains("ledConfig")) - { - QJsonObject oldLedConfig = config["ledConfig"].toObject(); - if (!oldLedConfig.contains("classic")) - { - QJsonObject newLedConfig; - newLedConfig.insert("classic", oldLedConfig); - QJsonObject defaultMatrixConfig{ {"ledshoriz", 1} - ,{"ledsvert", 1} - ,{"cabling","snake"} - ,{"start","top-left"} - }; - newLedConfig.insert("matrix", defaultMatrixConfig); - - config["ledConfig"] = newLedConfig; - migrated = true; - Info(_log, "Instance [%u]: LED-Config migrated", _instance); - } - } - - // LED Hardware count is leading for versions after alpha 9 - // Setting Hardware LED count to number of LEDs configured via layout, if layout number is greater than number of hardware LEDs - if (config.contains("device")) - { - QJsonObject newDeviceConfig = config["device"].toObject(); - - if (newDeviceConfig.contains("hardwareLedCount")) - { - int hwLedcount = newDeviceConfig["hardwareLedCount"].toInt(); - if (config.contains("leds")) - { - const QJsonArray ledarr = config["leds"].toArray(); - int layoutLedCount = ledarr.size(); - - if (hwLedcount < layoutLedCount) - { - Warning(_log, "Instance [%u]: HwLedCount/Layout mismatch! Setting Hardware LED count to number of LEDs configured via layout", _instance); - hwLedcount = layoutLedCount; - newDeviceConfig["hardwareLedCount"] = hwLedcount; - migrated = true; - } - } - } - - if (newDeviceConfig.contains("type")) - { - QString type = newDeviceConfig["type"].toString(); - if (type == "atmoorb" || type == "fadecandy" || type == "philipshue") - { - if (newDeviceConfig.contains("output")) - { - newDeviceConfig["host"] = newDeviceConfig["output"].toString(); - newDeviceConfig.remove("output"); - migrated = true; - } - } - } - - if (migrated) - { - config["device"] = newDeviceConfig; - Debug(_log, "LED-Device records migrated"); - } - } - - if (config.contains("grabberV4L2")) - { - QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject(); - - if (newGrabberV4L2Config.contains("encoding_format")) - { - newGrabberV4L2Config.remove("encoding_format"); - newGrabberV4L2Config["grabberV4L2"] = newGrabberV4L2Config; - migrated = true; - } - - //Add new element enable - if (!newGrabberV4L2Config.contains("enable")) - { - newGrabberV4L2Config["enable"] = false; - migrated = true; - } - config["grabberV4L2"] = newGrabberV4L2Config; - Debug(_log, "GrabberV4L2 records migrated"); - } - - if (config.contains("grabberAudio")) - { - QJsonObject newGrabberAudioConfig = config["grabberAudio"].toObject(); - - //Add new element enable - if (!newGrabberAudioConfig.contains("enable")) - { - newGrabberAudioConfig["enable"] = false; - migrated = true; - } - config["grabberAudio"] = newGrabberAudioConfig; - Debug(_log, "GrabberAudio records migrated"); - } - - if (config.contains("framegrabber")) - { - QJsonObject newFramegrabberConfig = config["framegrabber"].toObject(); - - //Align element namings with grabberV4L2 - //Rename element type -> device - if (newFramegrabberConfig.contains("type")) - { - newFramegrabberConfig["device"] = newFramegrabberConfig["type"].toString(); - newFramegrabberConfig.remove("type"); - migrated = true; - } - //Rename element frequency_Hz -> fps - if (newFramegrabberConfig.contains("frequency_Hz")) - { - newFramegrabberConfig["fps"] = newFramegrabberConfig["frequency_Hz"].toInt(25); - newFramegrabberConfig.remove("frequency_Hz"); - migrated = true; - } - - //Rename element display -> input - if (newFramegrabberConfig.contains("display")) - { - newFramegrabberConfig["input"] = newFramegrabberConfig["display"]; - newFramegrabberConfig.remove("display"); - migrated = true; - } - - //Add new element enable - if (!newFramegrabberConfig.contains("enable")) - { - newFramegrabberConfig["enable"] = false; - migrated = true; - } - - config["framegrabber"] = newFramegrabberConfig; - Debug(_log, "Framegrabber records migrated"); - } - } - - //Migration steps for versions <= 2.0.12 - _previousVersion = targetVersion; - targetVersion.setVersion("2.0.12"); - if (_previousVersion <= targetVersion) - { - Info(_log, "Instance [%u]: Migrate from version [%s] to version [%s] or later", _instance, _previousVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); - - // Have Hostname/IP-address separate from port for LED-Devices - if (config.contains("device")) - { - QJsonObject newDeviceConfig = config["device"].toObject(); - - if (newDeviceConfig.contains("host")) - { - QString oldHost = newDeviceConfig["host"].toString(); - - // Resolve hostname and port - QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts); - - newDeviceConfig["host"] = addressparts[0]; - - if (addressparts.size() > 1) - { - if (!newDeviceConfig.contains("port")) - { - newDeviceConfig["port"] = addressparts[1].toInt(); - } - migrated = true; - } - } - - if (newDeviceConfig.contains("type")) - { - QString type = newDeviceConfig["type"].toString(); - if (type == "apa102") - { - if (newDeviceConfig.contains("colorOrder")) - { - QString colorOrder = newDeviceConfig["colorOrder"].toString(); - if (colorOrder == "bgr") - { - newDeviceConfig["colorOrder"] = "rgb"; - migrated = true; - } - } - } - } - - if (migrated) - { - config["device"] = newDeviceConfig; - Debug(_log, "LED-Device records migrated"); - } - } - - // Have Hostname/IP-address separate from port for Forwarder - if (config.contains("forwarder")) - { - QJsonObject newForwarderConfig = config["forwarder"].toObject(); - - QJsonArray json; - if (newForwarderConfig.contains("json")) - { - const QJsonArray oldJson = newForwarderConfig["json"].toArray(); - QJsonObject newJsonConfig; - - for (const QJsonValue& value : oldJson) - { - if (value.isString()) - { - QString oldHost = value.toString(); - // Resolve hostname and port - QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts); - QString host = addressparts[0]; - - if (host != "127.0.0.1") - { - newJsonConfig["host"] = host; - - if (addressparts.size() > 1) - { - newJsonConfig["port"] = addressparts[1].toInt(); - } - else - { - newJsonConfig["port"] = 19444; - } - newJsonConfig["name"] = host; - - json.append(newJsonConfig); - migrated = true; - } - } - } - - if (!json.isEmpty()) - { - newForwarderConfig["jsonapi"] = json; - } - newForwarderConfig.remove("json"); - migrated = true; - } - - QJsonArray flatbuffer; - if (newForwarderConfig.contains("flat")) - { - const QJsonArray oldFlatbuffer = newForwarderConfig["flat"].toArray(); - QJsonObject newFlattbufferConfig; - - for (const QJsonValue& value : oldFlatbuffer) - { - if (value.isString()) - { - QString oldHost = value.toString(); - // Resolve hostname and port - QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts); - QString host = addressparts[0]; - - if (host != "127.0.0.1") - { - newFlattbufferConfig["host"] = host; - - if (addressparts.size() > 1) - { - newFlattbufferConfig["port"] = addressparts[1].toInt(); - } - else - { - newFlattbufferConfig["port"] = 19400; - } - newFlattbufferConfig["name"] = host; - - flatbuffer.append(newFlattbufferConfig); - migrated = true; - } - } - - if (!flatbuffer.isEmpty()) - { - newForwarderConfig["flatbuffer"] = flatbuffer; - } - newForwarderConfig.remove("flat"); - migrated = true; - } - } - - if (json.isEmpty() && flatbuffer.isEmpty()) - { - newForwarderConfig["enable"] = false; - } - - if (migrated) - { - config["forwarder"] = newForwarderConfig; - Debug(_log, "Forwarder records migrated"); - } - } - } - - //Migration steps for versions <= 2.0.13 - _previousVersion = targetVersion; - targetVersion.setVersion("2.0.13"); - if (_previousVersion <= targetVersion) - { - Info(_log, "Instance [%u]: Migrate from version [%s] to version [%s] or later", _instance, _previousVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); - - - // Have Hostname/IP-address separate from port for LED-Devices - if (config.contains("device")) - { - QJsonObject newDeviceConfig = config["device"].toObject(); - - if (newDeviceConfig.contains("type")) - { - QString type = newDeviceConfig["type"].toString(); - - const QStringList serialDevices{ "adalight", "dmx", "atmo", "sedu", "tpm2", "karate" }; - if (serialDevices.contains(type)) - { - if (!newDeviceConfig.contains("rateList")) - { - newDeviceConfig["rateList"] = "CUSTOM"; - migrated = true; - } - } - - if (type == "adalight") - { - if (newDeviceConfig.contains("lightberry_apa102_mode")) - { - bool lightberry_apa102_mode = newDeviceConfig["lightberry_apa102_mode"].toBool(); - if (lightberry_apa102_mode) - { - newDeviceConfig["streamProtocol"] = "1"; - } - else - { - newDeviceConfig["streamProtocol"] = "0"; - } - newDeviceConfig.remove("lightberry_apa102_mode"); - migrated = true; - } - } - } - - if (migrated) - { - config["device"] = newDeviceConfig; - Debug(_log, "LED-Device records migrated"); - } - } - } - - //Migration steps for versions <= 2.0.16 - _previousVersion = targetVersion; - targetVersion.setVersion("2.0.16"); - if (_previousVersion <= targetVersion) - { - Info(_log, "Instance [%u]: Migrate from version [%s] to version [%s] or later", _instance, _previousVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); - - // Have Hostname/IP-address separate from port for LED-Devices - if (config.contains("device")) - { - QJsonObject newDeviceConfig = config["device"].toObject(); - - if (newDeviceConfig.contains("type")) - { - QString type = newDeviceConfig["type"].toString(); - - if (type == "philipshue") - { - if (newDeviceConfig.contains("groupId")) - { - if (newDeviceConfig["groupId"].isDouble()) - { - int groupID = newDeviceConfig["groupId"].toInt(); - newDeviceConfig["groupId"] = QString::number(groupID); - migrated = true; - } - } - - if (newDeviceConfig.contains("lightIds")) - { - QJsonArray lightIds = newDeviceConfig.value("lightIds").toArray(); - // Iterate through the JSON array and update integer values to strings - for (int i = 0; i < lightIds.size(); ++i) { - QJsonValue value = lightIds.at(i); - if (value.isDouble()) - { - int lightId = value.toInt(); - lightIds.replace(i, QString::number(lightId)); - migrated = true; - } - } - newDeviceConfig["lightIds"] = lightIds; - - } - } - - if (type == "nanoleaf") - { - if (newDeviceConfig.contains("panelStartPos")) - { - newDeviceConfig.remove("panelStartPos"); - migrated = true; - } - - if (newDeviceConfig.contains("panelOrderTopDown")) - { - int panelOrderTopDown; - if (newDeviceConfig["panelOrderTopDown"].isDouble()) - { - panelOrderTopDown = newDeviceConfig["panelOrderTopDown"].toInt(); - } - else - { - panelOrderTopDown = newDeviceConfig["panelOrderTopDown"].toString().toInt(); - } - - newDeviceConfig.remove("panelOrderTopDown"); - if (panelOrderTopDown == 0) - { - newDeviceConfig["panelOrderTopDown"] = "top2down"; - migrated = true; - } - else - { - if (panelOrderTopDown == 1) - { - newDeviceConfig["panelOrderTopDown"] = "bottom2up"; - migrated = true; - } - } - } - - if (newDeviceConfig.contains("panelOrderLeftRight")) - { - int panelOrderLeftRight; - if (newDeviceConfig["panelOrderLeftRight"].isDouble()) - { - panelOrderLeftRight = newDeviceConfig["panelOrderLeftRight"].toInt(); - } - else - { - panelOrderLeftRight = newDeviceConfig["panelOrderLeftRight"].toString().toInt(); - } - - newDeviceConfig.remove("panelOrderLeftRight"); - if (panelOrderLeftRight == 0) - { - newDeviceConfig["panelOrderLeftRight"] = "left2right"; - migrated = true; - } - else - { - if (panelOrderLeftRight == 1) - { - newDeviceConfig["panelOrderLeftRight"] = "right2left"; - migrated = true; - } - } - } - } - } - - if (migrated) - { - config["device"] = newDeviceConfig; - Debug(_log, "LED-Device records migrated"); - } - } - - if (config.contains("cecEvents")) - { - bool isCECEnabled {false}; - if (config.contains("grabberV4L2")) - { - QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject(); - if (newGrabberV4L2Config.contains("cecDetection")) - { - isCECEnabled = newGrabberV4L2Config.value("cecDetection").toBool(false); - newGrabberV4L2Config.remove("cecDetection"); - config["grabberV4L2"] = newGrabberV4L2Config; - - QJsonObject newGCecEventsConfig = config["cecEvents"].toObject(); - newGCecEventsConfig["enable"] = isCECEnabled; - if (!newGCecEventsConfig.contains("actions")) - { - QJsonObject action1 - { - {"action", "Suspend"}, - {"event", "standby"} - }; - QJsonObject action2 - { - {"action", "Resume"}, - {"event", "set stream path"} - }; - - QJsonArray actions { action1, action2 }; - newGCecEventsConfig.insert("actions",actions); - } - config["cecEvents"] = newGCecEventsConfig; - - migrated = true; - Debug(_log, "CEC configuration records migrated"); - } - } - } - } - } - } - return migrated; + return qMakePair (true, errorList ); } diff --git a/libsrc/hyperion/hyperion.schema.json b/libsrc/hyperion/hyperion.schema.json deleted file mode 100644 index ae22cfe2..00000000 --- a/libsrc/hyperion/hyperion.schema.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "type" : "object", - "required" : true, - "properties" : - { - "general" : - { - "$ref": "schema-general.json" - }, - "logger" : - { - "$ref": "schema-logger.json" - }, - "device" : - { - "$ref": "schema-device.json" - }, - "color" : - { - "$ref": "schema-color.json" - }, - "smoothing": - { - "$ref": "schema-smoothing.json" - }, - "grabberV4L2" : - { - "$ref": "schema-grabberV4L2.json" - }, - "grabberAudio" : - { - "$ref": "schema-grabberAudio.json" - }, - "framegrabber" : - { - "$ref": "schema-framegrabber.json" - }, - "blackborderdetector" : - { - "$ref": "schema-blackborderdetector.json" - }, - "foregroundEffect" : - { - "$ref": "schema-foregroundEffect.json" - }, - "backgroundEffect" : - { - "$ref": "schema-backgroundEffect.json" - }, - "forwarder" : - { - "$ref": "schema-forwarder.json" - }, - "jsonServer" : - { - "$ref": "schema-jsonServer.json" - }, - "flatbufServer": - { - "$ref": "schema-flatbufServer.json" - }, - "protoServer" : - { - "$ref": "schema-protoServer.json" - }, - "boblightServer" : - { - "$ref": "schema-boblightServer.json" - }, - "webConfig" : - { - "$ref": "schema-webConfig.json" - }, - "effects" : - { - "$ref": "schema-effects.json" - }, - "instCapture": - { - "$ref": "schema-instCapture.json" - }, - "network": - { - "$ref": "schema-network.json" - }, - "ledConfig": - { - "$ref": "schema-ledConfig.json" - }, - "leds": - { - "$ref": "schema-leds.json" - }, - "osEvents": - { - "$ref": "schema-osEvents.json" - }, - "cecEvents": - { - "$ref": "schema-cecEvents.json" - }, - "schedEvents": - { - "$ref": "schema-schedEvents.json" - } - }, - "additionalProperties" : false -} diff --git a/libsrc/hyperion/resource.qrc b/libsrc/hyperion/resource.qrc index b1c52ea1..60eaaccb 100644 --- a/libsrc/hyperion/resource.qrc +++ b/libsrc/hyperion/resource.qrc @@ -1,31 +1,39 @@ - hyperion.schema.json - schema/schema-general.json - schema/schema-logger.json - schema/schema-device.json - schema/schema-color.json - schema/schema-smoothing.json - schema/schema-grabberV4L2.json - schema/schema-grabberAudio.json - schema/schema-framegrabber.json - schema/schema-blackborderdetector.json - schema/schema-foregroundEffect.json schema/schema-backgroundEffect.json - schema/schema-forwarder.json - schema/schema-jsonServer.json - schema/schema-flatbufServer.json - schema/schema-protoServer.json + schema/schema-blackborderdetector.json schema/schema-boblightServer.json - schema/schema-webConfig.json + schema/schema-cecEvents.json + schema/schema-color.json + schema/schema-device.json schema/schema-effects.json + schema/schema-eventActions.json + schema/schema-flatbufServer.json + schema/schema-forwarder.json + schema/schema-framegrabber.json + schema/schema-foregroundEffect.json + schema/schema-general.json + schema/schema-grabberAudio.json + schema/schema-grabberV4L2.json + schema/schema-instCapture.json + schema/schema-jsonServer.json schema/schema-ledConfig.json schema/schema-leds.json - schema/schema-instCapture.json + schema/schema-logger.json schema/schema-network.json - schema/schema-eventActions.json - schema/schema-schedEvents.json schema/schema-osEvents.json - schema/schema-cecEvents.json + schema/schema-protoServer.json + schema/schema-schedEvents.json + schema/schema-smoothing.json + schema/schema-webConfig.json + + schema/schema-settings-default.json + schema/schema-settings-full.json + schema/schema-settings-full-relaxed.json + schema/schema-settings-global.json + schema/schema-settings-global-relaxed.json + schema/schema-settings-instance.json + schema/schema-settings-instance-relaxed.json + schema/schema-settings-ui.json diff --git a/libsrc/hyperion/schema/schema-cecEvents.json b/libsrc/hyperion/schema/schema-cecEvents.json index a5790a36..f0a1a967 100644 --- a/libsrc/hyperion/schema/schema-cecEvents.json +++ b/libsrc/hyperion/schema/schema-cecEvents.json @@ -1,6 +1,5 @@ { "type": "object", - "required": true, "properties": { "enable": { "type": "boolean", diff --git a/libsrc/hyperion/schema/schema-color.json b/libsrc/hyperion/schema/schema-color.json index baa344bf..43ff8eb7 100644 --- a/libsrc/hyperion/schema/schema-color.json +++ b/libsrc/hyperion/schema/schema-color.json @@ -1,7 +1,6 @@ { "type":"object", "title" : "edt_conf_color_heading_title", - "required" : true, "properties": { "imageToLedMappingType" : diff --git a/libsrc/hyperion/schema/schema-device.json b/libsrc/hyperion/schema/schema-device.json index 04d163d9..2f3ac963 100644 --- a/libsrc/hyperion/schema/schema-device.json +++ b/libsrc/hyperion/schema/schema-device.json @@ -74,7 +74,7 @@ "rewriteTime": { "properties": { "type": { - "enum": [ "file", "apa102", "apa104", "ws2801", "lpd6803", "lpd8806", "p9813", "sk6812spi", "sk6822spi", "sk9822", "ws2812spi", "ws281x", "piblaster", "adalight", "dmx", "atmo", "hyperionusbasp", "lightpack", "multilightpack", "paintpack", "rawhid", "sedu", "tpm2", "karate" ] + "enum": [ "file", "apa102", "apa104", "ws2801", "lpd6803", "lpd8806", "p9813", "sk6812spi", "sk6822spi", "sk9822", "ws2812spi", "ws281x", "piblaster", "adalight", "dmx", "atmo", "hyperionusbasp", "lightpack", "multilightpack", "paintpack", "rawhid", "sedu", "tpm2", "karate", "skydimo" ] } }, "additionalProperties": true diff --git a/libsrc/hyperion/schema/schema-flatbufServer.json b/libsrc/hyperion/schema/schema-flatbufServer.json index e86633d9..dc7fe85f 100644 --- a/libsrc/hyperion/schema/schema-flatbufServer.json +++ b/libsrc/hyperion/schema/schema-flatbufServer.json @@ -1,36 +1,41 @@ { "type" : "object", - "required" : true, "title" : "edt_conf_fbs_heading_title", - "properties" : - { - "enable" : - { - "type" : "boolean", - "required" : true, - "title" : "edt_conf_general_enable_title", - "default" : true, - "propertyOrder" : 1 + "properties": { + "enable": { + "type": "boolean", + "required": true, + "title": "edt_conf_general_enable_title", + "default": true, + "propertyOrder": 1 }, - "port" : - { - "type" : "integer", - "required" : true, - "title" : "edt_conf_general_port_title", - "minimum" : 1024, - "maximum" : 65535, - "default" : 19400, - "propertyOrder" : 2 + "port": { + "type": "integer", + "required": true, + "title": "edt_conf_general_port_title", + "minimum": 1024, + "maximum": 65535, + "default": 19400, + "propertyOrder": 2 }, - "timeout" : - { - "type" : "integer", - "required" : true, - "title" : "edt_conf_fbs_timeout_title", - "append" : "edt_append_s", - "minimum" : 1, - "default" : 5, - "propertyOrder" : 3 + "timeout": { + "type": "integer", + "required": true, + "title": "edt_conf_fbs_timeout_title", + "append": "edt_append_s", + "minimum": 1, + "default": 5, + "propertyOrder": 3 + }, + "pixelDecimation": { + "type": "integer", + "title": "edt_conf_fg_pixelDecimation_title", + "minimum": 1, + "maximum": 30, + "default": 1, + "required": false, + "access": "advanced", + "propertyOrder": 4 } }, "additionalProperties" : false diff --git a/libsrc/hyperion/schema/schema-foregroundEffect.json b/libsrc/hyperion/schema/schema-foregroundEffect.json index 71cf5d15..ad23fea6 100644 --- a/libsrc/hyperion/schema/schema-foregroundEffect.json +++ b/libsrc/hyperion/schema/schema-foregroundEffect.json @@ -7,6 +7,7 @@ { "type" : "boolean", "title" : "edt_conf_general_enable_title", + "required" : true, "default" : true, "propertyOrder" : 1 }, diff --git a/libsrc/hyperion/schema/schema-forwarder.json b/libsrc/hyperion/schema/schema-forwarder.json index 07b7d050..307321b5 100644 --- a/libsrc/hyperion/schema/schema-forwarder.json +++ b/libsrc/hyperion/schema/schema-forwarder.json @@ -1,7 +1,6 @@ { "type": "object", "title": "edt_conf_fw_heading_title", - "required": true, "properties": { "enable": { "type": "boolean", diff --git a/libsrc/hyperion/schema/schema-general.json b/libsrc/hyperion/schema/schema-general.json index 422054b5..5b75b9f8 100644 --- a/libsrc/hyperion/schema/schema-general.json +++ b/libsrc/hyperion/schema/schema-general.json @@ -1,7 +1,6 @@ { "type" : "object", "title" : "edt_conf_gen_heading_title", - "required" : true, "properties" : { "name" : @@ -44,15 +43,6 @@ }, "access" : "expert", "propertyOrder" : 4 - }, - "previousVersion" : - { - "type" : "string", - "options" : { - "hidden":true - }, - "access" : "expert", - "propertyOrder" : 5 } }, "additionalProperties" : false diff --git a/libsrc/hyperion/schema/schema-grabberAudio.json b/libsrc/hyperion/schema/schema-grabberAudio.json index 0fa780ee..183ddca6 100644 --- a/libsrc/hyperion/schema/schema-grabberAudio.json +++ b/libsrc/hyperion/schema/schema-grabberAudio.json @@ -1,6 +1,5 @@ { "type": "object", - "required": true, "title": "edt_conf_audio_heading_title", "properties": { "enable": { diff --git a/libsrc/hyperion/schema/schema-grabberV4L2.json b/libsrc/hyperion/schema/schema-grabberV4L2.json index 396f8868..e5ac3102 100644 --- a/libsrc/hyperion/schema/schema-grabberV4L2.json +++ b/libsrc/hyperion/schema/schema-grabberV4L2.json @@ -1,6 +1,5 @@ { "type" : "object", - "required" : true, "title" : "edt_conf_v4l2_heading_title", "properties": { @@ -24,6 +23,7 @@ "device": { "type": "string", "title": "edt_conf_enum_custom", + "default": "none", "options": { "hidden": true }, diff --git a/libsrc/hyperion/schema/schema-instCapture.json b/libsrc/hyperion/schema/schema-instCapture.json index a4076a6a..c07fd550 100644 --- a/libsrc/hyperion/schema/schema-instCapture.json +++ b/libsrc/hyperion/schema/schema-instCapture.json @@ -1,6 +1,5 @@ { "type" : "object", - "required" : true, "title" : "edt_conf_instC_heading_title", "properties": { "systemEnable": { diff --git a/libsrc/hyperion/schema/schema-jsonServer.json b/libsrc/hyperion/schema/schema-jsonServer.json index 798acf7c..e25229ae 100644 --- a/libsrc/hyperion/schema/schema-jsonServer.json +++ b/libsrc/hyperion/schema/schema-jsonServer.json @@ -1,6 +1,5 @@ { "type" : "object", - "required" : true, "title" : "edt_conf_js_heading_title", "properties" : { diff --git a/libsrc/hyperion/schema/schema-leds.json b/libsrc/hyperion/schema/schema-leds.json index b663f645..34f165bf 100644 --- a/libsrc/hyperion/schema/schema-leds.json +++ b/libsrc/hyperion/schema/schema-leds.json @@ -1,6 +1,5 @@ { "type": "array", - "required": true, "minItems": 1, "items": { "type": "object", @@ -61,4 +60,4 @@ }, "additionalProperties": false } -} \ No newline at end of file +} diff --git a/libsrc/hyperion/schema/schema-logger.json b/libsrc/hyperion/schema/schema-logger.json index 11243b05..d80e1686 100644 --- a/libsrc/hyperion/schema/schema-logger.json +++ b/libsrc/hyperion/schema/schema-logger.json @@ -6,6 +6,7 @@ "level" : { "type" : "string", + "required" : true, "enum" : ["silent", "warn", "verbose", "debug"], "title" : "edt_conf_log_level_title", "options" : { diff --git a/libsrc/hyperion/schema/schema-network.json b/libsrc/hyperion/schema/schema-network.json index b5bca5ae..0d72f7be 100644 --- a/libsrc/hyperion/schema/schema-network.json +++ b/libsrc/hyperion/schema/schema-network.json @@ -1,7 +1,6 @@ { "type" : "object", "title" : "edt_conf_net_heading_title", - "required" : true, "properties" : { "internetAccessAPI" : diff --git a/libsrc/hyperion/schema/schema-osEvents.json b/libsrc/hyperion/schema/schema-osEvents.json index 9adc9161..46befc61 100644 --- a/libsrc/hyperion/schema/schema-osEvents.json +++ b/libsrc/hyperion/schema/schema-osEvents.json @@ -1,6 +1,5 @@ { "type" : "object", - "required" : true, "title" : "edt_conf_os_events_heading_title", "properties": { "suspendEnable": { diff --git a/libsrc/hyperion/schema/schema-protoServer.json b/libsrc/hyperion/schema/schema-protoServer.json index 6a70e3f9..d7d391e4 100644 --- a/libsrc/hyperion/schema/schema-protoServer.json +++ b/libsrc/hyperion/schema/schema-protoServer.json @@ -1,6 +1,5 @@ { "type" : "object", - "required" : true, "title" : "edt_conf_pbs_heading_title", "properties" : { diff --git a/libsrc/hyperion/schema/schema-schedEvents.json b/libsrc/hyperion/schema/schema-schedEvents.json index 52fb8b96..85c7f782 100644 --- a/libsrc/hyperion/schema/schema-schedEvents.json +++ b/libsrc/hyperion/schema/schema-schedEvents.json @@ -1,6 +1,5 @@ { "type": "object", - "required": true, "properties": { "enable": { "type": "boolean", diff --git a/libsrc/hyperion/schema/schema-settings-default.json b/libsrc/hyperion/schema/schema-settings-default.json new file mode 100644 index 00000000..063a9b11 --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-default.json @@ -0,0 +1,17 @@ +{ + "type":"object", + "required":true, + "properties":{ + "global":{ + "type":"object", + "required":true, + "$ref":"schema-settings-global.json" + }, + "instance":{ + "type":"object", + "required":true, + "$ref":"schema-settings-instance.json" + } + }, + "additionalProperties":false +} diff --git a/libsrc/hyperion/schema/schema-settings-full-relaxed.json b/libsrc/hyperion/schema/schema-settings-full-relaxed.json new file mode 100644 index 00000000..dc7576a9 --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-full-relaxed.json @@ -0,0 +1,59 @@ +{ + "type":"object", + "required":true, + "properties":{ + "global":{ + "type":"object", + "required":false, + "properties":{ + "settings":{ + "type":"object", + "required":true, + "$ref":"schema-settings-global-relaxed.json" + }, + "uuid":{ + "type":"string", + "format":"uuid", + "required":false + } + }, + "additionalProperties":false + }, + "instanceIds":{ + "type":"array", + "required":false, + "items":{ + "type":"integer" + }, + "minItems":1 + }, + "instances":{ + "type":"array", + "required":false, + "items":{ + "type":"object", + "properties":{ + "enabled":{ + "type":"boolean" + }, + "id":{ + "type":"integer", + "minimum":0, + "maximum":255 + }, + "name":{ + "type":"string", + "minLength":5 + }, + "settings":{ + "type":"object", + "required":true, + "$ref":"schema-settings-instance-relaxed.json" + } + } + } + }, + "additionalProperties":true + }, + "additionalProperties":false +} diff --git a/libsrc/hyperion/schema/schema-settings-full.json b/libsrc/hyperion/schema/schema-settings-full.json new file mode 100644 index 00000000..f1b35f9c --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-full.json @@ -0,0 +1,59 @@ +{ + "type":"object", + "required":true, + "properties":{ + "global":{ + "type":"object", + "required":true, + "properties":{ + "settings":{ + "type":"object", + "required":true, + "$ref":"schema-settings-global.json" + }, + "uuid":{ + "type":"string", + "format":"uuid", + "required":false + } + }, + "additionalProperties":true + }, + "instanceIds":{ + "type":"array", + "required":false, + "items":{ + "type":"integer" + }, + "minItems":1 + }, + "instances":{ + "type":"array", + "required":true, + "items":{ + "type":"object", + "properties":{ + "enabled":{ + "type":"boolean" + }, + "id":{ + "type":"integer", + "minimum":0, + "maximum":255 + }, + "name":{ + "type":"string", + "minLength":5 + }, + "settings":{ + "type":"object", + "required":true, + "$ref":"schema-settings-instance.json" + } + } + } + }, + "additionalProperties":true + }, + "additionalProperties":false +} diff --git a/libsrc/hyperion/schema/schema-settings-global-relaxed.json b/libsrc/hyperion/schema/schema-settings-global-relaxed.json new file mode 100644 index 00000000..53070b91 --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-global-relaxed.json @@ -0,0 +1,62 @@ +{ + "type":"object", + "properties":{ + "cecEvents":{ + "required":false, + "$ref":"schema-cecEvents.json" + }, + "flatbufServer":{ + "required":false, + "$ref":"schema-flatbufServer.json" + }, + "forwarder":{ + "required":false, + "$ref":"schema-forwarder.json" + }, + "framegrabber":{ + "required":false, + "$ref":"schema-framegrabber.json" + }, + "general":{ + "required":false, + "$ref":"schema-general.json" + }, + "grabberAudio":{ + "required":false, + "$ref":"schema-grabberAudio.json" + }, + "grabberV4L2":{ + "required":false, + "$ref":"schema-grabberV4L2.json" + }, + "jsonServer":{ + "required":false, + "$ref":"schema-jsonServer.json" + }, + "logger":{ + "required":false, + "$ref":"schema-logger.json" + }, + "network":{ + "required":false, + "$ref":"schema-network.json" + }, + "osEvents":{ + "required":false, + "$ref":"schema-osEvents.json" + }, + "protoServer":{ + "required":false, + "$ref":"schema-protoServer.json" + }, + "schedEvents":{ + "required":false, + "$ref":"schema-schedEvents.json" + }, + "webConfig":{ + "required":false, + "$ref":"schema-webConfig.json" + } + }, + "additionalProperties":false +} diff --git a/libsrc/hyperion/schema/schema-settings-global.json b/libsrc/hyperion/schema/schema-settings-global.json new file mode 100644 index 00000000..6b1f3b85 --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-global.json @@ -0,0 +1,62 @@ +{ + "type":"object", + "properties":{ + "cecEvents":{ + "required":true, + "$ref":"schema-cecEvents.json" + }, + "flatbufServer":{ + "required":true, + "$ref":"schema-flatbufServer.json" + }, + "forwarder":{ + "required":true, + "$ref":"schema-forwarder.json" + }, + "framegrabber":{ + "required":true, + "$ref":"schema-framegrabber.json" + }, + "general":{ + "required":true, + "$ref":"schema-general.json" + }, + "grabberAudio":{ + "required":true, + "$ref":"schema-grabberAudio.json" + }, + "grabberV4L2":{ + "required":true, + "$ref":"schema-grabberV4L2.json" + }, + "jsonServer":{ + "required":true, + "$ref":"schema-jsonServer.json" + }, + "logger":{ + "required":true, + "$ref":"schema-logger.json" + }, + "network":{ + "required":true, + "$ref":"schema-network.json" + }, + "osEvents":{ + "required":true, + "$ref":"schema-osEvents.json" + }, + "protoServer":{ + "required":true, + "$ref":"schema-protoServer.json" + }, + "schedEvents":{ + "required":true, + "$ref":"schema-schedEvents.json" + }, + "webConfig":{ + "required":true, + "$ref":"schema-webConfig.json" + } + }, + "additionalProperties":true +} diff --git a/libsrc/hyperion/schema/schema-settings-instance-relaxed.json b/libsrc/hyperion/schema/schema-settings-instance-relaxed.json new file mode 100644 index 00000000..250e7c4e --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-instance-relaxed.json @@ -0,0 +1,50 @@ +{ + "type":"object", + "properties":{ + "backgroundEffect":{ + "required":false, + "$ref":"schema-backgroundEffect.json" + }, + "blackborderdetector":{ + "required":false, + "$ref":"schema-blackborderdetector.json" + }, + "boblightServer":{ + "required":false, + "$ref":"schema-boblightServer.json" + }, + "color":{ + "required":false, + "$ref":"schema-color.json" + }, + "device":{ + "required":false, + "$ref":"schema-device.json" + }, + "effects":{ + "required":false, + "$ref":"schema-effects.json" + }, + "foregroundEffect":{ + "required":false, + "$ref":"schema-foregroundEffect.json" + }, + "instCapture":{ + "required":false, + "$ref":"schema-instCapture.json" + }, + "ledConfig":{ + "required":false, + "$ref":"schema-ledConfig.json" + }, + "leds":{ + "required":false, + "$ref":"schema-leds.json" + }, + "smoothing":{ + "required":false, + "$ref":"schema-smoothing.json" + } + }, + "additionalProperties":false +} diff --git a/libsrc/hyperion/schema/schema-settings-instance.json b/libsrc/hyperion/schema/schema-settings-instance.json new file mode 100644 index 00000000..7bf78f49 --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-instance.json @@ -0,0 +1,50 @@ +{ + "type":"object", + "properties":{ + "backgroundEffect":{ + "required":true, + "$ref":"schema-backgroundEffect.json" + }, + "blackborderdetector":{ + "required":true, + "$ref":"schema-blackborderdetector.json" + }, + "boblightServer":{ + "required":true, + "$ref":"schema-boblightServer.json" + }, + "color":{ + "required":true, + "$ref":"schema-color.json" + }, + "device":{ + "required":true, + "$ref":"schema-device.json" + }, + "effects":{ + "required":true, + "$ref":"schema-effects.json" + }, + "foregroundEffect":{ + "required":true, + "$ref":"schema-foregroundEffect.json" + }, + "instCapture":{ + "required":true, + "$ref":"schema-instCapture.json" + }, + "ledConfig":{ + "required":true, + "$ref":"schema-ledConfig.json" + }, + "leds":{ + "required":true, + "$ref":"schema-leds.json" + }, + "smoothing":{ + "required":true, + "$ref":"schema-smoothing.json" + } + }, + "additionalProperties":true +} diff --git a/libsrc/hyperion/schema/schema-settings-ui.json b/libsrc/hyperion/schema/schema-settings-ui.json new file mode 100644 index 00000000..b78fd291 --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-ui.json @@ -0,0 +1,107 @@ +{ + "type":"object", + "required":true, + "properties":{ + "cecEvents":{ + "required":true, + "$ref":"schema-cecEvents.json" + }, + "flatbufServer":{ + "required":true, + "$ref":"schema-flatbufServer.json" + }, + "forwarder":{ + "required":true, + "$ref":"schema-forwarder.json" + }, + "framegrabber":{ + "required":true, + "$ref":"schema-framegrabber.json" + }, + "general":{ + "required":true, + "$ref":"schema-general.json" + }, + "grabberAudio":{ + "required":true, + "$ref":"schema-grabberAudio.json" + }, + "grabberV4L2":{ + "required":true, + "$ref":"schema-grabberV4L2.json" + }, + "jsonServer":{ + "required":true, + "$ref":"schema-jsonServer.json" + }, + "logger":{ + "required":true, + "$ref":"schema-logger.json" + }, + "network":{ + "required":true, + "$ref":"schema-network.json" + }, + "osEvents":{ + "required":true, + "$ref":"schema-osEvents.json" + }, + "protoServer":{ + "required":true, + "$ref":"schema-protoServer.json" + }, + "schedEvents":{ + "required":true, + "$ref":"schema-schedEvents.json" + }, + "webConfig":{ + "required":true, + "$ref":"schema-webConfig.json" + }, + "backgroundEffect":{ + "required":true, + "$ref":"schema-backgroundEffect.json" + }, + "blackborderdetector":{ + "required":true, + "$ref":"schema-blackborderdetector.json" + }, + "boblightServer":{ + "required":true, + "$ref":"schema-boblightServer.json" + }, + "color":{ + "required":true, + "$ref":"schema-color.json" + }, + "device":{ + "required":true, + "$ref":"schema-device.json" + }, + "effects":{ + "required":true, + "$ref":"schema-effects.json" + }, + "foregroundEffect":{ + "required":true, + "$ref":"schema-foregroundEffect.json" + }, + "instCapture":{ + "required":true, + "$ref":"schema-instCapture.json" + }, + "ledConfig":{ + "required":true, + "$ref":"schema-ledConfig.json" + }, + "leds":{ + "required":true, + "$ref":"schema-leds.json" + }, + "smoothing":{ + "required":true, + "$ref":"schema-smoothing.json" + } + }, + "additionalProperties":false +} diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 6a03e3b8..e281ca97 100644 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -115,7 +115,11 @@ if(ENABLE_DEV_NETWORK) if(NOT DEFAULT_USE_SYSTEM_MBEDTLS_LIBS) if(MBEDTLS_LIBRARIES) include_directories(${MBEDTLS_INCLUDE_DIR}) - target_link_libraries(leddevice ${MBEDTLS_LIBRARIES}) + target_link_libraries( + leddevice + ${MBEDTLS_LIBRARIES} + $<$:bcrypt.lib> + ) target_include_directories(leddevice PRIVATE ${MBEDTLS_INCLUDE_DIR}) endif (MBEDTLS_LIBRARIES) endif() diff --git a/libsrc/leddevice/LedDeviceSchemas.qrc b/libsrc/leddevice/LedDeviceSchemas.qrc index 7c179650..7cd6a235 100644 --- a/libsrc/leddevice/LedDeviceSchemas.qrc +++ b/libsrc/leddevice/LedDeviceSchemas.qrc @@ -42,5 +42,6 @@ schemas/schema-ws2812_ftdi.json schemas/schema-apa102_ftdi.json schemas/schema-sk6812_ftdi.json + schemas/schema-skydimo.json diff --git a/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp b/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp index f13b8677..98b848b1 100644 --- a/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp +++ b/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp @@ -59,10 +59,6 @@ bool LedDeviceAdalight::init(const QJsonObject &deviceConfig) Debug( _log, "Adalight driver uses standard Adalight protocol"); break; - case Adalight::SKYDIMO: - Debug( _log, "Adalight driver uses Skydimo protocol"); - break; - default: Error( _log, "Adalight driver - unsupported protocol"); return false; @@ -92,18 +88,6 @@ void LedDeviceAdalight::prepareHeader() } } break; - case Adalight::SKYDIMO: - { - _bufferLength = static_cast(HEADER_SIZE + _ledRGBCount); - _ledBuffer.resize(static_cast(_bufferLength), 0x00); - _ledBuffer[0] = 'A'; - _ledBuffer[1] = 'd'; - _ledBuffer[2] = 'a'; - _ledBuffer[3] = 0; - _ledBuffer[4] = 0; - _ledBuffer[5] = static_cast(_ledCount); - } - break; case Adalight::AWA: { _bufferLength = static_cast(HEADER_SIZE + _ledRGBCount + 8); diff --git a/libsrc/leddevice/dev_serial/LedDeviceAdalight.h b/libsrc/leddevice/dev_serial/LedDeviceAdalight.h index d0752dff..56065127 100644 --- a/libsrc/leddevice/dev_serial/LedDeviceAdalight.h +++ b/libsrc/leddevice/dev_serial/LedDeviceAdalight.h @@ -10,8 +10,7 @@ typedef enum ProtocolType { ADA = 0, LBAPA, - AWA, - SKYDIMO + AWA } PROTOCOLTYPE; } diff --git a/libsrc/leddevice/dev_serial/LedDeviceSkydimo.cpp b/libsrc/leddevice/dev_serial/LedDeviceSkydimo.cpp new file mode 100644 index 00000000..ec788a1c --- /dev/null +++ b/libsrc/leddevice/dev_serial/LedDeviceSkydimo.cpp @@ -0,0 +1,72 @@ +#include "LedDeviceSkydimo.h" +#include "utils/Logger.h" + +#include + +// Constants +namespace { + +constexpr int HEADER_SIZE {6}; + +} //End of constants + +LedDeviceSkydimo::LedDeviceSkydimo(const QJsonObject &deviceConfig) + : ProviderRs232(deviceConfig) +{ +} + +LedDevice* LedDeviceSkydimo::construct(const QJsonObject &deviceConfig) +{ + return new LedDeviceSkydimo(deviceConfig); +} + +bool LedDeviceSkydimo::init(const QJsonObject &deviceConfig) +{ + bool isInitOK = false; + + // Initialise sub-class + if ( ProviderRs232::init(deviceConfig) ) + { + prepareHeader(); + isInitOK = true; + } + return isInitOK; +} + +void LedDeviceSkydimo::prepareHeader() +{ + _bufferLength = static_cast(HEADER_SIZE + _ledRGBCount); + _ledBuffer.resize(static_cast(_bufferLength), 0x00); + _ledBuffer[0] = 'A'; + _ledBuffer[1] = 'd'; + _ledBuffer[2] = 'a'; + _ledBuffer[3] = 0; + _ledBuffer[4] = 0; + _ledBuffer[5] = static_cast(_ledCount); + + Debug( _log, "Skydimo header for %d leds (size: %d): %c%c%c 0x%02x 0x%02x 0x%02x", _ledCount, _ledBuffer.size(), + _ledBuffer[0], _ledBuffer[1], _ledBuffer[2], _ledBuffer[3], _ledBuffer[4], _ledBuffer[5] ); +} + +int LedDeviceSkydimo::write(const std::vector & ledValues) +{ + if (_ledCount != ledValues.size()) + { + Warning(_log, "Skydimo LED count has changed (old: %d, new: %d). Rebuilding header.", _ledCount, ledValues.size()); + _ledCount = static_cast(ledValues.size()); + _ledRGBCount = _ledCount * 3; + prepareHeader(); + } + + if (_bufferLength > static_cast(_ledBuffer.size())) + { + Warning(_log, "Skydimo buffer's size has changed. Skipping refresh."); + return 0; + } + + assert(HEADER_SIZE + ledValues.size() * sizeof(ColorRgb) <= _ledBuffer.size()); + + memcpy(HEADER_SIZE + _ledBuffer.data(), ledValues.data(), ledValues.size() * sizeof(ColorRgb)); + + return writeBytes(_bufferLength, _ledBuffer.data()); +} diff --git a/libsrc/leddevice/dev_serial/LedDeviceSkydimo.h b/libsrc/leddevice/dev_serial/LedDeviceSkydimo.h new file mode 100644 index 00000000..01bdf0f7 --- /dev/null +++ b/libsrc/leddevice/dev_serial/LedDeviceSkydimo.h @@ -0,0 +1,56 @@ +#ifndef LEDEVICESKYDIMO_H +#define LEDEVICESKYDIMO_H + +// hyperion includes +#include "ProviderRs232.h" + +/// +/// Implementation of the LedDevice interface for writing to a Skydimo LED-device. +/// +class LedDeviceSkydimo : public ProviderRs232 +{ + Q_OBJECT + +public: + + /// + /// @brief Constructs a Skydimo LED-device + /// + /// @param deviceConfig Device's configuration as JSON-Object + /// + explicit LedDeviceSkydimo(const QJsonObject &deviceConfig); + + /// + /// @brief Constructs the LED-device + /// + /// @param[in] deviceConfig Device's configuration as JSON-Object + /// @return LedDevice constructed + static LedDevice* construct(const QJsonObject &deviceConfig); + +private: + + /// + /// @brief Initialise the device's configuration + /// + /// @param[in] deviceConfig the JSON device configuration + /// @return True, if success + /// + bool init(const QJsonObject &deviceConfig) override; + + /// + /// @brief Prepare the protocol's header + /// + void prepareHeader(); + + /// + /// @brief Writes the RGB-Color values to the LEDs. + /// + /// @param[in] ledValues The RGB-color per LED + /// @return Zero on success, else negative + /// + int write(const std::vector & ledValues) override; + + qint64 _bufferLength; +}; + +#endif // LEDEVICESKYDIMO_H diff --git a/libsrc/leddevice/schemas/schema-adalight.json b/libsrc/leddevice/schemas/schema-adalight.json index 699852a3..f1e0ed17 100644 --- a/libsrc/leddevice/schemas/schema-adalight.json +++ b/libsrc/leddevice/schemas/schema-adalight.json @@ -11,10 +11,10 @@ "streamProtocol": { "type": "string", "title": "edt_dev_spec_stream_protocol_title", - "enum": [ "0", "1", "2", "3" ], + "enum": [ "0", "1", "2" ], "default": "0", "options": { - "enum_titles": [ "edt_dev_spec_ada_mode_title", "edt_dev_spec_LBap102Mode_title","edt_dev_spec_awa_mode_title", "edt_dev_spec_skydimo_mode_title" ] + "enum_titles": [ "edt_dev_spec_ada_mode_title", "edt_dev_spec_LBap102Mode_title","edt_dev_spec_awa_mode_title"] }, "propertyOrder": 2 }, diff --git a/libsrc/leddevice/schemas/schema-skydimo.json b/libsrc/leddevice/schemas/schema-skydimo.json new file mode 100644 index 00000000..56e5c29f --- /dev/null +++ b/libsrc/leddevice/schemas/schema-skydimo.json @@ -0,0 +1,61 @@ +{ + "type":"object", + "required":true, + "properties":{ + "output": { + "type": "string", + "title":"edt_dev_spec_outputPath_title", + "default":"auto", + "propertyOrder" : 1 + }, + "rateList": { + "type": "string", + "title":"edt_dev_spec_baudrate_title", + "enum": [ "CUSTOM","9600","14400","19200","28800","33600","38400","56000","57600","76800","115200","128000","153600","230400","256000","307200","460800","921600","1000000","1500000","2000000","3000000","4000000" ], + "options": { + "enum_titles": [ "edt_conf_enum_custom" ] + }, + "default": "115200", + "access": "advanced", + "propertyOrder" : 3 + }, + "rate": { + "type": "integer", + "title":"", + "default": 115200, + "access": "advanced", + "propertyOrder" : 4 + }, + "delayAfterConnect": { + "type": "integer", + "title":"edt_dev_spec_delayAfterConnect_title", + "default": 0, + "append" : "ms", + "access" : "expert", + "propertyOrder" : 5 + }, + "latchTime": { + "type": "integer", + "title": "edt_dev_spec_latchtime_title", + "default": 30, + "append": "edt_append_ms", + "minimum": 0, + "maximum": 1000, + "access": "expert", + "options": { + "infoText": "edt_dev_spec_latchtime_title_info" + }, + "propertyOrder": 6 + }, + "rewriteTime": { + "type": "integer", + "title":"edt_dev_general_rewriteTime_title", + "default": 1000, + "append" : "edt_append_ms", + "minimum": 0, + "access" : "expert", + "propertyOrder" : 7 + } + }, + "additionalProperties": true +} diff --git a/libsrc/python/PythonInit.cpp b/libsrc/python/PythonInit.cpp index 5684c24d..5f8067fb 100644 --- a/libsrc/python/PythonInit.cpp +++ b/libsrc/python/PythonInit.cpp @@ -18,7 +18,7 @@ #include #ifdef _WIN32 - #include +#include #endif #define STRINGIFY2(x) #x @@ -44,14 +44,14 @@ PythonInit::PythonInit() #if (PY_VERSION_HEX >= 0x03080000) status = PyConfig_SetString(&config, &config.program_name, programName); if (PyStatus_Exception(status)) { - goto exception; + handlePythonError(status, config); + return; } - else #else Py_SetProgramName(programName); #endif { - // set Python module path when exists + // set Python module path when it exists QString py_path = QDir::cleanPath(qApp->applicationDirPath() + "/../lib/python" + STRINGIFY(PYTHON_VERSION_MAJOR) + "." + STRINGIFY(PYTHON_VERSION_MINOR)); QString py_file = QDir::cleanPath(qApp->applicationDirPath() + "/python" + STRINGIFY(PYTHON_VERSION_MAJOR) + STRINGIFY(PYTHON_VERSION_MINOR) + ".zip"); QString py_framework = QDir::cleanPath(qApp->applicationDirPath() + "/../Frameworks/Python.framework/Versions/Current/lib/python" + STRINGIFY(PYTHON_VERSION_MAJOR) + "." + STRINGIFY(PYTHON_VERSION_MINOR)); @@ -59,21 +59,23 @@ PythonInit::PythonInit() if (QFile(py_file).exists() || QDir(py_path).exists() || QDir(py_framework).exists()) { #if (PY_VERSION_HEX >= 0x030C0000) - config.site_import = 0; + config.site_import = 0; #else - Py_NoSiteFlag++; + Py_NoSiteFlag++; #endif if (QFile(py_file).exists()) // Windows { #if (PY_VERSION_HEX >= 0x03080000) status = PyConfig_SetBytesString(&config, &config.home, QSTRING_CSTR(py_file)); if (PyStatus_Exception(status)) { - goto exception; + handlePythonError(status, config); + return; } config.module_search_paths_set = 1; status = PyWideStringList_Append(&config.module_search_paths, const_cast(py_file.toStdWString().c_str())); if (PyStatus_Exception(status)) { - goto exception; + handlePythonError(status, config); + return; } #else Py_SetPythonHome(Py_DecodeLocale(py_file.toLatin1().data(), nullptr)); @@ -85,18 +87,21 @@ PythonInit::PythonInit() #if (PY_VERSION_HEX >= 0x03080000) status = PyConfig_SetBytesString(&config, &config.home, QSTRING_CSTR(QDir::cleanPath(qApp->applicationDirPath() + "/../"))); if (PyStatus_Exception(status)) { - goto exception; + handlePythonError(status, config); + return; } config.module_search_paths_set = 1; status = PyWideStringList_Append(&config.module_search_paths, const_cast(QDir(py_path).absolutePath().toStdWString().c_str())); if (PyStatus_Exception(status)) { - goto exception; + handlePythonError(status, config); + return; } status = PyWideStringList_Append(&config.module_search_paths, const_cast(QDir(py_path + "/lib-dynload").absolutePath().toStdWString().c_str())); if (PyStatus_Exception(status)) { - goto exception; + handlePythonError(status, config); + return; } #else QStringList python_paths; @@ -114,18 +119,21 @@ PythonInit::PythonInit() #if (PY_VERSION_HEX >= 0x03080000) status = PyConfig_SetBytesString(&config, &config.home, QSTRING_CSTR(QDir::cleanPath(qApp->applicationDirPath() + "/../Frameworks/Python.framework/Versions/Current"))); if (PyStatus_Exception(status)) { - goto exception; + handlePythonError(status, config); + return; } config.module_search_paths_set = 1; status = PyWideStringList_Append(&config.module_search_paths, const_cast(QDir(py_framework).absolutePath().toStdWString().c_str())); if (PyStatus_Exception(status)) { - goto exception; + handlePythonError(status, config); + return; } status = PyWideStringList_Append(&config.module_search_paths, const_cast(QDir(py_framework + "/lib-dynload").absolutePath().toStdWString().c_str())); if (PyStatus_Exception(status)) { - goto exception; + handlePythonError(status, config); + return; } #else QStringList python_paths; @@ -146,7 +154,8 @@ PythonInit::PythonInit() #if (PY_VERSION_HEX >= 0x03080000) status = Py_InitializeFromConfig(&config); if (PyStatus_Exception(status)) { - goto exception; + handlePythonError(status, config); + return; } PyConfig_Clear(&config); #endif @@ -154,7 +163,8 @@ PythonInit::PythonInit() // init Python Debug(Logger::getInstance("DAEMON"), "Initializing Python interpreter"); Py_InitializeEx(0); - if ( !Py_IsInitialized() ) + + if (!Py_IsInitialized()) { throw std::runtime_error("Initializing Python failed!"); } @@ -165,20 +175,28 @@ PythonInit::PythonInit() #endif mainThreadState = PyEval_SaveThread(); - return; +} +// Error handling function to replace goto exception #if (PY_VERSION_HEX >= 0x03080000) -exception: +void PythonInit::handlePythonError(PyStatus status, PyConfig& config) +{ Error(Logger::getInstance("DAEMON"), "Initializing Python config failed with error [%s]", status.err_msg); PyConfig_Clear(&config); - throw std::runtime_error("Initializing Python failed!"); -#endif } +#endif PythonInit::~PythonInit() { Debug(Logger::getInstance("DAEMON"), "Cleaning up Python interpreter"); + +#if (PY_VERSION_HEX < 0x030C0000) PyEval_RestoreThread(mainThreadState); - Py_Finalize(); +#else + PyThreadState_Swap(mainThreadState); +#endif + + int rc = Py_FinalizeEx(); + Debug(Logger::getInstance("DAEMON"), "Cleaning up Python interpreter %s", rc == 0 ? "succeeded" : "failed"); } diff --git a/libsrc/python/PythonProgram.cpp b/libsrc/python/PythonProgram.cpp index e39839f2..6f0b08c9 100644 --- a/libsrc/python/PythonProgram.cpp +++ b/libsrc/python/PythonProgram.cpp @@ -1,173 +1,238 @@ #include #include + #include #include PyThreadState* mainThreadState; -PythonProgram::PythonProgram(const QString & name, Logger * log) : - _name(name), _log(log), _tstate(nullptr) +PythonProgram::PythonProgram(const QString& name, Logger* log) : + _name(name) + , _log(log) + , _tstate(nullptr) { // we probably need to wait until mainThreadState is available - while(mainThreadState == nullptr){}; + QThread::msleep(10); + while (mainThreadState == nullptr) + { + QThread::msleep(10); // Wait with delay to avoid busy waiting + } + // Create a new subinterpreter for this thread +#if (PY_VERSION_HEX < 0x030C0000) // get global lock PyEval_RestoreThread(mainThreadState); - - // Initialize a new thread state _tstate = Py_NewInterpreter(); - if(_tstate == nullptr) - { -#if (PY_VERSION_HEX >= 0x03020000) - PyThreadState_Swap(mainThreadState); - PyEval_SaveThread(); #else - PyEval_ReleaseLock(); + PyThreadState* prev = PyThreadState_Swap(NULL); + + // Create a new interpreter configuration object + PyInterpreterConfig config{}; + + // Set configuration options + config.use_main_obmalloc = 0; + config.allow_fork = 0; + config.allow_exec = 0; + config.allow_threads = 1; + config.allow_daemon_threads = 0; + config.check_multi_interp_extensions = 1; + config.gil = PyInterpreterConfig_OWN_GIL; + Py_NewInterpreterFromConfig(&_tstate, &config); #endif - Error(_log, "Failed to get thread state for %s",QSTRING_CSTR(_name)); + + if (_tstate == nullptr) + { + PyThreadState_Swap(mainThreadState); +#if (PY_VERSION_HEX < 0x030C0000) + PyEval_SaveThread(); +#endif + Error(_log, "Failed to get thread state for %s", QSTRING_CSTR(_name)); return; } +#if (PY_VERSION_HEX < 0x030C0000) PyThreadState_Swap(_tstate); +#endif } PythonProgram::~PythonProgram() { if (!_tstate) - return; - - // stop sub threads if needed - for (PyThreadState* s = PyInterpreterState_ThreadHead(_tstate->interp), *old = nullptr; s;) { - if (s == _tstate) - { - s = s->next; - continue; - } - if (old != s) - { - Debug(_log,"ID %s: Waiting on thread %u", QSTRING_CSTR(_name), s->thread_id); - old = s; - } - - Py_BEGIN_ALLOW_THREADS; - QThread::msleep(100); - Py_END_ALLOW_THREADS; - - s = PyInterpreterState_ThreadHead(_tstate->interp); + return; } +#if (PY_VERSION_HEX < 0x030C0000) + PyThreadState* prev_thread_state = PyThreadState_Swap(_tstate); +#endif + // Clean up the thread state Py_EndInterpreter(_tstate); -#if (PY_VERSION_HEX >= 0x03020000) - PyThreadState_Swap(mainThreadState); + +#if (PY_VERSION_HEX < 0x030C0000) + PyThreadState_Swap(prev_thread_state); PyEval_SaveThread(); -#else - PyEval_ReleaseLock(); #endif } -void PythonProgram::execute(const QByteArray & python_code) +void PythonProgram::execute(const QByteArray& python_code) { if (!_tstate) + { return; + } +#if (PY_VERSION_HEX < 0x030C0000) + PyThreadState_Swap(_tstate); +#else + PyThreadState* prev_thread_state = PyThreadState_Swap(_tstate); +#endif + PyObject* main_module = PyImport_ImportModule("__main__"); + if (!main_module) + { + // Restore the previous thread state +#if (PY_VERSION_HEX < 0x030C0000) + PyThreadState_Swap(mainThreadState); +#else + PyThreadState_Swap(prev_thread_state); +#endif + return; + } - PyObject *main_module = PyImport_ImportModule("__main__"); // New Reference - PyObject *main_dict = PyModule_GetDict(main_module); // Borrowed reference - Py_INCREF(main_dict); // Incref "main_dict" to use it in PyRun_String(), because PyModule_GetDict() has decref "main_dict" - Py_DECREF(main_module); // // release "main_module" when done - PyObject *result = PyRun_String(python_code.constData(), Py_file_input, main_dict, main_dict); // New Reference - + PyObject* main_dict = PyModule_GetDict(main_module); // Borrowed reference to globals + PyObject* result = PyRun_String(python_code.constData(), Py_file_input, main_dict, main_dict); if (!result) { - if (PyErr_Occurred()) // Nothing needs to be done for a borrowed reference + if (PyErr_Occurred()) { - Error(_log,"###### PYTHON EXCEPTION ######"); - Error(_log,"## In effect '%s'", QSTRING_CSTR(_name)); - /* Objects all initialized to NULL for Py_XDECREF */ - PyObject *errorType = NULL, *errorValue = NULL, *errorTraceback = NULL; + PyObject* errorType = NULL, * errorValue = NULL, * errorTraceback = NULL; - PyErr_Fetch(&errorType, &errorValue, &errorTraceback); // New Reference or NULL + PyErr_Fetch(&errorType, &errorValue, &errorTraceback); PyErr_NormalizeException(&errorType, &errorValue, &errorTraceback); - // Extract exception message from "errorValue" - if(errorValue) - { - QString message; - if(PyObject_HasAttrString(errorValue, "__class__")) - { - PyObject *classPtr = PyObject_GetAttrString(errorValue, "__class__"); // New Reference - PyObject *class_name = NULL; /* Object "class_name" initialized to NULL for Py_XDECREF */ - class_name = PyObject_GetAttrString(classPtr, "__name__"); // New Reference or NULL + // Check if the exception is a SystemExit + PyObject* systemExitType = PyExc_SystemExit; + bool isSystemExit = PyObject_IsInstance(errorValue, systemExitType); - if(class_name && PyUnicode_Check(class_name)) + if (isSystemExit) + { + // Extract the exit argument + PyObject* exitArg = PyObject_GetAttrString(errorValue, "code"); + + if (exitArg) + { + QString logErrorText; + if (PyTuple_Check(exitArg)) { + PyObject* errorMessage = PyTuple_GetItem(exitArg, 0); // Borrowed reference + PyObject* exitCode = PyTuple_GetItem(exitArg, 1); // Borrowed reference + + if (exitCode && PyLong_Check(exitCode)) + { + logErrorText = QString("[%1]: ").arg(PyLong_AsLong(exitCode)); + } + + if (errorMessage && PyUnicode_Check(errorMessage)) { + logErrorText.append(PyUnicode_AsUTF8(errorMessage)); + } + } + else if (PyUnicode_Check(exitArg)) { + // If the code is just a string, treat it as an error message + logErrorText.append(PyUnicode_AsUTF8(exitArg)); + } + else if (PyLong_Check(exitArg)) { + // If the code is just an integer, treat it as an exit code + logErrorText = QString("[%1]").arg(PyLong_AsLong(exitArg)); + } + + Error(_log, "Effect '%s' failed with error %s", QSTRING_CSTR(_name), QSTRING_CSTR(logErrorText)); + + Py_DECREF(exitArg); // Release the reference + } + else + { + Debug(_log, "No 'code' attribute found on SystemExit exception."); + } + // Clear the error so it won't propagate + PyErr_Clear(); + + Py_DECREF(systemExitType); + return; + } + Py_DECREF(systemExitType); + + if (errorValue) + { + Error(_log, "###### PYTHON EXCEPTION ######"); + Error(_log, "## In effect '%s'", QSTRING_CSTR(_name)); + + QString message; + if (PyObject_HasAttrString(errorValue, "__class__")) + { + PyObject* classPtr = PyObject_GetAttrString(errorValue, "__class__"); + PyObject* class_name = classPtr ? PyObject_GetAttrString(classPtr, "__name__") : NULL; + + if (class_name && PyUnicode_Check(class_name)) message.append(PyUnicode_AsUTF8(class_name)); - Py_DECREF(classPtr); // release "classPtr" when done - Py_XDECREF(class_name); // Use Py_XDECREF() to ignore NULL references + Py_XDECREF(class_name); + Py_DECREF(classPtr); } - // Object "class_name" initialized to NULL for Py_XDECREF - PyObject *valueString = NULL; - valueString = PyObject_Str(errorValue); // New Reference or NULL + PyObject* valueString = PyObject_Str(errorValue); - if(valueString && PyUnicode_Check(valueString)) + if (valueString && PyUnicode_Check(valueString)) { - if(!message.isEmpty()) + if (!message.isEmpty()) message.append(": "); - message.append(PyUnicode_AsUTF8(valueString)); } - Py_XDECREF(valueString); // Use Py_XDECREF() to ignore NULL references + Py_XDECREF(valueString); Error(_log, "## %s", QSTRING_CSTR(message)); } - // Extract exception message from "errorTraceback" - if(errorTraceback) + if (errorTraceback) { - // Object "tracebackList" initialized to NULL for Py_XDECREF - PyObject *tracebackModule = NULL, *methodName = NULL, *tracebackList = NULL; - QString tracebackMsg; + PyObject* tracebackModule = PyImport_ImportModule("traceback"); + PyObject* methodName = PyUnicode_FromString("format_exception"); + PyObject* tracebackList = tracebackModule && methodName + ? PyObject_CallMethodObjArgs(tracebackModule, methodName, errorType, errorValue, errorTraceback, NULL) + : NULL; - tracebackModule = PyImport_ImportModule("traceback"); // New Reference or NULL - methodName = PyUnicode_FromString("format_exception"); // New Reference or NULL - tracebackList = PyObject_CallMethodObjArgs(tracebackModule, methodName, errorType, errorValue, errorTraceback, NULL); // New Reference or NULL - - if(tracebackList) + if (tracebackList) { - PyObject* iterator = PyObject_GetIter(tracebackList); // New Reference - + PyObject* iterator = PyObject_GetIter(tracebackList); PyObject* item; - while( (item = PyIter_Next(iterator)) ) // New Reference + while ((item = PyIter_Next(iterator))) { - Error(_log, "## %s",QSTRING_CSTR(QString(PyUnicode_AsUTF8(item)).trimmed())); - Py_DECREF(item); // release "item" when done + Error(_log, "## %s", QSTRING_CSTR(QString(PyUnicode_AsUTF8(item)).trimmed())); + Py_DECREF(item); } - Py_DECREF(iterator); // release "iterator" when done + Py_DECREF(iterator); } - // Use Py_XDECREF() to ignore NULL references Py_XDECREF(tracebackModule); Py_XDECREF(methodName); Py_XDECREF(tracebackList); - // Give the exception back to python and print it to stderr in case anyone else wants it. - Py_XINCREF(errorType); - Py_XINCREF(errorValue); - Py_XINCREF(errorTraceback); - PyErr_Restore(errorType, errorValue, errorTraceback); - //PyErr_PrintEx(0); // Remove this line to switch off stderr output } - Error(_log,"###### EXCEPTION END ######"); + Error(_log, "###### EXCEPTION END ######"); } + // Clear the error so it won't propagate + PyErr_Clear(); } else { - Py_DECREF(result); // release "result" when done + Py_DECREF(result); // Release result when done } - Py_DECREF(main_dict); // release "main_dict" when done + Py_DECREF(main_module); + + // Restore the previous thread state +#if (PY_VERSION_HEX < 0x030C0000) + PyThreadState_Swap(mainThreadState); +#else + PyThreadState_Swap(prev_thread_state); +#endif } diff --git a/libsrc/ssdp/SSDPHandler.cpp b/libsrc/ssdp/SSDPHandler.cpp index 32999642..5f2dd021 100644 --- a/libsrc/ssdp/SSDPHandler.cpp +++ b/libsrc/ssdp/SSDPHandler.cpp @@ -4,7 +4,7 @@ #include "SSDPDescription.h" #include #include -#include +#include #include #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) @@ -40,7 +40,8 @@ SSDPHandler::~SSDPHandler() void SSDPHandler::initServer() { - _uuid = AuthManager::getInstance()->getID(); + MetaTable metaTable; + _uuid = metaTable.getUUID(); SSDPServer::setUuid(_uuid); // announce targets diff --git a/libsrc/utils/JsonUtils.cpp b/libsrc/utils/JsonUtils.cpp index 1fd44f68..e974e0ca 100644 --- a/libsrc/utils/JsonUtils.cpp +++ b/libsrc/utils/JsonUtils.cpp @@ -7,155 +7,251 @@ //qt includes #include #include +#include +#include #include #include namespace JsonUtils { - QPair readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError) +QPair readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError) +{ + QJsonValue value(obj); + + QPair result = readFile(path, value,log, ignError); + obj = value.toObject(); + return result; +} + +QPair readFile(const QString& path, QJsonValue& obj, Logger* log, bool ignError) +{ + QString data; + if(!FileUtils::readFile(path, data, log, ignError)) { - QString data; - if(!FileUtils::readFile(path, data, log, ignError)) + return qMakePair(false, QStringList(QString("Error reading file: %1").arg(path))); + } + + QPair parsingResult = JsonUtils::parse(path, data, obj, log); + return parsingResult; +} + + +bool readSchema(const QString& path, QJsonObject& obj, Logger* log) +{ + QJsonObject schema; + if(!readFile(path, schema, log).first) + { + return false; + } + + if(!resolveRefs(schema, obj, log)) + { + return false; + } + + return true; +} + +QPair parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log) +{ + QJsonValue value(obj); + + QPair result = JsonUtils::parse(path, data, value, log); + obj = value.toObject(); + return result; +} + +QPair parse(const QString& path, const QString& data, QJsonArray& arr, Logger* log) +{ + QJsonValue value(arr); + + QPair result = JsonUtils::parse(path, data, value, log); + arr = value.toArray(); + return result; +} + +QPair parse(const QString& path, const QString& data, QJsonValue& value, Logger* log) +{ + QJsonDocument doc; + QPair parsingResult = JsonUtils::parse(path, data, doc, log); + value = doc.object(); + return parsingResult; +} + +QPair parse(const QString& path, const QString& data, QJsonDocument& doc, Logger* log) +{ + QStringList errorList; + + QJsonParseError error; + doc = QJsonDocument::fromJson(data.toUtf8(), &error); + + if (error.error != QJsonParseError::NoError) + { + int errorLine = 1; + int errorColumn = 1; + + int lastNewlineIndex = data.lastIndexOf("\n", error.offset - 1); + if (lastNewlineIndex != -1) { - return qMakePair(false, QStringList(QString("Error reading file: %1").arg(path))); + errorColumn = error.offset - lastNewlineIndex ; } + errorLine += data.left(error.offset).count('\n'); - QPair parsingResult = JsonUtils::parse(path, data, obj, log); - return parsingResult; + const QString errorMessage = QString("JSON parse error @(%1): %2, line: %3, column: %4, Data: '%5'") + .arg(path) + .arg(error.errorString()) + .arg(errorLine) + .arg(errorColumn) + .arg(data); + errorList.push_back(errorMessage); + Error(log, "%s", QSTRING_CSTR(errorMessage)); + + return qMakePair(false, errorList); + } + return qMakePair(true, errorList); +} + +QPair validate(const QString& file, const QJsonValue& json, const QString& schemaPath, Logger* log) +{ + // get the schema data + QJsonObject schema; + + QPair readResult = readFile(schemaPath, schema, log); + if(!readResult.first) + { + return readResult; } - bool readSchema(const QString& path, QJsonObject& obj, Logger* log) + QPair validationResult = validate(file, json, schema, log); + return validationResult; +} + +QPair validate(const QString& file, const QJsonValue& json, const QJsonObject& schema, Logger* log) +{ + QStringList errorList; + + QJsonSchemaChecker schemaChecker; + schemaChecker.setSchema(schema); + if (!schemaChecker.validate(json).first) { - QJsonObject schema; - if(!readFile(path, schema, log).first) - return false; - - if(!resolveRefs(schema, obj, log)) - return false; - - return true; - } - - QPair parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log) - { - QJsonDocument doc; - QPair parsingResult = JsonUtils::parse(path, data, doc, log); - obj = doc.object(); - return parsingResult; - } - - QPair parse(const QString& path, const QString& data, QJsonArray& arr, Logger* log) - { - QJsonDocument doc; - - QPair parsingResult = JsonUtils::parse(path, data, doc, log); - arr = doc.array(); - return parsingResult; - } - - QPair parse(const QString& path, const QString& data, QJsonDocument& doc, Logger* log) - { - QStringList errorList; - - QJsonParseError error; - doc = QJsonDocument::fromJson(data.toUtf8(), &error); - - if (error.error != QJsonParseError::NoError) + const QStringList &errors = schemaChecker.getMessages(); + for (const auto& error : errors) { - int errorLine = 1; - int errorColumn = 1; - - int lastNewlineIndex = data.lastIndexOf("\n", error.offset - 1); - if (lastNewlineIndex != -1) - { - errorColumn = error.offset - lastNewlineIndex ; - } - errorLine += data.left(error.offset).count('\n'); - - const QString errorMessage = QString("JSON parse error: %1, line: %2, column: %3, Data: '%4'") - .arg(error.errorString()) - .arg(errorLine) - .arg(errorColumn) - .arg(data); + QString errorMessage = QString("JSON parse error @(%1) - %2") + .arg(file, error); errorList.push_back(errorMessage); Error(log, "%s", QSTRING_CSTR(errorMessage)); - - return qMakePair(false, errorList); } - return qMakePair(true, errorList); + return qMakePair(false, errorList); } + return qMakePair(true, errorList); +} - QPair validate(const QString& file, const QJsonObject& json, const QString& schemaPath, Logger* log) +QPair correct(const QString& file, QJsonValue& json, const QJsonObject& schema, Logger* log) +{ + bool wasCorrected {false}; + QStringList corrections; + QJsonValue correctJson; + + QJsonSchemaChecker schemaChecker; + schemaChecker.setSchema(schema); + if (!schemaChecker.validate(json).first) { - // get the schema data - QJsonObject schema; + Warning(log, "Fixing JSON data!"); + json = schemaChecker.getAutoCorrectedConfig(json); + wasCorrected = true; - QPair readResult = readFile(schemaPath, schema, log); - if(!readResult.first) + const QStringList &correctionMessages = schemaChecker.getMessages(); + for (const auto& correction : correctionMessages) { - return readResult; + QString message = QString("JSON fix @(%1) - %2") + .arg(file, correction); + corrections.push_back(message); + Warning(log, "%s", QSTRING_CSTR(message)); } - - QPair validationResult = validate(file, json, schema, log); - return validationResult; } + return qMakePair(wasCorrected, corrections); +} - QPair validate(const QString& file, const QJsonObject& json, const QJsonObject& schema, Logger* log) +bool write(const QString& filename, const QJsonObject& json, Logger* log) +{ + QJsonDocument doc; + + doc.setObject(json); + QByteArray data = doc.toJson(QJsonDocument::Indented); + + return FileUtils::writeFile(filename, data, log); +} + +bool resolveRefs(const QJsonObject& schema, QJsonObject& obj, Logger* log) +{ + for (QJsonObject::const_iterator i = schema.begin(); i != schema.end(); ++i) { - QStringList errorList; + QString attribute = i.key(); + const QJsonValue & attributeValue = *i; - QJsonSchemaChecker schemaChecker; - schemaChecker.setSchema(schema); - if (!schemaChecker.validate(json).first) + if (attribute == "$ref" && attributeValue.isString()) { - const QStringList &errors = schemaChecker.getMessages(); - for (const auto& error : errors) + if(!readSchema(":/" + attributeValue.toString(), obj, log)) { - QString errorMessage = QString("JSON parse error: %1") - .arg(error); - errorList.push_back(errorMessage); - Error(log, "%s", QSTRING_CSTR(errorMessage)); - } - return qMakePair(false, errorList); - } - return qMakePair(true, errorList); - } - - bool write(const QString& filename, const QJsonObject& json, Logger* log) - { - QJsonDocument doc; - - doc.setObject(json); - QByteArray data = doc.toJson(QJsonDocument::Indented); - - if(!FileUtils::writeFile(filename, data, log)) - return false; - - return true; - } - - bool resolveRefs(const QJsonObject& schema, QJsonObject& obj, Logger* log) - { - for (QJsonObject::const_iterator i = schema.begin(); i != schema.end(); ++i) - { - QString attribute = i.key(); - const QJsonValue & attributeValue = *i; - - if (attribute == "$ref" && attributeValue.isString()) - { - if(!readSchema(":/" + attributeValue.toString(), obj, log)) - { - Error(log,"Error while getting schema ref: %s",QSTRING_CSTR(QString(":/" + attributeValue.toString()))); - return false; - } - } - else if (attributeValue.isObject()) - obj.insert(attribute, resolveRefs(attributeValue.toObject(), obj, log)); - else - { - obj.insert(attribute, attributeValue); + Error(log,"Error while getting schema ref: %s",QSTRING_CSTR(QString(":/" + attributeValue.toString()))); + return false; } } - return true; + else if (attributeValue.isObject()) + { + obj.insert(attribute, resolveRefs(attributeValue.toObject(), obj, log)); + } + else + { + obj.insert(attribute, attributeValue); + } } + return true; +} + +QString jsonValueToQString(const QJsonValue &value, QJsonDocument::JsonFormat format) +{ + switch (value.type()) { + case QJsonValue::Object: + { + return QJsonDocument(value.toObject()).toJson(format); + } + case QJsonValue::Array: + { + return QJsonDocument(value.toArray()).toJson(format); + } + case QJsonValue::String: + case QJsonValue::Double: + case QJsonValue::Bool: + { + return value.toString(); + } + case QJsonValue::Null: + { + return "Null"; + } + default: + break; + } + return QString(); +} + +QJsonObject mergeJsonObjects(const QJsonObject &obj1, const QJsonObject &obj2, bool overrideObj1) +{ + QJsonObject result = obj1; + + for (auto it = obj2.begin(); it != obj2.end(); ++it) { + if (result.contains(it.key())) { + if (overrideObj1) + { + result[it.key()] = it.value(); + } + } else { + result[it.key()] = it.value(); + } + } + + return result; +} }; diff --git a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp index 8cd0be75..04bb0a81 100644 --- a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp +++ b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp @@ -40,7 +40,7 @@ QStringList QJsonSchemaChecker::getMessages() const return _messages; } -QPair QJsonSchemaChecker::validate(const QJsonObject& value, bool ignoreRequired) +QPair QJsonSchemaChecker::validate(const QJsonValue& value, bool ignoreRequired) { // initialize state _ignoreRequired = ignoreRequired; @@ -56,7 +56,7 @@ QPair QJsonSchemaChecker::validate(const QJsonObject& value, bool ig return QPair(!_error, !_schemaError); } -QJsonObject QJsonSchemaChecker::getAutoCorrectedConfig(const QJsonObject& value, bool ignoreRequired) +QJsonValue QJsonSchemaChecker::getAutoCorrectedConfig(const QJsonValue& value, bool ignoreRequired) { _ignoreRequired = ignoreRequired; const QStringList sequence = QStringList() << "remove" << "modify" << "create"; diff --git a/resources/resources.qrc.in b/resources/resources.qrc.in index d5ed6c80..abc5d1c9 100644 --- a/resources/resources.qrc.in +++ b/resources/resources.qrc.in @@ -1,6 +1,6 @@ - ${CMAKE_BINARY_DIR}/config/hyperion.config.json.default + ${CMAKE_BINARY_DIR}/settings/hyperion.settings.json.default ${HYPERION_RES} diff --git a/config/.gitignore b/settings/.gitignore similarity index 100% rename from config/.gitignore rename to settings/.gitignore diff --git a/settings/hyperion.settings.json.default b/settings/hyperion.settings.json.default new file mode 100644 index 00000000..0f79800d --- /dev/null +++ b/settings/hyperion.settings.json.default @@ -0,0 +1,309 @@ +{ + "global":{ + "cecEvents":{ + "actions":[ + { + "action":"Suspend", + "event":"standby" + }, + { + "action":"Resume", + "event":"set stream path" + } + ], + "enable":false + }, + "flatbufServer":{ + "enable":true, + "port":19400, + "timeout":5 + }, + "forwarder":{ + "enable":false, + "jsonapi":[ + + ], + "flatbuffer":[ + + ] + }, + "framegrabber":{ + "enable":false, + "device":"auto", + "input":0, + "width":80, + "height":45, + "fps":10, + "pixelDecimation":8, + "cropLeft":0, + "cropRight":0, + "cropTop":0, + "cropBottom":0 + }, + "general":{ + "name":"My Hyperion Config", + "configVersion":"configVersionValue", + "watchedVersionBranch":"Stable", + "showOptHelp":true + }, + "grabberAudio":{ + "enable":false, + "device":"auto", + "audioEffect":"vuMeter", + "vuMeter":{ + "flip":"NO_CHANGE", + "hotColor":[ + 255, + 0, + 0 + ], + "multiplier":1, + "safeColor":[ + 0, + 255, + 0 + ], + "safeValue":45, + "tolerance":5, + "warnColor":[ + 255, + 255, + 0 + ], + "warnValue":80 + } + }, + "grabberV4L2":{ + "enable":false, + "device":"none", + "input":0, + "encoding":"NO_CHANGE", + "width":0, + "height":0, + "fps":15, + "flip":"NO_CHANGE", + "fpsSoftwareDecimation":0, + "sizeDecimation":8, + "cropLeft":0, + "cropRight":0, + "cropTop":0, + "cropBottom":0, + "redSignalThreshold":0, + "greenSignalThreshold":100, + "blueSignalThreshold":0, + "signalDetection":false, + "noSignalCounterThreshold":200, + "sDVOffsetMin":0.1, + "sDVOffsetMax":0.9, + "sDHOffsetMin":0.4, + "sDHOffsetMax":0.46, + "hardware_brightness":0, + "hardware_contrast":0, + "hardware_saturation":0, + "hardware_hue":0 + }, + "jsonServer":{ + "port":19444 + }, + "logger":{ + "level":"warn" + }, + "network":{ + "internetAccessAPI":false, + "restirctedInternetAccessAPI":false, + "ipWhitelist":[ + + ], + "localApiAuth":false + }, + "osEvents":{ + "suspendEnable":true, + "lockEnable":true + }, + "protoServer":{ + "enable":true, + "port":19445, + "timeout":5 + }, + "schedEvents":{ + "enable":false + }, + "webConfig":{ + "document_root":"", + "port":8090, + "sslPort":8092, + "crtPath":"", + "keyPath":"", + "keyPassPhrase":"" + } + }, + "instance":{ + "backgroundEffect":{ + "enable":false, + "type":"effect", + "color":[ + 255, + 138, + 0 + ], + "effect":"Warm mood blobs" + }, + "blackborderdetector":{ + "enable":true, + "threshold":5, + "unknownFrameCnt":600, + "borderFrameCnt":50, + "maxInconsistentCnt":10, + "blurRemoveCnt":1, + "mode":"default" + }, + "boblightServer":{ + "enable":false, + "port":19333, + "priority":128 + }, + "color":{ + "imageToLedMappingType":"multicolor_mean", + "channelAdjustment":[ + { + "id":"default", + "leds":"*", + "white":[ + 255, + 255, + 255 + ], + "red":[ + 255, + 0, + 0 + ], + "green":[ + 0, + 255, + 0 + ], + "blue":[ + 0, + 0, + 255 + ], + "cyan":[ + 0, + 255, + 255 + ], + "magenta":[ + 255, + 0, + 255 + ], + "yellow":[ + 255, + 255, + 0 + ], + "gammaRed":2.2, + "gammaGreen":2.2, + "gammaBlue":2.2, + "backlightThreshold":0, + "backlightColored":false, + "brightness":100, + "brightnessCompensation":100, + "saturationGain":1.0, + "brightnessGain":1.0 + } + ] + }, + "device":{ + "type":"file", + "hardwareLedCount":1, + "autoStart":true, + "output":"/dev/null", + "colorOrder":"rgb", + "latchTime":0, + "rewriteTime":0, + "enableAttempts":6, + "enableAttemptsInterval":15 + }, + "effects":{ + "paths":[ + "$ROOT/custom-effects" + ], + "disable":[ + "" + ] + }, + "foregroundEffect":{ + "enable":true, + "type":"effect", + "color":[ + 0, + 0, + 255 + ], + "effect":"Rainbow swirl fast", + "duration_ms":3000 + }, + "instCapture":{ + "systemEnable":false, + "systemGrabberDevice":"NONE", + "systemPriority":250, + "v4lEnable":false, + "v4lGrabberDevice":"NONE", + "v4lPriority":240, + "audioEnable":false, + "audioGrabberDevice":"NONE", + "audioPriority":230 + }, + "ledConfig":{ + "classic":{ + "top":1, + "bottom":0, + "left":0, + "right":0, + "glength":0, + "gpos":0, + "position":0, + "reverse":false, + "hdepth":8, + "vdepth":5, + "overlap":0, + "edgegap":0, + "ptlh":0, + "ptlv":0, + "ptrh":100, + "ptrv":0, + "pblh":0, + "pblv":100, + "pbrh":100, + "pbrv":100 + }, + "matrix":{ + "ledshoriz":1, + "ledsvert":1, + "cabling":"snake", + "direction":"horizontal", + "start":"top-left" + } + }, + "leds":[ + { + "hmax":1, + "hmin":0, + "vmax":0.08, + "vmin":0 + } + ], + "smoothing":{ + "enable":true, + "type":"linear", + "time_ms":150, + "updateFrequency":25.0000, + "interpolationRate":25.0000, + "decay":1, + "dithering":false, + "updateDelay":0 + } + } +} diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp index 1b1bb739..731001e1 100644 --- a/src/hyperiond/hyperiond.cpp +++ b/src/hyperiond/hyperiond.cpp @@ -65,14 +65,14 @@ HyperionDaemon* HyperionDaemon::daemon = nullptr; -HyperionDaemon::HyperionDaemon(const QString& rootPath, QObject* parent, bool logLvlOverwrite, bool readonlyMode) +HyperionDaemon::HyperionDaemon(const QString& rootPath, QObject* parent, bool logLvlOverwrite) : QObject(parent), _log(Logger::getInstance("DAEMON")) - , _instanceManager(new HyperionIManager(rootPath, this, readonlyMode)) - , _settingsManager(new SettingsManager(GLOABL_INSTANCE_ID, this, readonlyMode)) // init settings, this settingsManager accesses global settings which are independent from instances + , _instanceManager(new HyperionIManager(this)) + , _settingsManager(new SettingsManager(GLOABL_INSTANCE_ID, this)) // init settings, this settingsManager accesses global settings which are independent from instances #if defined(ENABLE_EFFECTENGINE) , _pyInit(new PythonInit()) #endif - , _authManager(new AuthManager(this, readonlyMode)) + , _authManager(new AuthManager(this)) , _netOrigin(new NetOrigin(this)) , _currVideoMode(VideoMode::VIDEO_2D) { diff --git a/src/hyperiond/hyperiond.h b/src/hyperiond/hyperiond.h index cf64c134..e2f95ece 100644 --- a/src/hyperiond/hyperiond.h +++ b/src/hyperiond/hyperiond.h @@ -114,7 +114,7 @@ class HyperionDaemon : public QObject friend SysTray; public: - HyperionDaemon(const QString& rootPath, QObject *parent, bool logLvlOverwrite, bool readonlyMode = false); + HyperionDaemon(const QString& rootPath, QObject *parent, bool logLvlOverwrite); ~HyperionDaemon() override; /// diff --git a/src/hyperiond/main.cpp b/src/hyperiond/main.cpp index 6dfd35c4..a9dffe9f 100644 --- a/src/hyperiond/main.cpp +++ b/src/hyperiond/main.cpp @@ -1,3 +1,4 @@ + #include #include #include @@ -35,6 +36,7 @@ #include #include #include +#include #include <../../include/db/AuthTable.h> #include "detectProcess.h" @@ -108,11 +110,11 @@ int main(int argc, char** argv) // check if we are running already an instance // TODO Allow one session per user - #ifdef _WIN32 - const char* processName = "hyperiond.exe"; - #else - const char* processName = "hyperiond"; - #endif +#ifdef _WIN32 + const char* processName = "hyperiond.exe"; +#else + const char* processName = "hyperiond"; +#endif // Initialising QCoreApplication QScopedPointer app(createApplication(argc, argv)); @@ -131,15 +133,18 @@ int main(int argc, char** argv) BooleanOption & versionOption = parser.add (0x0, "version", "Show version information"); Option & userDataOption = parser.add