From 179ee316d029288cb1cc22a6ecc488fcaadd76e1 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Mon, 9 Dec 2024 06:21:53 +0100 Subject: [PATCH] Remove max LED number constraint from Matrix layout (#1805) --- CHANGELOG.md | 3 + effects/candle.py | 3 + effects/gif.py | 4 ++ effects/ledtest-seq.py | 3 + effects/ledtest.py | 3 + effects/plasma.py | 3 + effects/running_dots.py | 5 +- effects/shutdown.py | 3 + effects/sparks.py | 3 + effects/swirl.py | 5 +- effects/traces.py | 3 + effects/trails.py | 5 +- effects/x-mas.py | 5 +- include/api/JsonAPI.h | 4 +- include/api/JsonCallbacks.h | 4 +- include/effectengine/Effect.h | 2 + include/effectengine/EffectModule.h | 49 ++++++------- include/hyperion/Hyperion.h | 12 ++++ libsrc/api/JsonAPI.cpp | 16 +++-- libsrc/api/JsonCallbacks.cpp | 74 +++++++++++++------- libsrc/effectengine/Effect.cpp | 11 ++- libsrc/effectengine/EffectModule.cpp | 8 +++ libsrc/hyperion/Hyperion.cpp | 56 +++++++++++++-- libsrc/hyperion/schema/schema-ledConfig.json | 2 - libsrc/jsonserver/JsonClientConnection.cpp | 7 +- libsrc/webserver/WebJsonRpc.cpp | 7 +- libsrc/webserver/WebJsonRpc.h | 2 +- libsrc/webserver/WebSocketClient.cpp | 5 +- 28 files changed, 231 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d605e4ec1..8cbc6c900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow to force starting Hyperion in read-only mode (`--readonlyMode`) - JSON-API: Support to query for a dedicated set of configuration items for a set of instances - JSON-API: Support to save a dedicated set of configuration items for a set of instances +- JSON-API: Limit update emission frequency: Images (25Hz), raw LED-Colors (40Hz) & LED-Device data (200Hz) +- Effects: Limit the maximum update rate to 200Hz **JSON-API** - New subscription support for event updates, i.e. `Suspend, Resume, Idle, idleResume, Restart, Quit`. @@ -44,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Refactored: Python to enable parallel effect processing under Python 3.12 - Fixed: Python 3.12 crashes (#1747) - osX Grabber: Use ScreenCaptureKit under macOS 15 and above +- Removed maximum LED number constraint from Matrix layout schema which was not synced with the UI behaviour (#1804) **JSON-API** - Refactored JSON-API to ensure consistent authorization behaviour across sessions and single requests with token authorization. diff --git a/effects/candle.py b/effects/candle.py index bbe77b352..edcc6b110 100644 --- a/effects/candle.py +++ b/effects/candle.py @@ -20,6 +20,9 @@ sleepTime = float(hyperion.args.get('sleepTime', 0.14)) +# Limit update rate +sleepTime = max(hyperion.lowestUpdateInterval(), sleepTime) + candles = hyperion.args.get('candles', "all") ledlist = hyperion.args.get('ledlist', "1") diff --git a/effects/gif.py b/effects/gif.py index 936f19b91..81ffe72ac 100644 --- a/effects/gif.py +++ b/effects/gif.py @@ -11,6 +11,10 @@ grayscale = bool(hyperion.args.get('grayscale', False)) sleepTime = 1./framesPerSecond + +# Limit update rate +sleepTime = max(hyperion.lowestUpdateInterval(), sleepTime) + imageFrameList = [] if imageData: diff --git a/effects/ledtest-seq.py b/effects/ledtest-seq.py index 535c06bb9..092ba1f1c 100644 --- a/effects/ledtest-seq.py +++ b/effects/ledtest-seq.py @@ -4,6 +4,9 @@ # Get parameters sleepTime = float(hyperion.args.get('sleepTime', 0.5)) +# Limit update rate +sleepTime = max(hyperion.lowestUpdateInterval(), sleepTime) + def TestRgb( iteration ): switcher = { diff --git a/effects/ledtest.py b/effects/ledtest.py index bbdb70561..ad5aa5565 100644 --- a/effects/ledtest.py +++ b/effects/ledtest.py @@ -11,6 +11,9 @@ testleds = hyperion.args.get('testleds', "all") ledlist = hyperion.args.get('ledlist', "1") +# Limit update rate +sleepTime = max(hyperion.lowestUpdateInterval(), sleepTime) + testlist = () if (testleds == "list") and (type(ledlist) is str): for s in ledlist.split(','): diff --git a/effects/plasma.py b/effects/plasma.py index ffc5dba55..fd4e0be21 100644 --- a/effects/plasma.py +++ b/effects/plasma.py @@ -6,6 +6,9 @@ height = hyperion.imageHeight() sleepTime = float(hyperion.args.get('sleepTime', 0.2)) +# Limit update rate +sleepTime = max(hyperion.lowestUpdateInterval(), sleepTime) + def mapto(x, in_min, in_max, out_min, out_max): return float((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min) diff --git a/effects/running_dots.py b/effects/running_dots.py index 2fb9569b3..d51065934 100644 --- a/effects/running_dots.py +++ b/effects/running_dots.py @@ -4,8 +4,11 @@ sleepTime = float(hyperion.args.get('speed', 1.5)) * 0.005 whiteLevel = int(hyperion.args.get('whiteLevel', 0)) lvl = int(hyperion.args.get('colorLevel', 220)) + +# Limit update rate +sleepTime = max(hyperion.lowestUpdateInterval(), sleepTime) -# check value +# check value whiteLevel = min( whiteLevel, 254 ) lvl = min( lvl, 255 ) diff --git a/effects/shutdown.py b/effects/shutdown.py index 12209988e..e71c6b0d6 100644 --- a/effects/shutdown.py +++ b/effects/shutdown.py @@ -21,6 +21,9 @@ def setPixel(x,y,rgb): imageData = bytearray(height * width * (0,0,0)) +# Limit update rate +sleepTime = max(hyperion.lowestUpdateInterval(), sleepTime) + # Start the write data loop if initialBlink: for i in range(6): diff --git a/effects/sparks.py b/effects/sparks.py index d7e35d771..c43443240 100644 --- a/effects/sparks.py +++ b/effects/sparks.py @@ -8,6 +8,9 @@ color = list(hyperion.args.get('color', (255,255,255))) randomColor = bool(hyperion.args.get('random-color', False)) +# Limit update rate +sleepTime = max(hyperion.lowestUpdateInterval(), sleepTime) + # Check parameters rotationTime = max(0.1, rotationTime) diff --git a/effects/swirl.py b/effects/swirl.py index fa5d0ed4c..66034e721 100644 --- a/effects/swirl.py +++ b/effects/swirl.py @@ -22,7 +22,10 @@ def getPoint(rand = True ,x = 0.5, y = 0.5): def getSTime(rt, steps = 360): rt = float(rt) sleepTime = max(0.1, rt) / steps - + + # Limit update rate + sleepTime = max(hyperion.lowestUpdateInterval(), sleepTime) + # adapt sleeptime to hardware minStepTime= float(hyperion.latchTime)/1000.0 if minStepTime == 0: minStepTime = 0.001 diff --git a/effects/traces.py b/effects/traces.py index 37338b144..aaeda242a 100644 --- a/effects/traces.py +++ b/effects/traces.py @@ -10,6 +10,9 @@ if minStepTime == 0: minStepTime = 0.001 factor = 1 if sleepTime > minStepTime else int(math.ceil(minStepTime/sleepTime)) +# Limit update rate +sleepTime = max(hyperion.lowestUpdateInterval(), sleepTime) + runners = [ { "i":0, "pos":0, "c":0, "step":9, "lvl":255}, { "i":1, "pos":0, "c":0, "step":8, "lvl":255}, diff --git a/effects/trails.py b/effects/trails.py index 3f8ba2e05..e626468c5 100644 --- a/effects/trails.py +++ b/effects/trails.py @@ -11,7 +11,10 @@ color = list(hyperion.args.get('color', (255,255,255))) randomise = bool(hyperion.args.get('random', False)) iWidth = hyperion.imageWidth() -iHeight = hyperion.imageHeight() +iHeight = hyperion.imageHeight() + +# Limit update rate +sleepTime = max(hyperion.lowestUpdateInterval(), sleepTime) class trail: def __init__(self): diff --git a/effects/x-mas.py b/effects/x-mas.py index b0bf706e3..0221941f3 100644 --- a/effects/x-mas.py +++ b/effects/x-mas.py @@ -4,7 +4,10 @@ sleepTime = float(hyperion.args.get('sleepTime', 1000))/1000.0 length = hyperion.args.get('length', 1) color1 = hyperion.args.get('color1', (255,255,255)) -color2 = hyperion.args.get('color2', (255,0,0)) +color2 = hyperion.args.get('color2', (255,0,0)) + +# Limit update rate +sleepTime = max(hyperion.lowestUpdateInterval(), sleepTime) # Initialize the led data i = 0 diff --git a/include/api/JsonAPI.h b/include/api/JsonAPI.h index 60b092f5e..3ac43269f 100644 --- a/include/api/JsonAPI.h +++ b/include/api/JsonAPI.h @@ -50,6 +50,8 @@ class JsonAPI : public API /// void initialize(); + QSharedPointer getCallBack() const; + public slots: private slots: @@ -82,7 +84,7 @@ private slots: /// /// Signal emits with the reply message provided with handleMessage() /// - void callbackMessage(QJsonObject); + void callbackReady(QJsonObject); /// /// Signal emits whenever a JSON-message should be forwarded diff --git a/include/api/JsonCallbacks.h b/include/api/JsonCallbacks.h index a5c14ce45..14b30479f 100644 --- a/include/api/JsonCallbacks.h +++ b/include/api/JsonCallbacks.h @@ -93,7 +93,7 @@ class JsonCallbacks : public QObject /// @brief Emits whenever a new json mesage callback is ready to send /// @param The JsonObject message /// - void newCallback(QJsonObject); + void callbackReady(QJsonObject); private slots: /// @@ -182,6 +182,8 @@ private slots: /// construct callback msg void doCallback(Subscription::Type cmd, const QVariant& data); + void doCallback(Subscription::Type cmd, const QJsonArray& data); + void doCallback(Subscription::Type cmd, const QJsonObject& data); Logger *_log; Hyperion* _hyperion; diff --git a/include/effectengine/Effect.h b/include/effectengine/Effect.h index 294f5efc5..e4dc826b7 100644 --- a/include/effectengine/Effect.h +++ b/include/effectengine/Effect.h @@ -103,4 +103,6 @@ class Effect : public QThread QImage _image; QPainter* _painter; QVector _imageStack; + + double _lowestUpdateIntervalInSeconds; }; diff --git a/include/effectengine/EffectModule.h b/include/effectengine/EffectModule.h index a9c5a4fd3..44c3bd2f4 100644 --- a/include/effectengine/EffectModule.h +++ b/include/effectengine/EffectModule.h @@ -21,28 +21,29 @@ class EffectModule : public QObject // Wrapper methods for Python interpreter extra buildin methods static PyMethodDef effectMethods[]; - static PyObject* wrapSetColor(PyObject* self, PyObject* args); - static PyObject* wrapSetImage(PyObject* self, PyObject* args); - static PyObject* wrapGetImage(PyObject* self, PyObject* args); - static PyObject* wrapAbort(PyObject* self, PyObject* args); - static PyObject* wrapImageShow(PyObject* self, PyObject* args); - static PyObject* wrapImageLinearGradient(PyObject* self, PyObject* args); - static PyObject* wrapImageConicalGradient(PyObject* self, PyObject* args); - static PyObject* wrapImageRadialGradient(PyObject* self, PyObject* args); - static PyObject* wrapImageSolidFill(PyObject* self, PyObject* args); - static PyObject* wrapImageDrawLine(PyObject* self, PyObject* args); - static PyObject* wrapImageDrawPoint(PyObject* self, PyObject* args); - static PyObject* wrapImageDrawRect(PyObject* self, PyObject* args); - static PyObject* wrapImageDrawPolygon(PyObject* self, PyObject* args); - static PyObject* wrapImageDrawPie(PyObject* self, PyObject* args); - static PyObject* wrapImageSetPixel(PyObject* self, PyObject* args); - static PyObject* wrapImageGetPixel(PyObject* self, PyObject* args); - static PyObject* wrapImageSave(PyObject* self, PyObject* args); - static PyObject* wrapImageMinSize(PyObject* self, PyObject* args); - static PyObject* wrapImageWidth(PyObject* self, PyObject* args); - static PyObject* wrapImageHeight(PyObject* self, PyObject* args); - static PyObject* wrapImageCRotate(PyObject* self, PyObject* args); - static PyObject* wrapImageCOffset(PyObject* self, PyObject* args); - static PyObject* wrapImageCShear(PyObject* self, PyObject* args); - static PyObject* wrapImageResetT(PyObject* self, PyObject* args); + static PyObject* wrapSetColor (PyObject *self, PyObject *args); + static PyObject* wrapSetImage (PyObject *self, PyObject *args); + static PyObject* wrapGetImage (PyObject *self, PyObject *args); + static PyObject* wrapAbort (PyObject *self, PyObject *args); + static PyObject* wrapImageShow (PyObject *self, PyObject *args); + static PyObject* wrapImageLinearGradient (PyObject *self, PyObject *args); + static PyObject* wrapImageConicalGradient (PyObject *self, PyObject *args); + static PyObject* wrapImageRadialGradient (PyObject *self, PyObject *args); + static PyObject* wrapImageSolidFill (PyObject *self, PyObject *args); + static PyObject* wrapImageDrawLine (PyObject *self, PyObject *args); + static PyObject* wrapImageDrawPoint (PyObject *self, PyObject *args); + static PyObject* wrapImageDrawRect (PyObject *self, PyObject *args); + static PyObject* wrapImageDrawPolygon (PyObject *self, PyObject *args); + static PyObject* wrapImageDrawPie (PyObject *self, PyObject *args); + static PyObject* wrapImageSetPixel (PyObject *self, PyObject *args); + static PyObject* wrapImageGetPixel (PyObject *self, PyObject *args); + static PyObject* wrapImageSave (PyObject *self, PyObject *args); + static PyObject* wrapImageMinSize (PyObject *self, PyObject *args); + static PyObject* wrapImageWidth (PyObject *self, PyObject *args); + static PyObject* wrapImageHeight (PyObject *self, PyObject *args); + static PyObject* wrapImageCRotate (PyObject *self, PyObject *args); + static PyObject* wrapImageCOffset (PyObject *self, PyObject *args); + static PyObject* wrapImageCShear (PyObject *self, PyObject *args); + static PyObject* wrapImageResetT (PyObject *self, PyObject *args); + static PyObject* wrapLowestUpdateInterval (PyObject* self, PyObject* args); }; diff --git a/include/hyperion/Hyperion.h b/include/hyperion/Hyperion.h index 6a3772055..546416233 100644 --- a/include/hyperion/Hyperion.h +++ b/include/hyperion/Hyperion.h @@ -2,6 +2,7 @@ // stl includes #include +#include // QT includes #include @@ -11,6 +12,7 @@ #include #include #include +#include // hyperion-utils includes #include @@ -604,4 +606,14 @@ private slots: /// Boblight instance BoblightServer* _boblightServer; #endif + + QElapsedTimer _imageTimer; // Timer for controlling image emission frequency + QElapsedTimer _rawLedDataTimer; // Timer for controlling rawLedColors emission frequency + QElapsedTimer _ledDeviceDataTimer; // Timer for controlling LedDevice data emission frequency + qint64 _lastImageEmission; // Last timestamp of image signal emission + qint64 _lastRawLedDataEmission; // Last timestamp of rawLedColors signal emission + qint64 _lastLedDeviceDataEmission; // Last timestamp of ledDeviceData signal emission + std::chrono::milliseconds _imageEmissionInterval; + std::chrono::milliseconds _rawLedDataEmissionInterval; + std::chrono::milliseconds _ledDeviceDataEmissionInterval; }; diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 52e4c6af8..ac5fcdc1e 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -85,6 +85,11 @@ JsonAPI::JsonAPI(QString peerAddress, Logger *log, bool localConnection, QObject _jsonCB = QSharedPointer(new JsonCallbacks( _log, _peerAddress, parent)); } +QSharedPointer JsonAPI::getCallBack() const +{ + return _jsonCB; +} + void JsonAPI::initialize() { // init API, REQUIRED! @@ -97,9 +102,6 @@ void JsonAPI::initialize() // listen for killed instances connect(_instanceManager, &HyperionIManager::instanceStateChanged, this, &JsonAPI::handleInstanceStateChange); - // pipe callbacks from subscriptions to parent - connect(_jsonCB.data(), &JsonCallbacks::newCallback, this, &JsonAPI::callbackMessage); - // notify hyperion about a jsonMessageForward if (_hyperion != nullptr) { @@ -1537,7 +1539,7 @@ void JsonAPI::sendSuccessReply(const JsonApiCommand& cmd) void JsonAPI::sendSuccessReply(const QString &command, int tan, InstanceCmd::Type isInstanceCmd) { - emit callbackMessage(getBasicCommandReply(true, command, tan , isInstanceCmd)); + emit callbackReady(getBasicCommandReply(true, command, tan , isInstanceCmd)); } void JsonAPI::sendSuccessDataReply(const QJsonValue &infoData, const JsonApiCommand& cmd) @@ -1572,7 +1574,7 @@ void JsonAPI::sendSuccessDataReplyWithError(const QJsonValue &infoData, const QS reply["errorData"] = errorsArray; } - emit callbackMessage(reply); + emit callbackReady(reply); } void JsonAPI::sendErrorReply(const QString &error, const JsonApiCommand& cmd) @@ -1601,7 +1603,7 @@ void JsonAPI::sendErrorReply(const QString &error, const QStringList& errorDetai reply["errorData"] = errorsArray; } - emit callbackMessage(reply); + emit callbackReady(reply); } void JsonAPI::sendNewRequest(const QJsonValue &infoData, const JsonApiCommand& cmd) @@ -1621,7 +1623,7 @@ void JsonAPI::sendNewRequest(const QJsonValue &infoData, const QString &command, request["info"] = infoData; - emit callbackMessage(request); + emit callbackReady(request); } void JsonAPI::sendNoAuthorization(const JsonApiCommand& cmd) diff --git a/libsrc/api/JsonCallbacks.cpp b/libsrc/api/JsonCallbacks.cpp index 0e1567270..44369cac1 100644 --- a/libsrc/api/JsonCallbacks.cpp +++ b/libsrc/api/JsonCallbacks.cpp @@ -282,22 +282,43 @@ QStringList JsonCallbacks::getSubscribedCommands() const } void JsonCallbacks::doCallback(Subscription::Type cmd, const QVariant& data) +{ + if (data.userType() == QMetaType::QJsonArray) + { + doCallback(cmd, data.toJsonArray()); + } + else + { + doCallback(cmd, data.toJsonObject()); + } +} + +void JsonCallbacks::doCallback(Subscription::Type cmd, const QJsonArray& data) { QJsonObject obj; obj["command"] = Subscription::toString(cmd); if (Subscription::isInstanceSpecific(cmd)) { - obj["instance"] = _hyperion->getInstanceIndex(); + obj.insert("instance", _hyperion->getInstanceIndex()); } + obj.insert("data", data); - if (data.userType() == QMetaType::QJsonArray) { - obj["data"] = data.toJsonArray(); - } else { - obj["data"] = data.toJsonObject(); + emit callbackReady(obj); +} + +void JsonCallbacks::doCallback(Subscription::Type cmd, const QJsonObject& data) +{ + QJsonObject obj; + obj["command"] = Subscription::toString(cmd); + + if (Subscription::isInstanceSpecific(cmd)) + { + obj.insert("instance", _hyperion->getInstanceIndex()); } + obj.insert("data", data); - emit newCallback(obj); + emit callbackReady(obj); } void JsonCallbacks::handleComponentState(hyperion::Components comp, bool state) @@ -306,7 +327,7 @@ void JsonCallbacks::handleComponentState(hyperion::Components comp, bool state) data["name"] = componentToIdString(comp); data["enabled"] = state; - doCallback(Subscription::ComponentsUpdate, QVariant(data)); + doCallback(Subscription::ComponentsUpdate, data); } void JsonCallbacks::handlePriorityUpdate(int currentPriority, const PriorityMuxer::InputsMap& activeInputs) @@ -315,7 +336,7 @@ void JsonCallbacks::handlePriorityUpdate(int currentPriority, const PriorityMuxe data["priorities"] = JsonInfo::getPrioritiestInfo(currentPriority, activeInputs); data["priorities_autoselect"] = _hyperion->sourceAutoSelectEnabled(); - doCallback(Subscription::PrioritiesUpdate, QVariant(data)); + doCallback(Subscription::PrioritiesUpdate, data); } void JsonCallbacks::handleImageToLedsMappingChange(int mappingType) @@ -323,7 +344,7 @@ void JsonCallbacks::handleImageToLedsMappingChange(int mappingType) QJsonObject data; data["imageToLedMappingType"] = ImageProcessor::mappingTypeToStr(mappingType); - doCallback(Subscription::ImageToLedMappingUpdate, QVariant(data)); + doCallback(Subscription::ImageToLedMappingUpdate, data); } void JsonCallbacks::handleAdjustmentChange() @@ -335,7 +356,7 @@ void JsonCallbacks::handleVideoModeChange(VideoMode mode) { QJsonObject data; data["videomode"] = QString(videoMode2String(mode)); - doCallback(Subscription::VideomodeUpdate, QVariant(data)); + doCallback(Subscription::VideomodeUpdate, data); } #if defined(ENABLE_EFFECTENGINE) @@ -343,29 +364,29 @@ void JsonCallbacks::handleEffectListChange() { QJsonObject effects; effects["effects"] = JsonInfo::getEffects(_hyperion); - doCallback(Subscription::EffectsUpdate, QVariant(effects)); + doCallback(Subscription::EffectsUpdate, effects); } #endif void JsonCallbacks::handleSettingsChange(settings::type type, const QJsonDocument& data) { - QJsonObject dat; + QJsonObject obj; if(data.isObject()) { - dat[typeToString(type)] = data.object(); + obj[typeToString(type)] = data.object(); } else { - dat[typeToString(type)] = data.array(); + obj[typeToString(type)] = data.array(); } - doCallback(Subscription::SettingsUpdate, QVariant(dat)); + doCallback(Subscription::SettingsUpdate, obj); } void JsonCallbacks::handleLedsConfigChange(settings::type type, const QJsonDocument& data) { if(type == settings::LEDS) { - QJsonObject dat; - dat[typeToString(type)] = data.array(); - doCallback(Subscription::LedsUpdate, QVariant(dat)); + QJsonObject obj; + obj[typeToString(type)] = data.array(); + doCallback(Subscription::LedsUpdate, obj); } } @@ -385,7 +406,7 @@ void JsonCallbacks::handleTokenChange(const QVector sub["last_use"] = entry.lastUse; arr.push_back(sub); } - doCallback(Subscription::TokenUpdate, QVariant(arr)); + doCallback(Subscription::TokenUpdate, arr); } void JsonCallbacks::handleLedColorUpdate(const std::vector &ledColors) @@ -393,13 +414,16 @@ void JsonCallbacks::handleLedColorUpdate(const std::vector &ledColors) QJsonObject result; QJsonArray leds; - for (const auto &color : ledColors) + // Avoid copying by appending RGB values directly + for (const auto& color : ledColors) { - leds << QJsonValue(color.red) << QJsonValue(color.green) << QJsonValue(color.blue); + leds.append(QJsonValue(color.red)); + leds.append(QJsonValue(color.green)); + leds.append(QJsonValue(color.blue)); } result["leds"] = leds; - doCallback(Subscription::LedColorsUpdate, QVariant(result)); + doCallback(Subscription::LedColorsUpdate, result); } void JsonCallbacks::handleImageUpdate(const Image &image) @@ -413,7 +437,7 @@ void JsonCallbacks::handleImageUpdate(const Image &image) QJsonObject result; result["image"] = "data:image/jpg;base64," + QString(byteArray.toBase64()); - doCallback(Subscription::ImageUpdate, QVariant(result)); + doCallback(Subscription::ImageUpdate, result); } void JsonCallbacks::handleLogMessageUpdate(const Logger::T_LOG_MESSAGE &msg) @@ -445,7 +469,7 @@ void JsonCallbacks::handleLogMessageUpdate(const Logger::T_LOG_MESSAGE &msg) } result.insert("messages", messageArray); - doCallback(Subscription::LogMsgUpdate, QVariant(result)); + doCallback(Subscription::LogMsgUpdate, result); } void JsonCallbacks::handleEventUpdate(const Event &event) @@ -454,6 +478,6 @@ void JsonCallbacks::handleEventUpdate(const Event &event) result["event"] = eventToString(event); - doCallback(Subscription::EventUpdate, QVariant(result)); + doCallback(Subscription::EventUpdate, result); } diff --git a/libsrc/effectengine/Effect.cpp b/libsrc/effectengine/Effect.cpp index c01f26432..b4fcc1dac 100644 --- a/libsrc/effectengine/Effect.cpp +++ b/libsrc/effectengine/Effect.cpp @@ -13,7 +13,13 @@ // python utils #include -Effect::Effect(Hyperion* hyperion, int priority, int timeout, const QString& script, const QString& name, const QJsonObject& args, const QString& imageData) + +// Constants +namespace { + int DEFAULT_MAX_UPDATE_RATE_HZ { 200 }; +} //End of constants + +Effect::Effect(Hyperion *hyperion, int priority, int timeout, const QString &script, const QString &name, const QJsonObject &args, const QString &imageData) : QThread() , _hyperion(hyperion) , _priority(priority) @@ -26,7 +32,8 @@ Effect::Effect(Hyperion* hyperion, int priority, int timeout, const QString& scr , _endTime(-1) , _interupt(false) , _imageSize(hyperion->getLedGridSize()) - , _image(_imageSize, QImage::Format_ARGB32_Premultiplied) + , _image(_imageSize,QImage::Format_ARGB32_Premultiplied) + , _lowestUpdateIntervalInSeconds(1/static_cast(DEFAULT_MAX_UPDATE_RATE_HZ)) { _colors.resize(_hyperion->getLedCount()); _colors.fill(ColorRgb::BLACK); diff --git a/libsrc/effectengine/EffectModule.cpp b/libsrc/effectengine/EffectModule.cpp index d3ce4a495..4a15b545d 100644 --- a/libsrc/effectengine/EffectModule.cpp +++ b/libsrc/effectengine/EffectModule.cpp @@ -184,6 +184,7 @@ PyMethodDef EffectModule::effectMethods[] = { {"imageCOffset" , EffectModule::wrapImageCOffset , METH_VARARGS, "Add offset to the coordinate system"}, {"imageCShear" , EffectModule::wrapImageCShear , METH_VARARGS, "Shear of coordinate system by the given horizontal/vertical axis"}, {"imageResetT" , EffectModule::wrapImageResetT , METH_NOARGS, "Resets all coords modifications (rotate,offset,shear)"}, + {"lowestUpdateInterval" , EffectModule::wrapLowestUpdateInterval , METH_NOARGS, "Gets the lowest permissible interval time in seconds"}, {NULL, NULL, 0, NULL} }; @@ -1106,3 +1107,10 @@ PyObject* EffectModule::wrapImageResetT(PyObject* self, PyObject* args) getEffect()->_painter->resetTransform(); Py_RETURN_NONE; } + +PyObject* EffectModule::wrapLowestUpdateInterval(PyObject* self, PyObject* args) +{ + qDebug() << "_lowestUpdateIntervalInSeconds: " << getEffect()->_lowestUpdateIntervalInSeconds; + + return Py_BuildValue("d", getEffect()->_lowestUpdateIntervalInSeconds); +} diff --git a/libsrc/hyperion/Hyperion.cpp b/libsrc/hyperion/Hyperion.cpp index db9703141..1e5131166 100644 --- a/libsrc/hyperion/Hyperion.cpp +++ b/libsrc/hyperion/Hyperion.cpp @@ -49,6 +49,13 @@ #include #endif +// Constants +namespace { + constexpr std::chrono::milliseconds DEFAULT_MAX_IMAGE_EMISSION_INTERVAL{ 40 }; // 25 Hz + constexpr std::chrono::milliseconds DEFAULT_MAX_RAW_LED_DATA_EMISSION_INTERVAL{ 25 }; // 40 Hz + constexpr std::chrono::milliseconds DEFAULT_MAX_LED_DEVICE_DATA_EMISSION_INTERVAL{ 5 }; // 200 Hz +} //End of constants + Hyperion::Hyperion(quint8 instance) : QObject() , _instIndex(instance) @@ -75,6 +82,12 @@ Hyperion::Hyperion(quint8 instance) #if defined(ENABLE_BOBLIGHT_SERVER) , _boblightServer(nullptr) #endif + , _lastImageEmission(0) + , _lastRawLedDataEmission(0) + , _lastLedDeviceDataEmission(0) + , _imageEmissionInterval(DEFAULT_MAX_IMAGE_EMISSION_INTERVAL) + , _rawLedDataEmissionInterval(DEFAULT_MAX_RAW_LED_DATA_EMISSION_INTERVAL) + , _ledDeviceDataEmissionInterval(DEFAULT_MAX_LED_DEVICE_DATA_EMISSION_INTERVAL) { qRegisterMetaType("ComponentList"); @@ -109,6 +122,8 @@ void Hyperion::start() // handle hwLedCount _hwLedCount = getSetting(settings::DEVICE).object()["hardwareLedCount"].toInt(getLedCount()); + + // Initialize colororder vector for (const Led& led : _ledString.leds()) { @@ -180,6 +195,15 @@ void Hyperion::start() connect(GlobalSignals::getInstance(), &GlobalSignals::setGlobalColor, this, &Hyperion::setColor); connect(GlobalSignals::getInstance(), &GlobalSignals::setGlobalImage, this, &Hyperion::setInputImage); + // Limit LED data emission, if high rumber of LEDs configured + _rawLedDataEmissionInterval = (_ledString.leds().size() > 1000) ? 2 * DEFAULT_MAX_RAW_LED_DATA_EMISSION_INTERVAL : DEFAULT_MAX_RAW_LED_DATA_EMISSION_INTERVAL; + _ledDeviceDataEmissionInterval = (_hwLedCount > 1000) ? 2 * DEFAULT_MAX_LED_DEVICE_DATA_EMISSION_INTERVAL : DEFAULT_MAX_LED_DEVICE_DATA_EMISSION_INTERVAL; + + // Set up timers to throttle specific signals + _imageTimer.start(); // Start the elapsed timer for image signal throttling + _rawLedDataTimer.start(); // Start the elapsed timer for rawLedColors throttling + _ledDeviceDataTimer.start(); // Start the elapsed timer for LED-Device data throttling + // if there is no startup / background effect and no sending capture interface we probably want to push once BLACK (as PrioMuxer won't emit a priority change) update(); @@ -267,6 +291,7 @@ void Hyperion::handleSettingsUpdate(settings::type type, const QJsonDocument& co std::vector color(_ledString.leds().size(), ColorRgb{0,0,0}); _ledBuffer = color; + _ledStringColorOrder.clear(); for (const Led& led : _ledString.leds()) { @@ -276,6 +301,10 @@ void Hyperion::handleSettingsUpdate(settings::type type, const QJsonDocument& co // handle hwLedCount update _hwLedCount = getSetting(settings::DEVICE).object()["hardwareLedCount"].toInt(getLedCount()); + // Limit LED data emission, if high rumber of LEDs configured + _rawLedDataEmissionInterval = (_ledString.leds().size() > 1000) ? 2 * DEFAULT_MAX_RAW_LED_DATA_EMISSION_INTERVAL : DEFAULT_MAX_RAW_LED_DATA_EMISSION_INTERVAL; + _ledDeviceDataEmissionInterval = (_hwLedCount > 1000) ? 2 * DEFAULT_MAX_LED_DEVICE_DATA_EMISSION_INTERVAL : DEFAULT_MAX_LED_DEVICE_DATA_EMISSION_INTERVAL; + // change in leds are also reflected in adjustment delete _raw2ledAdjustment; _raw2ledAdjustment = hyperion::createLedColorsAdjustment(static_cast(_ledString.leds().size()), getSetting(settings::COLOR).object()); @@ -672,7 +701,14 @@ void Hyperion::update() Image image = priorityInfo.image; if (image.width() > 1 || image.height() > 1) { - emit currentImage(image); + _imageEmissionInterval = (image.width() > 1280) ? 2 * DEFAULT_MAX_IMAGE_EMISSION_INTERVAL : DEFAULT_MAX_IMAGE_EMISSION_INTERVAL; + // Throttle the emission of currentImage(image) signal + qint64 elapsedImageEmissionTime = _imageTimer.elapsed(); + if (elapsedImageEmissionTime - _lastImageEmission >= _imageEmissionInterval.count()) + { + _lastImageEmission = elapsedImageEmissionTime; + emit currentImage(image); // Emit the image signal at the controlled rate + } _ledBuffer = _imageProcessor->process(image); } else @@ -692,9 +728,15 @@ void Hyperion::update() } } - // emit rawLedColors before transform - emit rawLedColors(_ledBuffer); + // Throttle the emission of rawLedColors(_ledBuffer) signal + qint64 elapsedRawLedDataEmissionTime = _rawLedDataTimer.elapsed(); + if (elapsedRawLedDataEmissionTime - _lastRawLedDataEmission >= _rawLedDataEmissionInterval.count()) + { + _lastRawLedDataEmission = elapsedRawLedDataEmissionTime; + emit rawLedColors(_ledBuffer); // Emit the rawLedColors signal at the controlled rate + } + // Start transformations _raw2ledAdjustment->applyAdjustment(_ledBuffer); int i = 0; @@ -740,7 +782,13 @@ void Hyperion::update() // Smoothing is disabled if (! _deviceSmooth->enabled()) { - emit ledDeviceData(_ledBuffer); + // Throttle the emission of LED-Device data signal + qint64 elapsedLedDeviceDataEmissionTime = _ledDeviceDataTimer.elapsed(); + if (elapsedLedDeviceDataEmissionTime - _lastLedDeviceDataEmission >= _ledDeviceDataEmissionInterval.count()) + { + _lastLedDeviceDataEmission = elapsedLedDeviceDataEmissionTime; + emit ledDeviceData(_ledBuffer); + } } else { diff --git a/libsrc/hyperion/schema/schema-ledConfig.json b/libsrc/hyperion/schema/schema-ledConfig.json index 3a3c68098..5a7a854a5 100644 --- a/libsrc/hyperion/schema/schema-ledConfig.json +++ b/libsrc/hyperion/schema/schema-ledConfig.json @@ -124,13 +124,11 @@ "ledshoriz": { "type": "integer", "minimum": 0, - "maximum": 50, "default": 0 }, "ledsvert": { "type": "integer", "minimum": 0, - "maximum": 50, "default": 0 }, "cabling": { diff --git a/libsrc/jsonserver/JsonClientConnection.cpp b/libsrc/jsonserver/JsonClientConnection.cpp index 8af1346ad..cdd2fe8af 100644 --- a/libsrc/jsonserver/JsonClientConnection.cpp +++ b/libsrc/jsonserver/JsonClientConnection.cpp @@ -1,6 +1,7 @@ // project includes #include "JsonClientConnection.h" #include +#include // qt inc #include @@ -17,8 +18,9 @@ JsonClientConnection::JsonClientConnection(QTcpSocket *socket, bool localConnect // create a new instance of JsonAPI _jsonAPI = new JsonAPI(socket->peerAddress().toString(), _log, localConnection, this); // get the callback messages from JsonAPI and send it to the client - connect(_jsonAPI, &JsonAPI::callbackMessage, this , &JsonClientConnection::sendMessage); + connect(_jsonAPI, &JsonAPI::callbackReady, this , &JsonClientConnection::sendMessage); connect(_jsonAPI, &JsonAPI::forceClose, this , [&](){ _socket->close(); } ); + connect(_jsonAPI->getCallBack().get(), &JsonCallbacks::callbackReady, this, &JsonClientConnection::sendMessage); _jsonAPI->initialize(); } @@ -47,7 +49,8 @@ void JsonClientConnection::readRequest() qint64 JsonClientConnection::sendMessage(QJsonObject message) { QJsonDocument writer(message); - QByteArray data = writer.toJson(QJsonDocument::Compact) + "\n"; + QByteArray data = writer.toJson(QJsonDocument::Compact); + data.append('\n'); if (!_socket || (_socket->state() != QAbstractSocket::ConnectedState)) return 0; return _socket->write(data.data(), data.size()); diff --git a/libsrc/webserver/WebJsonRpc.cpp b/libsrc/webserver/WebJsonRpc.cpp index 345215d9e..a0edf7192 100644 --- a/libsrc/webserver/WebJsonRpc.cpp +++ b/libsrc/webserver/WebJsonRpc.cpp @@ -5,6 +5,7 @@ #include "QtHttpClientWrapper.h" #include +#include WebJsonRpc::WebJsonRpc(QtHttpRequest* request, QtHttpServer* server, bool localConnection, QtHttpClientWrapper* parent) : QObject(parent) @@ -14,8 +15,10 @@ WebJsonRpc::WebJsonRpc(QtHttpRequest* request, QtHttpServer* server, bool localC { const QString client = request->getClientInfo().clientAddress.toString(); _jsonAPI = new JsonAPI(client, _log, localConnection, this, true); - connect(_jsonAPI, &JsonAPI::callbackMessage, this, &WebJsonRpc::handleCallback); + connect(_jsonAPI, &JsonAPI::callbackReady, this, &WebJsonRpc::sendCallbackMessage); connect(_jsonAPI, &JsonAPI::forceClose, [&]() { _wrapper->closeConnection(); _stopHandle = true; }); + connect(_jsonAPI->getCallBack().get(), &JsonCallbacks::callbackReady, this, &WebJsonRpc::sendCallbackMessage); + _jsonAPI->initialize(); } @@ -31,7 +34,7 @@ void WebJsonRpc::handleMessage(QtHttpRequest* request) } } -void WebJsonRpc::handleCallback(QJsonObject obj) +void WebJsonRpc::sendCallbackMessage(QJsonObject obj) { // guard against wrong callbacks; TODO: Remove when JSONAPI is more solid if(!_unlocked) return; diff --git a/libsrc/webserver/WebJsonRpc.h b/libsrc/webserver/WebJsonRpc.h index 2eed7ed8d..695bcb14b 100644 --- a/libsrc/webserver/WebJsonRpc.h +++ b/libsrc/webserver/WebJsonRpc.h @@ -26,5 +26,5 @@ class WebJsonRpc : public QObject { bool _unlocked = false; private slots: - void handleCallback(QJsonObject obj); + void sendCallbackMessage(QJsonObject obj); }; diff --git a/libsrc/webserver/WebSocketClient.cpp b/libsrc/webserver/WebSocketClient.cpp index 3cf8adbcd..a4eab0e3f 100644 --- a/libsrc/webserver/WebSocketClient.cpp +++ b/libsrc/webserver/WebSocketClient.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -24,9 +25,11 @@ WebSocketClient::WebSocketClient(QtHttpRequest* request, QTcpSocket* sock, bool // Json processor _jsonAPI.reset(new JsonAPI(client, _log, localConnection, this)); - connect(_jsonAPI.get(), &JsonAPI::callbackMessage, this, &WebSocketClient::sendMessage); + connect(_jsonAPI.get(), &JsonAPI::callbackReady, this, &WebSocketClient::sendMessage); connect(_jsonAPI.get(), &JsonAPI::forceClose, this,[this]() { this->sendClose(CLOSECODE::NORMAL); }); + connect(_jsonAPI->getCallBack().get(), &JsonCallbacks::callbackReady, this, &WebSocketClient::sendMessage); + connect(this, &WebSocketClient::handleMessage, _jsonAPI.get(), &JsonAPI::handleMessage); Debug(_log, "New connection from %s", QSTRING_CSTR(client));