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('' + $.i18n('general_btn_ok') + ' ');
}
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('' + $.i18n('general_btn_ok') + ' ');
}
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 ();
}