From 3dd5ee43872a644a5a291d5ddbe215f521b1caf4 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Tue, 8 Apr 2025 20:55:21 +0200 Subject: [PATCH 01/26] Fix - Retries are not done when connection is destructed --- libsrc/flatbufserver/FlatBufferConnection.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libsrc/flatbufserver/FlatBufferConnection.cpp b/libsrc/flatbufserver/FlatBufferConnection.cpp index fcba8ffaa..fb6d3ef62 100644 --- a/libsrc/flatbufserver/FlatBufferConnection.cpp +++ b/libsrc/flatbufserver/FlatBufferConnection.cpp @@ -30,16 +30,21 @@ FlatBufferConnection::FlatBufferConnection(const QString& origin, const QHostAdd // init connect connectToRemoteHost(); - // start the connection timer _timer.setInterval(5000); - connect(&_timer, &QTimer::timeout, this, &FlatBufferConnection::connectToRemoteHost); + + //Trigger the retry timer when connection dropped + connect(this, &FlatBufferConnection::isDisconnected, &_timer, static_cast(&QTimer::start)); _timer.start(); } FlatBufferConnection::~FlatBufferConnection() { _timer.stop(); + + //Stop retrying on disconnect + disconnect(this, &FlatBufferConnection::isDisconnected, &_timer, static_cast(&QTimer::start)); + Debug(_log, "Closing connection with host: %s, port [%u]", QSTRING_CSTR(_host.toString()), _port); _socket.close(); } @@ -58,7 +63,6 @@ void FlatBufferConnection::onDisconnected() _isRegistered = false, Info(_log, "Disconnected from target host: %s, port [%u]", QSTRING_CSTR(_host.toString()), _port); emit isDisconnected(); - _timer.start(); } @@ -225,8 +229,8 @@ bool FlatBufferConnection::parseReply(const hyperionnet::Reply *reply) } else { - _timer.stop(); _isRegistered = true; + _timer.stop(); Debug(_log,"Client \"%s\" registered successfully with target host: %s, port [%u]", QSTRING_CSTR(_origin), QSTRING_CSTR(_host.toString()), _port); emit isReadyToSend(); } From 617e1233e11bc582cb3fbc8c8483149cf2952c14 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Tue, 8 Apr 2025 22:41:01 +0200 Subject: [PATCH 02/26] Remove costly unpack and streamline code --- libsrc/flatbufserver/FlatBufferClient.cpp | 200 +++++++++++++--------- libsrc/flatbufserver/FlatBufferClient.h | 11 +- 2 files changed, 122 insertions(+), 89 deletions(-) diff --git a/libsrc/flatbufserver/FlatBufferClient.cpp b/libsrc/flatbufserver/FlatBufferClient.cpp index 5ca102a72..8c50db9af 100644 --- a/libsrc/flatbufserver/FlatBufferClient.cpp +++ b/libsrc/flatbufserver/FlatBufferClient.cpp @@ -1,5 +1,6 @@ #include "FlatBufferClient.h" #include +#include // qt #include @@ -28,7 +29,7 @@ FlatBufferClient::FlatBufferClient(QTcpSocket* socket, int timeout, QObject *par , _processingMessage(false) { _imageResampler.setPixelDecimation(1); - + // timer setup _timeoutTimer.reset(new QTimer(this)); _timeoutTimer->setSingleShot(true); @@ -57,59 +58,71 @@ void FlatBufferClient::readyRead() } } -void FlatBufferClient::processNextMessage() +bool FlatBufferClient::processNextMessageInline() { - if (_processingMessage) { return; } // Avoid re-entrancy + if (_processingMessage) { return false; } // Avoid re-entrancy // Wait for at least 4 bytes to read the message size if (_receiveBuffer.size() < 4) { - return; + return false; } _processingMessage = true; - uint32_t messageSize; - memcpy(&messageSize, _receiveBuffer.constData(), sizeof(uint32_t)); - messageSize = qFromBigEndian(messageSize); + // 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); - _receiveBuffer.clear(); _processingMessage = false; - return; + return true; } // Wait for full message if (_receiveBuffer.size() < static_cast(messageSize + 4)) { _processingMessage = false; - return; + return false; } - // Extract the message and remove it from the buffer - _lastMessage = _receiveBuffer.mid(4, messageSize); - _receiveBuffer.remove(0, messageSize + 4); - - const uint8_t* msgData = reinterpret_cast(_lastMessage.constData()); + // 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); if (!hyperionnet::VerifyRequestBuffer(verifier)) { Error(_log, "Invalid FlatBuffer message received"); sendErrorReply("Invalid FlatBuffer message received"); _processingMessage = false; - QMetaObject::invokeMethod(this, &FlatBufferClient::processNextMessage, Qt::QueuedConnection); - return; + + // Clear the buffer in case of an invalid message + _receiveBuffer.clear(); + return true; } // Invoke message handling - QMetaObject::invokeMethod(this, [this]() { - const auto* msgData = reinterpret_cast(_lastMessage.constData()); + QMetaObject::invokeMethod(this, [this, msgData, messageSize]() { handleMessage(hyperionnet::GetRequest(msgData)); _processingMessage = false; - QMetaObject::invokeMethod(this, &FlatBufferClient::processNextMessage, Qt::QueuedConnection); + + // 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; +} + +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 + } } void FlatBufferClient::noDataReceived() @@ -201,55 +214,80 @@ void FlatBufferClient::handleRegisterCommand(const hyperionnet::Register *regReq void FlatBufferClient::handleImageCommand(const hyperionnet::Image *image) { - Image imageRGB; - // extract parameters int const duration = image->duration(); + if (image->data_as_RawImage() != nullptr) { const auto* img = static_cast(image->data_as_RawImage()); - hyperionnet::RawImageT rawImageNative; - img->UnPackTo(&rawImageNative); - - const int width = rawImageNative.width; - const int height = rawImageNative.height; + // Read image properties directly from FlatBuffer + const int width = img->width(); + const int height = img->height(); + const auto* data = img->data(); - if (width <= 0 || height <= 0 || rawImageNative.data.empty()) + if (width <= 0 || height <= 0 || data == nullptr || data->size() == 0) { sendErrorReply("Invalid width and/or height or no raw image data provided"); return; } - // check consistency of the size of the received data - int const bytesPerPixel = rawImageNative.data.size() / (width * height); + // Check consistency of image data size + const int dataSize = data->size(); + const int bytesPerPixel = dataSize / (width * height); if (bytesPerPixel != 3 && bytesPerPixel != 4) { sendErrorReply("Size of image data does not match with the width and height"); return; } - imageRGB.resize(width, height); - processRawImage(rawImageNative, bytesPerPixel, _imageResampler, imageRGB); + // Only resize if needed (reuse memory) + if (_imageOutputBuffer.width() != width || _imageOutputBuffer.height() != height) + { + _imageOutputBuffer.resize(width, height); + } + + processRawImage(data->data(), width, height, bytesPerPixel, _imageResampler, _imageOutputBuffer); } else if (image->data_as_NV12Image() != nullptr) { const auto* img = static_cast(image->data_as_NV12Image()); - hyperionnet::NV12ImageT nv12ImageNative; - img->UnPackTo(&nv12ImageNative); - - const int width = nv12ImageNative.width; - const int height = nv12ImageNative.height; - - if (width <= 0 || height <= 0 || nv12ImageNative.data_y.empty() || nv12ImageNative.data_uv.empty()) - { - sendErrorReply("Invalid width and/or height or no complete NV12 image data provided"); - return; - } - - imageRGB.resize(width, height); - processNV12Image(nv12ImageNative, _imageResampler, imageRGB); + const int width = img->width(); + const int height = img->height(); + const auto* data_y = img->data_y(); + const auto* data_uv = img->data_uv(); + + if (width <= 0 || height <= 0 || data_y == nullptr || data_uv == nullptr || + data_y->size() == 0 || data_uv->size() == 0) + { + sendErrorReply("Invalid width and/or height or no complete NV12 image data provided"); + return; + } + + // 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 required_size = y_size + uv_size; + if (_combinedNv12Buffer.capacity() < required_size) + { + _combinedNv12Buffer.reserve(required_size); + } + std::memcpy(_combinedNv12Buffer.data(), data_y->data(), y_size); + 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; + + // Resize only when needed + if (_imageOutputBuffer.width() != width || _imageOutputBuffer.height() != height) + { + _imageOutputBuffer.resize(width, height); + } + + // Process image + processNV12Image(_combinedNv12Buffer.data(), width, height, stride_y, _imageResampler, _imageOutputBuffer); } else { @@ -257,8 +295,8 @@ void FlatBufferClient::handleImageCommand(const hyperionnet::Image *image) return; } - emit setGlobalInputImage(_priority, imageRGB, duration); - emit setBufferImage("FlatBuffer", imageRGB); + emit setGlobalInputImage(_priority, _imageOutputBuffer, duration); + emit setBufferImage("FlatBuffer", _imageOutputBuffer); // send reply sendSuccessReply(); @@ -319,49 +357,41 @@ void FlatBufferClient::sendErrorReply(const QString& error) sendMessage(_builder.GetBufferPointer(), _builder.GetSize()); } -inline void FlatBufferClient::processRawImage(const hyperionnet::RawImageT& raw_image, int bytesPerPixel, ImageResampler& resampler, Image& outputImage) { - - int const width = raw_image.width; - int const height = raw_image.height; - +inline void FlatBufferClient::processRawImage(const uint8_t* buffer, + int width, + int height, + int bytesPerPixel, + ImageResampler& resampler, + Image& outputImage) +{ int const lineLength = width * bytesPerPixel; PixelFormat const pixelFormat = (bytesPerPixel == 4) ? PixelFormat::RGB32 : PixelFormat::RGB24; - // Process the image resampler.processImage( - raw_image.data.data(), // Raw RGB/RGBA buffer - width, // Image width - height, // Image height - lineLength, // Line length - pixelFormat, // Pixel format (RGB24/RGB32) - outputImage // Output image - ); + buffer, // Raw buffer + width, + height, + lineLength, + pixelFormat, + outputImage + ); } -inline void FlatBufferClient::processNV12Image(const hyperionnet::NV12ImageT& nv12_image, ImageResampler& resampler, Image& outputImage) { - // Combine data_y and data_uv into a single buffer - int const width = nv12_image.width; - int const height = nv12_image.height; - - size_t const y_size = nv12_image.data_y.size(); - size_t const uv_size = nv12_image.data_uv.size(); - std::vector combined_buffer(y_size + uv_size); - - std::memcpy(combined_buffer.data(), nv12_image.data_y.data(), y_size); - std::memcpy(combined_buffer.data() + y_size, nv12_image.data_uv.data(), uv_size); - - // Determine line length (stride_y) - int const lineLength = nv12_image.stride_y > 0 ? nv12_image.stride_y : width; - - PixelFormat const pixelFormat = PixelFormat::NV12; +inline void FlatBufferClient::processNV12Image(const uint8_t* nv12_data, + int width, + int height, + int stride_y, + ImageResampler& resampler, + Image& outputImage) +{ + PixelFormat pixelFormat = PixelFormat::NV12; - // Process the image resampler.processImage( - combined_buffer.data(), // Combined NV12 buffer - width, // Image width - height, // Image height - lineLength, // Line length for Y plane - pixelFormat, // Pixel format (NV12) - outputImage // Output image - ); + nv12_data, // Combined NV12 buffer + width, + height, + stride_y, + pixelFormat, + outputImage + ); } diff --git a/libsrc/flatbufserver/FlatBufferClient.h b/libsrc/flatbufserver/FlatBufferClient.h index d6064349e..c4271459c 100644 --- a/libsrc/flatbufserver/FlatBufferClient.h +++ b/libsrc/flatbufserver/FlatBufferClient.h @@ -81,7 +81,6 @@ private slots: /// @brief Is called whenever the socket got new data to read /// void readyRead(); - void processNextMessage(); /// /// @brief Is called when the socket closed the connection, also requests thread exit @@ -123,6 +122,9 @@ private slots: /// void handleNotImplemented(); + void processNextMessage(); + bool processNextMessageInline(); + /// /// Send a message to the connected client /// @param data to be send @@ -142,8 +144,8 @@ private slots: /// void sendErrorReply(const QString& error); - void processRawImage(const hyperionnet::RawImageT& raw_image, int bytesPerPixel, ImageResampler& resampler, Image& outputImage); - void processNV12Image(const hyperionnet::NV12ImageT& nv12_image, ImageResampler& resampler, Image& outputImage); + 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); private: Logger * _log; @@ -156,12 +158,13 @@ private slots: QByteArray _receiveBuffer; + Image _imageOutputBuffer; ImageResampler _imageResampler; + std::vector _combinedNv12Buffer; // Flatbuffers builder flatbuffers::FlatBufferBuilder _builder; bool _processingMessage; - QByteArray _lastMessage; }; #endif // FLATBUFFERCLIENT_H From 5086c118eaf3dacd65087a93fa844f3e11eda8df Mon Sep 17 00:00:00 2001 From: LordGrey Date: Thu, 10 Apr 2025 21:57:19 +0200 Subject: [PATCH 03/26] Update Flatbuffer client code --- libsrc/flatbufserver/FlatBufferClient.cpp | 47 +++++++++-------------- libsrc/flatbufserver/FlatBufferClient.h | 3 +- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/libsrc/flatbufserver/FlatBufferClient.cpp b/libsrc/flatbufserver/FlatBufferClient.cpp index 8c50db9af..a04bfa5bf 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,20 +50,19 @@ void FlatBufferClient::readyRead() { if (_socket == nullptr) { return; } - while (_socket->bytesAvailable() > 0) - { - _timeoutTimer->start(); - _receiveBuffer += _socket->readAll(); - processNextMessage(); - } + _timeoutTimer->start(); + _receiveBuffer += _socket->readAll(); + + processNextMessage(); } -bool FlatBufferClient::processNextMessageInline() +bool FlatBufferClient::processNextMessage() { if (_processingMessage) { return false; } // Avoid re-entrancy // Wait for at least 4 bytes to read the message size - if (_receiveBuffer.size() < 4) { + if (_receiveBuffer.size() < 4) + { return false; } @@ -71,12 +70,13 @@ bool FlatBufferClient::processNextMessageInline() // 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]; + uint32_t const messageSize = (raw[0] << 24) | (raw[1] << 16) | (raw[2] << 8) | raw[3]; - // Validate message size - if (messageSize == 0 || messageSize > FLATBUFFER_MAX_MSG_LENGTH) + // // Validate message size + if (messageSize == 0) { - Warning(_log, "Invalid message size: %d - dropping received data", messageSize); + Warning(_log, "Invalid message size: %u - dropping received data", messageSize); + _receiveBuffer.clear(); _processingMessage = false; return true; } @@ -88,6 +88,9 @@ bool FlatBufferClient::processNextMessageInline() return false; } + // Remove the processed message from the buffer (header + body) + _receiveBuffer.remove(0, messageSize + 4); + // 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); @@ -97,34 +100,22 @@ bool FlatBufferClient::processNextMessageInline() sendErrorReply("Invalid FlatBuffer message received"); _processingMessage = false; - // Clear the buffer in case of an invalid message - _receiveBuffer.clear(); + QMetaObject::invokeMethod(this, &FlatBufferClient::processNextMessage, Qt::QueuedConnection); return true; } // Invoke message handling - QMetaObject::invokeMethod(this, [this, msgData, messageSize]() { + QMetaObject::invokeMethod(this, [this, msgData]() { handleMessage(hyperionnet::GetRequest(msgData)); _processingMessage = false; - // 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(); + QMetaObject::invokeMethod(this, &FlatBufferClient::processNextMessage, Qt::QueuedConnection); }); return true; } -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 - } -} - void FlatBufferClient::noDataReceived() { Error(_log,"No data received for %dms - drop connection with client \"%s\"",_timeout, QSTRING_CSTR(QString("%1@%2").arg(_origin, _clientAddress))); diff --git a/libsrc/flatbufserver/FlatBufferClient.h b/libsrc/flatbufserver/FlatBufferClient.h index c4271459c..1308d3716 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 From 70fe77c28870e3945a8cf62b229d6e5f7f306ce9 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Fri, 18 Apr 2025 21:15:51 +0200 Subject: [PATCH 04/26] Support for dominant color processing on a full image which is applied to all LEDs (#1853) --- CHANGELOG.md | 1 + assets/webconfig/i18n/en.json | 6 +- include/hyperion/ImageProcessor.h | 21 ++++-- include/hyperion/ImageToLedsMap.h | 86 ++++++++++++++++++++++-- libsrc/hyperion/ImageProcessor.cpp | 32 +++++++-- libsrc/hyperion/schema/schema-color.json | 6 +- 6 files changed, 131 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce2db672..057b12231 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) diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index ed4faf8c3..18369da8f 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", 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/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"] } } }, From de551bb1be25f7bc9618b5f8586c520aa63ec3c0 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Mon, 21 Apr 2025 09:19:44 +0200 Subject: [PATCH 05/26] Allow uni dominant enums for API --- libsrc/api/JSONRPC_schema/schema-processing.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsrc/api/JSONRPC_schema/schema-processing.json b/libsrc/api/JSONRPC_schema/schema-processing.json index 0ca7616d8..d49ea8833 100644 --- a/libsrc/api/JSONRPC_schema/schema-processing.json +++ b/libsrc/api/JSONRPC_schema/schema-processing.json @@ -18,7 +18,7 @@ }, "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 From 550d7a2db8bfcdd6a644976610c6e75bfdb9a78c Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Mon, 21 Apr 2025 09:22:28 +0200 Subject: [PATCH 06/26] Fix C&P issue --- libsrc/api/JSONRPC_schema/schema-processing.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsrc/api/JSONRPC_schema/schema-processing.json b/libsrc/api/JSONRPC_schema/schema-processing.json index d49ea8833..680050e83 100644 --- a/libsrc/api/JSONRPC_schema/schema-processing.json +++ b/libsrc/api/JSONRPC_schema/schema-processing.json @@ -18,7 +18,7 @@ }, "mappingType": { "type" : "string", - "enum" : ["multicolor_mean","multicolor_mean_squared", "unicolor_mean", "dominant_color", "unicolor_dominant", "dominant_color_advanced", "unicolor_dominant_advanced"], + "enum" : ["multicolor_mean","multicolor_mean_squared", "unicolor_mean", "dominant_color", "unicolor_dominant", "dominant_color_advanced", "unicolor_dominant_advanced"] } }, "additionalProperties": false From fa6fa14db096cd21a6c588374ba4963c0784bd23 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Mon, 21 Apr 2025 16:50:46 +0200 Subject: [PATCH 07/26] Missing remote buttons' text --- assets/webconfig/i18n/en.json | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 18369da8f..41192c475 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -1101,11 +1101,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", From c7fbbc793cd3786a1b64b55d6b9ab9b1e38aae8f Mon Sep 17 00:00:00 2001 From: LordGrey Date: Mon, 21 Apr 2025 17:05:02 +0200 Subject: [PATCH 08/26] Allow to provide additional details in error dialogue --- assets/webconfig/i18n/en.json | 5 +++- assets/webconfig/js/content_index.js | 4 +-- assets/webconfig/js/hyperion.js | 41 ++++++++++++++++++++++------ assets/webconfig/js/ui_utils.js | 25 +++++++++++++++-- 4 files changed, 60 insertions(+), 15 deletions(-) diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 41192c475..4ad991925 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -1232,6 +1232,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..320dd9394 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -242,11 +242,11 @@ $(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")) { + if (event.reason.message == "No Authorization" && getStorage("loginToken")) { removeStorage("loginToken"); requestRequiresDefaultPasswortChange(); } else { - showInfoDialog("error", "Error", event.reason); + showInfoDialog("error", "", event.reason.message, event.reason.details); } }); diff --git a/assets/webconfig/js/hyperion.js b/assets/webconfig/js/hyperion.js index 6cb2e35fb..a93121105 100644 --- a/assets/webconfig/js/hyperion.js +++ b/assets/webconfig/js/hyperion.js @@ -126,27 +126,50 @@ 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 : []; + console.log("[window.websocket::onmessage] ", error, ", Description:", errorData); + $(window.hyperion).trigger({ + type: "error", + reason: { + 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: [] + } + }); } } 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'); From 662c3b48245f7948642d4c3038d3b1bfe9525d49 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Mon, 21 Apr 2025 17:32:57 +0200 Subject: [PATCH 09/26] Ensure UI getConfig always get instances --- assets/webconfig/js/content_index.js | 6 +++--- assets/webconfig/js/hyperion.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index 320dd9394..2123508be 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]; } - } } diff --git a/assets/webconfig/js/hyperion.js b/assets/webconfig/js/hyperion.js index a93121105..66e36913a 100644 --- a/assets/webconfig/js/hyperion.js +++ b/assets/webconfig/js/hyperion.js @@ -359,12 +359,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 }; } From ad81c00c87ad5a6bff5d59914375a20a37851d8f Mon Sep 17 00:00:00 2001 From: LordGrey Date: Mon, 21 Apr 2025 19:02:36 +0200 Subject: [PATCH 10/26] Stop API service when Hyperion is quitting --- include/api/JsonAPI.h | 2 +- libsrc/api/JsonAPI.cpp | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) 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/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 8d75c75f3..17aea2e77 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, [=](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); @@ -330,7 +350,7 @@ void JsonAPI::handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject return; } - // Execute the command for each valid instance + // Execute the command for each valid instance; Hyperion is about to quit for (const auto &instanceId : std::as_const(instanceIds)) { if (isRunningInstanceRequired == InstanceCmd::MustRun_Yes || _currInstanceIndex == NO_INSTANCE_ID) From b371d01b04ac1b8d97ca2ed498208ef0cdcd6458 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Mon, 21 Apr 2025 21:43:41 +0200 Subject: [PATCH 11/26] Update UI that Single and Multiple instance commands are correctly supported --- assets/webconfig/js/hyperion.js | 83 +++++++++++++++++---------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/assets/webconfig/js/hyperion.js b/assets/webconfig/js/hyperion.js index 66e36913a..b92423d72 100644 --- a/assets/webconfig/js/hyperion.js +++ b/assets/webconfig/js/hyperion.js @@ -173,7 +173,7 @@ function initWebSocket() { } } -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 @@ -187,6 +187,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); @@ -289,18 +294,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) { @@ -308,7 +313,7 @@ function requestInstanceCreate(name) { } function requestInstanceSwitch(instance) { - sendToHyperion("instance", "switchTo", { instance: Number(instance) }); + sendToHyperion("instance", "switchTo", {}, Number(instance)); } function requestServerInfo(instance) { @@ -325,12 +330,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(); } @@ -394,40 +395,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 }, @@ -435,10 +436,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], @@ -446,10 +447,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, @@ -458,18 +459,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 @@ -539,7 +540,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, @@ -547,7 +548,7 @@ function requestTestEffect(name, pythonScript, args, imageData) { pythonScript, imageData }; - sendToHyperion("effect", "", data); + sendToHyperion("effect", "", data, instanceIds); } function requestDeleteEffect(name) { @@ -564,19 +565,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) { From 926e0ff67a669e19e2137a343d483b94407d0085 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Mon, 21 Apr 2025 21:53:23 +0200 Subject: [PATCH 12/26] Fix that effects are not created/deleted per instance --- include/api/JsonApiCommand.h | 4 ++-- libsrc/api/JSONRPC_schema/schema-create-effect.json | 5 ----- libsrc/api/JSONRPC_schema/schema-delete-effect.json | 5 ----- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/include/api/JsonApiCommand.h b/include/api/JsonApiCommand.h index 80caeae57..08cbc95be 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 } }, 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" }, From 507f552f3517f0ff4856e21784898e82a9e1e9e1 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Mon, 21 Apr 2025 22:22:59 +0200 Subject: [PATCH 13/26] Fix dangling reference --- libsrc/api/JsonAPI.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 17aea2e77..85be1cde5 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -90,11 +90,11 @@ JsonAPI::JsonAPI(QString peerAddress, Logger *log, bool localConnection, QObject qRegisterMetaType("Event"); - connect(EventHandler::getInstance().data(), &EventHandler::signalEvent, [=](const Event &event) { + connect(EventHandler::getInstance().data(), &EventHandler::signalEvent, [log, this](const Event &event) { if (event == Event::Quit) { _isServiceAvailable = false; - Info(_log, "JSON-API service stopped"); + Info(log, "JSON-API service stopped"); } }); From f90aa8698426dee734bcd635da6c9abd07ea8aee Mon Sep 17 00:00:00 2001 From: LordGrey Date: Tue, 22 Apr 2025 09:38:22 +0200 Subject: [PATCH 14/26] Correct instance dependencies on API commaands --- doc/development/JSON-API _Commands_Overview.md | 10 +++++----- include/api/JsonApiCommand.h | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) 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/JsonApiCommand.h b/include/api/JsonApiCommand.h index 08cbc95be..866d3529e 100644 --- a/include/api/JsonApiCommand.h +++ b/include/api/JsonApiCommand.h @@ -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 } }, From 359add230d51d64121864c9e6f66811e24d43b70 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Tue, 22 Apr 2025 09:54:47 +0200 Subject: [PATCH 15/26] Add failing command to Error dialog --- assets/webconfig/js/content_index.js | 11 +++++++++-- assets/webconfig/js/hyperion.js | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index 2123508be..70d7af7b7 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -242,11 +242,18 @@ $(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.message == "No Authorization" && getStorage("loginToken")) { + + const error = event.reason; + + if (error?.message === "No Authorization" && getStorage("loginToken")) { removeStorage("loginToken"); requestRequiresDefaultPasswortChange(); } else { - showInfoDialog("error", "", event.reason.message, event.reason.details); + const errorDetails = [ + `Command: "${error?.cmd}"`, + error?.details || "No additional details." + ]; + showInfoDialog("error", "", error?.message || "Unknown error", errorDetails); } }); diff --git a/assets/webconfig/js/hyperion.js b/assets/webconfig/js/hyperion.js index b92423d72..8d168463c 100644 --- a/assets/webconfig/js/hyperion.js +++ b/assets/webconfig/js/hyperion.js @@ -131,6 +131,7 @@ function initWebSocket() { $(window.hyperion).trigger({ type: "error", reason: { + cmd: cmd, message: error, details: errorData.map((item) => item.description || "") } From 6fd41829c1efdbf47ae2f2a379502c51c4d0f644 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Tue, 22 Apr 2025 10:24:20 +0200 Subject: [PATCH 16/26] Fix - Return correct mapping type for a running instance --- libsrc/api/JsonInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From b09754c72b706828234d619f0c6e48051c7cdbae Mon Sep 17 00:00:00 2001 From: LordGrey Date: Tue, 22 Apr 2025 10:26:04 +0200 Subject: [PATCH 17/26] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 057b12231..d1adf65ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ 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 **JSON-API** - New subscription support for event updates, i.e. `Suspend, Resume, Idle, idleResume, Restart, Quit`. @@ -71,9 +72,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 From 7b4051cdf4d196c9ef3420a5a3e1621374b236cc Mon Sep 17 00:00:00 2001 From: LordGrey Date: Tue, 22 Apr 2025 18:02:10 +0200 Subject: [PATCH 18/26] Cleanup Flatbuffer processing --- include/utils/ImageResampler.h | 2 +- libsrc/flatbufserver/FlatBufferClient.cpp | 114 ++++++------------ libsrc/flatbufserver/FlatBufferClient.h | 6 +- libsrc/flatbufserver/FlatBufferConnection.cpp | 4 +- libsrc/utils/ImageResampler.cpp | 18 +-- 5 files changed, 54 insertions(+), 90 deletions(-) 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/flatbufserver/FlatBufferClient.cpp b/libsrc/flatbufserver/FlatBufferClient.cpp index a04bfa5bf..e1c88af37 100644 --- a/libsrc/flatbufserver/FlatBufferClient.cpp +++ b/libsrc/flatbufserver/FlatBufferClient.cpp @@ -53,67 +53,31 @@ void FlatBufferClient::readyRead() _timeoutTimer->start(); _receiveBuffer += _socket->readAll(); - processNextMessage(); -} - -bool FlatBufferClient::processNextMessage() -{ - 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 const messageSize = (raw[0] << 24) | (raw[1] << 16) | (raw[2] << 8) | raw[3]; - - // // Validate message size - if (messageSize == 0) + // check if we can read a header + while(_receiveBuffer.size() >= 4) { - Warning(_log, "Invalid message size: %u - dropping received data", messageSize); - _receiveBuffer.clear(); - _processingMessage = false; - return true; - } + // 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]; - // Wait for full message - if (_receiveBuffer.size() < static_cast(messageSize + 4)) - { - _processingMessage = false; - return false; - } + // check if we can read a complete message + if((uint32_t) _receiveBuffer.size() < messageSize + 4) { return; } - // Remove the processed message from the buffer (header + body) - _receiveBuffer.remove(0, messageSize + 4); + // 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); - // 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); + flatbuffers::Verifier verifier(msgData, messageSize); - if (!hyperionnet::VerifyRequestBuffer(verifier)) { - Error(_log, "Invalid FlatBuffer message received"); - sendErrorReply("Invalid FlatBuffer message received"); - _processingMessage = false; + if (!hyperionnet::VerifyRequestBuffer(verifier)) { + Error(_log, "Invalid FlatBuffer message received"); + sendErrorReply("Invalid FlatBuffer message received"); + continue; + } - QMetaObject::invokeMethod(this, &FlatBufferClient::processNextMessage, Qt::QueuedConnection); - return true; + const auto *message = hyperionnet::GetRequest(msgData); + handleMessage(message); } - - // Invoke message handling - QMetaObject::invokeMethod(this, [this, msgData]() { - handleMessage(hyperionnet::GetRequest(msgData)); - _processingMessage = false; - - // Continue processing the next message - QMetaObject::invokeMethod(this, &FlatBufferClient::processNextMessage, Qt::QueuedConnection); - }); - - return true; } void FlatBufferClient::noDataReceived() @@ -213,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) @@ -224,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"); @@ -244,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 || @@ -257,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); @@ -269,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) @@ -326,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(); } @@ -349,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( @@ -369,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 1308d3716..4a8886d53 100644 --- a/libsrc/flatbufserver/FlatBufferClient.h +++ b/libsrc/flatbufserver/FlatBufferClient.h @@ -143,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; @@ -157,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/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)); From 94a8209252aac32508eead3abd3c377eb0e818ab Mon Sep 17 00:00:00 2001 From: LordGrey Date: Thu, 24 Apr 2025 17:58:19 +0200 Subject: [PATCH 19/26] Use switched instance for API calls without instance provided --- libsrc/api/JsonAPI.cpp | 58 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 85be1cde5..be6f32b17 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -280,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()) { @@ -291,14 +291,27 @@ 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 or "all" is given + if (instances.isEmpty() || instances.contains("all")) { instanceIds = (isRunningInstanceRequired == InstanceCmd::MustRun_Yes) ? runningInstanceIds : _instanceManager->getInstanceIds(); } @@ -350,7 +363,8 @@ void JsonAPI::handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject return; } - // Execute the command for each valid instance; Hyperion is about to quit + // 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) @@ -365,6 +379,12 @@ void JsonAPI::handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject handleCommand(cmd, message); } } + + //Switch back to current instance, if command was executred 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) @@ -1482,15 +1502,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)) From de2833332cad0a32260442cd7bc43961a6ce78d8 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Thu, 24 Apr 2025 17:59:16 +0200 Subject: [PATCH 20/26] Treat an empty instance array as "all instances" for multi-instance commands --- libsrc/api/JSONRPC_schema/schema-adjustment.json | 3 +-- libsrc/api/JSONRPC_schema/schema-clear.json | 3 +-- libsrc/api/JSONRPC_schema/schema-clearall.json | 3 +-- libsrc/api/JSONRPC_schema/schema-color.json | 3 +-- libsrc/api/JSONRPC_schema/schema-componentstate.json | 3 +-- libsrc/api/JSONRPC_schema/schema-image.json | 3 +-- libsrc/api/JSONRPC_schema/schema-processing.json | 3 +-- libsrc/api/JSONRPC_schema/schema-sourceselect.json | 3 +-- 8 files changed, 8 insertions(+), 16 deletions(-) diff --git a/libsrc/api/JSONRPC_schema/schema-adjustment.json b/libsrc/api/JSONRPC_schema/schema-adjustment.json index 3afe5bcd4..c57503bfa 100644 --- a/libsrc/api/JSONRPC_schema/schema-adjustment.json +++ b/libsrc/api/JSONRPC_schema/schema-adjustment.json @@ -10,8 +10,7 @@ "instance" : { "type": "array", "required": false, - "items" : {}, - "minItems": 1 + "items" : {} }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-clear.json b/libsrc/api/JSONRPC_schema/schema-clear.json index b55be0a13..96c32b432 100644 --- a/libsrc/api/JSONRPC_schema/schema-clear.json +++ b/libsrc/api/JSONRPC_schema/schema-clear.json @@ -10,8 +10,7 @@ "instance" : { "type": "array", "required": false, - "items" : {}, - "minItems": 1 + "items" : {} }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-clearall.json b/libsrc/api/JSONRPC_schema/schema-clearall.json index 5d5d2d22c..1d1b8b0f1 100644 --- a/libsrc/api/JSONRPC_schema/schema-clearall.json +++ b/libsrc/api/JSONRPC_schema/schema-clearall.json @@ -10,8 +10,7 @@ "instance" : { "type": "array", "required": false, - "items" : {}, - "minItems": 1 + "items" : {} }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-color.json b/libsrc/api/JSONRPC_schema/schema-color.json index eeeba069a..0d95df16f 100644 --- a/libsrc/api/JSONRPC_schema/schema-color.json +++ b/libsrc/api/JSONRPC_schema/schema-color.json @@ -10,8 +10,7 @@ "instance" : { "type": "array", "required": false, - "items" : {}, - "minItems": 1 + "items" : {} }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-componentstate.json b/libsrc/api/JSONRPC_schema/schema-componentstate.json index 10ca3bb62..8b8f7b0ef 100644 --- a/libsrc/api/JSONRPC_schema/schema-componentstate.json +++ b/libsrc/api/JSONRPC_schema/schema-componentstate.json @@ -12,8 +12,7 @@ "instance" : { "type": "array", "required": false, - "items" : {}, - "minItems": 1 + "items" : {} }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-image.json b/libsrc/api/JSONRPC_schema/schema-image.json index fbd2ff402..3bf46aeb0 100644 --- a/libsrc/api/JSONRPC_schema/schema-image.json +++ b/libsrc/api/JSONRPC_schema/schema-image.json @@ -10,8 +10,7 @@ "instance" : { "type": "array", "required": false, - "items" : {}, - "minItems": 1 + "items" : {} }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-processing.json b/libsrc/api/JSONRPC_schema/schema-processing.json index 680050e83..e5c7c5206 100644 --- a/libsrc/api/JSONRPC_schema/schema-processing.json +++ b/libsrc/api/JSONRPC_schema/schema-processing.json @@ -10,8 +10,7 @@ "instance" : { "type": "array", "required": false, - "items" : {}, - "minItems": 1 + "items" : {} }, "tan" : { "type" : "integer" diff --git a/libsrc/api/JSONRPC_schema/schema-sourceselect.json b/libsrc/api/JSONRPC_schema/schema-sourceselect.json index 8763595c3..5feff238a 100644 --- a/libsrc/api/JSONRPC_schema/schema-sourceselect.json +++ b/libsrc/api/JSONRPC_schema/schema-sourceselect.json @@ -10,8 +10,7 @@ "instance" : { "type": "array", "required": false, - "items" : {}, - "minItems": 1 + "items" : {} }, "tan" : { "type" : "integer" From 0be52eb5883ffe55d37fbbf97cfb6b670a65274b Mon Sep 17 00:00:00 2001 From: LordGrey Date: Thu, 24 Apr 2025 18:22:26 +0200 Subject: [PATCH 21/26] Fixed - Last update of an effect event is not removed in sources overview --- CHANGELOG.md | 1 + assets/webconfig/js/content_remote.js | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1adf65ce..0cf0a026a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ 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. diff --git a/assets/webconfig/js/content_remote.js b/assets/webconfig/js/content_remote.js index 58f8ea0cc..afa2a3ef3 100644 --- a/assets/webconfig/js/content_remote.js +++ b/assets/webconfig/js/content_remote.js @@ -129,6 +129,11 @@ $(document).ready(function () { const prios = window.serverInfo.priorities; let clearAll = false; + if (prios.length === 0) { + $('.sstbody').append('No sources available'); + return; + } + // Iterate over priorities for (let i = 0; i < prios.length; i++) { let origin = prios[i].origin ? prios[i].origin : "System"; From df61844d92e5404ca14dad7adb57ceb9c8498b74 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Fri, 25 Apr 2025 16:43:39 +0200 Subject: [PATCH 22/26] Translate no sources text element --- assets/webconfig/i18n/en.json | 1 + assets/webconfig/js/content_remote.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 4ad991925..8e24e7bbe 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -1092,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", diff --git a/assets/webconfig/js/content_remote.js b/assets/webconfig/js/content_remote.js index afa2a3ef3..5b463c47f 100644 --- a/assets/webconfig/js/content_remote.js +++ b/assets/webconfig/js/content_remote.js @@ -124,13 +124,14 @@ $(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('No sources available'); + $('.sstbody').append(`${$.i18n('remote_input_no_sources')}`); + $('#auto_btn').empty(); return; } From 7ddc117f2cff82d7a48044cbd4d82d013af132c0 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Sat, 26 Apr 2025 18:00:31 +0200 Subject: [PATCH 23/26] Fix CodeQL finding --- assets/webconfig/js/content_index.js | 14 ++++++---- assets/webconfig/js/hyperion.js | 41 +++++++++++++++++++++------- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index 70d7af7b7..2ba9ff900 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -242,17 +242,21 @@ $(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) - + const error = event.reason; if (error?.message === "No Authorization" && getStorage("loginToken")) { removeStorage("loginToken"); requestRequiresDefaultPasswortChange(); } else { - const errorDetails = [ - `Command: "${error?.cmd}"`, - error?.details || "No additional details." - ]; + 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/hyperion.js b/assets/webconfig/js/hyperion.js index 8d168463c..00b772eae 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 @@ -116,7 +124,8 @@ function initWebSocket() { const success = response.success; const cmd = response.command; const tan = response.tan - if (success || typeof (success) == "undefined") { + + if (success || typeof (success) === "undefined") { $(window.hyperion).trigger({ type: "cmd-" + cmd, response: response }); } else @@ -127,37 +136,49 @@ function initWebSocket() { window.location.reload(); } else { const errorData = Array.isArray(response.errorData) ? response.errorData : []; - console.log("[window.websocket::onmessage] ", error, ", Description:", errorData); + + const safeError = sanitizeForLog(error); + const safeErrorData = errorData.map((item) => sanitizeForLog(item.description || "")); + + console.log("[window.websocket::onmessage] ", safeError, ", Description:", safeErrorData); + $(window.hyperion).trigger({ type: "error", reason: { cmd: cmd, - message: error, - details: errorData.map((item) => item.description || "") + message: safeError, + details: safeErrorData } }); } } } catch (exception_error) { - console.log("[window.websocket::onmessage] ", exception_error); + const safeExceptionMessage = sanitizeForLog(exception_error.message || 'Unknown error'); + const safeExceptionStack = sanitizeForLog(exception_error.stack || ''); + + console.log("[window.websocket::onmessage] ", safeExceptionMessage); + $(window.hyperion).trigger({ type: "error", reason: { - message: $.i18n("ws_processing_exception") + ": " + exception_error.message, - details: [exception_error.stack] + message: $.i18n("ws_processing_exception") + ": " + safeExceptionMessage, + details: [safeExceptionStack] } }); } }; window.websocket.onerror = function (error) { - console.log("[window.websocket::onerror] ", error); + const safeError = sanitizeForLog(error?.message || String(error)); + + console.log("[window.websocket::onerror] ", safeError); + $(window.hyperion).trigger({ type: "error", reason: { - message: $.i18n("ws_error_occured"), - details: [error] + message: $.i18n("ws_error_occured"), + details: [safeError] } }); }; From 266537bf1fca6a4460e6dee1bcd7789294c2d9ad Mon Sep 17 00:00:00 2001 From: LordGrey Date: Sat, 26 Apr 2025 19:03:34 +0200 Subject: [PATCH 24/26] JSON API clean-up --- .../api/JSONRPC_schema/schema-adjustment.json | 10 ++++-- libsrc/api/JSONRPC_schema/schema-clear.json | 10 ++++-- .../api/JSONRPC_schema/schema-clearall.json | 10 ++++-- libsrc/api/JSONRPC_schema/schema-color.json | 10 ++++-- .../JSONRPC_schema/schema-componentstate.json | 10 ++++-- libsrc/api/JSONRPC_schema/schema-config.json | 35 +++++++++++-------- libsrc/api/JSONRPC_schema/schema-effect.json | 10 ++++-- libsrc/api/JSONRPC_schema/schema-image.json | 10 ++++-- .../api/JSONRPC_schema/schema-instance.json | 2 +- .../JSONRPC_schema/schema-instancedata.json | 2 +- .../api/JSONRPC_schema/schema-ledcolors.json | 2 +- .../api/JSONRPC_schema/schema-leddevice.json | 3 -- .../api/JSONRPC_schema/schema-processing.json | 10 ++++-- .../api/JSONRPC_schema/schema-serverinfo.json | 2 +- .../JSONRPC_schema/schema-sourceselect.json | 10 ++++-- libsrc/api/JsonAPI.cpp | 33 +++++++++-------- 16 files changed, 103 insertions(+), 66 deletions(-) diff --git a/libsrc/api/JSONRPC_schema/schema-adjustment.json b/libsrc/api/JSONRPC_schema/schema-adjustment.json index c57503bfa..c96e07f8e 100644 --- a/libsrc/api/JSONRPC_schema/schema-adjustment.json +++ b/libsrc/api/JSONRPC_schema/schema-adjustment.json @@ -7,10 +7,14 @@ "required" : true, "enum" : ["adjustment"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {} + "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 96c32b432..495388e7d 100644 --- a/libsrc/api/JSONRPC_schema/schema-clear.json +++ b/libsrc/api/JSONRPC_schema/schema-clear.json @@ -7,10 +7,14 @@ "required" : true, "enum" : ["clear"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {} + "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 1d1b8b0f1..32733bd70 100644 --- a/libsrc/api/JSONRPC_schema/schema-clearall.json +++ b/libsrc/api/JSONRPC_schema/schema-clearall.json @@ -7,10 +7,14 @@ "required" : true, "enum" : ["clearall"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {} + "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 0d95df16f..1eda846a0 100644 --- a/libsrc/api/JSONRPC_schema/schema-color.json +++ b/libsrc/api/JSONRPC_schema/schema-color.json @@ -7,10 +7,14 @@ "required" : true, "enum" : ["color"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {} + "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 8b8f7b0ef..3c89db11f 100644 --- a/libsrc/api/JSONRPC_schema/schema-componentstate.json +++ b/libsrc/api/JSONRPC_schema/schema-componentstate.json @@ -9,10 +9,14 @@ "required" : true, "enum" : ["componentstate"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {} + "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-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 3bf46aeb0..707d4f6fc 100644 --- a/libsrc/api/JSONRPC_schema/schema-image.json +++ b/libsrc/api/JSONRPC_schema/schema-image.json @@ -7,10 +7,14 @@ "required" : true, "enum" : ["image"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {} + "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 e5c7c5206..0e00c7815 100644 --- a/libsrc/api/JSONRPC_schema/schema-processing.json +++ b/libsrc/api/JSONRPC_schema/schema-processing.json @@ -7,10 +7,14 @@ "required" : true, "enum" : ["processing"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {} + "items": { + "type": "integer", + "minimum": 0, + "maximum": 254 + }, + "uniqueItems": true }, "tan" : { "type" : "integer" 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 5feff238a..352480fcc 100644 --- a/libsrc/api/JSONRPC_schema/schema-sourceselect.json +++ b/libsrc/api/JSONRPC_schema/schema-sourceselect.json @@ -7,10 +7,14 @@ "required" : true, "enum" : ["sourceselect"] }, - "instance" : { + "instance": { "type": "array", - "required": false, - "items" : {} + "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 be6f32b17..055e0af18 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -310,24 +310,25 @@ void JsonAPI::handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject QSet instanceIds; QStringList errorDetails; - // Determine instance IDs, if empty array provided or "all" is given - if (instances.isEmpty() || 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())); @@ -339,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) ) @@ -347,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 @@ -380,7 +383,7 @@ void JsonAPI::handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject } } - //Switch back to current instance, if command was executred against multiple instances + //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); @@ -1759,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; } From 7a0ddcd7e4f6adcb5a426b26a5a2c3d3aa5199ae Mon Sep 17 00:00:00 2001 From: LordGrey Date: Sat, 26 Apr 2025 21:26:03 +0200 Subject: [PATCH 25/26] Http-Server: Support Cross-Origin Resource Sharing (CORS) (#1496) --- CHANGELOG.md | 1 + libsrc/webserver/QtHttpClientWrapper.cpp | 41 +++++++++++++-- libsrc/webserver/QtHttpClientWrapper.h | 3 ++ libsrc/webserver/QtHttpHeader.cpp | 67 +++++++++++++----------- libsrc/webserver/QtHttpHeader.h | 5 +- libsrc/webserver/QtHttpReply.h | 1 + libsrc/webserver/QtHttpServer.cpp | 2 +- libsrc/webserver/StaticFileServing.cpp | 1 - 8 files changed, 83 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cf0a026a..59e3d5891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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`. 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 (); } From 6b40df5b9c6fe0bc2dd3e893d2b5589edd366c67 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Sun, 27 Apr 2025 14:16:54 +0200 Subject: [PATCH 26/26] Dress CodeQL Findings (2) --- assets/webconfig/js/hyperion.js | 34 +++++++------------ .../js/wizards/LedDevice_philipshue.js | 10 ++++++ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/assets/webconfig/js/hyperion.js b/assets/webconfig/js/hyperion.js index 00b772eae..d79097c5d 100644 --- a/assets/webconfig/js/hyperion.js +++ b/assets/webconfig/js/hyperion.js @@ -124,8 +124,7 @@ function initWebSocket() { const success = response.success; const cmd = response.command; const tan = response.tan - - if (success || typeof (success) === "undefined") { + if (success || typeof (success) == "undefined") { $(window.hyperion).trigger({ type: "cmd-" + cmd, response: response }); } else @@ -137,48 +136,41 @@ function initWebSocket() { } else { const errorData = Array.isArray(response.errorData) ? response.errorData : []; - const safeError = sanitizeForLog(error); - const safeErrorData = errorData.map((item) => sanitizeForLog(item.description || "")); - - console.log("[window.websocket::onmessage] ", safeError, ", Description:", safeErrorData); + // 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: safeError, - details: safeErrorData + message: error, + details: errorData.map((item) => item.description || "") } }); } } } catch (exception_error) { - const safeExceptionMessage = sanitizeForLog(exception_error.message || 'Unknown error'); - const safeExceptionStack = sanitizeForLog(exception_error.stack || ''); - - console.log("[window.websocket::onmessage] ", safeExceptionMessage); - + console.log("[window.websocket::onmessage] ", exception_error); $(window.hyperion).trigger({ type: "error", reason: { - message: $.i18n("ws_processing_exception") + ": " + safeExceptionMessage, - details: [safeExceptionStack] + message: $.i18n("ws_processing_exception") + ": " + exception_error.message, + details: [exception_error.stack] } }); } }; window.websocket.onerror = function (error) { - const safeError = sanitizeForLog(error?.message || String(error)); - - console.log("[window.websocket::onerror] ", safeError); - + console.log("[window.websocket::onerror] ", error); $(window.hyperion).trigger({ type: "error", reason: { - message: $.i18n("ws_error_occured"), - details: [safeError] + message: $.i18n("ws_error_occured"), + details: [error] } }); }; 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] = {};