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