diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce2db672..59e3d5891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for Skydimo devices - Support gaps on Matrix Layout (#1696) - Support a configurable grabber inactive detection time interval (#1740) +- Support for dominant color processing on a full image which is applied to all LEDs (#1853) - 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 freely select source and target instances to be used by forwarder - Support to import, export and backup Hyperion's full configuration via the UI, JSON-API and commandline (`--importConfig, --exportConfig`) (#804) @@ -25,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Effects: Limit the maximum update rate to 200Hz - Systray: Support multiple instances - UI: Validate that key ports do not overlap across editors and pages +- UI: Provide additional details in error dialogue +- Http-Server: Support Cross-Origin Resource Sharing (CORS) (#1496) **JSON-API** - New subscription support for event updates, i.e. `Suspend, Resume, Idle, idleResume, Restart, Quit`. @@ -64,15 +67,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed that LED Buffer and Layout might get out of sync. - Fixed Screen capture error (#1824) - Fixed Provide custom forwarding targets is not possible (#1713) +- Fixed Last update of an effect event is not removed in sources overview **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 - Configuration requests do not any longer require a running instance +- Ensure that API service does not process commands when Hyperion is quitting - Fixed: Handling of IP4 addresses wrapped in IPv6 for external network connections - Fixed: Local Admin API Authentication rejects valid tokens (#1251) - Fixed: Create a proper API response, when Effects are not part of a build +- Fixed: Return correct mapping type for a running instance ### Removed diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index ed4faf8c3..8e24e7bbe 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -407,7 +407,7 @@ "edt_conf_enum_dl_verbose2": "Verbosity level 2", "edt_conf_enum_dl_verbose3": "Verbosity level 3", "edt_conf_enum_dominant_color": "Dominant Color - per LED", - "edt_conf_enum_dominant_color_advanced": "Dominant Color Advanced - per LED", + "edt_conf_enum_dominant_color_advanced": "Dominant Color (advanced) - per LED", "edt_conf_enum_effect": "Effect", "edt_conf_enum_gbr": "GBR", "edt_conf_enum_grb": "GRB", @@ -432,7 +432,9 @@ "edt_conf_enum_transeffect_sudden": "Sudden", "edt_conf_enum_udp_ddp": "DDP", "edt_conf_enum_udp_raw": "RAW", - "edt_conf_enum_unicolor_mean": "Mean Color Image - applied to all LEDs", + "edt_conf_enum_unicolor_mean": "Image's mean color - applied to all LEDs", + "edt_conf_enum_unicolor_dominant": "Image's dominant color - applied to all LEDs", + "edt_conf_enum_unicolor_dominant_advanced": "Image's dominant color (advanced) - applied to all LEDs", "edt_conf_flatbufServer_heading_title": "Flatbuffer Server", "edt_conf_flatbufServer_timeout_expl": "If no data is received for the given period, the component will be (soft) disabled.", "edt_conf_flatbufServer_timeout_title": "Timeout", @@ -1090,6 +1092,7 @@ "remote_input_ip": "IP:", "remote_input_label": "Source Selection", "remote_input_label_autoselect": "Auto Selection", + "remote_input_no_sources": "No sources available", "remote_input_origin": "Origin", "remote_input_owner": "Type", "remote_input_priority": "Priority", @@ -1099,11 +1102,13 @@ "remote_losthint": "Note: All changes will be lost after a restart.", "remote_maptype_intro": "Usually the LED layout defines which LED covers a specific picture area. You can change it here: $1.", "remote_maptype_label": "Mapping type", - "remote_maptype_label_dominant_color": "Dominant Color", - "remote_maptype_label_dominant_color_advanced": "Dominant Color Advanced", - "remote_maptype_label_multicolor_mean": "Mean Color Simple", - "remote_maptype_label_multicolor_mean_squared": "Mean Color Squared", - "remote_maptype_label_unicolor_mean": "Mean Color Image", + "remote_maptype_label_dominant_color": "Dominant Color - simple", + "remote_maptype_label_dominant_color_advanced": "Dominant Color - advanced", + "remote_maptype_label_multicolor_mean": "Mean Color - simple", + "remote_maptype_label_multicolor_mean_squared": "Mean Color - squared", + "remote_maptype_label_unicolor_dominant": "Dominant Color whole image - simple", + "remote_maptype_label_unicolor_dominant_advanced": "Dominant Color whole image - advanced", + "remote_maptype_label_unicolor_mean": "Mean Color whole image", "remote_optgroup_syseffets": "System Effects", "remote_optgroup_templates_custom": "User Templates", "remote_optgroup_templates_system": "System Templates", @@ -1228,6 +1233,9 @@ "wiz_yeelight_desc2": "Now choose which lamps should be added. The position assigns the lamp to a specific position on your \"picture\". Disabled lamps won't be added. To identify single lamps press the button on the right.", "wiz_yeelight_intro1": "This wizard configures Hyperion for the Yeelight system. Features are the Yeelights' auto detection, setting each light to a specific position on your picture or disable it and tune the Hyperion settings automatically! So in short: All you need are some clicks and you are done!", "wiz_yeelight_title": "Yeelight Wizard", - "wiz_yeelight_unsupported": "Unsupported" + "wiz_yeelight_unsupported": "Unsupported", + "ws_error_occured": "WebSocket error occured", + "ws_not_supported": "Websocket is not supported by your browser", + "ws_processing_exception": "Exception during Websocket message processing" } diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index ae4869885..2ba9ff900 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -158,9 +158,10 @@ $(document).ready(function () { let instanceId = window.currentHyperionInstance; const config = event.response.info; const { instanceIds } = config; - if (instanceIds.length !== 0) { + + if (Array.isArray(instanceIds) && instanceIds.length !== 0) { if (!instanceIds.includes(window.currentHyperionInstance)) { - // If instanceID is not valid try to switch to the first enabled or or fall back to the first instance configured + // If instanceID is not valid try to switch to the first enabled or fall back to the first instance configured const { instances } = config; const firstEnabledInstanceId = instances.find((instance) => instance.enabled)?.id; @@ -170,7 +171,6 @@ $(document).ready(function () { } else { instanceId = window.currentHyperionInstance = instanceIds[0]; } - } } @@ -242,11 +242,22 @@ $(document).ready(function () { $(window.hyperion).on("error", function (event) { //If we are getting an error "No Authorization" back with a set loginToken we will forward to new Login (Token is expired. //e.g.: hyperiond was started new in the meantime) - if (event.reason == "No Authorization" && getStorage("loginToken")) { + + const error = event.reason; + + if (error?.message === "No Authorization" && getStorage("loginToken")) { removeStorage("loginToken"); requestRequiresDefaultPasswortChange(); } else { - showInfoDialog("error", "Error", event.reason); + const errorDetails = []; + + if (error?.cmd) { + errorDetails.push(`Command: "${error.cmd}"`); + } + + errorDetails.push(error?.details || "No additional details."); + + showInfoDialog("error", "", error?.message || "Unknown error", errorDetails); } }); diff --git a/assets/webconfig/js/content_remote.js b/assets/webconfig/js/content_remote.js index 58f8ea0cc..5b463c47f 100644 --- a/assets/webconfig/js/content_remote.js +++ b/assets/webconfig/js/content_remote.js @@ -124,11 +124,17 @@ $(document).ready(function () { // Update input select options based on priorities function updateInputSelect() { // Clear existing elements - $('.sstbody').empty(); + $('.sstbody').empty().html(''); const prios = window.serverInfo.priorities; let clearAll = false; + if (prios.length === 0) { + $('.sstbody').append(`${$.i18n('remote_input_no_sources')}`); + $('#auto_btn').empty(); + return; + } + // Iterate over priorities for (let i = 0; i < prios.length; i++) { let origin = prios[i].origin ? prios[i].origin : "System"; diff --git a/assets/webconfig/js/hyperion.js b/assets/webconfig/js/hyperion.js index 6cb2e35fb..d79097c5d 100644 --- a/assets/webconfig/js/hyperion.js +++ b/assets/webconfig/js/hyperion.js @@ -62,6 +62,14 @@ function connectionLostDetection(type) { } } +// Utility function to sanitize strings for safe logging +function sanitizeForLog(input) { + if (typeof input !== 'string') return ''; + return input + .replace(/[\n\r\t]/g, ' ') // Replace newlines, carriage returns, and tabs with space + .replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); // Remove ANSI escape codes +} + setInterval(connectionLostDetection, 3000); // init websocket to hyperion and bind socket events to jquery events of $(hyperion) object @@ -126,31 +134,60 @@ function initWebSocket() { if (error == "Service Unavailable") { window.location.reload(); } else { - $(window.hyperion).trigger({ type: "error", reason: error }); + const errorData = Array.isArray(response.errorData) ? response.errorData : []; + + // Sanitize provided input + const logError = error.replace(/\n|\r/g, ""); + const logErrorData = JSON.stringify(errorData).replace(/[\r\n\t]/g, ' ') + console.log("[window.websocket::onmessage] ", logError, ", Description:", logErrorData); + + $(window.hyperion).trigger({ + type: "error", + reason: { + cmd: cmd, + message: error, + details: errorData.map((item) => item.description || "") + } + }); } - 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) + console.log("[window.websocket::onmessage] ", exception_error); + $(window.hyperion).trigger({ + type: "error", + reason: { + message: $.i18n("ws_processing_exception") + ": " + exception_error.message, + details: [exception_error.stack] + } + }); } }; window.websocket.onerror = function (error) { - $(window.hyperion).trigger({ type: "error", reason: error }); - console.log("[window.websocket::onerror] ", error) + console.log("[window.websocket::onerror] ", error); + $(window.hyperion).trigger({ + type: "error", + reason: { + message: $.i18n("ws_error_occured"), + details: [error] + } + }); }; } } else { - $(window.hyperion).trigger("error"); - alert("Websocket is not supported by your browser"); + $(window.hyperion).trigger({ + type: "error", + reason: { + message: $.i18n("ws_not_supported"), + details: [] + } + }); } } -function sendToHyperion(command, subcommand, msg) { +function sendToHyperion(command, subcommand, msg, instanceIds = null) { const tan = Math.floor(Math.random() * 1000); // Generate a transaction number // Build the base object @@ -164,6 +201,11 @@ function sendToHyperion(command, subcommand, msg) { message.subcommand = subcommand; } + // Add the instanceID(s) the command is to be applied to + if (instanceIds != null) { + message.instance = instanceIds; + } + // Merge the msg object into the final message if provided if (msg && typeof msg === "object") { Object.assign(message, msg); @@ -266,18 +308,18 @@ function requestTokenDelete(id) { } function requestInstanceRename(instance, name) { - sendToHyperion("instance", "saveName", { instance: Number(instance), name }); + sendToHyperion("instance", "saveName", { name }, Number(instance)); } function requestInstanceStartStop(instance, start) { if (start) - sendToHyperion("instance", "startInstance", { instance: Number(instance) }); + sendToHyperion("instance", "startInstance", {}, Number(instance)); else - sendToHyperion("instance", "stopInstance", { instance: Number(instance) }); + sendToHyperion("instance", "stopInstance", {}, Number(instance)); } function requestInstanceDelete(instance) { - sendToHyperion("instance", "deleteInstance", { instance: Number(instance) }); + sendToHyperion("instance", "deleteInstance", {}, Number(instance)); } function requestInstanceCreate(name) { @@ -285,7 +327,7 @@ function requestInstanceCreate(name) { } function requestInstanceSwitch(instance) { - sendToHyperion("instance", "switchTo", { instance: Number(instance) }); + sendToHyperion("instance", "switchTo", {}, Number(instance)); } function requestServerInfo(instance) { @@ -302,12 +344,8 @@ function requestServerInfo(instance) { "event-update" ] }; - - if (instance !== null && instance !== undefined && !isNaN(Number(instance))) { - data.instance = Number(instance); - } - sendToHyperion("serverinfo", "getInfo", data); + sendToHyperion("serverinfo", "getInfo", data, Number(instance)); return Promise.resolve(); } @@ -336,12 +374,12 @@ const requestServerConfig = { createFilter(globalTypes = [], instances = [], instanceTypes = []) { const filter = { configFilter: { - global: { types: globalTypes }, + global: { types: globalTypes } }, }; // Handle instances: remove "null" if present and add to filter if not empty - if (instances.length && !(instances.length === 1 && instances[0] === null)) { + if (instances.length) { filter.configFilter.instances = { ids: instances }; } @@ -371,40 +409,40 @@ function requestServerConfigReload() { sendToHyperion("config", "reload"); } -function requestLedColorsStart() { +function requestLedColorsStart(instanceId = window.currentHyperionInstance) { window.ledStreamActive = true; - sendToHyperion("ledcolors", "ledstream-start"); + sendToHyperion("ledcolors", "ledstream-start", {} , instanceId); } -function requestLedColorsStop() { +function requestLedColorsStop(instanceId = window.currentHyperionInstance) { window.ledStreamActive = false; - sendToHyperion("ledcolors", "ledstream-stop"); + sendToHyperion("ledcolors", "ledstream-stop", {} , instanceId); } -function requestLedImageStart() { +function requestLedImageStart(instanceId = window.currentHyperionInstance) { window.imageStreamActive = true; - sendToHyperion("ledcolors", "imagestream-start"); + sendToHyperion("ledcolors", "imagestream-start", {} , instanceId); } -function requestLedImageStop() { +function requestLedImageStop(instanceId = window.currentHyperionInstance) { window.imageStreamActive = false; - sendToHyperion("ledcolors", "imagestream-stop"); + sendToHyperion("ledcolors", "imagestream-stop", {} , instanceId); } -function requestPriorityClear(priority) { +function requestPriorityClear(priority, instanceIds = [window.currentHyperionInstance]) { if (typeof priority !== 'number') priority = INPUT.FG_PRIORITY; $(window.hyperion).trigger({ type: "stopBrowerScreenCapture" }); - sendToHyperion("clear", "", { priority }); + sendToHyperion("clear", "", { priority }, instanceIds); } -function requestClearAll() { +function requestClearAll(instanceIds = [window.currentHyperionInstance]) { $(window.hyperion).trigger({ type: "stopBrowerScreenCapture" }); - requestPriorityClear(-1) + requestPriorityClear(-1, instanceIds) } -function requestPlayEffect(name, duration) { +function requestPlayEffect(name, duration, instanceIds = [window.currentHyperionInstance]) { $(window.hyperion).trigger({ type: "stopBrowerScreenCapture" }); const data = { effect: { name }, @@ -412,10 +450,10 @@ function requestPlayEffect(name, duration) { duration: validateDuration(duration), origin: INPUT.ORIGIN, }; - sendToHyperion("effect", "", data); + sendToHyperion("effect", "", data, instanceIds); } -function requestSetColor(r, g, b, duration) { +function requestSetColor(r, g, b, duration, instanceIds = [window.currentHyperionInstance]) { $(window.hyperion).trigger({ type: "stopBrowerScreenCapture" }); const data = { color: [r, g, b], @@ -423,10 +461,10 @@ function requestSetColor(r, g, b, duration) { duration: validateDuration(duration), origin: INPUT.ORIGIN }; - sendToHyperion("color", "", data); + sendToHyperion("color", "", data, instanceIds); } -function requestSetImage(imagedata, duration, name) { +function requestSetImage(imagedata, duration, name, instanceIds = [window.currentHyperionInstance]) { const data = { imagedata, priority: INPUT.FG_PRIORITY, @@ -435,18 +473,18 @@ function requestSetImage(imagedata, duration, name) { origin: INPUT.ORIGIN, name }; - sendToHyperion("image", "", data); + sendToHyperion("image", "", data, instanceIds); } -function requestSetComponentState(component, state) { - sendToHyperion("componentstate", "", { componentstate: { component, state } }); +function requestSetComponentState(component, state, instanceIds = [window.currentHyperionInstance]) { + sendToHyperion("componentstate", "", { componentstate: { component, state } }, instanceIds); } -function requestSetSource(priority) { +function requestSetSource(priority, instanceIds = [window.currentHyperionInstance]) { if (priority == "auto") - sendToHyperion("sourceselect", "", { auto: true }); + sendToHyperion("sourceselect", "", { auto: true }, instanceIds); else - sendToHyperion("sourceselect", "", { priority }); + sendToHyperion("sourceselect", "", { priority }, instanceIds); } // Function to transform the legacy config into thee new API format @@ -516,7 +554,7 @@ function requestWriteEffect(name, script, args, imageData) { sendToHyperion("create-effect", "", data); } -function requestTestEffect(name, pythonScript, args, imageData) { +function requestTestEffect(name, pythonScript, args, imageData, instanceIds = [window.currentHyperionInstance]) { const data = { effect: { name, args }, priority: INPUT.FG_PRIORITY, @@ -524,7 +562,7 @@ function requestTestEffect(name, pythonScript, args, imageData) { pythonScript, imageData }; - sendToHyperion("effect", "", data); + sendToHyperion("effect", "", data, instanceIds); } function requestDeleteEffect(name) { @@ -541,19 +579,19 @@ function requestLoggingStop() { sendToHyperion("logging", "stop"); } -function requestMappingType(mappingType) { - sendToHyperion("processing", "", { mappingType }); +function requestMappingType(mappingType, instanceIds = [window.currentHyperionInstance]) { + sendToHyperion("processing", "", { mappingType }, instanceIds); } function requestVideoMode(newMode) { sendToHyperion("videomode", "", { videoMode: newMode }); } -function requestAdjustment(type, value, complete) { +function requestAdjustment(type, value, complete, instanceIds = [window.currentHyperionInstance]) { if (complete === true) - sendToHyperion("adjustment", "", { adjustment: type }); + sendToHyperion("adjustment", "", { adjustment: type }, useCurrentInstance); else - sendToHyperion("adjustment", "", { adjustment: { [type]: value } }); + sendToHyperion("adjustment", "", { adjustment: { [type]: value } }, instanceIds); } async function requestLedDeviceDiscovery(ledDeviceType, params) { diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index 91e93f453..029557991 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -307,7 +307,7 @@ function setClassByBool(obj, enable, class1, class2) { } } -function showInfoDialog(type, header, message) { +function showInfoDialog(type, header = "", message = "", details = []) { if (type == "success") { $('#id_body').html(''); if (header == "") @@ -321,9 +321,10 @@ function showInfoDialog(type, header, message) { $('#id_footer').html(''); } else if (type == "error") { - $('#id_body').html(''); - if (header == "") + $('#id_body').html(''); + if (header == "") { $('#id_body').append('

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

'); + } $('#id_footer').html(''); } else if (type == "select") { @@ -394,6 +395,24 @@ function showInfoDialog(type, header, message) { if (type == "select" || type == "iswitch") $('#id_body').append(''); + // Append details if available + if (Array.isArray(details) && details.length > 0) { + + // Create a container div for additional details with proper styles + const detailsContent = $('
').css({ + 'text-align': 'left', + 'white-space': 'pre-wrap', // Ensures newlines are respected + 'word-wrap': 'break-word', // Prevents long words from overflowing + 'margin-top': '15px' + }); + + detailsContent.append('
'); + details.forEach((desc, index) => { + detailsContent.append(document.createTextNode(`${index + 1}. ${desc}\n`)); + }); + $('#id_body').append(detailsContent); + } + if (getStorage("darkMode") == "on") $('#id_logo').attr("src", 'img/hyperion/logo_negativ.png'); diff --git a/assets/webconfig/js/wizards/LedDevice_philipshue.js b/assets/webconfig/js/wizards/LedDevice_philipshue.js index 8c1d4c14d..2635f2e88 100644 --- a/assets/webconfig/js/wizards/LedDevice_philipshue.js +++ b/assets/webconfig/js/wizards/LedDevice_philipshue.js @@ -20,6 +20,11 @@ const philipshueWizard = (() => { let isAPIv2Ready = true; let isEntertainmentReady = true; + function isSafeKey(key) { + const unsafeKeys = ['__proto__', 'prototype', 'constructor']; + return typeof key === 'string' && !unsafeKeys.includes(key); + } + function checkHueBridge(cb, hueUser) { const usr = (typeof hueUser != "undefined") ? hueUser : 'config'; if (usr === 'config') { @@ -349,6 +354,11 @@ const philipshueWizard = (() => { const ledType = 'philipshue'; const key = hostAddress; + if (!isSafeKey(key) || !isSafeKey(username)) { + cb(false, username); + return; + } + //Create ledType cache entry if (!devicesProperties[ledType]) { devicesProperties[ledType] = {}; diff --git a/doc/development/JSON-API _Commands_Overview.md b/doc/development/JSON-API _Commands_Overview.md index 89b5b64f4..fbbb6a5b0 100644 --- a/doc/development/JSON-API _Commands_Overview.md +++ b/doc/development/JSON-API _Commands_Overview.md @@ -52,8 +52,8 @@ _http/s Support_ | config | restoreconfig | Admin | No | No | Yes | | config | setconfig | Admin | No | No | Yes | | correction | | Yes | Single | Yes | Yes | -| create-effect | | Yes | Single | Yes | Yes | -| delete-effect | | Yes | Single | Yes | Yes | +| create-effect | | Yes | No | No | Yes | +| delete-effect | | Yes | No | No | Yes | | effect | | Yes | Multi | Yes | Yes | | image | | Yes | Multi | Yes | Yes | | inputsource | discover | Yes | No | No | Yes | @@ -71,9 +71,9 @@ _http/s Support_ | ledcolors | ledstream-start | Yes | Single | Yes | Yes | | ledcolors | ledstream-stop | Yes | Single | Yes | Yes | | leddevice | addAuthorization | Yes | Single | Yes | Yes | -| leddevice | discover | Yes | Single | Yes | Yes | -| leddevice | getProperties | Yes | Single | Yes | Yes | -| leddevice | identify | Yes | Single | Yes | Yes | +| leddevice | discover | Yes | No | No | Yes | +| leddevice | getProperties | Yes | No | No | Yes | +| leddevice | identify | Yes | No | No | Yes | | logging | start | Yes | No | No | Yes | | logging | stop | Yes | No | No | Yes | | processing | | Yes | Multi | Yes | Yes | diff --git a/include/api/JsonAPI.h b/include/api/JsonAPI.h index 83c74d8dd..766aab36e 100644 --- a/include/api/JsonAPI.h +++ b/include/api/JsonAPI.h @@ -421,6 +421,6 @@ private slots: // The JsonCallbacks instance which handles data subscription/notifications QSharedPointer _jsonCB; - + bool _isServiceAvailable; }; diff --git a/include/api/JsonApiCommand.h b/include/api/JsonApiCommand.h index 80caeae57..866d3529e 100644 --- a/include/api/JsonApiCommand.h +++ b/include/api/JsonApiCommand.h @@ -316,8 +316,8 @@ class ApiCommandRegister { { {"config", "restoreconfig"}, { Command::Config, SubCommand::RestoreConfig, Authorization::Admin, InstanceCmd::No, InstanceCmd::MustRun_No, NoListenerCmd::Yes } }, { {"config", "setconfig"}, { Command::Config, SubCommand::SetConfig, Authorization::Admin, InstanceCmd::No, InstanceCmd::MustRun_No, NoListenerCmd::Yes } }, { {"correction", ""}, { Command::Correction, SubCommand::Empty, Authorization::Yes, InstanceCmd::Single, InstanceCmd::MustRun_Yes, NoListenerCmd::Yes } }, - { {"create-effect", ""}, { Command::CreateEffect, SubCommand::Empty, Authorization::Yes, InstanceCmd::Single, InstanceCmd::MustRun_Yes, NoListenerCmd::Yes } }, - { {"delete-effect", ""}, { Command::DeleteEffect, SubCommand::Empty, Authorization::Yes, InstanceCmd::Single, InstanceCmd::MustRun_Yes, NoListenerCmd::Yes } }, + { {"create-effect", ""}, { Command::CreateEffect, SubCommand::Empty, Authorization::Yes, InstanceCmd::No, InstanceCmd::MustRun_No, NoListenerCmd::Yes } }, + { {"delete-effect", ""}, { Command::DeleteEffect, SubCommand::Empty, Authorization::Yes, InstanceCmd::No, InstanceCmd::MustRun_No, NoListenerCmd::Yes } }, { {"effect", ""}, { Command::Effect, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, InstanceCmd::MustRun_Yes, NoListenerCmd::Yes } }, { {"image", ""}, { Command::Image, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, InstanceCmd::MustRun_Yes, NoListenerCmd::Yes } }, { {"inputsource", "discover"}, { Command::InputSource, SubCommand::Discover, Authorization::Yes, InstanceCmd::No, InstanceCmd::MustRun_No, NoListenerCmd::Yes } }, @@ -335,9 +335,9 @@ class ApiCommandRegister { { {"ledcolors", "ledstream-start"}, { Command::LedColors, SubCommand::LedStreamStart, Authorization::Yes, InstanceCmd::Single, InstanceCmd::MustRun_Yes, NoListenerCmd::Yes } }, { {"ledcolors", "ledstream-stop"}, { Command::LedColors, SubCommand::LedStreamStop, Authorization::Yes, InstanceCmd::Single, InstanceCmd::MustRun_Yes, NoListenerCmd::Yes } }, { {"leddevice", "addAuthorization"}, { Command::LedDevice, SubCommand::AddAuthorization, Authorization::Yes, InstanceCmd::Single, InstanceCmd::MustRun_Yes, NoListenerCmd::Yes } }, - { {"leddevice", "discover"}, { Command::LedDevice, SubCommand::Discover, Authorization::Yes, InstanceCmd::Single, InstanceCmd::MustRun_Yes, NoListenerCmd::Yes } }, - { {"leddevice", "getProperties"}, { Command::LedDevice, SubCommand::GetProperties, Authorization::Yes, InstanceCmd::Single, InstanceCmd::MustRun_Yes, NoListenerCmd::Yes } }, - { {"leddevice", "identify"}, { Command::LedDevice, SubCommand::Identify, Authorization::Yes, InstanceCmd::Single, InstanceCmd::MustRun_Yes, NoListenerCmd::Yes } }, + { {"leddevice", "discover"}, { Command::LedDevice, SubCommand::Discover, Authorization::Yes, InstanceCmd::No, InstanceCmd::MustRun_No, NoListenerCmd::Yes } }, + { {"leddevice", "getProperties"}, { Command::LedDevice, SubCommand::GetProperties, Authorization::Yes, InstanceCmd::No, InstanceCmd::MustRun_No, NoListenerCmd::Yes } }, + { {"leddevice", "identify"}, { Command::LedDevice, SubCommand::Identify, Authorization::Yes, InstanceCmd::No, InstanceCmd::MustRun_No, NoListenerCmd::Yes } }, { {"logging", "start"}, { Command::Logging, SubCommand::Start, Authorization::Yes, InstanceCmd::No, InstanceCmd::MustRun_No, NoListenerCmd::Yes } }, { {"logging", "stop"}, { Command::Logging, SubCommand::Stop, Authorization::Yes, InstanceCmd::No, InstanceCmd::MustRun_No, NoListenerCmd::Yes } }, { {"processing", ""}, { Command::Processing, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, InstanceCmd::MustRun_Yes, NoListenerCmd::Yes } }, diff --git a/include/hyperion/ImageProcessor.h b/include/hyperion/ImageProcessor.h index 84f4e26c3..f588b221d 100644 --- a/include/hyperion/ImageProcessor.h +++ b/include/hyperion/ImageProcessor.h @@ -141,13 +141,19 @@ public slots: colors = _imageToLedColors->getUniLedColor(image); break; case 2: - colors = _imageToLedColors->getMeanLedColorSqrt(image); + colors = _imageToLedColors->getMeanSqrtLedColor(image); break; case 3: colors = _imageToLedColors->getDominantLedColor(image); break; case 4: - colors = _imageToLedColors->getDominantLedColorAdv(image); + colors = _imageToLedColors->getDominantUniLedColor(image); + break; + case 5: + colors = _imageToLedColors->getDominantAdvLedColor(image); + break; + case 6: + colors = _imageToLedColors->getDominantAdvUniLedColor(image); break; default: colors = _imageToLedColors->getMeanLedColor(image); @@ -186,14 +192,21 @@ public slots: _imageToLedColors->getUniLedColor(image, ledColors); break; case 2: - _imageToLedColors->getMeanLedColorSqrt(image, ledColors); + _imageToLedColors->getMeanSqrtLedColor(image, ledColors); break; case 3: _imageToLedColors->getDominantLedColor(image, ledColors); break; case 4: - _imageToLedColors->getDominantLedColorAdv(image, ledColors); + _imageToLedColors->getDominantUniLedColor(image, ledColors); + break; + case 5: + _imageToLedColors->getDominantAdvLedColor(image, ledColors); break; + case 6: + _imageToLedColors->getDominantAdvUniLedColor(image, ledColors); + break; + default: _imageToLedColors->getMeanLedColor(image, ledColors); } diff --git a/include/hyperion/ImageToLedsMap.h b/include/hyperion/ImageToLedsMap.h index d720581a5..2b33cf14e 100644 --- a/include/hyperion/ImageToLedsMap.h +++ b/include/hyperion/ImageToLedsMap.h @@ -128,10 +128,10 @@ namespace hyperion /// @return The vector containing the output /// template - std::vector getMeanLedColorSqrt(const Image & image) const + std::vector getMeanSqrtLedColor(const Image & image) const { std::vector colors(_colorsMap.size(), ColorRgb{0,0,0}); - getMeanLedColorSqrt(image, colors); + getMeanSqrtLedColor(image, colors); return colors; } @@ -143,7 +143,7 @@ namespace hyperion /// @param[out] ledColors The vector containing the output /// template - void getMeanLedColorSqrt(const Image & image, std::vector & ledColors) const + void getMeanSqrtLedColor(const Image & image, std::vector & ledColors) const { if(_colorsMap.size() != ledColors.size()) { @@ -239,7 +239,43 @@ namespace hyperion } /// - /// Determines the dominant color using a k-means algorithm for each LED using the LED area mapping given + /// Determines the dominant color of the image and assigns it to all LEDs + /// + /// @param[in] image The image from which to extract the led color + /// + /// @return The vector containing the output + /// + template + std::vector getDominantUniLedColor(const Image & image) const + { + std::vector colors(_colorsMap.size(), ColorRgb{0,0,0}); + getDominantUniLedColor(image, colors); + return colors; + } + + /// + /// Determines the dominant color of the image and assigns it to all LEDs + /// + /// @param[in] image The image from which to extract the LED colors + /// @param[out] ledColors The vector containing the output + /// + template + void getDominantUniLedColor(const Image & image, std::vector & ledColors) const + { + if(_colorsMap.size() != ledColors.size()) + { + Debug(_log, "ImageToLedsMap: colorsMap.size != ledColors.size -> %d != %d", _colorsMap.size(), ledColors.size()); + return; + } + + // calculate dominant color + const ColorRgb color = calculateDominantColor(image); + //Update all LEDs with same color + std::fill(ledColors.begin(),ledColors.end(), color); + } + + /// + /// Determines the dominant color using a k-means algorithm for each LED using the LED area mapping given /// at construction. /// /// @param[in] image The image from which to extract the LED color @@ -247,10 +283,10 @@ namespace hyperion /// @return The vector containing the output /// template - std::vector getDominantLedColorAdv(const Image & image) const + std::vector getDominantAdvLedColor(const Image & image) const { std::vector colors(_colorsMap.size(), ColorRgb{0,0,0}); - getDominantLedColorAdv(image, colors); + getDominantAdvLedColor(image, colors); return colors; } @@ -262,7 +298,7 @@ namespace hyperion /// @param[out] ledColors The vector containing the output /// template - void getDominantLedColorAdv(const Image & image, std::vector & ledColors) const + void getDominantAdvLedColor(const Image & image, std::vector & ledColors) const { // Sanity check for the number of LEDs if(_colorsMap.size() != ledColors.size()) @@ -280,6 +316,42 @@ namespace hyperion } } + /// + /// Determines the dominant color of the image using a k-means algorithm and assigns it to all LEDs + /// + /// @param[in] image The image from which to extract the led color + /// + /// @return The vector containing the output + /// + template + std::vector getDominantAdvUniLedColor(const Image & image) const + { + std::vector colors(_colorsMap.size(), ColorRgb{0,0,0}); + getDominantAdvUniLedColor(image, colors); + return colors; + } + + /// + /// Determines the dominant color of the image using a k-means algorithm and assigns it to all LEDs + /// + /// @param[in] image The image from which to extract the LED colors + /// @param[out] ledColors The vector containing the output + /// + template + void getDominantAdvUniLedColor(const Image & image, std::vector & ledColors) const + { + if(_colorsMap.size() != ledColors.size()) + { + Debug(_log, "ImageToLedsMap: colorsMap.size != ledColors.size -> %d != %d", _colorsMap.size(), ledColors.size()); + return; + } + + // calculate dominant color + const ColorRgb color = calculateDominantColorAdv(image); + //Update all LEDs with same color + std::fill(ledColors.begin(),ledColors.end(), color); + } + private: Logger* _log; diff --git a/include/utils/ImageResampler.h b/include/utils/ImageResampler.h index 4f6dab462..e1affba2a 100644 --- a/include/utils/ImageResampler.h +++ b/include/utils/ImageResampler.h @@ -17,7 +17,7 @@ class ImageResampler void setCropping(int cropLeft, int cropRight, int cropTop, int cropBottom); void setVideoMode(VideoMode mode) { _videoMode = mode; } void setFlipMode(FlipMode mode) { _flipMode = mode; } - void processImage(const uint8_t * data, int width, int height, int lineLength, PixelFormat pixelFormat, Image & outputImage) const; + void processImage(const uint8_t * data, int width, int height, size_t lineLength, PixelFormat pixelFormat, Image & outputImage) const; private: int _horizontalDecimation; diff --git a/libsrc/api/JSONRPC_schema/schema-adjustment.json b/libsrc/api/JSONRPC_schema/schema-adjustment.json index 3afe5bcd4..c96e07f8e 100644 --- a/libsrc/api/JSONRPC_schema/schema-adjustment.json +++ b/libsrc/api/JSONRPC_schema/schema-adjustment.json @@ -7,11 +7,14 @@ "required" : true, "enum" : ["adjustment"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {}, - "minItems": 1 + "items": { + "type": "integer", + "minimum": 0, + "maximum": 254 + }, + "uniqueItems": true }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-clear.json b/libsrc/api/JSONRPC_schema/schema-clear.json index b55be0a13..495388e7d 100644 --- a/libsrc/api/JSONRPC_schema/schema-clear.json +++ b/libsrc/api/JSONRPC_schema/schema-clear.json @@ -7,11 +7,14 @@ "required" : true, "enum" : ["clear"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {}, - "minItems": 1 + "items": { + "type": "integer", + "minimum": 0, + "maximum": 254 + }, + "uniqueItems": true }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-clearall.json b/libsrc/api/JSONRPC_schema/schema-clearall.json index 5d5d2d22c..32733bd70 100644 --- a/libsrc/api/JSONRPC_schema/schema-clearall.json +++ b/libsrc/api/JSONRPC_schema/schema-clearall.json @@ -7,11 +7,14 @@ "required" : true, "enum" : ["clearall"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {}, - "minItems": 1 + "items": { + "type": "integer", + "minimum": 0, + "maximum": 254 + }, + "uniqueItems": true }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-color.json b/libsrc/api/JSONRPC_schema/schema-color.json index eeeba069a..1eda846a0 100644 --- a/libsrc/api/JSONRPC_schema/schema-color.json +++ b/libsrc/api/JSONRPC_schema/schema-color.json @@ -7,11 +7,14 @@ "required" : true, "enum" : ["color"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {}, - "minItems": 1 + "items": { + "type": "integer", + "minimum": 0, + "maximum": 254 + }, + "uniqueItems": true }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-componentstate.json b/libsrc/api/JSONRPC_schema/schema-componentstate.json index 10ca3bb62..3c89db11f 100644 --- a/libsrc/api/JSONRPC_schema/schema-componentstate.json +++ b/libsrc/api/JSONRPC_schema/schema-componentstate.json @@ -9,11 +9,14 @@ "required" : true, "enum" : ["componentstate"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {}, - "minItems": 1 + "items": { + "type": "integer", + "minimum": 0, + "maximum": 254 + }, + "uniqueItems": true }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-config.json b/libsrc/api/JSONRPC_schema/schema-config.json index ceefac031..87ea641a2 100644 --- a/libsrc/api/JSONRPC_schema/schema-config.json +++ b/libsrc/api/JSONRPC_schema/schema-config.json @@ -1,41 +1,46 @@ { "type":"object", "required":true, - "properties":{ + "properties": { "command": { - "type" : "string", - "required" : true, - "enum" : ["config"] + "type": "string", + "required": true, + "enum": ["config"] }, "subcommand": { - "type" : "string", - "required" : true, - "enum" : ["getconfig","getschema","setconfig","restoreconfig","reload"] + "type": "string", + "required": true, + "enum": ["getconfig","getschema","setconfig","restoreconfig","reload"] }, - "tan" : { - "type" : "integer" + "tan": { + "type": "integer" }, "configFilter": { - "global" : { + "global": { "types": { "type": "array", "required": false, - "items" : { + "items": { "type" : "string" } } }, - "instances" : { - "ids" : { + "instances": { + "ids": { "type": "array", "required": true, - "items" : {}, + "items": { + "type" : "integer", + "minimum": 0, + "maximum": 254 + }, + "uniqueItems": true, "minItems": 1 }, "types": { "type": "array", "required": false, - "items" :{ + "items": { "type" : "string" } } diff --git a/libsrc/api/JSONRPC_schema/schema-create-effect.json b/libsrc/api/JSONRPC_schema/schema-create-effect.json index 2eceb9a19..97b55056f 100644 --- a/libsrc/api/JSONRPC_schema/schema-create-effect.json +++ b/libsrc/api/JSONRPC_schema/schema-create-effect.json @@ -7,11 +7,6 @@ "required" : true, "enum" : ["create-effect"] }, - "instance" : { - "type" : "integer", - "minimum": 0, - "maximum": 255 - }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-delete-effect.json b/libsrc/api/JSONRPC_schema/schema-delete-effect.json index bdbdee7ce..8279f8544 100644 --- a/libsrc/api/JSONRPC_schema/schema-delete-effect.json +++ b/libsrc/api/JSONRPC_schema/schema-delete-effect.json @@ -8,11 +8,6 @@ "required" : true, "enum" : ["delete-effect"] }, - "instance" : { - "type" : "integer", - "minimum": 0, - "maximum": 255 - }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-effect.json b/libsrc/api/JSONRPC_schema/schema-effect.json index 5bd0aff6e..7109e22be 100644 --- a/libsrc/api/JSONRPC_schema/schema-effect.json +++ b/libsrc/api/JSONRPC_schema/schema-effect.json @@ -7,10 +7,14 @@ "required" : true, "enum" : ["effect"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {}, + "items": { + "type": "integer", + "minimum": 0, + "maximum": 254 + }, + "uniqueItems": true, "minItems": 1 }, "tan" : { diff --git a/libsrc/api/JSONRPC_schema/schema-image.json b/libsrc/api/JSONRPC_schema/schema-image.json index fbd2ff402..707d4f6fc 100644 --- a/libsrc/api/JSONRPC_schema/schema-image.json +++ b/libsrc/api/JSONRPC_schema/schema-image.json @@ -7,11 +7,14 @@ "required" : true, "enum" : ["image"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {}, - "minItems": 1 + "items": { + "type": "integer", + "minimum": 0, + "maximum": 254 + }, + "uniqueItems": true }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-instance.json b/libsrc/api/JSONRPC_schema/schema-instance.json index 234e4c3e4..e0601cd1c 100644 --- a/libsrc/api/JSONRPC_schema/schema-instance.json +++ b/libsrc/api/JSONRPC_schema/schema-instance.json @@ -18,7 +18,7 @@ "instance" : { "type" : "integer", "minimum" : 0, - "maximum" : 255 + "maximum" : 254 }, "name": { "type": "string", diff --git a/libsrc/api/JSONRPC_schema/schema-instancedata.json b/libsrc/api/JSONRPC_schema/schema-instancedata.json index 3910d37c0..6787e8ecd 100644 --- a/libsrc/api/JSONRPC_schema/schema-instancedata.json +++ b/libsrc/api/JSONRPC_schema/schema-instancedata.json @@ -15,7 +15,7 @@ "instance" : { "type": "integer", "minimum": 0, - "maximum": 255 + "maximum": 254 }, "format" : { "type" : "string", diff --git a/libsrc/api/JSONRPC_schema/schema-ledcolors.json b/libsrc/api/JSONRPC_schema/schema-ledcolors.json index 086914ec5..c1b8da3ca 100644 --- a/libsrc/api/JSONRPC_schema/schema-ledcolors.json +++ b/libsrc/api/JSONRPC_schema/schema-ledcolors.json @@ -10,7 +10,7 @@ "instance" : { "type" : "integer", "minimum": 0, - "maximum": 255 + "maximum": 254 }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-leddevice.json b/libsrc/api/JSONRPC_schema/schema-leddevice.json index ac74342cf..5065ea0dd 100644 --- a/libsrc/api/JSONRPC_schema/schema-leddevice.json +++ b/libsrc/api/JSONRPC_schema/schema-leddevice.json @@ -7,9 +7,6 @@ "required" : true, "enum" : ["leddevice"] }, - "instance" : { - "type" : "integer" - }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-processing.json b/libsrc/api/JSONRPC_schema/schema-processing.json index 0ca7616d8..0e00c7815 100644 --- a/libsrc/api/JSONRPC_schema/schema-processing.json +++ b/libsrc/api/JSONRPC_schema/schema-processing.json @@ -7,18 +7,21 @@ "required" : true, "enum" : ["processing"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {}, - "minItems": 1 + "items": { + "type": "integer", + "minimum": 0, + "maximum": 254 + }, + "uniqueItems": true }, "tan" : { "type" : "integer" }, "mappingType": { "type" : "string", - "enum" : ["multicolor_mean", "unicolor_mean", "multicolor_mean_squared", "dominant_color", "dominant_color_advanced"] + "enum" : ["multicolor_mean","multicolor_mean_squared", "unicolor_mean", "dominant_color", "unicolor_dominant", "dominant_color_advanced", "unicolor_dominant_advanced"] } }, "additionalProperties": false diff --git a/libsrc/api/JSONRPC_schema/schema-serverinfo.json b/libsrc/api/JSONRPC_schema/schema-serverinfo.json index 74b414532..ab6cac85d 100644 --- a/libsrc/api/JSONRPC_schema/schema-serverinfo.json +++ b/libsrc/api/JSONRPC_schema/schema-serverinfo.json @@ -14,7 +14,7 @@ "instance" : { "type" : "integer", "minimum": 0, - "maximum": 255 + "maximum": 254 }, "data": { "type": ["null", "array"], diff --git a/libsrc/api/JSONRPC_schema/schema-sourceselect.json b/libsrc/api/JSONRPC_schema/schema-sourceselect.json index 8763595c3..352480fcc 100644 --- a/libsrc/api/JSONRPC_schema/schema-sourceselect.json +++ b/libsrc/api/JSONRPC_schema/schema-sourceselect.json @@ -7,11 +7,14 @@ "required" : true, "enum" : ["sourceselect"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {}, - "minItems": 1 + "items": { + "type": "integer", + "minimum": 0, + "maximum": 254 + }, + "uniqueItems": true }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 8d75c75f3..055e0af18 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -84,10 +84,20 @@ JsonAPI::JsonAPI(QString peerAddress, Logger *log, bool localConnection, QObject ,_noListener(noListener) ,_peerAddress (std::move(peerAddress)) ,_jsonCB (nullptr) + ,_isServiceAvailable(false) { Q_INIT_RESOURCE(JSONRPC_schemas); qRegisterMetaType("Event"); + + connect(EventHandler::getInstance().data(), &EventHandler::signalEvent, [log, this](const Event &event) { + if (event == Event::Quit) + { + _isServiceAvailable = false; + Info(log, "JSON-API service stopped"); + } + }); + _jsonCB = QSharedPointer(new JsonCallbacks( _log, _peerAddress, parent)); } @@ -117,6 +127,9 @@ void JsonAPI::initialize() // notify the forwarder about a jsonMessageForward request QObject::connect(this, &JsonAPI::forwardJsonMessage, GlobalSignals::getInstance(), &GlobalSignals::forwardJsonMessage, Qt::UniqueConnection); #endif + + Info(_log, "JSON-API service is ready to process requests"); + _isServiceAvailable = true; } bool JsonAPI::handleInstanceSwitch(quint8 instanceID, bool /*forced*/) @@ -183,6 +196,13 @@ void JsonAPI::handleMessage(const QString &messageString, const QString &httpAut return; } + // Do not further handle requests, if service is not available + if (!_isServiceAvailable) + { + sendErrorReply("Service Unavailable", cmd); + return; + } + if (_noListener) { setAuthorization(false); @@ -260,8 +280,8 @@ void JsonAPI::handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject QJsonArray instances; const QJsonValue instanceElement = message.value("instance"); - // Extract instance(s) from the message - if (!(instanceElement.isUndefined() && instanceElement.isNull())) + // Extract instanceIds(s) from the message + if (!(instanceElement.isUndefined() || instanceElement.isNull())) { if (instanceElement.isDouble()) { @@ -271,30 +291,44 @@ void JsonAPI::handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject instances = instanceElement.toArray(); } } + else + { + // If no instance element is given use the one that was switched to before + if (instanceElement.isUndefined() && _currInstanceIndex != NO_INSTANCE_ID) + { + instances.append(_currInstanceIndex); + } + else + { + sendErrorReply("No instance(s) given nor switched to a valid one yet", cmd); + return; + } + } InstanceCmd::MustRun const isRunningInstanceRequired = cmd.getInstanceMustRun(); QSet const runningInstanceIds = _instanceManager->getRunningInstanceIdx(); QSet instanceIds; QStringList errorDetails; - // Determine instance IDs, if not provided or "all" is given - if (instanceElement.isUndefined() || instanceElement.isNull() || instances.contains("all")) + // Determine instance IDs, if empty array provided apply command to all instances + if (instances.isEmpty()) { instanceIds = (isRunningInstanceRequired == InstanceCmd::MustRun_Yes) ? runningInstanceIds : _instanceManager->getInstanceIds(); } else { + QSet const configuredInstanceIds = _instanceManager->getInstanceIds(); + //Resolve instances provided and test, if they need to be running for (const auto &instance : std::as_const(instances)) { - if (!instance.isDouble()) + quint8 const instanceId = static_cast(instance.toInt()); + if (!configuredInstanceIds.contains(instanceId)) { - errorDetails.append("Not a valid instance: " + instance.toVariant().toString()); + errorDetails.append(QString("Not a valid instance id: [%1]").arg(instanceId)); continue; } - quint8 const instanceId = static_cast(instance.toInt()); - if (isRunningInstanceRequired == InstanceCmd::MustRun_Yes && !runningInstanceIds.contains(instanceId)) { errorDetails.append(QString("Instance [%1] is not running, but the (sub-) command requires a running instance.").arg(instance.toVariant().toString())); @@ -306,7 +340,7 @@ void JsonAPI::handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject } } - // Handle cases where no valid instances are found + // Handle cases where no instances are found if (instanceIds.isEmpty()) { if (errorDetails.isEmpty() && (cmd.getInstanceCmdType() == InstanceCmd::No_or_Single || cmd.getInstanceCmdType() == InstanceCmd::No_or_Multi) ) @@ -314,13 +348,15 @@ void JsonAPI::handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject handleCommand(cmd, message); return; } - errorDetails.append("No instance(s) provided, but required"); + errorDetails.append("No valid instance(s) provided"); } - - // Check if multiple instances are allowed - if (instanceIds.size() > 1 && cmd.getInstanceCmdType() != InstanceCmd::Multi) + else { - errorDetails.append("Command does not support multiple instances"); + // Check if multiple instances are allowed + if (instanceIds.size() > 1 && cmd.getInstanceCmdType() != InstanceCmd::Multi) + { + errorDetails.append("Command does not support multiple instances"); + } } // If there are errors, send a response and exit @@ -331,6 +367,7 @@ void JsonAPI::handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject } // Execute the command for each valid instance + quint8 const currentInstance = _currInstanceIndex; for (const auto &instanceId : std::as_const(instanceIds)) { if (isRunningInstanceRequired == InstanceCmd::MustRun_Yes || _currInstanceIndex == NO_INSTANCE_ID) @@ -345,6 +382,12 @@ void JsonAPI::handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject handleCommand(cmd, message); } } + + //Switch back to current instance, if command was executed against multiple instances + if (currentInstance != _currInstanceIndex && (cmd.getInstanceCmdType() == InstanceCmd::Multi || cmd.getInstanceCmdType() == InstanceCmd::No_or_Multi)) + { + handleInstanceSwitch(currentInstance); + } } void JsonAPI::handleCommand(const JsonApiCommand& cmd, const QJsonObject &message) @@ -1462,15 +1505,31 @@ void JsonAPI::handleInstanceCommand(const QJsonObject &message, const JsonApiCom QString replyMsg; QStringList errorDetails; - const quint8 instanceID = static_cast(message["instance"].toInt()); - const QString instanceName = _instanceManager->getInstanceName(instanceID); - const QString &name = message["name"].toString(); + QJsonValue const instanceValue = message["instance"]; - if(cmd.subCommand != SubCommand::CreateInstance && !_instanceManager->doesInstanceExist(instanceID)) + const quint8 instanceID = static_cast(instanceValue.toInt()); + if(cmd.subCommand != SubCommand::CreateInstance) { - sendErrorReply( QString("Hyperion instance [%1] does not exist.").arg(instanceID), cmd); - return; + QString errorText; + if (instanceValue.isUndefined()) + { + errorText = "No instance provided, but required"; + + } else if (!_instanceManager->doesInstanceExist(instanceID)) + { + errorText = QString("Hyperion instance [%1] does not exist.").arg(instanceID); + } + + if (!errorText.isEmpty()) + { + sendErrorReply( errorText, cmd); + return; + } } + + const QString instanceName = _instanceManager->getInstanceName(instanceID); + const QString &name = message["name"].toString(); + switch (cmd.subCommand) { case SubCommand::SwitchTo: if (handleInstanceSwitch(instanceID)) @@ -1703,10 +1762,6 @@ QJsonObject JsonAPI::getBasicCommandReply(bool success, const QString &command, reply["command"] = command; reply["tan"] = tan; - if ((_currInstanceIndex != NO_INSTANCE_ID) && instanceCmdType != InstanceCmd::No) - { - reply["instance"] = _currInstanceIndex; - } return reply; } diff --git a/libsrc/api/JsonInfo.cpp b/libsrc/api/JsonInfo.cpp index 49fc30076..c62a9a430 100644 --- a/libsrc/api/JsonInfo.cpp +++ b/libsrc/api/JsonInfo.cpp @@ -46,7 +46,7 @@ QJsonObject JsonInfo::getInfo(const Hyperion* hyperion, Logger* log) { info["priorities_autoselect"] = hyperion->sourceAutoSelectEnabled(); info["videomode"] = QString(videoMode2String(hyperion->getCurrentVideoMode())); - info["imageToLedMappingType"] = ImageProcessor::mappingTypeToStr(0); + info["imageToLedMappingType"] = ImageProcessor::mappingTypeToStr(hyperion->getLedMappingType()); info["leds"] = hyperion->getSetting(settings::LEDS).array(); } else diff --git a/libsrc/flatbufserver/FlatBufferClient.cpp b/libsrc/flatbufserver/FlatBufferClient.cpp index 8c50db9af..e1c88af37 100644 --- a/libsrc/flatbufserver/FlatBufferClient.cpp +++ b/libsrc/flatbufserver/FlatBufferClient.cpp @@ -15,7 +15,7 @@ namespace { const int FLATBUFFER_PRIORITY_MIN = 100; const int FLATBUFFER_PRIORITY_MAX = 199; -const int FLATBUFFER_MAX_MSG_LENGTH = 10'000'000; + } //End of constants FlatBufferClient::FlatBufferClient(QTcpSocket* socket, int timeout, QObject *parent) @@ -50,78 +50,33 @@ void FlatBufferClient::readyRead() { if (_socket == nullptr) { return; } - while (_socket->bytesAvailable() > 0) - { - _timeoutTimer->start(); - _receiveBuffer += _socket->readAll(); - processNextMessage(); - } -} - -bool FlatBufferClient::processNextMessageInline() -{ - if (_processingMessage) { return false; } // Avoid re-entrancy - - // Wait for at least 4 bytes to read the message size - if (_receiveBuffer.size() < 4) { - return false; - } - - _processingMessage = true; - - // Directly read message size (no memcpy) - const uint8_t* raw = reinterpret_cast(_receiveBuffer.constData()); - uint32_t messageSize = (raw[0] << 24) | (raw[1] << 16) | (raw[2] << 8) | raw[3]; - - // Validate message size - if (messageSize == 0 || messageSize > FLATBUFFER_MAX_MSG_LENGTH) - { - Warning(_log, "Invalid message size: %d - dropping received data", messageSize); - _processingMessage = false; - return true; - } + _timeoutTimer->start(); + _receiveBuffer += _socket->readAll(); - // Wait for full message - if (_receiveBuffer.size() < static_cast(messageSize + 4)) + // check if we can read a header + while(_receiveBuffer.size() >= 4) { - _processingMessage = false; - return false; - } - - // Extract the message and remove it from the buffer (no copying) - const uint8_t* msgData = reinterpret_cast(_receiveBuffer.constData() + 4); - flatbuffers::Verifier verifier(msgData, messageSize); + // Directly read message size + const uint8_t* raw = reinterpret_cast(_receiveBuffer.constData()); + uint32_t const messageSize = (raw[0] << 24) | (raw[1] << 16) | (raw[2] << 8) | raw[3]; - if (!hyperionnet::VerifyRequestBuffer(verifier)) { - Error(_log, "Invalid FlatBuffer message received"); - sendErrorReply("Invalid FlatBuffer message received"); - _processingMessage = false; + // check if we can read a complete message + if((uint32_t) _receiveBuffer.size() < messageSize + 4) { return; } - // Clear the buffer in case of an invalid message - _receiveBuffer.clear(); - return true; - } + // extract message without header and remove header + msg from buffer :: QByteArray::remove() does not return the removed data + const uint8_t* msgData = reinterpret_cast(_receiveBuffer.constData() + 4); + _receiveBuffer.remove(0, messageSize + 4); - // Invoke message handling - QMetaObject::invokeMethod(this, [this, msgData, messageSize]() { - handleMessage(hyperionnet::GetRequest(msgData)); - _processingMessage = false; + flatbuffers::Verifier verifier(msgData, messageSize); - // Remove the processed message from the buffer (header + body) - _receiveBuffer.remove(0, messageSize + 4); // Clear the processed message + header - - // Continue processing the next message - processNextMessage(); - }); - - return true; -} + if (!hyperionnet::VerifyRequestBuffer(verifier)) { + Error(_log, "Invalid FlatBuffer message received"); + sendErrorReply("Invalid FlatBuffer message received"); + continue; + } -void FlatBufferClient::processNextMessage() -{ - // Run the message processing inline until the buffer is empty or we can't process further - while (processNextMessageInline()) { - // Keep processing as long as we can + const auto *message = hyperionnet::GetRequest(msgData); + handleMessage(message); } } @@ -222,8 +177,8 @@ void FlatBufferClient::handleImageCommand(const hyperionnet::Image *image) const auto* img = static_cast(image->data_as_RawImage()); // Read image properties directly from FlatBuffer - const int width = img->width(); - const int height = img->height(); + int32_t const width = img->width(); + int32_t const height = img->height(); const auto* data = img->data(); if (width <= 0 || height <= 0 || data == nullptr || data->size() == 0) @@ -233,8 +188,8 @@ void FlatBufferClient::handleImageCommand(const hyperionnet::Image *image) } // Check consistency of image data size - const int dataSize = data->size(); - const int bytesPerPixel = dataSize / (width * height); + auto dataSize = data->size(); + int const bytesPerPixel = dataSize / (width * height); if (bytesPerPixel != 3 && bytesPerPixel != 4) { sendErrorReply("Size of image data does not match with the width and height"); @@ -253,9 +208,9 @@ void FlatBufferClient::handleImageCommand(const hyperionnet::Image *image) { const auto* img = static_cast(image->data_as_NV12Image()); - const int width = img->width(); - const int height = img->height(); - const auto* data_y = img->data_y(); + int32_t const width = img->width(); + int32_t const height = img->height(); + const auto* const data_y = img->data_y(); const auto* data_uv = img->data_uv(); if (width <= 0 || height <= 0 || data_y == nullptr || data_uv == nullptr || @@ -266,10 +221,10 @@ void FlatBufferClient::handleImageCommand(const hyperionnet::Image *image) } // Combine Y and UV into one contiguous buffer (reuse class member buffer) - const size_t y_size = data_y->size(); - const size_t uv_size = data_uv->size(); + size_t const y_size = data_y->size(); + size_t const uv_size = data_uv->size(); - size_t required_size = y_size + uv_size; + size_t const required_size = y_size + uv_size; if (_combinedNv12Buffer.capacity() < required_size) { _combinedNv12Buffer.reserve(required_size); @@ -278,7 +233,7 @@ void FlatBufferClient::handleImageCommand(const hyperionnet::Image *image) std::memcpy(_combinedNv12Buffer.data() + y_size, data_uv->data(), uv_size); // Determine stride for Y - const int stride_y = img->stride_y() > 0 ? img->stride_y() : width; + int32_t const stride_y = img->stride_y() > 0 ? img->stride_y() : width; // Resize only when needed if (_imageOutputBuffer.width() != width || _imageOutputBuffer.height() != height) @@ -335,7 +290,7 @@ void FlatBufferClient::sendMessage(const uint8_t* data, size_t size) // write message _socket->write(reinterpret_cast(header), sizeof(header)); - _socket->write(reinterpret_cast(data), size); + _socket->write(reinterpret_cast(data), static_cast(size)); _socket->flush(); } @@ -358,13 +313,13 @@ void FlatBufferClient::sendErrorReply(const QString& error) } inline void FlatBufferClient::processRawImage(const uint8_t* buffer, - int width, - int height, + int32_t width, + int32_t height, int bytesPerPixel, - ImageResampler& resampler, + const ImageResampler& resampler, Image& outputImage) { - int const lineLength = width * bytesPerPixel; + const size_t lineLength = static_cast(width) * bytesPerPixel; PixelFormat const pixelFormat = (bytesPerPixel == 4) ? PixelFormat::RGB32 : PixelFormat::RGB24; resampler.processImage( @@ -378,13 +333,13 @@ inline void FlatBufferClient::processRawImage(const uint8_t* buffer, } inline void FlatBufferClient::processNV12Image(const uint8_t* nv12_data, - int width, - int height, - int stride_y, - ImageResampler& resampler, + int32_t width, + int32_t height, + int32_t stride_y, + const ImageResampler& resampler, Image& outputImage) { - PixelFormat pixelFormat = PixelFormat::NV12; + PixelFormat const pixelFormat = PixelFormat::NV12; resampler.processImage( nv12_data, // Combined NV12 buffer diff --git a/libsrc/flatbufserver/FlatBufferClient.h b/libsrc/flatbufserver/FlatBufferClient.h index c4271459c..4a8886d53 100644 --- a/libsrc/flatbufserver/FlatBufferClient.h +++ b/libsrc/flatbufserver/FlatBufferClient.h @@ -122,8 +122,7 @@ private slots: /// void handleNotImplemented(); - void processNextMessage(); - bool processNextMessageInline(); + bool processNextMessage(); /// /// Send a message to the connected client @@ -144,8 +143,8 @@ private slots: /// void sendErrorReply(const QString& error); - void processRawImage(const uint8_t* buffer, int width, int height, int bytesPerPixel, ImageResampler& resampler, Image& outputImage); - void processNV12Image(const uint8_t* nv12_data, int width, int height, int stride_y, ImageResampler& resampler, Image& outputImage); + void processRawImage(const uint8_t* buffer, int32_t width, int32_t height, int bytesPerPixel, const ImageResampler& resampler, Image& outputImage); + void processNV12Image(const uint8_t* nv12_data, int32_t width, int32_t height, int32_t stride_y, const ImageResampler& resampler, Image& outputImage); private: Logger * _log; @@ -158,8 +157,8 @@ private slots: QByteArray _receiveBuffer; - Image _imageOutputBuffer; ImageResampler _imageResampler; + Image _imageOutputBuffer; std::vector _combinedNv12Buffer; // Flatbuffers builder diff --git a/libsrc/flatbufserver/FlatBufferConnection.cpp b/libsrc/flatbufserver/FlatBufferConnection.cpp index fb6d3ef62..df4ecd2fa 100644 --- a/libsrc/flatbufserver/FlatBufferConnection.cpp +++ b/libsrc/flatbufserver/FlatBufferConnection.cpp @@ -124,10 +124,10 @@ void FlatBufferConnection::setImage(const Image &image) if (!isClientRegistered()) return; const uint8_t* buffer = reinterpret_cast(image.memptr()); - size_t bufferSize = image.size(); + qsizetype bufferSize = image.size(); // Convert the buffer into QByteArray - QByteArray imageData = QByteArray::fromRawData(reinterpret_cast(buffer), static_cast(bufferSize)); + QByteArray imageData = QByteArray::fromRawData(reinterpret_cast(buffer), bufferSize); setImage(imageData, image.width(), image.height()); } diff --git a/libsrc/hyperion/ImageProcessor.cpp b/libsrc/hyperion/ImageProcessor.cpp index 54fd90706..7af90eec8 100644 --- a/libsrc/hyperion/ImageProcessor.cpp +++ b/libsrc/hyperion/ImageProcessor.cpp @@ -40,11 +40,15 @@ void ImageProcessor::registerProcessingUnit( // global transform method int ImageProcessor::mappingTypeToInt(const QString& mappingType) { - if (mappingType == "unicolor_mean" ) + if (mappingType == "multicolor_mean" ) { - return 1; + return 0; } else if (mappingType == "multicolor_mean_squared" ) + { + return 1; + } + else if (mappingType == "unicolor_mean" ) { return 2; } @@ -52,10 +56,18 @@ int ImageProcessor::mappingTypeToInt(const QString& mappingType) { return 3; } - else if (mappingType == "dominant_color_advanced" ) + else if (mappingType == "unicolor_dominant" ) { return 4; } + else if (mappingType == "dominant_color_advanced" ) + { + return 5; + } + else if (mappingType == "unicolor_dominant_advanced" ) + { + return 6; + } return 0; } // global transform method @@ -63,18 +75,28 @@ QString ImageProcessor::mappingTypeToStr(int mappingType) { QString typeText; switch (mappingType) { + + case 0: + typeText = "multicolor_mean"; + break; case 1: - typeText = "unicolor_mean"; + typeText = "multicolor_mean_squared"; break; case 2: - typeText = "multicolor_mean_squared"; + typeText = "unicolor_mean"; break; case 3: typeText = "dominant_color"; break; case 4: + typeText = "unicolor_dominant"; + break; + case 5: typeText = "dominant_color_advanced"; break; + case 6: + typeText = "unicolor_dominant_advanced"; + break; default: typeText = "multicolor_mean"; break; diff --git a/libsrc/hyperion/schema/schema-color.json b/libsrc/hyperion/schema/schema-color.json index 43ff8eb7c..565dccfd2 100644 --- a/libsrc/hyperion/schema/schema-color.json +++ b/libsrc/hyperion/schema/schema-color.json @@ -8,10 +8,10 @@ "type" : "string", "required" : true, "title" : "edt_conf_color_imageToLedMappingType_title", - "enum" : ["multicolor_mean", "unicolor_mean", "multicolor_mean_squared", "dominant_color", "dominant_color_advanced"], + "enum" : ["multicolor_mean","multicolor_mean_squared", "unicolor_mean", "dominant_color", "unicolor_dominant", "dominant_color_advanced", "unicolor_dominant_advanced"], "default" : "multicolor_mean", "options" : { - "enum_titles" : ["edt_conf_enum_multicolor_mean", "edt_conf_enum_unicolor_mean", "edt_conf_enum_multicolor_mean_squared", "edt_conf_enum_dominant_color", "edt_conf_enum_dominant_color_advanced"] + "enum_titles" : ["edt_conf_enum_multicolor_mean","edt_conf_enum_multicolor_mean_squared", "edt_conf_enum_unicolor_mean", "edt_conf_enum_dominant_color", "edt_conf_enum_unicolor_dominant", "edt_conf_enum_dominant_color_advanced", "edt_conf_enum_unicolor_dominant_advanced"] }, "propertyOrder" : 1 }, @@ -24,7 +24,7 @@ "propertyOrder": 2, "options": { "dependencies": { - "imageToLedMappingType": "dominant_color_advanced" + "imageToLedMappingType": ["dominant_color_advanced", "unicolor_dominant_advanced"] } } }, diff --git a/libsrc/utils/ImageResampler.cpp b/libsrc/utils/ImageResampler.cpp index 793620a50..a68053b0e 100644 --- a/libsrc/utils/ImageResampler.cpp +++ b/libsrc/utils/ImageResampler.cpp @@ -22,7 +22,7 @@ void ImageResampler::setCropping(int cropLeft, int cropRight, int cropTop, int c _cropBottom = cropBottom; } -void ImageResampler::processImage(const uint8_t * data, int width, int height, int lineLength, PixelFormat pixelFormat, Image &outputImage) const +void ImageResampler::processImage(const uint8_t * data, int width, int height, size_t lineLength, PixelFormat pixelFormat, Image &outputImage) const { int cropLeft = _cropLeft; int cropRight = _cropRight; @@ -89,7 +89,7 @@ void ImageResampler::processImage(const uint8_t * data, int width, int height, i for (int xDest = xDestStart, xSource = cropLeft + (_horizontalDecimation >> 1); xDest <= xDestEnd; xSource += _horizontalDecimation, ++xDest) { ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); - int index = lineLength * ySource + (xSource << 1); + size_t index = lineLength * ySource + (xSource << 1); uint8_t y = data[index+1]; uint8_t u = ((xSource&1) == 0) ? data[index ] : data[index-2]; uint8_t v = ((xSource&1) == 0) ? data[index+2] : data[index ]; @@ -106,7 +106,7 @@ void ImageResampler::processImage(const uint8_t * data, int width, int height, i for (int xDest = xDestStart, xSource = cropLeft + (_horizontalDecimation >> 1); xDest <= xDestEnd; xSource += _horizontalDecimation, ++xDest) { ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); - int index = lineLength * ySource + (xSource << 1); + size_t index = lineLength * ySource + (xSource << 1); uint8_t y = data[index]; uint8_t u = ((xSource&1) == 0) ? data[index+1] : data[index-1]; uint8_t v = ((xSource&1) == 0) ? data[index+3] : data[index+1]; @@ -123,7 +123,7 @@ void ImageResampler::processImage(const uint8_t * data, int width, int height, i for (int xDest = xDestStart, xSource = cropLeft + (_horizontalDecimation >> 1); xDest <= xDestEnd; xSource += _horizontalDecimation, ++xDest) { ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); - int index = lineLength * ySource + (xSource << 1); + size_t index = lineLength * ySource + (xSource << 1); rgb.blue = (data[index] & 0x1f) << 3; rgb.green = (((data[index+1] & 0x7) << 3) | (data[index] & 0xE0) >> 5) << 2; rgb.red = (data[index+1] & 0xF8); @@ -139,7 +139,7 @@ void ImageResampler::processImage(const uint8_t * data, int width, int height, i for (int xDest = xDestStart, xSource = cropLeft + (_horizontalDecimation >> 1); xDest <= xDestEnd; xSource += _horizontalDecimation, ++xDest) { ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); - int index = lineLength * ySource + (xSource << 1) + xSource; + size_t index = lineLength * ySource + (xSource << 1) + xSource; rgb.red = data[index ]; rgb.green = data[index+1]; rgb.blue = data[index+2]; @@ -155,7 +155,7 @@ void ImageResampler::processImage(const uint8_t * data, int width, int height, i for (int xDest = xDestStart, xSource = cropLeft + (_horizontalDecimation >> 1); xDest <= xDestEnd; xSource += _horizontalDecimation, ++xDest) { ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); - int index = lineLength * ySource + (xSource << 1) + xSource; + size_t index = lineLength * ySource + (xSource << 1) + xSource; rgb.blue = data[index ]; rgb.green = data[index+1]; rgb.red = data[index+2]; @@ -171,7 +171,7 @@ void ImageResampler::processImage(const uint8_t * data, int width, int height, i for (int xDest = xDestStart, xSource = cropLeft + (_horizontalDecimation >> 1); xDest <= xDestEnd; xSource += _horizontalDecimation, ++xDest) { ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); - int index = lineLength * ySource + (xSource << 2); + size_t index = lineLength * ySource + (xSource << 2); rgb.red = data[index ]; rgb.green = data[index+1]; rgb.blue = data[index+2]; @@ -187,7 +187,7 @@ void ImageResampler::processImage(const uint8_t * data, int width, int height, i for (int xDest = xDestStart, xSource = cropLeft + (_horizontalDecimation >> 1); xDest <= xDestEnd; xSource += _horizontalDecimation, ++xDest) { ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); - int index = lineLength * ySource + (xSource << 2); + size_t index = lineLength * ySource + (xSource << 2); rgb.blue = data[index ]; rgb.green = data[index+1]; rgb.red = data[index+2]; @@ -200,7 +200,7 @@ void ImageResampler::processImage(const uint8_t * data, int width, int height, i { for (int yDest = yDestStart, ySource = cropTop + (_verticalDecimation >> 1); yDest <= yDestEnd; ySource += _verticalDecimation, ++yDest) { - int uOffset = (height + ySource / 2) * lineLength; + size_t uOffset = (height + ySource / 2) * lineLength; for (int xDest = xDestStart, xSource = cropLeft + (_horizontalDecimation >> 1); xDest <= xDestEnd; xSource += _horizontalDecimation, ++xDest) { ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); diff --git a/libsrc/webserver/QtHttpClientWrapper.cpp b/libsrc/webserver/QtHttpClientWrapper.cpp index 62a3d0daa..9eaac181d 100644 --- a/libsrc/webserver/QtHttpClientWrapper.cpp +++ b/libsrc/webserver/QtHttpClientWrapper.cpp @@ -47,6 +47,22 @@ QString QtHttpClientWrapper::getGuid (void) return m_guid; } +void QtHttpClientWrapper::injectCorsHeaders(QtHttpReply* reply) +{ + if (reply == nullptr) + return; + + // Add CORS headers if not already set + if (reply->getHeader(QtHttpHeader::AccessControlAllowOrigin).isEmpty()) + reply->addHeader(QtHttpHeader::AccessControlAllowOrigin, "*"); + + if (reply->getHeader(QtHttpHeader::AccessControlAllowMethods).isEmpty()) + reply->addHeader(QtHttpHeader::AccessControlAllowMethods, "POST, GET, OPTIONS"); + + if (reply->getHeader(QtHttpHeader::AccessControlAllowHeaders).isEmpty()) + reply->addHeader(QtHttpHeader::AccessControlAllowHeaders, "Authorization, Content-Type"); +} + void QtHttpClientWrapper::onClientDataReceived (void) { if (m_sockClient != Q_NULLPTR) @@ -168,6 +184,23 @@ void QtHttpClientWrapper::onClientDataReceived (void) { case RequestParsed: // a valid request has ben fully parsed { + // Handle CORS Preflight (OPTIONS) + if (m_currentRequest->getCommand() == "OPTIONS") + { + QtHttpReply reply(m_serverHandle); + reply.setStatusCode(QtHttpReply::NoContent); // 204 No Content + + injectCorsHeaders(&reply); + reply.addHeader(QtHttpHeader::AccessControlMaxAge, "86400"); // Cache for 1 day + + connect(&reply, &QtHttpReply::requestSendHeaders, this, &QtHttpClientWrapper::onReplySendHeadersRequested, Qt::UniqueConnection); + connect(&reply, &QtHttpReply::requestSendData, this, &QtHttpClientWrapper::onReplySendDataRequested, Qt::UniqueConnection); + + m_parsingStatus = sendReplyToClient(&reply); + + return; // Important: return early, do NOT continue to normal POST/jsonrpc handling + } + const auto& upgradeValue = m_currentRequest->getHeader(QtHttpHeader::Upgrade).toLower(); if (upgradeValue == "websocket") { @@ -231,8 +264,9 @@ void QtHttpClientWrapper::onClientDataReceived (void) } QtHttpReply reply (m_serverHandle); - connect (&reply, &QtHttpReply::requestSendHeaders, this, &QtHttpClientWrapper::onReplySendHeadersRequested); - connect (&reply, &QtHttpReply::requestSendData, this, &QtHttpClientWrapper::onReplySendDataRequested); + connect(&reply, &QtHttpReply::requestSendHeaders, this, &QtHttpClientWrapper::onReplySendHeadersRequested, Qt::UniqueConnection); + connect(&reply, &QtHttpReply::requestSendData, this, &QtHttpClientWrapper::onReplySendDataRequested, Qt::UniqueConnection); + emit m_serverHandle->requestNeedsReply (m_currentRequest, &reply); // allow app to handle request m_parsingStatus = sendReplyToClient (&reply); @@ -266,6 +300,8 @@ void QtHttpClientWrapper::onReplySendHeadersRequested (void) if (reply != Q_NULLPTR) { + injectCorsHeaders(reply); + QByteArray data; // HTTP Version + Status Code + Status Msg data.append (QtHttpServer::HTTP_VERSION.toUtf8()); @@ -286,7 +322,6 @@ void QtHttpClientWrapper::onReplySendHeadersRequested (void) } const QList & headersList = reply->getHeadersList (); - foreach (const QByteArray & header, headersList) { data.append (header); diff --git a/libsrc/webserver/QtHttpClientWrapper.h b/libsrc/webserver/QtHttpClientWrapper.h index c0dedb629..99bb88352 100644 --- a/libsrc/webserver/QtHttpClientWrapper.h +++ b/libsrc/webserver/QtHttpClientWrapper.h @@ -57,6 +57,9 @@ protected slots: void onReplySendDataRequested (void); private: + + void injectCorsHeaders(QtHttpReply* reply); + QString m_guid; ParsingStatus m_parsingStatus; QTcpSocket * m_sockClient; diff --git a/libsrc/webserver/QtHttpHeader.cpp b/libsrc/webserver/QtHttpHeader.cpp index 1eaf56ae1..63677a074 100644 --- a/libsrc/webserver/QtHttpHeader.cpp +++ b/libsrc/webserver/QtHttpHeader.cpp @@ -3,35 +3,38 @@ #include -const QByteArray & QtHttpHeader::Server = QByteArrayLiteral ("Server"); -const QByteArray & QtHttpHeader::Date = QByteArrayLiteral ("Date"); -const QByteArray & QtHttpHeader::Host = QByteArrayLiteral ("Host"); -const QByteArray & QtHttpHeader::Accept = QByteArrayLiteral ("Accept"); -const QByteArray & QtHttpHeader::Cookie = QByteArrayLiteral ("Cookie"); -const QByteArray & QtHttpHeader::ContentType = QByteArrayLiteral ("Content-Type"); -const QByteArray & QtHttpHeader::ContentLength = QByteArrayLiteral ("Content-Length"); -const QByteArray & QtHttpHeader::Connection = QByteArrayLiteral ("Connection"); -const QByteArray & QtHttpHeader::UserAgent = QByteArrayLiteral ("User-Agent"); -const QByteArray & QtHttpHeader::AcceptCharset = QByteArrayLiteral ("Accept-Charset"); -const QByteArray & QtHttpHeader::AcceptEncoding = QByteArrayLiteral ("Accept-Encoding"); -const QByteArray & QtHttpHeader::AcceptLanguage = QByteArrayLiteral ("Accept-Language"); -const QByteArray & QtHttpHeader::Authorization = QByteArrayLiteral ("Authorization"); -const QByteArray & QtHttpHeader::CacheControl = QByteArrayLiteral ("Cache-Control"); -const QByteArray & QtHttpHeader::ContentMD5 = QByteArrayLiteral ("Content-MD5"); -const QByteArray & QtHttpHeader::ProxyAuthorization = QByteArrayLiteral ("Proxy-Authorization"); -const QByteArray & QtHttpHeader::Range = QByteArrayLiteral ("Range"); -const QByteArray & QtHttpHeader::ContentEncoding = QByteArrayLiteral ("Content-Encoding"); -const QByteArray & QtHttpHeader::ContentLanguage = QByteArrayLiteral ("Content-Language"); -const QByteArray & QtHttpHeader::ContentLocation = QByteArrayLiteral ("Content-Location"); -const QByteArray & QtHttpHeader::ContentRange = QByteArrayLiteral ("Content-Range"); -const QByteArray & QtHttpHeader::Expires = QByteArrayLiteral ("Expires"); -const QByteArray & QtHttpHeader::LastModified = QByteArrayLiteral ("Last-Modified"); -const QByteArray & QtHttpHeader::Location = QByteArrayLiteral ("Location"); -const QByteArray & QtHttpHeader::SetCookie = QByteArrayLiteral ("Set-Cookie"); -const QByteArray & QtHttpHeader::TransferEncoding = QByteArrayLiteral ("Transfer-Encoding"); -const QByteArray & QtHttpHeader::ContentDisposition = QByteArrayLiteral ("Content-Disposition"); -const QByteArray & QtHttpHeader::AccessControlAllow = QByteArrayLiteral ("Access-Control-Allow-Origin"); -const QByteArray & QtHttpHeader::Upgrade = QByteArrayLiteral ("Upgrade"); -const QByteArray & QtHttpHeader::SecWebSocketKey = QByteArrayLiteral ("Sec-WebSocket-Key"); -const QByteArray & QtHttpHeader::SecWebSocketProtocol = QByteArrayLiteral ("Sec-WebSocket-Protocol"); -const QByteArray & QtHttpHeader::SecWebSocketVersion = QByteArrayLiteral ("Sec-WebSocket-Version"); +const QByteArray & QtHttpHeader::Server = QByteArrayLiteral ("Server"); +const QByteArray & QtHttpHeader::Date = QByteArrayLiteral ("Date"); +const QByteArray & QtHttpHeader::Host = QByteArrayLiteral ("Host"); +const QByteArray & QtHttpHeader::Accept = QByteArrayLiteral ("Accept"); +const QByteArray & QtHttpHeader::Cookie = QByteArrayLiteral ("Cookie"); +const QByteArray & QtHttpHeader::ContentType = QByteArrayLiteral ("Content-Type"); +const QByteArray & QtHttpHeader::ContentLength = QByteArrayLiteral ("Content-Length"); +const QByteArray & QtHttpHeader::Connection = QByteArrayLiteral ("Connection"); +const QByteArray & QtHttpHeader::UserAgent = QByteArrayLiteral ("User-Agent"); +const QByteArray & QtHttpHeader::AcceptCharset = QByteArrayLiteral ("Accept-Charset"); +const QByteArray & QtHttpHeader::AcceptEncoding = QByteArrayLiteral ("Accept-Encoding"); +const QByteArray & QtHttpHeader::AcceptLanguage = QByteArrayLiteral ("Accept-Language"); +const QByteArray & QtHttpHeader::Authorization = QByteArrayLiteral ("Authorization"); +const QByteArray & QtHttpHeader::CacheControl = QByteArrayLiteral ("Cache-Control"); +const QByteArray & QtHttpHeader::ContentMD5 = QByteArrayLiteral ("Content-MD5"); +const QByteArray & QtHttpHeader::ProxyAuthorization = QByteArrayLiteral ("Proxy-Authorization"); +const QByteArray & QtHttpHeader::Range = QByteArrayLiteral ("Range"); +const QByteArray & QtHttpHeader::ContentEncoding = QByteArrayLiteral ("Content-Encoding"); +const QByteArray & QtHttpHeader::ContentLanguage = QByteArrayLiteral ("Content-Language"); +const QByteArray & QtHttpHeader::ContentLocation = QByteArrayLiteral ("Content-Location"); +const QByteArray & QtHttpHeader::ContentRange = QByteArrayLiteral ("Content-Range"); +const QByteArray & QtHttpHeader::Expires = QByteArrayLiteral ("Expires"); +const QByteArray & QtHttpHeader::LastModified = QByteArrayLiteral ("Last-Modified"); +const QByteArray & QtHttpHeader::Location = QByteArrayLiteral ("Location"); +const QByteArray & QtHttpHeader::SetCookie = QByteArrayLiteral ("Set-Cookie"); +const QByteArray & QtHttpHeader::TransferEncoding = QByteArrayLiteral ("Transfer-Encoding"); +const QByteArray & QtHttpHeader::ContentDisposition = QByteArrayLiteral ("Content-Disposition"); +const QByteArray & QtHttpHeader::AccessControlAllowOrigin = QByteArrayLiteral ("Access-Control-Allow-Origin"); +const QByteArray & QtHttpHeader::AccessControlAllowMethods = QByteArrayLiteral ("Access-Control-Allow-Methods"); +const QByteArray & QtHttpHeader::AccessControlAllowHeaders = QByteArrayLiteral ("Access-Control-Allow-Headers"); +const QByteArray & QtHttpHeader::AccessControlMaxAge = QByteArrayLiteral ("Access-Control-Max-Age"); +const QByteArray & QtHttpHeader::Upgrade = QByteArrayLiteral ("Upgrade"); +const QByteArray & QtHttpHeader::SecWebSocketKey = QByteArrayLiteral ("Sec-WebSocket-Key"); +const QByteArray & QtHttpHeader::SecWebSocketProtocol = QByteArrayLiteral ("Sec-WebSocket-Protocol"); +const QByteArray & QtHttpHeader::SecWebSocketVersion = QByteArrayLiteral ("Sec-WebSocket-Version"); diff --git a/libsrc/webserver/QtHttpHeader.h b/libsrc/webserver/QtHttpHeader.h index 9e2a85e9c..7706af7fe 100644 --- a/libsrc/webserver/QtHttpHeader.h +++ b/libsrc/webserver/QtHttpHeader.h @@ -33,7 +33,10 @@ class QtHttpHeader static const QByteArray & SetCookie; static const QByteArray & TransferEncoding; static const QByteArray & ContentDisposition; - static const QByteArray & AccessControlAllow; + static const QByteArray & AccessControlAllowOrigin; + static const QByteArray & AccessControlAllowMethods; + static const QByteArray & AccessControlAllowHeaders; + static const QByteArray & AccessControlMaxAge; // Websocket specific headers static const QByteArray & Upgrade; static const QByteArray & SecWebSocketKey; diff --git a/libsrc/webserver/QtHttpReply.h b/libsrc/webserver/QtHttpReply.h index 4f60a9100..150aa19dd 100644 --- a/libsrc/webserver/QtHttpReply.h +++ b/libsrc/webserver/QtHttpReply.h @@ -19,6 +19,7 @@ class QtHttpReply : public QObject enum StatusCode { Ok = 200, + NoContent = 204, SeeOther = 303, BadRequest = 400, Forbidden = 403, diff --git a/libsrc/webserver/QtHttpServer.cpp b/libsrc/webserver/QtHttpServer.cpp index d48aa4848..2f61882a9 100644 --- a/libsrc/webserver/QtHttpServer.cpp +++ b/libsrc/webserver/QtHttpServer.cpp @@ -36,7 +36,7 @@ void QtHttpServerWrapper::incomingConnection (qintptr handle) QtHttpServer::QtHttpServer (QObject * parent) : QObject (parent) , m_useSsl (false) - , m_serverName (QStringLiteral ("The Qt6 HTTP Server")) + , m_serverName (QStringLiteral ("The Hyperion HTTP Server")) , m_netOrigin (NetOrigin::getInstance()) , m_sockServer (nullptr) { diff --git a/libsrc/webserver/StaticFileServing.cpp b/libsrc/webserver/StaticFileServing.cpp index 3f3bc5a7c..66ffb7a09 100644 --- a/libsrc/webserver/StaticFileServing.cpp +++ b/libsrc/webserver/StaticFileServing.cpp @@ -146,7 +146,6 @@ void StaticFileServing::onRequestNeedsReply (QtHttpRequest * request, QtHttpRepl { reply->addHeader ("Content-Type", mime.name().toLocal8Bit()); } - reply->addHeader(QtHttpHeader::AccessControlAllow, "*" ); reply->appendRawData (data); file.close (); }