From 6fa7bab6f7485a1d6fa69ab58f9ab493c382016e Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Tue, 27 Dec 2022 08:36:10 +0100 Subject: [PATCH 01/23] Add CodeQL for GitHub code scanning (#1548) * Create codeql.yml * Addressing codeql findings --- .github/config/codeql.yml | 4 + .github/workflows/codeql.yml | 76 + README.md | 2 +- .../network_bridge/udpraw_serialadalight.py | 6 +- assets/webconfig/js/ui_utils.js | 2680 ++++++++--------- effects/knight-rider.py | 108 +- effects/rainbow-mood.py | 46 +- effects/running_dots.py | 86 +- effects/swirl.py | 5 +- effects/trails.py | 196 +- effects/waves.py | 148 +- effects/x-mas.py | 60 +- include/api/API.h | 7 +- include/blackborder/BlackBorderDetector.h | 2 - include/mdns/MdnsBrowser.h | 37 - include/utils/NetUtils.h | 2 +- include/utils/RgbTransform.h | 2 +- include/utils/hyperion.h | 4 +- include/utils/jsonschema/QJsonUtils.h | 6 +- libsrc/api/API.cpp | 7 - libsrc/api/JsonAPI.cpp | 63 +- libsrc/api/JsonCB.cpp | 4 - libsrc/blackborder/BlackBorderDetector.cpp | 2 - libsrc/blackborder/BlackBorderProcessor.cpp | 7 +- .../BoblightClientConnection.cpp | 5 - libsrc/cec/CECHandler.cpp | 38 +- libsrc/db/DBManager.cpp | 2 +- libsrc/effectengine/EffectEngine.cpp | 2 - libsrc/effectengine/EffectModule.cpp | 75 +- libsrc/forwarder/MessageForwarder.cpp | 2 +- libsrc/grabber/video/EncoderThread.cpp | 4 - libsrc/grabber/video/v4l2/V4L2Grabber.cpp | 12 +- libsrc/grabber/x11/X11Grabber.cpp | 12 +- libsrc/grabber/xcb/XcbGrabber.cpp | 12 +- libsrc/hyperion/AuthManager.cpp | 22 +- libsrc/hyperion/Hyperion.cpp | 3 - libsrc/hyperion/LinearColorSmoothing.cpp | 2 - libsrc/hyperion/MultiColorAdjustment.cpp | 3 - libsrc/hyperion/schema/schema-webConfig.json | 2 +- libsrc/leddevice/LedDevice.cpp | 23 +- libsrc/leddevice/LedDeviceTemplate.cpp | 2 + .../dev_hid/LedDeviceHyperionUsbasp.cpp | 4 +- .../leddevice/dev_hid/LedDeviceLightpack.cpp | 2 - libsrc/leddevice/dev_hid/ProviderHID.cpp | 5 +- .../leddevice/dev_net/LedDeviceNanoleaf.cpp | 13 +- .../leddevice/dev_net/LedDevicePhilipsHue.cpp | 5 +- libsrc/leddevice/dev_net/LedDeviceRazer.cpp | 4 - libsrc/leddevice/dev_net/LedDeviceUdpE131.cpp | 19 +- libsrc/leddevice/dev_net/LedDeviceUdpE131.h | 42 +- libsrc/leddevice/dev_net/LedDeviceWled.cpp | 3 +- .../leddevice/dev_net/LedDeviceYeelight.cpp | 20 +- libsrc/leddevice/dev_net/LedDeviceYeelight.h | 1 - libsrc/leddevice/dev_net/ProviderRestApi.cpp | 22 +- libsrc/leddevice/dev_other/LedDeviceFile.cpp | 3 - .../dev_other/LedDevicePiBlaster.cpp | 2 - libsrc/leddevice/dev_serial/LedDeviceDMX.cpp | 1 - .../leddevice/dev_serial/LedDeviceKarate.cpp | 1 - libsrc/leddevice/dev_serial/LedDeviceSedu.cpp | 1 - libsrc/leddevice/dev_serial/ProviderRs232.cpp | 7 - libsrc/leddevice/dev_spi/LedDeviceSK9822.cpp | 4 - .../leddevice/dev_spi/LedDeviceSk6822SPI.cpp | 6 +- libsrc/mdns/MdnsBrowser.cpp | 4 +- libsrc/ssdp/SSDPDiscover.cpp | 13 +- libsrc/ssdp/SSDPServer.cpp | 1 - libsrc/utils/DefaultSignalHandler.cpp | 6 +- libsrc/utils/JsonUtils.cpp | 3 - libsrc/utils/Logger.cpp | 3 - libsrc/utils/RgbChannelAdjustment.cpp | 1 - libsrc/utils/RgbTransform.cpp | 2 +- .../utils/jsonschema/QJsonSchemaChecker.cpp | 11 +- libsrc/webserver/QtHttpClientWrapper.cpp | 4 +- libsrc/webserver/StaticFileServing.cpp | 3 + libsrc/webserver/StaticFileServing.h | 2 - libsrc/webserver/WebSocketClient.cpp | 10 +- libsrc/webserver/WebSocketClient.h | 2 +- .../hyperion-framebuffer.cpp | 7 - src/hyperion-qt/hyperion-qt.cpp | 8 - src/hyperion-remote/hyperion-remote.cpp | 8 - src/hyperion-v4l2/ScreenshotHandler.cpp | 7 +- src/hyperion-v4l2/hyperion-v4l2.cpp | 7 - src/hyperion-x11/hyperion-x11.cpp | 7 - src/hyperion-xcb/hyperion-xcb.cpp | 7 - test/jsonchecks/jsonschema.py | 16 +- 83 files changed, 1984 insertions(+), 2094 deletions(-) create mode 100644 .github/config/codeql.yml create mode 100644 .github/workflows/codeql.yml mode change 100755 => 100644 assets/firmware/arduino/network_bridge/udpraw_serialadalight.py diff --git a/.github/config/codeql.yml b/.github/config/codeql.yml new file mode 100644 index 00000000..d1492f7c --- /dev/null +++ b/.github/config/codeql.yml @@ -0,0 +1,4 @@ +name: "CodeQL config" +paths-ignore: + - 'dependencies/external/' + - 'assets/webconfig/js/lib' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..a2c530b1 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,76 @@ +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: "36 18 * * 4" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ python, javascript, cpp ] + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Packages (cpp) + if: ${{ matrix.language == 'cpp' }} + run: | + sudo apt-get update + sudo apt-get install --yes git cmake build-essential qtbase5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libcec-dev libxcb-image0-dev libxcb-util0-dev libxcb-shm0-dev libxcb-render0-dev libxcb-randr0-dev libxrandr-dev libxrender-dev libavahi-core-dev libavahi-compat-libdnssd-dev libturbojpeg0-dev libjpeg-dev libssl-dev + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + config-file: ./.github/config/codeql.yml + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" + upload: False + output: sarif-results + + - name: Filter SARIF + uses: advanced-security/filter-sarif@v1 + with: + patterns: | + -**/dependencies/** + -**/moc_*.cpp + -**/libsrc/flatbufserver/hyperion_request_generated.h + -**/libsrc/protoserver/message.pb.cc + -**/libsrc/protoserver/message.pb.h + input: sarif-results/${{ matrix.language }}.sarif + output: sarif-results/${{ matrix.language }}.sarif + + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: sarif-results/${{ matrix.language }}.sarif + - name: Upload loc as a Build Artifact + uses: actions/upload-artifact@v2.2.0 + with: + name: sarif-results + path: sarif-results + retention-days: 1 + diff --git a/README.md b/README.md index 5afd7e7e..cf3f7d8b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Latest-Release](https://img.shields.io/github/v/release/hyperion-project/hyperion.ng?include_prereleases&label=Latest%20Release&logo=github&logoColor=white&color=0f83e7)](https://github.com/hyperion-project/hyperion.ng/releases) [![GitHub Actions](https://github.com/hyperion-project/hyperion.ng/workflows/Hyperion%20CI%20Build/badge.svg?branch=master)](https://github.com/hyperion-project/hyperion.ng/actions) -[![LGTM](https://img.shields.io/lgtm/grade/cpp/github/hyperion-project/hyperion.ng?label=Code%20Quality&logo=lgtm&logoColor=white&color=4bc51d)](https://lgtm.com/projects/g/hyperion-project/hyperion.ng/context:cpp) +[![CodeQL Analysis](https://github.com/hyperion-project/hyperion.ng/actions/workflows/codeql.yml/badge.svg)](https://github.com/hyperion-project/hyperion.ng/actions/workflows/codeql.yml) [![Forum](https://img.shields.io/website/https/hyperion-project.org.svg?label=Forum&down_color=red&down_message=offline&up_color=4bc51d&up_message=online&logo=homeadvisor&logoColor=white)](https://www.hyperion-project.org) [![Documentation](https://img.shields.io/website/https/docs.hyperion-project.org.svg?label=Documentation&down_color=red&down_message=offline&up_color=4bc51d&up_message=online&logo=read-the-docs)](https://docs.hyperion-project.org) [![Discord](https://img.shields.io/discord/785578322167463937?label=Discord&logo=discord&logoColor=white&color=4bc51d)](https://discord.gg/khkR8Vx3ff) diff --git a/assets/firmware/arduino/network_bridge/udpraw_serialadalight.py b/assets/firmware/arduino/network_bridge/udpraw_serialadalight.py old mode 100755 new mode 100644 index b46597a9..452f9dd7 --- a/assets/firmware/arduino/network_bridge/udpraw_serialadalight.py +++ b/assets/firmware/arduino/network_bridge/udpraw_serialadalight.py @@ -153,10 +153,9 @@ to this service over the network. srv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - srv.bind(('0.0.0.0', args.localport)) # lgtm [py/bind-socket-all-network-interfaces] + srv.bind(('0.0.0.0', args.localport)) try: - intentional_exit = False while True: try: while True: @@ -180,7 +179,7 @@ to this service over the network. # probably got disconnected break except KeyboardInterrupt: - intentional_exit = True + # intentional_exit raise except socket.error as msg: if args.develop: @@ -190,6 +189,7 @@ to this service over the network. ser_to_net.socket = None sys.stderr.write('Disconnected\n') except KeyboardInterrupt: + # do not handle exceptions pass sys.stderr.write('\n--- exit ---\n') diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index 74a27061..c20c5fc1 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -1,1340 +1,1340 @@ -var prevTag; - -function removeOverlay() { - $("#loading_overlay").removeClass("overlay"); -} - -function reload() { - location.reload(); -} - -function storageComp() { - if (typeof (Storage) !== "undefined") - return true; - return false; -} - -function getStorage(item) { - if (storageComp()) { - return localStorage.getItem(item); - } - return null; -} - -function setStorage(item, value) { - if (storageComp()) { - localStorage.setItem(item, value); - } -} - -function removeStorage(item) { - if (storageComp()) { - localStorage.removeItem(item); - } -} - -function debugMessage(msg) { - if (window.debugMessagesActive) { - console.log(msg); - } -} - -function validateDuration(d) { - if (typeof d === "undefined" || d < 0) - return ENDLESS; - else - return d *= 1000; -} - -function getHashtag() { - if (getStorage('lasthashtag') != null) - return getStorage('lasthashtag'); - else { - var tag = document.URL; - tag = tag.substr(tag.indexOf("#") + 1); - if (tag == "" || typeof tag === "undefined" || tag.startsWith("http")) - tag = "dashboard" - return tag; - } -} - -function loadContent(event, forceRefresh) { - var tag; - - var lastSelectedInstance = getStorage('lastSelectedInstance'); - - if (lastSelectedInstance && (lastSelectedInstance != window.currentHyperionInstance)) { - if (window.serverInfo.instance[lastSelectedInstance] && window.serverInfo.instance[lastSelectedInstance].running) { - instanceSwitch(lastSelectedInstance); - } else { - removeStorage('lastSelectedInstance'); - } - } - - if (typeof event != "undefined") { - tag = event.currentTarget.hash; - tag = tag.substr(tag.indexOf("#") + 1); - setStorage('lasthashtag', tag); - } - else - tag = getHashtag(); - - if (forceRefresh || prevTag != tag) { - prevTag = tag; - $("#page-content").off(); - $("#page-content").load("/content/" + tag + ".html", function (response, status, xhr) { - if (status == "error") { - tag = 'dashboard'; - console.log("Could not find page:", prevTag, ", Redirecting to:", tag); - setStorage('lasthashtag', tag); - - $("#page-content").load("/content/" + tag + ".html", function (response, status, xhr) { - if (status == "error") { - $("#page-content").html('

' + encode_utf8(tag) + '
' + $.i18n('info_404') + '

'); - removeOverlay(); - } - }); - } - updateUiOnInstance(window.currentHyperionInstance); - }); - } -} - -function getInstanceNameByIndex(index) { - var instData = window.serverInfo.instance - for (var key in instData) { - if (instData[key].instance == index) - return instData[key].friendly_name; - } - return "unknown" -} - -function updateHyperionInstanceListing() { - if (window.serverInfo.instance) { - var data = window.serverInfo.instance.filter(entry => entry.running); - $('#hyp_inst_listing').html(""); - for (var key in data) { - var currInstMarker = (data[key].instance == window.currentHyperionInstance) ? "component-on" : ""; - - var html = '
  • \ - \ -
    \ - \ - '+ data[key].friendly_name + ' \ -
    \ -
    \ -
  • ' - - if (data.length - 1 > key) - html += '
  • ' - - $('#hyp_inst_listing').append(html); - - $('#hyperioninstance_' + data[key].instance).off().on("click", function (e) { - var inst = e.currentTarget.id.split("_")[1] - instanceSwitch(inst) - }); - } - } -} - -function initLanguageSelection() { - // Initialise language selection list with languages supported - for (var i = 0; i < availLang.length; i++) { - $("#language-select").append(''); - } - - var langLocale = storedLang; - - //Test, if language is supported by hyperion - var langIdx = availLang.indexOf(langLocale); - if (langIdx > -1) { - langText = availLangText[langIdx]; - } else { - // If language is not supported by hyperion, try fallback language - langLocale = $.i18n().options.fallbackLocale.substring(0, 2); - langIdx = availLang.indexOf(langLocale); - if (langIdx > -1) { - langText = availLangText[langIdx]; - } else { - langLocale = 'en'; - langIdx = availLang.indexOf(langLocale); - if (langIdx > -1) { - langText = availLangText[langIdx]; - } - } - } - - $('#language-select').prop('title', langText); - $("#language-select").val(langIdx); - $("#language-select").selectpicker("refresh"); -} - -function updateUiOnInstance(inst) { - $("#active_instance_friendly_name").text(getInstanceNameByIndex(inst)); - if (window.serverInfo.instance.filter(entry => entry.running).length > 1) { - $('#btn_hypinstanceswitch').toggle(true); - $('#active_instance_dropdown').prop('disabled', false); - $('#active_instance_dropdown').css('cursor', 'pointer'); - $("#active_instance_dropdown").css("pointer-events", "auto"); - } else { - $('#btn_hypinstanceswitch').toggle(false); - $('#active_instance_dropdown').prop('disabled', true); - $("#active_instance_dropdown").css('cursor', 'default'); - $("#active_instance_dropdown").css("pointer-events", "none"); - } -} - -function instanceSwitch(inst) { - requestInstanceSwitch(inst) - window.currentHyperionInstance = inst; - window.currentHyperionInstanceName = getInstanceNameByIndex(inst); - setStorage('lastSelectedInstance', inst) -} - -function loadContentTo(containerId, fileName) { - $(containerId).load("/content/" + fileName + ".html"); -} - -function toggleClass(obj, class1, class2) { - if ($(obj).hasClass(class1)) { - $(obj).removeClass(class1); - $(obj).addClass(class2); - } - else { - $(obj).removeClass(class2); - $(obj).addClass(class1); - } -} - -function setClassByBool(obj, enable, class1, class2) { - if (enable) { - $(obj).removeClass(class1); - $(obj).addClass(class2); - } - else { - $(obj).removeClass(class2); - $(obj).addClass(class1); - } -} - -function showInfoDialog(type, header, message) { - if (type == "success") { - $('#id_body').html(''); - if (header == "") - $('#id_body').append('

    ' + $.i18n('infoDialog_general_success_title') + '

    '); - $('#id_footer').html(''); - } - else if (type == "warning") { - $('#id_body').html(''); - if (header == "") - $('#id_body').append('

    ' + $.i18n('infoDialog_general_warning_title') + '

    '); - $('#id_footer').html(''); - } - else if (type == "error") { - $('#id_body').html(''); - if (header == "") - $('#id_body').append('

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

    '); - $('#id_footer').html(''); - } - else if (type == "select") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - else if (type == "iswitch") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - else if (type == "uilock") { - $('#id_body').html(''); - $('#id_footer').html('' + $.i18n('InfoDialog_nowrite_foottext') + ''); - } - else if (type == "import") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - else if (type == "delInst") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - else if (type == "renInst") { - $('#id_body_rename').html('
    '); - $('#id_body_rename').append('

    ' + header + '

    '); - $('#id_body_rename').append(''); - $('#id_footer_rename').html(''); - $('#id_footer_rename').append(''); - } - else if (type == "changePassword") { - $('#id_body_rename').html('
    '); - $('#id_body_rename').append('

    ' + header + '


    '); - $('#id_body_rename').append('

    ' + $.i18n('infoDialog_username_text') + - '


    '); - $('#id_body_rename').append('

    ' + $.i18n('infoDialog_password_current_text') + - '


    '); - $('#id_body_rename').append('

    ' + $.i18n('infoDialog_password_new_text') + - '

    '); - $('#id_body_rename').append('
    ' + $.i18n('infoDialog_password_minimum_length') + '
    '); - $('#id_footer_rename').html(''); - $('#id_footer_rename').append(''); - } - else if (type == "checklist") { - $('#id_body').html(''); - $('#id_body').append('

    ' + $.i18n('infoDialog_checklist_title') + '

    '); - $('#id_body').append(header); - $('#id_footer').html(''); - } - else if (type == "newToken") { - $('#id_body').html(''); - $('#id_footer').html(''); - } - else if (type == "grantToken") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - - if (type != "renInst") { - $('#id_body').append('

    ' + header + '

    '); - $('#id_body').append(message); - } - - if (type == "select" || type == "iswitch") - $('#id_body').append(''); - - if (getStorage("darkMode") == "on") - $('#id_logo').attr("src", 'img/hyperion/logo_negativ.png'); - - $(type == "renInst" || type == "changePassword" ? "#modal_dialog_rename" : "#modal_dialog").modal({ - backdrop: "static", - keyboard: false, - show: true - }); - - $(document).on('click', '[data-dismiss-modal]', function () { - var target = $(this).attr('data-dismiss-modal'); - $(target).modal('hide'); // lgtm [js/xss-through-dom] - }); -} - -function createHintH(type, text, container) { - type = String(type); - if (type == "intro") - tclass = "introd"; - - $('#' + container).prepend('

    ' + text + '


    '); -} - -function createHint(type, text, container, buttonid, buttontxt) { - var fe, tclass; - - if (type == "intro") { - fe = ''; - tclass = "intro-hint"; - } - else if (type == "info") { - fe = '
    Information
    '; - tclass = "info-hint"; - } - else if (type == "wizard") { - fe = '
    Information
    '; - tclass = "wizard-hint"; - } - else if (type == "warning") { - fe = '
    Information
    '; - tclass = "warning-hint"; - } - - if (buttonid) - buttonid = '

    '; - else - buttonid = ""; - - if (type == "intro") - $('#' + container).prepend('

    ' + $.i18n("conf_helptable_expl") + '

    ' + text + '
    '); - else if (type == "wizard") - $('#' + container).prepend('

    ' + $.i18n("wiz_wizavail") + '

    ' + $.i18n('wiz_guideyou', text) + buttonid + '
    '); - else { - createTable('', 'htb', container, true, tclass); - $('#' + container + ' .htb').append(createTableRow([fe, text], false, true)); - } -} - -function createEffHint(title, text) { - return '

    ' + title + '

    ' + text + '
    '; -} - -function valValue(id, value, min, max) { - if (typeof max === 'undefined' || max == "") - max = 999999; - - if (Number(value) > Number(max)) { - $('#' + id).val(max); - showInfoDialog("warning", "", $.i18n('edt_msg_error_maximum_incl', max)); - return max; - } - else if (Number(value) < Number(min)) { - $('#' + id).val(min); - showInfoDialog("warning", "", $.i18n('edt_msg_error_minimum_incl', min)); - return min; - } - return value; -} - -function readImg(input, cb) { - if (input.files && input.files[0]) { - var reader = new FileReader(); - // inject fileName property - reader.fileName = input.files[0].name - - reader.onload = function (e) { - cb(e.target.result, e.target.fileName); - } - reader.readAsDataURL(input.files[0]); - } -} - -function isJsonString(str) { - try { - JSON.parse(str); - } - catch (e) { - return e; - } - return ""; -} - -function createJsonEditor(container, schema, setconfig, usePanel, arrayre) { - $('#' + container).off(); - $('#' + container).html(""); - - if (typeof arrayre === 'undefined') - arrayre = true; - - var editor = new JSONEditor(document.getElementById(container), - { - theme: 'bootstrap3', - iconlib: "fontawesome4", - disable_collapse: 'true', - form_name_root: 'sa', - disable_edit_json: true, - disable_properties: true, - disable_array_reorder: arrayre, - no_additional_properties: true, - disable_array_delete_all_rows: true, - disable_array_delete_last_row: true, - access: storedAccess, - schema: { - title: '', - properties: schema - } - }); - - if (usePanel) { - $('#' + container + ' .well').first().removeClass('well well-sm'); - $('#' + container + ' h4').first().remove(); - $('#' + container + ' .well').first().removeClass('well well-sm'); - } - - if (setconfig) { - for (var key in editor.root.editors) { - editor.getEditor("root." + key).setValue(Object.assign({}, editor.getEditor("root." + key).value, window.serverConfig[key])); - } - } - - return editor; -} - -function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal, addSelect, addCustom, addCustomAsFirst, customText) { - var editor = rootEditor.getEditor(path); - var orginalProperties = editor.schema.properties[key]; - - var orginalWatchFunctions = rootEditor.watchlist[path + "." + key]; - rootEditor.unwatch(path + "." + key); - - var newSchema = []; - newSchema[key] = - { - "type": "string", - "enum": [], - "required": true, - "options": { "enum_titles": [], "infoText": "" }, - "propertyOrder": 1 - }; - - //Add additional elements to overwrite defaults - for (var item in addElements) { - newSchema[key][item] = addElements[item]; - } - - if (orginalProperties) { - if (orginalProperties["title"]) { - newSchema[key]["title"] = orginalProperties["title"]; - } - - if (orginalProperties["options"] && orginalProperties["options"]["infoText"]) { - newSchema[key]["options"]["infoText"] = orginalProperties["options"]["infoText"]; - } - - if (orginalProperties["propertyOrder"]) { - newSchema[key]["propertyOrder"] = orginalProperties["propertyOrder"]; - } - } - - if (addCustom) { - - if (newTitelVals.length === 0) { - newTitelVals = [...newEnumVals]; - } - - if (!!!customText) { - customText = "edt_conf_enum_custom"; - } - - if (addCustomAsFirst) { - newEnumVals.unshift("CUSTOM"); - newTitelVals.unshift(customText); - } else { - newEnumVals.push("CUSTOM"); - newTitelVals.push(customText); - } - - if (newSchema[key].options.infoText) { - var customInfoText = newSchema[key].options.infoText + "_custom"; - newSchema[key].options.infoText = customInfoText; - } - } - - if (addSelect) { - newEnumVals.unshift("SELECT"); - newTitelVals.unshift("edt_conf_enum_please_select"); - newDefaultVal = "SELECT"; - } - - if (newEnumVals) { - newSchema[key]["enum"] = newEnumVals; - } - - if (newTitelVals) { - newSchema[key]["options"]["enum_titles"] = newTitelVals; - } - if (newDefaultVal) { - newSchema[key]["default"] = newDefaultVal; - } - - editor.original_schema.properties[key] = orginalProperties; - editor.schema.properties[key] = newSchema[key]; - rootEditor.validator.schema.properties[editor.key].properties[key] = newSchema[key]; - - editor.removeObjectProperty(key); - delete editor.cached_editors[key]; - editor.addObjectProperty(key); - - if (orginalWatchFunctions) { - for (var i = 0; i < orginalWatchFunctions.length; i++) { - rootEditor.watch(path + "." + key, orginalWatchFunctions[i]); - } - } - rootEditor.notifyWatchers(path + "." + key); -} - -function updateJsonEditorMultiSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal) { - var editor = rootEditor.getEditor(path); - var orginalProperties = editor.schema.properties[key]; - - var orginalWatchFunctions = rootEditor.watchlist[path + "." + key]; - rootEditor.unwatch(path + "." + key); - - var newSchema = []; - newSchema[key] = - { - "type": "array", - "format": "select", - "items": { - "type": "string", - "enum": [], - "options": { "enum_titles": [] }, - }, - "options": { "infoText": "" }, - "default": [], - "propertyOrder": 1 - }; - - //Add additional elements to overwrite defaults - for (var item in addElements) { - newSchema[key][item] = addElements[item]; - } - - if (orginalProperties) { - if (orginalProperties["title"]) { - newSchema[key]["title"] = orginalProperties["title"]; - } - - if (orginalProperties["options"] && orginalProperties["options"]["infoText"]) { - newSchema[key]["options"]["infoText"] = orginalProperties["options"]["infoText"]; - } - - if (orginalProperties["propertyOrder"]) { - newSchema[key]["propertyOrder"] = orginalProperties["propertyOrder"]; - } - } - - if (newEnumVals) { - newSchema[key]["items"]["enum"] = newEnumVals; - } - - if (newTitelVals) { - newSchema[key]["items"]["options"]["enum_titles"] = newTitelVals; - } - - if (newDefaultVal) { - newSchema[key]["default"] = newDefaultVal; - } - - editor.original_schema.properties[key] = orginalProperties; - editor.schema.properties[key] = newSchema[key]; - rootEditor.validator.schema.properties[editor.key].properties[key] = newSchema[key]; - - editor.removeObjectProperty(key); - delete editor.cached_editors[key]; - editor.addObjectProperty(key); - - if (orginalWatchFunctions) { - for (var i = 0; i < orginalWatchFunctions.length; i++) { - rootEditor.watch(path + "." + key, orginalWatchFunctions[i]); - } - } - rootEditor.notifyWatchers(path + "." + key); -} - -function updateJsonEditorRange(rootEditor, path, key, minimum, maximum, defaultValue, step, clear) { - var editor = rootEditor.getEditor(path); - - //Preserve current value when updating range - var currentValue = rootEditor.getEditor(path + "." + key).getValue(); - - var orginalProperties = editor.schema.properties[key]; - var newSchema = []; - newSchema[key] = orginalProperties; - - if (clear) { - delete newSchema[key]["minimum"]; - delete newSchema[key]["maximum"]; - delete newSchema[key]["default"]; - delete newSchema[key]["step"]; - } - - if (typeof minimum !== "undefined") { - newSchema[key]["minimum"] = minimum; - } - if (typeof maximum !== "undefined") { - newSchema[key]["maximum"] = maximum; - } - if (typeof defaultValue !== "undefined") { - newSchema[key]["default"] = defaultValue; - currentValue = defaultValue; - } - - if (typeof step !== "undefined") { - newSchema[key]["step"] = step; - } - - editor.original_schema.properties[key] = orginalProperties; - editor.schema.properties[key] = newSchema[key]; - rootEditor.validator.schema.properties[editor.key].properties[key] = newSchema[key]; - - editor.removeObjectProperty(key); - delete editor.cached_editors[key]; - editor.addObjectProperty(key); - - // Restore current (new default) value for new range - rootEditor.getEditor(path + "." + key).setValue(currentValue); -} - -function addJsonEditorHostValidation() { - - JSONEditor.defaults.custom_validators.push(function (schema, value, path) { - var errors = []; - - if (!jQuery.isEmptyObject(value)) { - switch (schema.format) { - case "hostname_or_ip": - if (!isValidHostnameOrIP(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msgcust_error_hostname_ip') - }); - } - break; - case "hostname_or_ip4": - if (!isValidHostnameOrIP4(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msgcust_error_hostname_ip4') - }); - } - break; - - //Remove, when new json-editor 2.x is used - case "ipv4": - if (!isValidIPv4(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msg_error_ipv4') - }); - } - break; - case "ipv6": - if (!isValidIPv6(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msg_error_ipv6') - }); - } - break; - case "hostname": - if (!isValidHostname(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msg_error_hostname') - }); - } - break; - - default: - } - } - return errors; - }); -} - -function buildWL(link, linkt, cl) { - var baseLink = "https://docs.hyperion-project.org/"; - var lang; - - if (typeof linkt == "undefined") - linkt = "Placeholder"; - - if (storedLang == "de" || navigator.locale == "de") - lang = "de"; - else - lang = "en"; - - if (cl === true) { - linkt = $.i18n(linkt); - return '

    ' + linkt + '

    ' + $.i18n('general_wiki_moreto', linkt) + ': ' + linkt + '
    ' - } - else - return ': ' + linkt + ''; -} - -function rgbToHex(rgb) { - if (rgb.length == 3) { - return "#" + - ("0" + parseInt(rgb[0], 10).toString(16)).slice(-2) + - ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) + - ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2); - } - else - debugMessage('rgbToHex: Given rgb is no array or has wrong length'); -} - -function hexToRgb(hex) { - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } : { - r: 0, - g: 0, - b: 0 - }; -} - -/* - Show a notification - @param type Valid types are "info","success","warning","danger" - @param message The message to show - @param title A title (optional) - @param addhtml Add custom html to the notification end - */ -function showNotification(type, message, title = "", addhtml = "") { - if (title == "") { - switch (type) { - case "info": - title = $.i18n('infoDialog_general_info_title'); - break; - case "success": - title = $.i18n('infoDialog_general_success_title'); - break; - case "warning": - title = $.i18n('infoDialog_general_warning_title'); - break; - case "danger": - title = $.i18n('infoDialog_general_error_title'); - break; - } - } - - $.notify({ - // options - title: title, - message: message - }, { - // settings - type: type, - animate: { - enter: 'animate__animated animate__fadeInDown', - exit: 'animate__animated animate__fadeOutUp' - }, - placement: { - align: 'center' - }, - mouse_over: 'pause', - template: '' - }); -} - -function createCP(id, color, cb) { - if (Array.isArray(color)) - color = rgbToHex(color); - else if (color == "undefined") - color = "#AA3399"; - - if (color.startsWith("#")) { - $('#' + id).colorpicker({ - format: 'rgb', - customClass: 'colorpicker-2x', - color: color, - sliders: { - saturation: { - maxLeft: 200, - maxTop: 200 - }, - hue: { - maxTop: 200 - }, - } - }); - $('#' + id).colorpicker().on('changeColor', function (e) { - var rgb = e.color.toRGB(); - var hex = e.color.toHex(); - cb(rgb, hex, e); - }); - } - else - debugMessage('createCP: Given color is not legit'); -} - -// Creates a table with thead and tbody ids -// @param string hid : a class for thead -// @param string bid : a class for tbody -// @param string cont : a container id to html() the table -// @param string bless: if true the table is borderless -function createTable(hid, bid, cont, bless, tclass) { - var table = document.createElement('table'); - var thead = document.createElement('thead'); - var tbody = document.createElement('tbody'); - - table.className = "table"; - if (bless === true) - table.className += " borderless"; - if (typeof tclass !== "undefined") - table.className += " " + tclass; - table.style.marginBottom = "0px"; - if (hid != "") - thead.className = hid; - tbody.className = bid; - if (hid != "") - table.appendChild(thead); - table.appendChild(tbody); - - $('#' + cont).append(table); -} - -// Creates a table row -// @param array list :innerHTML content for / -// @param bool head :if null or false it's body -// @param bool align :if null or false no alignment -// -// @return : with or as child(s) -function createTableRow(list, head, align) { - var row = document.createElement('tr'); - - for (var i = 0; i < list.length; i++) { - if (head === true) - var el = document.createElement('th'); - else - var el = document.createElement('td'); - - if (align) - el.style.verticalAlign = "middle"; - - var purifyConfig = { - ADD_TAGS: ['button'], - ADD_ATTR: ['onclick'] - }; - el.innerHTML = DOMPurify.sanitize(list[i], purifyConfig); - row.appendChild(el); - } - return row; -} - -function createRow(id) { - var el = document.createElement('div'); - el.className = "row"; - el.setAttribute('id', id); - return el; -} - -function createOptPanel(phicon, phead, bodyid, footerid, css, panelId) { - phead = '' + phead; - - var pfooter = document.createElement('button'); - pfooter.className = "btn btn-primary"; - pfooter.setAttribute("id", footerid); - pfooter.innerHTML = '' + $.i18n('general_button_savesettings'); - - return createPanel(phead, "", pfooter, "panel-default", bodyid, css, panelId); -} - -function compareTwoValues(key1, key2, order = 'asc') { - return function innerSort(a, b) { - if (!a.hasOwnProperty(key1) || !b.hasOwnProperty(key1)) { - // property key1 doesn't exist on either object - return 0; - } - - const varA1 = (typeof a[key1] === 'string') - ? a[key1].toUpperCase() : a[key1]; - const varB1 = (typeof b[key1] === 'string') - ? b[key1].toUpperCase() : b[key1]; - - let comparison = 0; - if (varA1 > varB1) { - comparison = 1; - } else { - if (varA1 < varB1) { - comparison = -1; - } else { - if (!a.hasOwnProperty(key2) || !b.hasOwnProperty(key2)) { - // property key2 doesn't exist on either object - return 0; - } - - const varA2 = (typeof a[key2] === 'string') - ? a[key2].toUpperCase() : a[key2]; - const varB2 = (typeof b[key1] === 'string') - ? b[key2].toUpperCase() : b[key2]; - - if (varA2 > varB2) { - comparison = 1; - } else { - comparison = -1; - } - } - } - return ( - (order === 'desc') ? (comparison * -1) : comparison - ); - }; -} - -function sortProperties(list) { - for (var key in list) { - list[key].key = key; - } - list = $.map(list, function (value, index) { - return [value]; - }); - return list.sort(function (a, b) { - return a.propertyOrder - b.propertyOrder; - }); -} - -function createHelpTable(list, phead, panelId) { - var table = document.createElement('table'); - var thead = document.createElement('thead'); - var tbody = document.createElement('tbody'); - list = sortProperties(list); - - phead = '' + phead + ' ' + $.i18n("conf_helptable_expl"); - - table.className = 'table table-hover borderless'; - - thead.appendChild(createTableRow([$.i18n('conf_helptable_option'), $.i18n('conf_helptable_expl')], true, false)); - - for (var key in list) { - if (list[key].access != 'system') { - // break one iteration (in the loop), if the schema has the entry hidden=true - if ("options" in list[key] && "hidden" in list[key].options && (list[key].options.hidden)) - continue; - if ("access" in list[key] && ((list[key].access == "advanced" && storedAccess == "default") || (list[key].access == "expert" && storedAccess != "expert"))) - continue; - var text = list[key].title.replace('title', 'expl'); - tbody.appendChild(createTableRow([$.i18n(list[key].title), $.i18n(text)], false, false)); - - if (list[key].items && list[key].items.properties) { - var ilist = sortProperties(list[key].items.properties); - for (var ikey in ilist) { - // break one iteration (in the loop), if the schema has the entry hidden=true - if ("options" in ilist[ikey] && "hidden" in ilist[ikey].options && (ilist[ikey].options.hidden)) - continue; - if ("access" in ilist[ikey] && ((ilist[ikey].access == "advanced" && storedAccess == "default") || (ilist[ikey].access == "expert" && storedAccess != "expert"))) - continue; - var itext = ilist[ikey].title.replace('title', 'expl'); - tbody.appendChild(createTableRow([$.i18n(ilist[ikey].title), $.i18n(itext)], false, false)); - } - } - } - } - table.appendChild(thead); - table.appendChild(tbody); - - return createPanel(phead, table, undefined, undefined, undefined, undefined, panelId); -} - -function createPanel(head, body, footer, type, bodyid, css, panelId) { - var cont = document.createElement('div'); - var p = document.createElement('div'); - var phead = document.createElement('div'); - var pbody = document.createElement('div'); - var pfooter = document.createElement('div'); - - cont.className = "col-lg-6"; - - if (typeof type == 'undefined') - type = 'panel-default'; - - p.className = 'panel ' + type; - if (typeof panelId != 'undefined') { - p.setAttribute("id", panelId); - } - - phead.className = 'panel-heading ' + css; - pbody.className = 'panel-body'; - pfooter.className = 'panel-footer'; - - phead.innerHTML = head; - - if (typeof bodyid != 'undefined') { - pfooter.style.textAlign = 'right'; - pbody.setAttribute("id", bodyid); - } - - if (typeof body != 'undefined' && body != "") - pbody.appendChild(body); - - if (typeof footer != 'undefined') - pfooter.appendChild(footer); - - p.appendChild(phead); - p.appendChild(pbody); - - if (typeof footer != 'undefined') { - pfooter.style.textAlign = "right"; - p.appendChild(pfooter); - } - - cont.appendChild(p); - - return cont; -} - -function createSelGroup(group) { - var el = document.createElement('optgroup'); - el.setAttribute('label', group); - return el; -} - -function createSelOpt(opt, title) { - var el = document.createElement('option'); - el.setAttribute('value', opt); - if (typeof title == 'undefined') - el.innerHTML = opt; - else - el.innerHTML = title; - return el; -} - -function createSel(array, group, split) { - if (array.length != 0) { - var el = createSelGroup(group); - for (var i = 0; i < array.length; i++) { - var opt; - if (split) { - opt = array[i].split(":") - opt = createSelOpt(opt[0], opt[1]) - } - else - opt = createSelOpt(array[i]) - el.appendChild(opt); - } - return el; - } -} - -function performTranslation() { - $('[data-i18n]').i18n(); -} - -function encode_utf8(s) { - return unescape(encodeURIComponent(s)); -} - -function getReleases(callback) { - $.ajax({ - url: window.gitHubReleaseApiUrl, - method: 'get', - error: function (XMLHttpRequest, textStatus, errorThrown) { - callback(false); - }, - success: function (releases) { - window.gitHubVersionList = releases; - var highestRelease = { - tag_name: '0.0.0' - }; - var highestAlphaRelease = { - tag_name: '0.0.0' - }; - var highestBetaRelease = { - tag_name: '0.0.0' - }; - var highestRcRelease = { - tag_name: '0.0.0' - }; - - for (var i in releases) { - //drafts will be ignored - if (releases[i].draft) - continue; - - if (releases[i].tag_name.includes('alpha')) { - if (sem = semverLite.gt(releases[i].tag_name, highestAlphaRelease.tag_name)) - highestAlphaRelease = releases[i]; - } - else if (releases[i].tag_name.includes('beta')) { - if (sem = semverLite.gt(releases[i].tag_name, highestBetaRelease.tag_name)) - highestBetaRelease = releases[i]; - } - else if (releases[i].tag_name.includes('rc')) { - if (semverLite.gt(releases[i].tag_name, highestRcRelease.tag_name)) - highestRcRelease = releases[i]; - } - else { - if (semverLite.gt(releases[i].tag_name, highestRelease.tag_name)) - highestRelease = releases[i]; - } - } - window.latestStableVersion = highestRelease; - window.latestBetaVersion = highestBetaRelease; - window.latestAlphaVersion = highestAlphaRelease; - window.latestRcVersion = highestRcRelease; - - if (window.serverConfig.general.watchedVersionBranch == "Beta" && semverLite.gt(highestBetaRelease.tag_name, highestRelease.tag_name)) - window.latestVersion = highestBetaRelease; - else - window.latestVersion = highestRelease; - - if (window.serverConfig.general.watchedVersionBranch == "Alpha" && semverLite.gt(highestAlphaRelease.tag_name, highestBetaRelease.tag_name)) - window.latestVersion = highestAlphaRelease; - - if (window.serverConfig.general.watchedVersionBranch == "Alpha" && semverLite.lt(highestAlphaRelease.tag_name, highestBetaRelease.tag_name)) - window.latestVersion = highestBetaRelease; - - //next two if statements are only necessary if we don't have a beta or stable release. We need one alpha release at least - if (window.latestVersion.tag_name == '0.0.0' && highestBetaRelease.tag_name != '0.0.0') - window.latestVersion = highestBetaRelease; - - if (window.latestVersion.tag_name == '0.0.0' && highestAlphaRelease.tag_name != '0.0.0') - window.latestVersion = highestAlphaRelease; - - callback(true); - } - }); -} - -function getSystemInfo() { - var sys = window.sysInfo.system; - var shy = window.sysInfo.hyperion; - - var info = "Hyperion Server:\n"; - info += '- Build: ' + shy.build + '\n'; - info += '- Build time: ' + shy.time + '\n'; - info += '- Git Remote: ' + shy.gitremote + '\n'; - info += '- Version: ' + shy.version + '\n'; - info += '- UI Lang: ' + storedLang + ' (BrowserLang: ' + navigator.language + ')\n'; - info += '- UI Access: ' + storedAccess + '\n'; - //info += '- Log lvl: ' + window.serverConfig.logger.level + '\n'; - info += '- Avail Screen Cap.: ' + window.serverInfo.grabbers.screen.available + '\n'; - info += '- Avail Video Cap.: ' + window.serverInfo.grabbers.video.available + '\n'; - info += '- Avail Services: ' + window.serverInfo.services + '\n'; - info += '- Config path: ' + shy.rootPath + '\n'; - info += '- Database: ' + (shy.readOnlyMode ? "ready-only" : "read/write") + '\n'; - - info += '\n'; - - info += 'Hyperion Server OS:\n'; - info += '- Distribution: ' + sys.prettyName + '\n'; - info += '- Architecture: ' + sys.architecture + '\n'; - - if (sys.cpuModelName) - info += '- CPU Model: ' + sys.cpuModelName + '\n'; - if (sys.cpuModelType) - info += '- CPU Type: ' + sys.cpuModelType + '\n'; - if (sys.cpuRevision) - info += '- CPU Revision: ' + sys.cpuRevision + '\n'; - if (sys.cpuHardware) - info += '- CPU Hardware: ' + sys.cpuHardware + '\n'; - - info += '- Kernel: ' + sys.kernelType + ' (' + sys.kernelVersion + ' (WS: ' + sys.wordSize + '))\n'; - info += '- Root/Admin: ' + sys.isUserAdmin + '\n'; - info += '- Qt Version: ' + sys.qtVersion + '\n'; - if (jQuery.inArray("effectengine", window.serverInfo.services) !== -1) { - info += '- Python Version: ' + sys.pyVersion + '\n'; - } - info += '- Browser: ' + navigator.userAgent; - return info; -} - -function handleDarkMode() { - $("", { - rel: "stylesheet", - type: "text/css", - href: "../css/darkMode.css" - }).appendTo("head"); - - setStorage("darkMode", "on"); - $('#btn_darkmode_icon').removeClass('fa fa-moon-o'); - $('#btn_darkmode_icon').addClass('mdi mdi-white-balance-sunny'); - $('#navbar_brand_logo').attr("src", 'img/hyperion/logo_negativ.png'); -} - -function isAccessLevelCompliant(accessLevel) { - var isOK = true; - if (accessLevel) { - if (accessLevel === 'system') { - isOK = false; - } - else if (accessLevel === 'advanced' && storedAccess === 'default') { - isOK = false; - } - else if (accessLevel === 'expert' && storedAccess !== 'expert') { - isOK = false; - } - } - return isOK -} - -function showInputOptions(path, elements, state) { - for (var i = 0; i < elements.length; i++) { - $('[data-schemapath="root.' + path + '.' + elements[i] + '"]').toggle(state); - } -} - -function showInputOptionForItem(editor, path, item, state) { - var accessLevel = editor.schema.properties[path].properties[item].access; - // Enable element only, if access level compliant - if (!state || isAccessLevelCompliant(accessLevel)) { - showInputOptions(path, [item], state); - } -} - -function showInputOptionsForKey(editor, item, showForKeys, state) { - var elements = []; - var keysToshow = []; - - if (Array.isArray(showForKeys)) { - keysToshow = showForKeys; - } else { - if (typeof showForKeys === 'string') { - keysToshow.push(showForKeys); - } else { - return - } - } - - for (var key in editor.schema.properties[item].properties) { - if ($.inArray(key, keysToshow) === -1) { - var accessLevel = editor.schema.properties[item].properties[key].access; - - //Always disable all elements, but only enable elements, if access level compliant - if (!state || isAccessLevelCompliant(accessLevel)) { - elements.push(key); - } - } - } - showInputOptions(item, elements, state); -} - -function encodeHTML(s) { - return s.replace(/&/g, '&').replace(/ 255) { - return false; - } - } - return true; -} - -function isValidIPv6(value) { - if (value.match( - '^(?:(?:(?:[a-fA-F0-9]{1,4}:){6}|(?=(?:[a-fA-F0-9]{0,4}:){2,6}(?:[0-9]{1,3}.){3}[0-9]{1,3}$)(([0-9a-fA-F]{1,4}:){1,5}|:)((:[0-9a-fA-F]{1,4}){1,5}:|:)|::(?:[a-fA-F0-9]{1,4}:){5})(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]).){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?=(?:[a-fA-F0-9]{0,4}:){0,7}[a-fA-F0-9]{0,4}$)(([0-9a-fA-F]{1,4}:){1,7}|:)((:[0-9a-fA-F]{1,4}){1,7}|:)|(?:[a-fA-F0-9]{1,4}:){7}:|:(:[a-fA-F0-9]{1,4}){7})$' - )) - return true; - else - return false; -} - -function isValidHostname(value) { - if (value.match( - '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])(.([a-zA-Z0-9]|[_a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$' //lgtm [js/redos] - )) - return true; - else - return false; -} - -function isValidServicename(value) { - if (value.match( - '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9 -]{0,61}[a-zA-Z0-9])(.([a-zA-Z0-9]|[_a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$' //lgtm [js/redos] - )) - return true; - else - return false; -} - -function isValidHostnameOrIP4(value) { - return (isValidHostname(value) || isValidIPv4(value)); -} - -function isValidHostnameOrIP(value) { - return (isValidHostnameOrIP4(value) || isValidIPv6(value) || isValidServicename(value)); -} - +var prevTag; + +function removeOverlay() { + $("#loading_overlay").removeClass("overlay"); +} + +function reload() { + location.reload(); +} + +function storageComp() { + if (typeof (Storage) !== "undefined") + return true; + return false; +} + +function getStorage(item) { + if (storageComp()) { + return localStorage.getItem(item); + } + return null; +} + +function setStorage(item, value) { + if (storageComp()) { + localStorage.setItem(item, value); + } +} + +function removeStorage(item) { + if (storageComp()) { + localStorage.removeItem(item); + } +} + +function debugMessage(msg) { + if (window.debugMessagesActive) { + console.log(msg); + } +} + +function validateDuration(d) { + if (typeof d === "undefined" || d < 0) + return ENDLESS; + else + return d *= 1000; +} + +function getHashtag() { + if (getStorage('lasthashtag') != null) + return getStorage('lasthashtag'); + else { + var tag = document.URL; + tag = tag.substr(tag.indexOf("#") + 1); + if (tag == "" || typeof tag === "undefined" || tag.startsWith("http")) + tag = "dashboard" + return tag; + } +} + +function loadContent(event, forceRefresh) { + var tag; + + var lastSelectedInstance = getStorage('lastSelectedInstance'); + + if (lastSelectedInstance && (lastSelectedInstance != window.currentHyperionInstance)) { + if (window.serverInfo.instance[lastSelectedInstance] && window.serverInfo.instance[lastSelectedInstance].running) { + instanceSwitch(lastSelectedInstance); + } else { + removeStorage('lastSelectedInstance'); + } + } + + if (typeof event != "undefined") { + tag = event.currentTarget.hash; + tag = tag.substr(tag.indexOf("#") + 1); + setStorage('lasthashtag', tag); + } + else + tag = getHashtag(); + + if (forceRefresh || prevTag != tag) { + prevTag = tag; + $("#page-content").off(); + $("#page-content").load("/content/" + tag + ".html", function (response, status, xhr) { + if (status == "error") { + tag = 'dashboard'; + console.log("Could not find page:", prevTag, ", Redirecting to:", tag); + setStorage('lasthashtag', tag); + + $("#page-content").load("/content/" + tag + ".html", function (response, status, xhr) { + if (status == "error") { + $("#page-content").html('

    ' + encode_utf8(tag) + '
    ' + $.i18n('info_404') + '

    '); + removeOverlay(); + } + }); + } + updateUiOnInstance(window.currentHyperionInstance); + }); + } +} + +function getInstanceNameByIndex(index) { + var instData = window.serverInfo.instance + for (var key in instData) { + if (instData[key].instance == index) + return instData[key].friendly_name; + } + return "unknown" +} + +function updateHyperionInstanceListing() { + if (window.serverInfo.instance) { + var data = window.serverInfo.instance.filter(entry => entry.running); + $('#hyp_inst_listing').html(""); + for (var key in data) { + var currInstMarker = (data[key].instance == window.currentHyperionInstance) ? "component-on" : ""; + + var html = '
  • \ + \ +
    \ + \ + '+ data[key].friendly_name + ' \ +
    \ +
    \ +
  • ' + + if (data.length - 1 > key) + html += '
  • ' + + $('#hyp_inst_listing').append(html); + + $('#hyperioninstance_' + data[key].instance).off().on("click", function (e) { + var inst = e.currentTarget.id.split("_")[1] + instanceSwitch(inst) + }); + } + } +} + +function initLanguageSelection() { + // Initialise language selection list with languages supported + for (var i = 0; i < availLang.length; i++) { + $("#language-select").append(''); + } + + var langLocale = storedLang; + + //Test, if language is supported by hyperion + var langIdx = availLang.indexOf(langLocale); + if (langIdx > -1) { + langText = availLangText[langIdx]; + } else { + // If language is not supported by hyperion, try fallback language + langLocale = $.i18n().options.fallbackLocale.substring(0, 2); + langIdx = availLang.indexOf(langLocale); + if (langIdx > -1) { + langText = availLangText[langIdx]; + } else { + langLocale = 'en'; + langIdx = availLang.indexOf(langLocale); + if (langIdx > -1) { + langText = availLangText[langIdx]; + } + } + } + + $('#language-select').prop('title', langText); + $("#language-select").val(langIdx); + $("#language-select").selectpicker("refresh"); +} + +function updateUiOnInstance(inst) { + $("#active_instance_friendly_name").text(getInstanceNameByIndex(inst)); + if (window.serverInfo.instance.filter(entry => entry.running).length > 1) { + $('#btn_hypinstanceswitch').toggle(true); + $('#active_instance_dropdown').prop('disabled', false); + $('#active_instance_dropdown').css('cursor', 'pointer'); + $("#active_instance_dropdown").css("pointer-events", "auto"); + } else { + $('#btn_hypinstanceswitch').toggle(false); + $('#active_instance_dropdown').prop('disabled', true); + $("#active_instance_dropdown").css('cursor', 'default'); + $("#active_instance_dropdown").css("pointer-events", "none"); + } +} + +function instanceSwitch(inst) { + requestInstanceSwitch(inst) + window.currentHyperionInstance = inst; + window.currentHyperionInstanceName = getInstanceNameByIndex(inst); + setStorage('lastSelectedInstance', inst) +} + +function loadContentTo(containerId, fileName) { + $(containerId).load("/content/" + fileName + ".html"); +} + +function toggleClass(obj, class1, class2) { + if ($(obj).hasClass(class1)) { + $(obj).removeClass(class1); + $(obj).addClass(class2); + } + else { + $(obj).removeClass(class2); + $(obj).addClass(class1); + } +} + +function setClassByBool(obj, enable, class1, class2) { + if (enable) { + $(obj).removeClass(class1); + $(obj).addClass(class2); + } + else { + $(obj).removeClass(class2); + $(obj).addClass(class1); + } +} + +function showInfoDialog(type, header, message) { + if (type == "success") { + $('#id_body').html(''); + if (header == "") + $('#id_body').append('

    ' + $.i18n('infoDialog_general_success_title') + '

    '); + $('#id_footer').html(''); + } + else if (type == "warning") { + $('#id_body').html(''); + if (header == "") + $('#id_body').append('

    ' + $.i18n('infoDialog_general_warning_title') + '

    '); + $('#id_footer').html(''); + } + else if (type == "error") { + $('#id_body').html(''); + if (header == "") + $('#id_body').append('

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

    '); + $('#id_footer').html(''); + } + else if (type == "select") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + else if (type == "iswitch") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + else if (type == "uilock") { + $('#id_body').html(''); + $('#id_footer').html('' + $.i18n('InfoDialog_nowrite_foottext') + ''); + } + else if (type == "import") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + else if (type == "delInst") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + else if (type == "renInst") { + $('#id_body_rename').html('
    '); + $('#id_body_rename').append('

    ' + header + '

    '); + $('#id_body_rename').append(''); + $('#id_footer_rename').html(''); + $('#id_footer_rename').append(''); + } + else if (type == "changePassword") { + $('#id_body_rename').html('
    '); + $('#id_body_rename').append('

    ' + header + '


    '); + $('#id_body_rename').append('

    ' + $.i18n('infoDialog_username_text') + + '


    '); + $('#id_body_rename').append('

    ' + $.i18n('infoDialog_password_current_text') + + '


    '); + $('#id_body_rename').append('

    ' + $.i18n('infoDialog_password_new_text') + + '

    '); + $('#id_body_rename').append('
    ' + $.i18n('infoDialog_password_minimum_length') + '
    '); + $('#id_footer_rename').html(''); + $('#id_footer_rename').append(''); + } + else if (type == "checklist") { + $('#id_body').html(''); + $('#id_body').append('

    ' + $.i18n('infoDialog_checklist_title') + '

    '); + $('#id_body').append(header); + $('#id_footer').html(''); + } + else if (type == "newToken") { + $('#id_body').html(''); + $('#id_footer').html(''); + } + else if (type == "grantToken") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + + if (type != "renInst") { + $('#id_body').append('

    ' + header + '

    '); + $('#id_body').append(message); + } + + if (type == "select" || type == "iswitch") + $('#id_body').append(''); + + if (getStorage("darkMode") == "on") + $('#id_logo').attr("src", 'img/hyperion/logo_negativ.png'); + + $(type == "renInst" || type == "changePassword" ? "#modal_dialog_rename" : "#modal_dialog").modal({ + backdrop: "static", + keyboard: false, + show: true + }); + + $(document).on('click', '[data-dismiss-modal]', function () { + var target = $(this).attr('data-dismiss-modal'); + $.find(target).modal('hide'); + }); +} + +function createHintH(type, text, container) { + type = String(type); + if (type == "intro") + tclass = "introd"; + + $('#' + container).prepend('

    ' + text + '


    '); +} + +function createHint(type, text, container, buttonid, buttontxt) { + var fe, tclass; + + if (type == "intro") { + fe = ''; + tclass = "intro-hint"; + } + else if (type == "info") { + fe = '
    Information
    '; + tclass = "info-hint"; + } + else if (type == "wizard") { + fe = '
    Information
    '; + tclass = "wizard-hint"; + } + else if (type == "warning") { + fe = '
    Information
    '; + tclass = "warning-hint"; + } + + if (buttonid) + buttonid = '

    '; + else + buttonid = ""; + + if (type == "intro") + $('#' + container).prepend('

    ' + $.i18n("conf_helptable_expl") + '

    ' + text + '
    '); + else if (type == "wizard") + $('#' + container).prepend('

    ' + $.i18n("wiz_wizavail") + '

    ' + $.i18n('wiz_guideyou', text) + buttonid + '
    '); + else { + createTable('', 'htb', container, true, tclass); + $('#' + container + ' .htb').append(createTableRow([fe, text], false, true)); + } +} + +function createEffHint(title, text) { + return '

    ' + title + '

    ' + text + '
    '; +} + +function valValue(id, value, min, max) { + if (typeof max === 'undefined' || max == "") + max = 999999; + + if (Number(value) > Number(max)) { + $('#' + id).val(max); + showInfoDialog("warning", "", $.i18n('edt_msg_error_maximum_incl', max)); + return max; + } + else if (Number(value) < Number(min)) { + $('#' + id).val(min); + showInfoDialog("warning", "", $.i18n('edt_msg_error_minimum_incl', min)); + return min; + } + return value; +} + +function readImg(input, cb) { + if (input.files && input.files[0]) { + var reader = new FileReader(); + // inject fileName property + reader.fileName = input.files[0].name + + reader.onload = function (e) { + cb(e.target.result, e.target.fileName); + } + reader.readAsDataURL(input.files[0]); + } +} + +function isJsonString(str) { + try { + JSON.parse(str); + } + catch (e) { + return e; + } + return ""; +} + +function createJsonEditor(container, schema, setconfig, usePanel, arrayre) { + $('#' + container).off(); + $('#' + container).html(""); + + if (typeof arrayre === 'undefined') + arrayre = true; + + var editor = new JSONEditor(document.getElementById(container), + { + theme: 'bootstrap3', + iconlib: "fontawesome4", + disable_collapse: 'true', + form_name_root: 'sa', + disable_edit_json: true, + disable_properties: true, + disable_array_reorder: arrayre, + no_additional_properties: true, + disable_array_delete_all_rows: true, + disable_array_delete_last_row: true, + access: storedAccess, + schema: { + title: '', + properties: schema + } + }); + + if (usePanel) { + $('#' + container + ' .well').first().removeClass('well well-sm'); + $('#' + container + ' h4').first().remove(); + $('#' + container + ' .well').first().removeClass('well well-sm'); + } + + if (setconfig) { + for (var key in editor.root.editors) { + editor.getEditor("root." + key).setValue(Object.assign({}, editor.getEditor("root." + key).value, window.serverConfig[key])); + } + } + + return editor; +} + +function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal, addSelect, addCustom, addCustomAsFirst, customText) { + var editor = rootEditor.getEditor(path); + var orginalProperties = editor.schema.properties[key]; + + var orginalWatchFunctions = rootEditor.watchlist[path + "." + key]; + rootEditor.unwatch(path + "." + key); + + var newSchema = []; + newSchema[key] = + { + "type": "string", + "enum": [], + "required": true, + "options": { "enum_titles": [], "infoText": "" }, + "propertyOrder": 1 + }; + + //Add additional elements to overwrite defaults + for (var item in addElements) { + newSchema[key][item] = addElements[item]; + } + + if (orginalProperties) { + if (orginalProperties["title"]) { + newSchema[key]["title"] = orginalProperties["title"]; + } + + if (orginalProperties["options"] && orginalProperties["options"]["infoText"]) { + newSchema[key]["options"]["infoText"] = orginalProperties["options"]["infoText"]; + } + + if (orginalProperties["propertyOrder"]) { + newSchema[key]["propertyOrder"] = orginalProperties["propertyOrder"]; + } + } + + if (addCustom) { + + if (newTitelVals.length === 0) { + newTitelVals = [...newEnumVals]; + } + + if (!!!customText) { + customText = "edt_conf_enum_custom"; + } + + if (addCustomAsFirst) { + newEnumVals.unshift("CUSTOM"); + newTitelVals.unshift(customText); + } else { + newEnumVals.push("CUSTOM"); + newTitelVals.push(customText); + } + + if (newSchema[key].options.infoText) { + var customInfoText = newSchema[key].options.infoText + "_custom"; + newSchema[key].options.infoText = customInfoText; + } + } + + if (addSelect) { + newEnumVals.unshift("SELECT"); + newTitelVals.unshift("edt_conf_enum_please_select"); + newDefaultVal = "SELECT"; + } + + if (newEnumVals) { + newSchema[key]["enum"] = newEnumVals; + } + + if (newTitelVals) { + newSchema[key]["options"]["enum_titles"] = newTitelVals; + } + if (newDefaultVal) { + newSchema[key]["default"] = newDefaultVal; + } + + editor.original_schema.properties[key] = orginalProperties; + editor.schema.properties[key] = newSchema[key]; + rootEditor.validator.schema.properties[editor.key].properties[key] = newSchema[key]; + + editor.removeObjectProperty(key); + delete editor.cached_editors[key]; + editor.addObjectProperty(key); + + if (orginalWatchFunctions) { + for (var i = 0; i < orginalWatchFunctions.length; i++) { + rootEditor.watch(path + "." + key, orginalWatchFunctions[i]); + } + } + rootEditor.notifyWatchers(path + "." + key); +} + +function updateJsonEditorMultiSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal) { + var editor = rootEditor.getEditor(path); + var orginalProperties = editor.schema.properties[key]; + + var orginalWatchFunctions = rootEditor.watchlist[path + "." + key]; + rootEditor.unwatch(path + "." + key); + + var newSchema = []; + newSchema[key] = + { + "type": "array", + "format": "select", + "items": { + "type": "string", + "enum": [], + "options": { "enum_titles": [] }, + }, + "options": { "infoText": "" }, + "default": [], + "propertyOrder": 1 + }; + + //Add additional elements to overwrite defaults + for (var item in addElements) { + newSchema[key][item] = addElements[item]; + } + + if (orginalProperties) { + if (orginalProperties["title"]) { + newSchema[key]["title"] = orginalProperties["title"]; + } + + if (orginalProperties["options"] && orginalProperties["options"]["infoText"]) { + newSchema[key]["options"]["infoText"] = orginalProperties["options"]["infoText"]; + } + + if (orginalProperties["propertyOrder"]) { + newSchema[key]["propertyOrder"] = orginalProperties["propertyOrder"]; + } + } + + if (newEnumVals) { + newSchema[key]["items"]["enum"] = newEnumVals; + } + + if (newTitelVals) { + newSchema[key]["items"]["options"]["enum_titles"] = newTitelVals; + } + + if (newDefaultVal) { + newSchema[key]["default"] = newDefaultVal; + } + + editor.original_schema.properties[key] = orginalProperties; + editor.schema.properties[key] = newSchema[key]; + rootEditor.validator.schema.properties[editor.key].properties[key] = newSchema[key]; + + editor.removeObjectProperty(key); + delete editor.cached_editors[key]; + editor.addObjectProperty(key); + + if (orginalWatchFunctions) { + for (var i = 0; i < orginalWatchFunctions.length; i++) { + rootEditor.watch(path + "." + key, orginalWatchFunctions[i]); + } + } + rootEditor.notifyWatchers(path + "." + key); +} + +function updateJsonEditorRange(rootEditor, path, key, minimum, maximum, defaultValue, step, clear) { + var editor = rootEditor.getEditor(path); + + //Preserve current value when updating range + var currentValue = rootEditor.getEditor(path + "." + key).getValue(); + + var orginalProperties = editor.schema.properties[key]; + var newSchema = []; + newSchema[key] = orginalProperties; + + if (clear) { + delete newSchema[key]["minimum"]; + delete newSchema[key]["maximum"]; + delete newSchema[key]["default"]; + delete newSchema[key]["step"]; + } + + if (typeof minimum !== "undefined") { + newSchema[key]["minimum"] = minimum; + } + if (typeof maximum !== "undefined") { + newSchema[key]["maximum"] = maximum; + } + if (typeof defaultValue !== "undefined") { + newSchema[key]["default"] = defaultValue; + currentValue = defaultValue; + } + + if (typeof step !== "undefined") { + newSchema[key]["step"] = step; + } + + editor.original_schema.properties[key] = orginalProperties; + editor.schema.properties[key] = newSchema[key]; + rootEditor.validator.schema.properties[editor.key].properties[key] = newSchema[key]; + + editor.removeObjectProperty(key); + delete editor.cached_editors[key]; + editor.addObjectProperty(key); + + // Restore current (new default) value for new range + rootEditor.getEditor(path + "." + key).setValue(currentValue); +} + +function addJsonEditorHostValidation() { + + JSONEditor.defaults.custom_validators.push(function (schema, value, path) { + var errors = []; + + if (!jQuery.isEmptyObject(value)) { + switch (schema.format) { + case "hostname_or_ip": + if (!isValidHostnameOrIP(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msgcust_error_hostname_ip') + }); + } + break; + case "hostname_or_ip4": + if (!isValidHostnameOrIP4(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msgcust_error_hostname_ip4') + }); + } + break; + + //Remove, when new json-editor 2.x is used + case "ipv4": + if (!isValidIPv4(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msg_error_ipv4') + }); + } + break; + case "ipv6": + if (!isValidIPv6(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msg_error_ipv6') + }); + } + break; + case "hostname": + if (!isValidHostname(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msg_error_hostname') + }); + } + break; + + default: + } + } + return errors; + }); +} + +function buildWL(link, linkt, cl) { + var baseLink = "https://docs.hyperion-project.org/"; + var lang; + + if (typeof linkt == "undefined") + linkt = "Placeholder"; + + if (storedLang == "de" || navigator.locale == "de") + lang = "de"; + else + lang = "en"; + + if (cl === true) { + linkt = $.i18n(linkt); + return '

    ' + linkt + '

    ' + $.i18n('general_wiki_moreto', linkt) + ': ' + linkt + '
    ' + } + else + return ': ' + linkt + ''; +} + +function rgbToHex(rgb) { + if (rgb.length == 3) { + return "#" + + ("0" + parseInt(rgb[0], 10).toString(16)).slice(-2) + + ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) + + ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2); + } + else + debugMessage('rgbToHex: Given rgb is no array or has wrong length'); +} + +function hexToRgb(hex) { + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : { + r: 0, + g: 0, + b: 0 + }; +} + +/* + Show a notification + @param type Valid types are "info","success","warning","danger" + @param message The message to show + @param title A title (optional) + @param addhtml Add custom html to the notification end + */ +function showNotification(type, message, title = "", addhtml = "") { + if (title == "") { + switch (type) { + case "info": + title = $.i18n('infoDialog_general_info_title'); + break; + case "success": + title = $.i18n('infoDialog_general_success_title'); + break; + case "warning": + title = $.i18n('infoDialog_general_warning_title'); + break; + case "danger": + title = $.i18n('infoDialog_general_error_title'); + break; + } + } + + $.notify({ + // options + title: title, + message: message + }, { + // settings + type: type, + animate: { + enter: 'animate__animated animate__fadeInDown', + exit: 'animate__animated animate__fadeOutUp' + }, + placement: { + align: 'center' + }, + mouse_over: 'pause', + template: '' + }); +} + +function createCP(id, color, cb) { + if (Array.isArray(color)) + color = rgbToHex(color); + else if (color == "undefined") + color = "#AA3399"; + + if (color.startsWith("#")) { + $('#' + id).colorpicker({ + format: 'rgb', + customClass: 'colorpicker-2x', + color: color, + sliders: { + saturation: { + maxLeft: 200, + maxTop: 200 + }, + hue: { + maxTop: 200 + }, + } + }); + $('#' + id).colorpicker().on('changeColor', function (e) { + var rgb = e.color.toRGB(); + var hex = e.color.toHex(); + cb(rgb, hex, e); + }); + } + else + debugMessage('createCP: Given color is not legit'); +} + +// Creates a table with thead and tbody ids +// @param string hid : a class for thead +// @param string bid : a class for tbody +// @param string cont : a container id to html() the table +// @param string bless: if true the table is borderless +function createTable(hid, bid, cont, bless, tclass) { + var table = document.createElement('table'); + var thead = document.createElement('thead'); + var tbody = document.createElement('tbody'); + + table.className = "table"; + if (bless === true) + table.className += " borderless"; + if (typeof tclass !== "undefined") + table.className += " " + tclass; + table.style.marginBottom = "0px"; + if (hid != "") + thead.className = hid; + tbody.className = bid; + if (hid != "") + table.appendChild(thead); + table.appendChild(tbody); + + $('#' + cont).append(table); +} + +// Creates a table row +// @param array list :innerHTML content for / +// @param bool head :if null or false it's body +// @param bool align :if null or false no alignment +// +// @return : with or as child(s) +function createTableRow(list, head, align) { + var row = document.createElement('tr'); + + for (var i = 0; i < list.length; i++) { + if (head === true) + var el = document.createElement('th'); + else + var el = document.createElement('td'); + + if (align) + el.style.verticalAlign = "middle"; + + var purifyConfig = { + ADD_TAGS: ['button'], + ADD_ATTR: ['onclick'] + }; + el.innerHTML = DOMPurify.sanitize(list[i], purifyConfig); + row.appendChild(el); + } + return row; +} + +function createRow(id) { + var el = document.createElement('div'); + el.className = "row"; + el.setAttribute('id', id); + return el; +} + +function createOptPanel(phicon, phead, bodyid, footerid, css, panelId) { + phead = '' + phead; + + var pfooter = document.createElement('button'); + pfooter.className = "btn btn-primary"; + pfooter.setAttribute("id", footerid); + pfooter.innerHTML = '' + $.i18n('general_button_savesettings'); + + return createPanel(phead, "", pfooter, "panel-default", bodyid, css, panelId); +} + +function compareTwoValues(key1, key2, order = 'asc') { + return function innerSort(a, b) { + if (!a.hasOwnProperty(key1) || !b.hasOwnProperty(key1)) { + // property key1 doesn't exist on either object + return 0; + } + + const varA1 = (typeof a[key1] === 'string') + ? a[key1].toUpperCase() : a[key1]; + const varB1 = (typeof b[key1] === 'string') + ? b[key1].toUpperCase() : b[key1]; + + let comparison = 0; + if (varA1 > varB1) { + comparison = 1; + } else { + if (varA1 < varB1) { + comparison = -1; + } else { + if (!a.hasOwnProperty(key2) || !b.hasOwnProperty(key2)) { + // property key2 doesn't exist on either object + return 0; + } + + const varA2 = (typeof a[key2] === 'string') + ? a[key2].toUpperCase() : a[key2]; + const varB2 = (typeof b[key1] === 'string') + ? b[key2].toUpperCase() : b[key2]; + + if (varA2 > varB2) { + comparison = 1; + } else { + comparison = -1; + } + } + } + return ( + (order === 'desc') ? (comparison * -1) : comparison + ); + }; +} + +function sortProperties(list) { + for (var key in list) { + list[key].key = key; + } + list = $.map(list, function (value, index) { + return [value]; + }); + return list.sort(function (a, b) { + return a.propertyOrder - b.propertyOrder; + }); +} + +function createHelpTable(list, phead, panelId) { + var table = document.createElement('table'); + var thead = document.createElement('thead'); + var tbody = document.createElement('tbody'); + list = sortProperties(list); + + phead = '' + phead + ' ' + $.i18n("conf_helptable_expl"); + + table.className = 'table table-hover borderless'; + + thead.appendChild(createTableRow([$.i18n('conf_helptable_option'), $.i18n('conf_helptable_expl')], true, false)); + + for (var key in list) { + if (list[key].access != 'system') { + // break one iteration (in the loop), if the schema has the entry hidden=true + if ("options" in list[key] && "hidden" in list[key].options && (list[key].options.hidden)) + continue; + if ("access" in list[key] && ((list[key].access == "advanced" && storedAccess == "default") || (list[key].access == "expert" && storedAccess != "expert"))) + continue; + var text = list[key].title.replace('title', 'expl'); + tbody.appendChild(createTableRow([$.i18n(list[key].title), $.i18n(text)], false, false)); + + if (list[key].items && list[key].items.properties) { + var ilist = sortProperties(list[key].items.properties); + for (var ikey in ilist) { + // break one iteration (in the loop), if the schema has the entry hidden=true + if ("options" in ilist[ikey] && "hidden" in ilist[ikey].options && (ilist[ikey].options.hidden)) + continue; + if ("access" in ilist[ikey] && ((ilist[ikey].access == "advanced" && storedAccess == "default") || (ilist[ikey].access == "expert" && storedAccess != "expert"))) + continue; + var itext = ilist[ikey].title.replace('title', 'expl'); + tbody.appendChild(createTableRow([$.i18n(ilist[ikey].title), $.i18n(itext)], false, false)); + } + } + } + } + table.appendChild(thead); + table.appendChild(tbody); + + return createPanel(phead, table, undefined, undefined, undefined, undefined, panelId); +} + +function createPanel(head, body, footer, type, bodyid, css, panelId) { + var cont = document.createElement('div'); + var p = document.createElement('div'); + var phead = document.createElement('div'); + var pbody = document.createElement('div'); + var pfooter = document.createElement('div'); + + cont.className = "col-lg-6"; + + if (typeof type == 'undefined') + type = 'panel-default'; + + p.className = 'panel ' + type; + if (typeof panelId != 'undefined') { + p.setAttribute("id", panelId); + } + + phead.className = 'panel-heading ' + css; + pbody.className = 'panel-body'; + pfooter.className = 'panel-footer'; + + phead.innerHTML = head; + + if (typeof bodyid != 'undefined') { + pfooter.style.textAlign = 'right'; + pbody.setAttribute("id", bodyid); + } + + if (typeof body != 'undefined' && body != "") + pbody.appendChild(body); + + if (typeof footer != 'undefined') + pfooter.appendChild(footer); + + p.appendChild(phead); + p.appendChild(pbody); + + if (typeof footer != 'undefined') { + pfooter.style.textAlign = "right"; + p.appendChild(pfooter); + } + + cont.appendChild(p); + + return cont; +} + +function createSelGroup(group) { + var el = document.createElement('optgroup'); + el.setAttribute('label', group); + return el; +} + +function createSelOpt(opt, title) { + var el = document.createElement('option'); + el.setAttribute('value', opt); + if (typeof title == 'undefined') + el.innerHTML = opt; + else + el.innerHTML = title; + return el; +} + +function createSel(array, group, split) { + if (array.length != 0) { + var el = createSelGroup(group); + for (var i = 0; i < array.length; i++) { + var opt; + if (split) { + opt = array[i].split(":") + opt = createSelOpt(opt[0], opt[1]) + } + else + opt = createSelOpt(array[i]) + el.appendChild(opt); + } + return el; + } +} + +function performTranslation() { + $('[data-i18n]').i18n(); +} + +function encode_utf8(s) { + return unescape(encodeURIComponent(s)); +} + +function getReleases(callback) { + $.ajax({ + url: window.gitHubReleaseApiUrl, + method: 'get', + error: function (XMLHttpRequest, textStatus, errorThrown) { + callback(false); + }, + success: function (releases) { + window.gitHubVersionList = releases; + var highestRelease = { + tag_name: '0.0.0' + }; + var highestAlphaRelease = { + tag_name: '0.0.0' + }; + var highestBetaRelease = { + tag_name: '0.0.0' + }; + var highestRcRelease = { + tag_name: '0.0.0' + }; + + for (var i in releases) { + //drafts will be ignored + if (releases[i].draft) + continue; + + if (releases[i].tag_name.includes('alpha')) { + if (sem = semverLite.gt(releases[i].tag_name, highestAlphaRelease.tag_name)) + highestAlphaRelease = releases[i]; + } + else if (releases[i].tag_name.includes('beta')) { + if (sem = semverLite.gt(releases[i].tag_name, highestBetaRelease.tag_name)) + highestBetaRelease = releases[i]; + } + else if (releases[i].tag_name.includes('rc')) { + if (semverLite.gt(releases[i].tag_name, highestRcRelease.tag_name)) + highestRcRelease = releases[i]; + } + else { + if (semverLite.gt(releases[i].tag_name, highestRelease.tag_name)) + highestRelease = releases[i]; + } + } + window.latestStableVersion = highestRelease; + window.latestBetaVersion = highestBetaRelease; + window.latestAlphaVersion = highestAlphaRelease; + window.latestRcVersion = highestRcRelease; + + if (window.serverConfig.general.watchedVersionBranch == "Beta" && semverLite.gt(highestBetaRelease.tag_name, highestRelease.tag_name)) + window.latestVersion = highestBetaRelease; + else + window.latestVersion = highestRelease; + + if (window.serverConfig.general.watchedVersionBranch == "Alpha" && semverLite.gt(highestAlphaRelease.tag_name, highestBetaRelease.tag_name)) + window.latestVersion = highestAlphaRelease; + + if (window.serverConfig.general.watchedVersionBranch == "Alpha" && semverLite.lt(highestAlphaRelease.tag_name, highestBetaRelease.tag_name)) + window.latestVersion = highestBetaRelease; + + //next two if statements are only necessary if we don't have a beta or stable release. We need one alpha release at least + if (window.latestVersion.tag_name == '0.0.0' && highestBetaRelease.tag_name != '0.0.0') + window.latestVersion = highestBetaRelease; + + if (window.latestVersion.tag_name == '0.0.0' && highestAlphaRelease.tag_name != '0.0.0') + window.latestVersion = highestAlphaRelease; + + callback(true); + } + }); +} + +function getSystemInfo() { + var sys = window.sysInfo.system; + var shy = window.sysInfo.hyperion; + + var info = "Hyperion Server:\n"; + info += '- Build: ' + shy.build + '\n'; + info += '- Build time: ' + shy.time + '\n'; + info += '- Git Remote: ' + shy.gitremote + '\n'; + info += '- Version: ' + shy.version + '\n'; + info += '- UI Lang: ' + storedLang + ' (BrowserLang: ' + navigator.language + ')\n'; + info += '- UI Access: ' + storedAccess + '\n'; + //info += '- Log lvl: ' + window.serverConfig.logger.level + '\n'; + info += '- Avail Screen Cap.: ' + window.serverInfo.grabbers.screen.available + '\n'; + info += '- Avail Video Cap.: ' + window.serverInfo.grabbers.video.available + '\n'; + info += '- Avail Services: ' + window.serverInfo.services + '\n'; + info += '- Config path: ' + shy.rootPath + '\n'; + info += '- Database: ' + (shy.readOnlyMode ? "ready-only" : "read/write") + '\n'; + + info += '\n'; + + info += 'Hyperion Server OS:\n'; + info += '- Distribution: ' + sys.prettyName + '\n'; + info += '- Architecture: ' + sys.architecture + '\n'; + + if (sys.cpuModelName) + info += '- CPU Model: ' + sys.cpuModelName + '\n'; + if (sys.cpuModelType) + info += '- CPU Type: ' + sys.cpuModelType + '\n'; + if (sys.cpuRevision) + info += '- CPU Revision: ' + sys.cpuRevision + '\n'; + if (sys.cpuHardware) + info += '- CPU Hardware: ' + sys.cpuHardware + '\n'; + + info += '- Kernel: ' + sys.kernelType + ' (' + sys.kernelVersion + ' (WS: ' + sys.wordSize + '))\n'; + info += '- Root/Admin: ' + sys.isUserAdmin + '\n'; + info += '- Qt Version: ' + sys.qtVersion + '\n'; + if (jQuery.inArray("effectengine", window.serverInfo.services) !== -1) { + info += '- Python Version: ' + sys.pyVersion + '\n'; + } + info += '- Browser: ' + navigator.userAgent; + return info; +} + +function handleDarkMode() { + $("", { + rel: "stylesheet", + type: "text/css", + href: "../css/darkMode.css" + }).appendTo("head"); + + setStorage("darkMode", "on"); + $('#btn_darkmode_icon').removeClass('fa fa-moon-o'); + $('#btn_darkmode_icon').addClass('mdi mdi-white-balance-sunny'); + $('#navbar_brand_logo').attr("src", 'img/hyperion/logo_negativ.png'); +} + +function isAccessLevelCompliant(accessLevel) { + var isOK = true; + if (accessLevel) { + if (accessLevel === 'system') { + isOK = false; + } + else if (accessLevel === 'advanced' && storedAccess === 'default') { + isOK = false; + } + else if (accessLevel === 'expert' && storedAccess !== 'expert') { + isOK = false; + } + } + return isOK +} + +function showInputOptions(path, elements, state) { + for (var i = 0; i < elements.length; i++) { + $('[data-schemapath="root.' + path + '.' + elements[i] + '"]').toggle(state); + } +} + +function showInputOptionForItem(editor, path, item, state) { + var accessLevel = editor.schema.properties[path].properties[item].access; + // Enable element only, if access level compliant + if (!state || isAccessLevelCompliant(accessLevel)) { + showInputOptions(path, [item], state); + } +} + +function showInputOptionsForKey(editor, item, showForKeys, state) { + var elements = []; + var keysToshow = []; + + if (Array.isArray(showForKeys)) { + keysToshow = showForKeys; + } else { + if (typeof showForKeys === 'string') { + keysToshow.push(showForKeys); + } else { + return + } + } + + for (var key in editor.schema.properties[item].properties) { + if ($.inArray(key, keysToshow) === -1) { + var accessLevel = editor.schema.properties[item].properties[key].access; + + //Always disable all elements, but only enable elements, if access level compliant + if (!state || isAccessLevelCompliant(accessLevel)) { + elements.push(key); + } + } + } + showInputOptions(item, elements, state); +} + +function encodeHTML(s) { + return s.replace(/&/g, '&').replace(/ 255) { + return false; + } + } + return true; +} + +function isValidIPv6(value) { + if (value.match( + '^(?:(?:(?:[a-fA-F0-9]{1,4}:){6}|(?=(?:[a-fA-F0-9]{0,4}:){2,6}(?:[0-9]{1,3}.){3}[0-9]{1,3}$)(([0-9a-fA-F]{1,4}:){1,5}|:)((:[0-9a-fA-F]{1,4}){1,5}:|:)|::(?:[a-fA-F0-9]{1,4}:){5})(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]).){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?=(?:[a-fA-F0-9]{0,4}:){0,7}[a-fA-F0-9]{0,4}$)(([0-9a-fA-F]{1,4}:){1,7}|:)((:[0-9a-fA-F]{1,4}){1,7}|:)|(?:[a-fA-F0-9]{1,4}:){7}:|:(:[a-fA-F0-9]{1,4}){7})$' + )) + return true; + else + return false; +} + +function isValidHostname(value) { + if (value.match( + '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])(.([a-zA-Z0-9]|[_a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$' + )) + return true; + else + return false; +} + +function isValidServicename(value) { + if (value.match( + '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9 -]{0,61}[a-zA-Z0-9])(.([a-zA-Z0-9]|[_a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$' + )) + return true; + else + return false; +} + +function isValidHostnameOrIP4(value) { + return (isValidHostname(value) || isValidIPv4(value)); +} + +function isValidHostnameOrIP(value) { + return (isValidHostnameOrIP4(value) || isValidIPv6(value) || isValidServicename(value)); +} + diff --git a/effects/knight-rider.py b/effects/knight-rider.py index 821ec377..15723933 100644 --- a/effects/knight-rider.py +++ b/effects/knight-rider.py @@ -1,54 +1,54 @@ -import hyperion, time - -# Get the parameters -speed = float(hyperion.args.get('speed', 1.0)) -fadeFactor = float(hyperion.args.get('fadeFactor', 0.7)) -color = hyperion.args.get('color', (255,0,0)) - -# Check parameters -speed = max(0.0001, speed) -fadeFactor = max(0.0, min(fadeFactor, 1.0)) - -# Initialize the led data -width = 25 -imageData = bytearray(width * (0,0,0)) -imageData[0] = color[0] -imageData[1] = color[1] -imageData[2] = color[2] - -# Calculate the sleep time and rotation increment -increment = 1 -sleepTime = 1.0 / (speed * width) -while sleepTime < 0.05: - increment *= 2 - sleepTime *= 2 - -# Start the write data loop -position = 0 -direction = 1 -while not hyperion.abort(): - hyperion.setImage(width, 1, imageData) - - # Move data into next state - for i in range(increment): - position += direction - if position == -1: - position = 1 - direction = 1 - elif position == width: - position = width-2 - direction = -1 - - # Fade the old data - for j in range(width): - imageData[3*j] = int(fadeFactor * imageData[3*j]) - imageData[3*j+1] = int(fadeFactor * imageData[3*j+1]) - imageData[3*j+2] = int(fadeFactor * imageData[3*j+2]) - - # Insert new data - imageData[3*position] = color[0] - imageData[3*position+1] = color[1] - imageData[3*position+2] = color[2] - - # Sleep for a while - time.sleep(sleepTime) +import hyperion, time + +# Get the parameters +speed = float(hyperion.args.get('speed', 1.0)) +fadeFactor = float(hyperion.args.get('fadeFactor', 0.7)) +color = hyperion.args.get('color', (255,0,0)) + +# Check parameters +speed = max(0.0001, speed) +fadeFactor = max(0.0, min(fadeFactor, 1.0)) + +# Initialize the led data +width = 25 +imageData = bytearray(width * (0,0,0)) +imageData[0] = color[0] +imageData[1] = color[1] +imageData[2] = color[2] + +# Calculate the sleep time and rotation increment +increment = 1 +sleepTime = 1.0 / (speed * width) +while sleepTime < 0.05: + increment *= 2 + sleepTime *= 2 + +# Start the write data loop +position = 0 +direction = 1 +while not hyperion.abort(): + hyperion.setImage(width, 1, imageData) + + # Move data into next state + for unused in range(increment): + position += direction + if position == -1: + position = 1 + direction = 1 + elif position == width: + position = width-2 + direction = -1 + + # Fade the old data + for j in range(width): + imageData[3*j] = int(fadeFactor * imageData[3*j]) + imageData[3*j+1] = int(fadeFactor * imageData[3*j+1]) + imageData[3*j+2] = int(fadeFactor * imageData[3*j+2]) + + # Insert new data + imageData[3*position] = color[0] + imageData[3*position+1] = color[1] + imageData[3*position+2] = color[2] + + # Sleep for a while + time.sleep(sleepTime) diff --git a/effects/rainbow-mood.py b/effects/rainbow-mood.py index ddad75f2..8a48abb7 100644 --- a/effects/rainbow-mood.py +++ b/effects/rainbow-mood.py @@ -1,23 +1,23 @@ -import hyperion, time, colorsys - -# Get the parameters -rotationTime = float(hyperion.args.get('rotation-time', 30.0)) -brightness = float(hyperion.args.get('brightness', 100))/100.0 -saturation = float(hyperion.args.get('saturation', 100))/100.0 -reverse = bool(hyperion.args.get('reverse', False)) - -# Calculate the sleep time and hue increment -sleepTime = 0.1 -hueIncrement = sleepTime / rotationTime - -# Switch direction if needed -if reverse: - increment = -increment - -# Start the write data loop -hue = 0.0 -while not hyperion.abort(): - rgb = colorsys.hsv_to_rgb(hue, saturation, brightness) - hyperion.setColor(int(255*rgb[0]), int(255*rgb[1]), int(255*rgb[2])) - hue = (hue + hueIncrement) % 1.0 - time.sleep(sleepTime) +import hyperion, time, colorsys + +# Get the parameters +rotationTime = float(hyperion.args.get('rotation-time', 30.0)) +brightness = float(hyperion.args.get('brightness', 100))/100.0 +saturation = float(hyperion.args.get('saturation', 100))/100.0 +reverse = bool(hyperion.args.get('reverse', False)) + +# Calculate the sleep time and hue increment +sleepTime = 0.1 +hueIncrement = sleepTime / rotationTime + +# Switch direction if needed +if reverse: + hueIncrement = -hueIncrement + +# Start the write data loop +hue = 0.0 +while not hyperion.abort(): + rgb = colorsys.hsv_to_rgb(hue, saturation, brightness) + hyperion.setColor(int(255*rgb[0]), int(255*rgb[1]), int(255*rgb[2])) + hue = (hue + hueIncrement) % 1.0 + time.sleep(sleepTime) diff --git a/effects/running_dots.py b/effects/running_dots.py index d807d356..2fb9569b 100644 --- a/effects/running_dots.py +++ b/effects/running_dots.py @@ -1,43 +1,43 @@ -import hyperion, time - -# get options from args -sleepTime = float(hyperion.args.get('speed', 1.5)) * 0.005 -whiteLevel = int(hyperion.args.get('whiteLevel', 0)) -lvl = int(hyperion.args.get('colorLevel', 220)) - -# check value -whiteLevel = min( whiteLevel, 254 ) -lvl = min( lvl, 255 ) - -if whiteLevel >= lvl: - lvl = 255 - -# Initialize the led data -ledData = bytearray() -for i in range(hyperion.ledCount): - ledData += bytearray((0,0,0)) - -runners = [ - { "pos":0, "step": 4, "lvl":lvl}, - { "pos":1, "step": 5, "lvl":lvl}, - { "pos":2, "step": 6, "lvl":lvl}, - { "pos":0, "step": 7, "lvl":lvl}, - { "pos":1, "step": 8, "lvl":lvl}, - { "pos":2, "step": 9, "lvl":lvl}, - #{ "pos":0, "step":10, "lvl":lvl}, - #{ "pos":1, "step":11, "lvl":lvl}, - #{ "pos":2, "step":12, "lvl":lvl}, -] - -# Start the write data loop -counter = 0 -while not hyperion.abort(): - counter += 1 - for r in runners: - if counter % r["step"] == 0: - ledData[r["pos"]] = whiteLevel - r["pos"] = (r["pos"]+3) % (hyperion.ledCount*3) - ledData[r["pos"]] = r["lvl"] - - hyperion.setColor(ledData) - time.sleep(sleepTime) +import hyperion, time + +# get options from args +sleepTime = float(hyperion.args.get('speed', 1.5)) * 0.005 +whiteLevel = int(hyperion.args.get('whiteLevel', 0)) +lvl = int(hyperion.args.get('colorLevel', 220)) + +# check value +whiteLevel = min( whiteLevel, 254 ) +lvl = min( lvl, 255 ) + +if whiteLevel >= lvl: + lvl = 255 + +# Initialize the led data +ledData = bytearray() +for unused in range(hyperion.ledCount): + ledData += bytearray((0,0,0)) + +runners = [ + { "pos":0, "step": 4, "lvl":lvl}, + { "pos":1, "step": 5, "lvl":lvl}, + { "pos":2, "step": 6, "lvl":lvl}, + { "pos":0, "step": 7, "lvl":lvl}, + { "pos":1, "step": 8, "lvl":lvl}, + { "pos":2, "step": 9, "lvl":lvl}, + #{ "pos":0, "step":10, "lvl":lvl}, + #{ "pos":1, "step":11, "lvl":lvl}, + #{ "pos":2, "step":12, "lvl":lvl}, +] + +# Start the write data loop +counter = 0 +while not hyperion.abort(): + counter += 1 + for r in runners: + if counter % r["step"] == 0: + ledData[r["pos"]] = whiteLevel + r["pos"] = (r["pos"]+3) % (hyperion.ledCount*3) + ledData[r["pos"]] = r["lvl"] + + hyperion.setColor(ledData) + time.sleep(sleepTime) diff --git a/effects/swirl.py b/effects/swirl.py index 47d8944b..fa5d0ed4 100644 --- a/effects/swirl.py +++ b/effects/swirl.py @@ -42,7 +42,7 @@ def buildGradient(cc, closeCircle = True): pos = 0 if len(cc[0]) == 4: withAlpha = True - + for c in cc: if withAlpha: alpha = int(c[3]*255) @@ -50,7 +50,7 @@ def buildGradient(cc, closeCircle = True): alpha = 255 pos += posfac ba += bytearray([pos,c[0],c[1],c[2],alpha]) - + if closeCircle: # last color as first color lC = cc[-1] @@ -61,6 +61,7 @@ def buildGradient(cc, closeCircle = True): ba += bytearray([0,lC[0],lC[1],lC[2],alpha]) return ba + return bytearray() def rotateAngle( increment = 1): global angle diff --git a/effects/trails.py b/effects/trails.py index 8d074dc2..3f8ba2e0 100644 --- a/effects/trails.py +++ b/effects/trails.py @@ -1,98 +1,98 @@ -import hyperion -import time -import colorsys -import random - -min_len = int(hyperion.args.get('min_len', 3)) -max_len = int(hyperion.args.get('max_len', 3)) -#iHeight = int(hyperion.args.get('iHeight', 8)) -trails = int(hyperion.args.get('int', 8)) -sleepTime = float(hyperion.args.get('speed', 1)) / 1000.0 -color = list(hyperion.args.get('color', (255,255,255))) -randomise = bool(hyperion.args.get('random', False)) -iWidth = hyperion.imageWidth() -iHeight = hyperion.imageHeight() - -class trail: - def __init__(self): - return - - def start(self, x, y, step, color, _len, _h): - self.pos = 0.0 - self.step = step - self.h = _h - self.x = x - self.data = [] - brigtness = color[2] - step_brigtness = color[2] / _len - for i in range(0, _len): - rgb = colorsys.hsv_to_rgb(color[0], color[1], brigtness) - self.data.insert(0, (int(255*rgb[0]), int(255*rgb[1]), int(255*rgb[2]))) - brigtness -= step_brigtness - - self.data.extend([(0,0,0)]*(_h-y)) - if len(self.data) < _h: - for i in range (_h-len(self.data)): - self.data.insert(0, (0,0,0)) - - def getdata(self): - self.pos += self.step - if self.pos > 1.0: - self.pos = 0.0 - self.data.pop() - self.data.insert(0, (0,0,0)) - return self.x, self.data[-self.h:], all(x == self.data[0] for x in self.data) - -tr = [] - -for i in range(trails): - r = {'exec': trail()} - - if randomise: - col = (random.uniform(0.0, 1.0),1,1) - else: - col = colorsys.rgb_to_hsv(color[0]/255.0, color[1]/255.0, color[2]/255.0) - - r['exec'].start( - random.randint(0, iWidth), - random.randint(0, iHeight), - random.uniform(0.2, 0.8), - col, - random.randint(min_len, max_len), - iHeight - ) - tr.append(r) - -# Start the write data loop -while not hyperion.abort(): - ledData = bytearray() - - for r in tr: - r['x'], r['data'], c = r['exec'].getdata() - if c: - if randomise: - col = (random.uniform(0.0, 1.0),1,1) - else: - col = colorsys.rgb_to_hsv(color[0]/255.0, color[1]/255.0, color[2]/255.0) - - r['exec'].start( - random.randint(0, iWidth), - random.randint(0, iHeight), - random.uniform(0.2, 0.8), - col, - random.randint(min_len, max_len), - iHeight - ) - - for y in range(0, iHeight): - for x in range(0, iWidth): - for r in tr: - if x == r['x']: - led = bytearray(r['data'][y]) - break - led = bytearray((0,0,0)) - ledData += led - - hyperion.setImage(iWidth,iHeight,ledData) - time.sleep(sleepTime) - +import hyperion +import time +import colorsys +import random + +min_len = int(hyperion.args.get('min_len', 3)) +max_len = int(hyperion.args.get('max_len', 3)) +#iHeight = int(hyperion.args.get('iHeight', 8)) +trails = int(hyperion.args.get('int', 8)) +sleepTime = float(hyperion.args.get('speed', 1)) / 1000.0 +color = list(hyperion.args.get('color', (255,255,255))) +randomise = bool(hyperion.args.get('random', False)) +iWidth = hyperion.imageWidth() +iHeight = hyperion.imageHeight() + +class trail: + def __init__(self): + return + + def start(self, x, y, step, color, _len, _h): + self.pos = 0.0 + self.step = step + self.h = _h + self.x = x + self.data = [] + brigtness = color[2] + step_brigtness = color[2] / _len + for i in range(0, _len): + rgb = colorsys.hsv_to_rgb(color[0], color[1], brigtness) + self.data.insert(0, (int(255*rgb[0]), int(255*rgb[1]), int(255*rgb[2]))) + brigtness -= step_brigtness + + self.data.extend([(0,0,0)]*(_h-y)) + if len(self.data) < _h: + for i in range (_h-len(self.data)): + self.data.insert(0, (0,0,0)) + + def getdata(self): + self.pos += self.step + if self.pos > 1.0: + self.pos = 0.0 + self.data.pop() + self.data.insert(0, (0,0,0)) + return self.x, self.data[-self.h:], all(x == self.data[0] for x in self.data) + +tr = [] + +for unused in range(trails): + r = {'exec': trail()} + + if randomise: + col = (random.uniform(0.0, 1.0),1,1) + else: + col = colorsys.rgb_to_hsv(color[0]/255.0, color[1]/255.0, color[2]/255.0) + + r['exec'].start( + random.randint(0, iWidth), + random.randint(0, iHeight), + random.uniform(0.2, 0.8), + col, + random.randint(min_len, max_len), + iHeight + ) + tr.append(r) + +# Start the write data loop +while not hyperion.abort(): + ledData = bytearray() + + for r in tr: + r['x'], r['data'], c = r['exec'].getdata() + if c: + if randomise: + col = (random.uniform(0.0, 1.0),1,1) + else: + col = colorsys.rgb_to_hsv(color[0]/255.0, color[1]/255.0, color[2]/255.0) + + r['exec'].start( + random.randint(0, iWidth), + random.randint(0, iHeight), + random.uniform(0.2, 0.8), + col, + random.randint(min_len, max_len), + iHeight + ) + + for y in range(0, iHeight): + for x in range(0, iWidth): + for r in tr: + if x == r['x']: + led = bytearray(r['data'][y]) + break + led = bytearray((0,0,0)) + ledData += led + + hyperion.setImage(iWidth,iHeight,ledData) + time.sleep(sleepTime) + diff --git a/effects/waves.py b/effects/waves.py index 05299dcc..f5add15f 100644 --- a/effects/waves.py +++ b/effects/waves.py @@ -1,77 +1,71 @@ -import hyperion, time, math, random - -randomCenter = bool(hyperion.args.get('random-center', False)) -centerX = float(hyperion.args.get('center_x', -0.15)) -centerY = float(hyperion.args.get('center_y', -0.25)) -rotationTime = float(hyperion.args.get('rotation_time', 90)) -colors = hyperion.args.get('colors', ((255,0,0),(255,255,0),(0,255,0),(0,255,255),(0,0,255),(255,0,255))) -reverse = bool(hyperion.args.get('reverse', False)) -reverseTime = int(hyperion.args.get('reverse_time', 0)) -#rotate = bool(hyperion.args.get('rotate', True)) -positions = [] - -# calc center if random -if randomCenter: - centerX = random.uniform(0.0, 1.0) - centerY = random.uniform(0.0, 1.0) - -rCenterX = int(round(float(hyperion.imageWidth())*centerX)) -rCenterY = int(round(float(hyperion.imageHeight())*centerY)) - -#calc interval -sleepTime = max(1/(255/rotationTime), 0.016) - -#calc diagonal -if centerX < 0.5: - cX = 1.0-centerX -else: - cX = 0.0+centerX - -if centerY < 0.5: - cY = 1.0-centerY -else: - cY = 0.0+centerY - -diag = int(round(math.sqrt(((cX*hyperion.imageWidth())**2)+((cY*hyperion.imageHeight())**2)))) -# some diagonal overhead -diag = int(diag*1.3) - -# calc positions -pos = 0 -step = int(255/len(colors)) -for _ in colors: - positions.append(pos) - pos += step - -# target time -targetTime = time.time()+float(reverseTime) - -#hyperion.imageCOffset(int(hyperion.imageWidth()/2), int(hyperion.imageHeight()/2)) - -while not hyperion.abort(): - # verify reverseTime, randomize reverseTime based on reverseTime up to reversedTime*2 - if reverseTime >= 1: - now = time.time() - if now > targetTime: - reverse = not reverse - targetTime = time.time()+random.uniform(float(reverseTime), float(reverseTime*2.0)) - # apply rotate - #if rotate: - # hyperion.imageCRotate(1) - # prepare bytearray with colors and positions - gradientBa = bytearray() - it = 0 - for color in colors: - gradientBa += bytearray((positions[it],color[0],color[1],color[2])) - it += 1 - - hyperion.imageRadialGradient(rCenterX,rCenterY, diag, gradientBa,0) - - # increment positions - for i, pos in enumerate(positions): - if reverse: - positions[i] = pos - 1 if pos >= 1 else 255 - else: - positions[i] = pos + 1 if pos <= 254 else 0 - hyperion.imageShow() - time.sleep(sleepTime) +import hyperion, time, math, random + +randomCenter = bool(hyperion.args.get('random-center', False)) +centerX = float(hyperion.args.get('center_x', -0.15)) +centerY = float(hyperion.args.get('center_y', -0.25)) +rotationTime = float(hyperion.args.get('rotation_time', 90)) +colors = hyperion.args.get('colors', ((255,0,0),(255,255,0),(0,255,0),(0,255,255),(0,0,255),(255,0,255))) +reverse = bool(hyperion.args.get('reverse', False)) +reverseTime = int(hyperion.args.get('reverse_time', 0)) +positions = [] + +# calc center if random +if randomCenter: + centerX = random.uniform(0.0, 1.0) + centerY = random.uniform(0.0, 1.0) + +rCenterX = int(round(float(hyperion.imageWidth())*centerX)) +rCenterY = int(round(float(hyperion.imageHeight())*centerY)) + +#calc interval +sleepTime = max(1/(255/rotationTime), 0.016) + +#calc diagonal +if centerX < 0.5: + cX = 1.0-centerX +else: + cX = 0.0+centerX + +if centerY < 0.5: + cY = 1.0-centerY +else: + cY = 0.0+centerY + +diag = int(round(math.hypot(cX*hyperion.imageWidth(),cY*hyperion.imageHeight()))) +# some diagonal overhead +diag = int(diag*1.3) + +# calc positions +pos = 0 +step = int(255/len(colors)) +for _ in colors: + positions.append(pos) + pos += step + +# target time +targetTime = time.time()+float(reverseTime) +while not hyperion.abort(): + # verify reverseTime, randomize reverseTime based on reverseTime up to reversedTime*2 + if reverseTime >= 1: + now = time.time() + if now > targetTime: + reverse = not reverse + targetTime = time.time()+random.uniform(float(reverseTime), float(reverseTime*2.0)) + + # prepare bytearray with colors and positions + gradientBa = bytearray() + it = 0 + for color in colors: + gradientBa += bytearray((positions[it],color[0],color[1],color[2])) + it += 1 + + hyperion.imageRadialGradient(rCenterX,rCenterY, diag, gradientBa,0) + + # increment positions + for i, pos in enumerate(positions): + if reverse: + positions[i] = pos - 1 if pos >= 1 else 255 + else: + positions[i] = pos + 1 if pos <= 254 else 0 + hyperion.imageShow() + time.sleep(sleepTime) diff --git a/effects/x-mas.py b/effects/x-mas.py index 4ccba418..b0bf706e 100644 --- a/effects/x-mas.py +++ b/effects/x-mas.py @@ -1,30 +1,30 @@ -import hyperion, time - -# Get the parameters -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)) - -# Initialize the led data -i = 0 -ledDataOdd = bytearray() -while i < hyperion.ledCount: - for l in range(length): - if i getAllComponents(); - - /// + /// /// @brief Check if Hyperion ist enabled /// @return True when enabled else false /// diff --git a/include/blackborder/BlackBorderDetector.h b/include/blackborder/BlackBorderDetector.h index 21f1444a..77438f4c 100644 --- a/include/blackborder/BlackBorderDetector.h +++ b/include/blackborder/BlackBorderDetector.h @@ -1,4 +1,3 @@ -//#include #pragma once // Utils includes @@ -219,7 +218,6 @@ namespace hyperion || !isBlack(image((width - x), y)) || !isBlack(image((width - x), (height - y)))) { -// std::cout << "y " << y << " lt " << int(isBlack(color1)) << " lb " << int(isBlack(color2)) << " rt " << int(isBlack(color3)) << " rb " << int(isBlack(color4)) << std::endl; firstNonBlackYPixelIndex = y; break; } diff --git a/include/mdns/MdnsBrowser.h b/include/mdns/MdnsBrowser.h index 0ac35a4e..b88a7f11 100644 --- a/include/mdns/MdnsBrowser.h +++ b/include/mdns/MdnsBrowser.h @@ -104,24 +104,6 @@ private slots: private: - // template ::value, int> = 0> - // static inline QMetaObject::Connection weakConnect(typename QtPrivate::FunctionPointer::Object* sender, - // Func1 signal, - // typename QtPrivate::FunctionPointer::Object* receiver, - // Func2 slot) - // { - // QMetaObject::Connection conn_normal = QObject::connect(sender, signal, receiver, slot); - - // QMetaObject::Connection* conn_delete = new QMetaObject::Connection(); - - // *conn_delete = QObject::connect(sender, signal, [conn_normal, conn_delete]() { - // QObject::disconnect(conn_normal); - // QObject::disconnect(*conn_delete); - // delete conn_delete; - // }); - // return conn_normal; - // } - template ::value, int> = 0> static inline QMetaObject::Connection weakConnect(typename QtPrivate::FunctionPointer::Object* sender, Func1 signal, @@ -139,25 +121,6 @@ private: return conn_normal; } - // template ::value, int> = 0> - // static inline QMetaObject::Connection weakConnect(typename QtPrivate::FunctionPointer::Object* sender, - // Func1 signal, - // typename QtPrivate::FunctionPointer::Object* receiver, - // Func2 slot) - // { - // Q_UNUSED(receiver); - // QMetaObject::Connection conn_normal = QObject::connect(sender, signal, slot); - - // QMetaObject::Connection* conn_delete = new QMetaObject::Connection(); - - // *conn_delete = QObject::connect(sender, signal, [conn_normal, conn_delete]() { - // QObject::disconnect(conn_normal); - // QObject::disconnect(*conn_delete); - // delete conn_delete; - // }); - // return conn_normal; - // } - /// The logger instance for mDNS-Service Logger* _log; diff --git a/include/utils/NetUtils.h b/include/utils/NetUtils.h index 856062a1..3b247e49 100644 --- a/include/utils/NetUtils.h +++ b/include/utils/NetUtils.h @@ -122,7 +122,7 @@ namespace NetUtils { { if (hostAddress.setAddress(hostname)) { - //Debug(log, "IP-address (%s) not required to be resolved.", QSTRING_CSTR(hostAddress.toString())); + // An IP-address is not required to be resolved isHostAddressOK = true; } else diff --git a/include/utils/RgbTransform.h b/include/utils/RgbTransform.h index d4e4bfd1..ec493a2d 100644 --- a/include/utils/RgbTransform.h +++ b/include/utils/RgbTransform.h @@ -44,7 +44,7 @@ public: int getBacklightThreshold() const; /// @param backlightThreshold New lower brightness - void setBacklightThreshold(int backlightThreshold); + void setBacklightThreshold(double backlightThreshold); /// @return The current state bool getBacklightColored() const; diff --git a/include/utils/hyperion.h b/include/utils/hyperion.h index 5349d38d..2a71c3ce 100644 --- a/include/utils/hyperion.h +++ b/include/utils/hyperion.h @@ -140,13 +140,12 @@ namespace hyperion { { // Special case for indices '*' => all leds adjustment->setAdjustmentForLed(colorAdjustment->_id, 0, ledCnt-1); - //Info(Logger::getInstance("HYPERION"), "ColorAdjustment '%s' => [0-%d]", QSTRING_CSTR(colorAdjustment->_id), ledCnt-1); continue; } if (!overallExp.match(ledIndicesStr).hasMatch()) { - //Error(Logger::getInstance("HYPERION"), "Given led indices %d not correct format: %s", i, QSTRING_CSTR(ledIndicesStr)); + // Given LED indices are not correctly formatted continue; } @@ -173,7 +172,6 @@ namespace hyperion { ss << index; } } - //Info(Logger::getInstance("HYPERION"), "ColorAdjustment '%s' => [%s]", QSTRING_CSTR(colorAdjustment->_id), ss.str().c_str()); } return adjustment; diff --git a/include/utils/jsonschema/QJsonUtils.h b/include/utils/jsonschema/QJsonUtils.h index 8626ae98..63c22d34 100644 --- a/include/utils/jsonschema/QJsonUtils.h +++ b/include/utils/jsonschema/QJsonUtils.h @@ -174,9 +174,9 @@ private: if (!path.isEmpty()) { - QJsonObject obj; - modifyValue(subValue, obj, path, newValue, property); - subValue = obj; + QJsonObject tempObj; + modifyValue(subValue, tempObj, path, newValue, property); + subValue = tempObj; } else if (newValue != QJsonValue::Null) subValue = newValue; diff --git a/libsrc/api/API.cpp b/libsrc/api/API.cpp index eaf4c04d..f255a8ea 100644 --- a/libsrc/api/API.cpp +++ b/libsrc/api/API.cpp @@ -294,13 +294,6 @@ bool API::setHyperionInstance(quint8 inst) return true; } -std::map API::getAllComponents() -{ - std::map comps; - //QMetaObject::invokeMethod(_hyperion, "getAllComponents", Qt::BlockingQueuedConnection, Q_RETURN_ARG(std::map, comps)); - return comps; -} - bool API::isHyperionEnabled() { int res; diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 60a832b4..bfa76b0a 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -144,7 +144,6 @@ void JsonAPI::handleMessage(const QString &messageString, const QString &httpAut { const QString ident = "JsonRpc@" + _peerAddress; QJsonObject message; - //std::cout << "JsonAPI::handleMessage | [" << static_cast(_hyperion->getInstanceIndex()) << "] Received: ["<< messageString.toStdString() << "]" << std::endl; // parse the message if (!JsonUtils::parse(ident, messageString, message, _log)) @@ -553,27 +552,6 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString info["ledDevices"] = ledDevices; QJsonObject grabbers; - - // *** Deprecated *** - //QJsonArray availableGrabbers; - //if ( GrabberWrapper::getInstance() != nullptr ) - //{ - // QStringList activeGrabbers = GrabberWrapper::getInstance()->getActive(_hyperion->getInstanceIndex()); - // QJsonArray activeGrabberNames; - // for (auto grabberName : activeGrabbers) - // { - // activeGrabberNames.append(grabberName); - // } - - // grabbers["active"] = activeGrabberNames; - //} - //for (auto grabber : GrabberWrapper::availableGrabbers(GrabberTypeFilter::ALL)) - //{ - // availableGrabbers.append(grabber); - //} - - //grabbers["available"] = availableGrabbers; - QJsonObject screenGrabbers; if (GrabberWrapper::getInstance() != nullptr) { @@ -687,7 +665,6 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString QJsonObject obj; obj.insert("friendly_name", entry["friendly_name"].toString()); obj.insert("instance", entry["instance"].toInt()); - //obj.insert("last_use", entry["last_use"].toString()); obj.insert("running", entry["running"].toBool()); instanceInfo.append(obj); } @@ -696,7 +673,7 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString // add leds configs info["leds"] = _hyperion->getSetting(settings::LEDS).array(); - // BEGIN | The following entries are derecated but used to ensure backward compatibility with hyperion Classic remote control + // BEGIN | The following entries are deprecated but used to ensure backward compatibility with hyperion Classic remote control // TODO Output the real transformation information instead of default // HOST NAME @@ -757,7 +734,6 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString const Hyperion::InputInfo &priorityInfo = _hyperion->getPriorityInfo(_hyperion->getCurrentPriority()); if (priorityInfo.componentId == hyperion::COMP_COLOR && !priorityInfo.ledColors.empty()) { - QJsonObject LEDcolor; // check if LED Color not Black (0,0,0) if ((priorityInfo.ledColors.begin()->red + priorityInfo.ledColors.begin()->green + @@ -1309,8 +1285,8 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject &message, const QString & // use comment // for user authorized sessions AuthManager::AuthDefinition def; - const QString res = API::createToken(comment, def); - if (res.isEmpty()) + const QString createTokenResult = API::createToken(comment, def); + if (createTokenResult.isEmpty()) { QJsonObject newTok; newTok["comment"] = def.comment; @@ -1320,7 +1296,7 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject &message, const QString & sendSuccessDataReply(QJsonDocument(newTok), command + "-" + subc, tan); return; } - sendErrorReply(res, command + "-" + subc, tan); + sendErrorReply(createTokenResult, command + "-" + subc, tan); return; } @@ -1328,13 +1304,13 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject &message, const QString & if (subc == "renameToken") { // use id/comment - const QString res = API::renameToken(id, comment); - if (res.isEmpty()) + const QString renameTokenResult = API::renameToken(id, comment); + if (renameTokenResult.isEmpty()) { sendSuccessReply(command + "-" + subc, tan); return; } - sendErrorReply(res, command + "-" + subc, tan); + sendErrorReply(renameTokenResult, command + "-" + subc, tan); return; } @@ -1342,13 +1318,13 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject &message, const QString & if (subc == "deleteToken") { // use id - const QString res = API::deleteToken(id); - if (res.isEmpty()) + const QString deleteTokenResult = API::deleteToken(id); + if (deleteTokenResult.isEmpty()) { sendSuccessReply(command + "-" + subc, tan); return; } - sendErrorReply(res, command + "-" + subc, tan); + sendErrorReply(deleteTokenResult, command + "-" + subc, tan); return; } @@ -1356,7 +1332,6 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject &message, const QString & if (subc == "requestToken") { // use id/comment - const QString &comment = message["comment"].toString().trimmed(); const bool &acc = message["accept"].toBool(true); if (acc) API::setNewTokenRequest(comment, id, tan); @@ -1373,7 +1348,7 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject &message, const QString & if (API::getPendingTokenRequests(vec)) { QJsonArray arr; - for (const auto &entry : vec) + for (const auto &entry : qAsConst(vec)) { QJsonObject obj; obj["comment"] = entry.comment; @@ -1556,12 +1531,8 @@ void JsonAPI::handleLedDeviceCommand(const QJsonObject &message, const QString & QString full_command = command + "-" + subc; // TODO: Validate that device type is a valid one -/* if ( ! valid type ) + { - sendErrorReply("Unknown device", full_command, tan); - } - else -*/ { QJsonObject config; config.insert("type", devType); LedDevice* ledDevice = nullptr; @@ -1623,12 +1594,7 @@ void JsonAPI::handleInputSourceCommand(const QJsonObject& message, const QString QString full_command = command + "-" + subc; // TODO: Validate that source type is a valid one -/* if ( ! valid type ) { - sendErrorReply("Unknown device", full_command, tan); - } - else -*/ { if (subc == "discover") { QJsonObject inputSourcesDiscovered; @@ -2007,6 +1973,11 @@ void JsonAPI::handleInstanceStateChange(InstanceState state, quint8 instance, co handleInstanceSwitch(); } break; + + case InstanceState::H_STARTED: + case InstanceState::H_STOPPED: + case InstanceState::H_CREATED: + case InstanceState::H_DELETED: default: break; } diff --git a/libsrc/api/JsonCB.cpp b/libsrc/api/JsonCB.cpp index 5c35b77b..965abf37 100644 --- a/libsrc/api/JsonCB.cpp +++ b/libsrc/api/JsonCB.cpp @@ -148,7 +148,6 @@ void JsonCB::resetSubscriptions() void JsonCB::setSubscriptionsTo(Hyperion* hyperion) { assert(hyperion); - //std::cout << "JsonCB::setSubscriptions for instance [" << static_cast(hyperion->getInstanceIndex()) << "] " << std::endl; // get current subs QStringList currSubs(getSubscribedCommands()); @@ -179,8 +178,6 @@ void JsonCB::doCallback(const QString& cmd, const QVariant& data) else obj["data"] = data.toJsonObject(); - //std::cout << "JsonCB::doCallback | [" << static_cast(_hyperion->getInstanceIndex()) << "] Send: [" << QJsonDocument(obj).toJson(QJsonDocument::Compact).toStdString() << "]" << std::endl; - emit newCallback(obj); } @@ -398,7 +395,6 @@ void JsonCB::handleInstanceChange() QJsonObject obj; obj.insert("friendly_name", entry["friendly_name"].toString()); obj.insert("instance", entry["instance"].toInt()); - //obj.insert("last_use", entry["last_use"].toString()); obj.insert("running", entry["running"].toBool()); arr.append(obj); } diff --git a/libsrc/blackborder/BlackBorderDetector.cpp b/libsrc/blackborder/BlackBorderDetector.cpp index 67df1b16..aea8fc60 100644 --- a/libsrc/blackborder/BlackBorderDetector.cpp +++ b/libsrc/blackborder/BlackBorderDetector.cpp @@ -23,7 +23,5 @@ uint8_t BlackBorderDetector::calculateThreshold(double threshold) const uint8_t blackborderThreshold = uint8_t(rgbThreshold); - //Debug(Logger::getInstance("BLACKBORDER"), "threshold set to %f (%d)", threshold , int(blackborderThreshold)); - return blackborderThreshold; } diff --git a/libsrc/blackborder/BlackBorderProcessor.cpp b/libsrc/blackborder/BlackBorderProcessor.cpp index eadc56c2..538ee534 100644 --- a/libsrc/blackborder/BlackBorderProcessor.cpp +++ b/libsrc/blackborder/BlackBorderProcessor.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -33,6 +34,8 @@ BlackBorderProcessor::BlackBorderProcessor(Hyperion* hyperion, QObject* parent) // listen for component state changes connect(_hyperion, &Hyperion::compStateChangeRequest, this, &BlackBorderProcessor::handleCompStateChangeRequest); + + _detector = new BlackBorderDetector(_oldThreshold); } BlackBorderProcessor::~BlackBorderProcessor() @@ -60,7 +63,7 @@ void BlackBorderProcessor::handleSettingsUpdate(settings::type type, const QJson _detectionMode = obj["mode"].toString("default"); const double newThreshold = obj["threshold"].toDouble(5.0) / 100.0; - if (_oldThreshold != newThreshold) + if (fabs(_oldThreshold - newThreshold) > std::numeric_limits::epsilon()) { _oldThreshold = newThreshold; @@ -140,8 +143,6 @@ bool BlackBorderProcessor::updateBorder(const BlackBorder & newDetectedBorder) // makes it look like the border detectionn is not working - since the new 3 line detection algorithm is more precise this became a problem specialy in dark scenes // wisc -// std::cout << "c: " << setw(2) << _currentBorder.verticalSize << " " << setw(2) << _currentBorder.horizontalSize << " p: " << setw(2) << _previousDetectedBorder.verticalSize << " " << setw(2) << _previousDetectedBorder.horizontalSize << " n: " << setw(2) << newDetectedBorder.verticalSize << " " << setw(2) << newDetectedBorder.horizontalSize << " c:i " << setw(2) << _consistentCnt << ":" << setw(2) << _inconsistentCnt << std::endl; - // set the consistency counter if (newDetectedBorder == _previousDetectedBorder) { diff --git a/libsrc/boblightserver/BoblightClientConnection.cpp b/libsrc/boblightserver/BoblightClientConnection.cpp index 4610ae91..242a2bb8 100644 --- a/libsrc/boblightserver/BoblightClientConnection.cpp +++ b/libsrc/boblightserver/BoblightClientConnection.cpp @@ -106,8 +106,6 @@ QString BoblightClientConnection::readMessage(const char* data, const size_t siz const int len = end - data + 1; const QString message = QString::fromLatin1(data, len); - //std::cout << bytes << ": \"" << message.toUtf8().constData() << "\"" << std::endl; - return message; } @@ -124,7 +122,6 @@ void BoblightClientConnection::socketClosed() void BoblightClientConnection::handleMessage(const QString& message) { - //std::cout << "boblight message: " << message.toStdString() << std::endl; QStringList messageParts = QStringUtils::split(message, ' ', QStringUtils::SplitBehavior::SkipEmptyParts); if (!messageParts.isEmpty()) { @@ -340,7 +337,6 @@ float BoblightClientConnection::parseFloat(const QString& s, bool *ok) const { if (ok) { - //std::cout << "FAIL L " << q << ": " << s.toUtf8().constData() << std::endl; *ok = false; } return 0; @@ -348,7 +344,6 @@ float BoblightClientConnection::parseFloat(const QString& s, bool *ok) const if (ok) { - //std::cout << "OK " << d << ": " << s.toUtf8().constData() << std::endl; *ok = true; } diff --git a/libsrc/cec/CECHandler.cpp b/libsrc/cec/CECHandler.cpp index f33c30c8..630fe334 100644 --- a/libsrc/cec/CECHandler.cpp +++ b/libsrc/cec/CECHandler.cpp @@ -12,7 +12,7 @@ #include /* Enable to turn on detailed CEC logs */ -// #define VERBOSE_CEC +#define NO_VERBOSE_CEC CECHandler::CECHandler() { @@ -138,9 +138,9 @@ bool CECHandler::openAdapter(const CECAdapterDescriptor & descriptor) if(!_cecAdapter->Open(descriptor.strComName)) { - Error(_logger, QString("Failed to open the CEC adaper on port %1") - .arg(descriptor.strComName) - .toLocal8Bit()); + Error(_logger, "%s", QSTRING_CSTR(QString("Failed to open the CEC adaper on port %1") + .arg(descriptor.strComName)) + ); return false; } @@ -149,9 +149,9 @@ bool CECHandler::openAdapter(const CECAdapterDescriptor & descriptor) void CECHandler::printAdapter(const CECAdapterDescriptor & descriptor) const { - Info(_logger, QString("CEC Adapter:").toLocal8Bit()); - Info(_logger, QString("\tName : %1").arg(descriptor.strComName).toLocal8Bit()); - Info(_logger, QString("\tPath : %1").arg(descriptor.strComPath).toLocal8Bit()); + Info(_logger, "%s", QSTRING_CSTR(QString("CEC Adapter:"))); + Info(_logger, "%s", QSTRING_CSTR(QString("\tName : %1").arg(descriptor.strComName))); + Info(_logger, "%s", QSTRING_CSTR(QString("\tPath : %1").arg(descriptor.strComPath))); } QString CECHandler::scan() const @@ -180,12 +180,12 @@ QString CECHandler::scan() const devices << device; - Info(_logger, QString("\tCECDevice: %1 / %2 / %3 / %4") - .arg(device["name"].toString()) - .arg(device["vendor"].toString()) - .arg(device["address"].toString()) - .arg(device["power"].toString()) - .toLocal8Bit()); + Info(_logger, "%s", QSTRING_CSTR(QString("\tCECDevice: %1 / %2 / %3 / %4") + .arg(device["name"].toString(), + device["vendor"].toString(), + device["address"].toString(), + device["power"].toString())) + ); } } @@ -305,16 +305,16 @@ void CECHandler::onCecCommandReceived(void * context, const CECCommand * command { if (command->opcode == CEC::CEC_OPCODE_SET_STREAM_PATH) { - Info(handler->_logger, QString("CEC source activated: %1") - .arg(adapter->ToString(command->initiator)) - .toLocal8Bit()); + Info(handler->_logger, "%s", QSTRING_CSTR(QString("CEC source activated: %1") + .arg(adapter->ToString(command->initiator))) + ); emit handler->cecEvent(CECEvent::On); } if (command->opcode == CEC::CEC_OPCODE_STANDBY) { - Info(handler->_logger, QString("CEC source deactivated: %1") - .arg(adapter->ToString(command->initiator)) - .toLocal8Bit()); + Info(handler->_logger, "%s", QSTRING_CSTR(QString("CEC source deactivated: %1") + .arg(adapter->ToString(command->initiator))) + ); emit handler->cecEvent(CECEvent::Off); } } diff --git a/libsrc/db/DBManager.cpp b/libsrc/db/DBManager.cpp index c2f42deb..a8711c15 100644 --- a/libsrc/db/DBManager.cpp +++ b/libsrc/db/DBManager.cpp @@ -50,7 +50,7 @@ QSqlDatabase DBManager::getDB() const db.setDatabaseName(_rootPath+"/db/"+_dbn+".db"); if(!db.open()) { - Error(_log, QSTRING_CSTR(db.lastError().text())); + Error(_log, "%s", QSTRING_CSTR(db.lastError().text())); throw std::runtime_error("Failed to open database connection!"); } return db; diff --git a/libsrc/effectengine/EffectEngine.cpp b/libsrc/effectengine/EffectEngine.cpp index a2e69918..f192df99 100644 --- a/libsrc/effectengine/EffectEngine.cpp +++ b/libsrc/effectengine/EffectEngine.cpp @@ -126,12 +126,10 @@ void EffectEngine::handleUpdatedEffectList() def.args["smoothing-time_ms"].toInt(), def.args["smoothing-updateFrequency"].toDouble(), 0 ); - //Debug( _log, "Customs Settings: Update effect %s, script %s, file %s, smoothCfg [%u]", QSTRING_CSTR(def.name), QSTRING_CSTR(def.script), QSTRING_CSTR(def.file), def.smoothCfg); } else { def.smoothCfg = SmoothingConfigID::SYSTEM; - //Debug( _log, "Default Settings: Update effect %s, script %s, file %s, smoothCfg [%u]", QSTRING_CSTR(def.name), QSTRING_CSTR(def.script), QSTRING_CSTR(def.file), def.smoothCfg); } _availableEffects.push_back(def); } diff --git a/libsrc/effectengine/EffectModule.cpp b/libsrc/effectengine/EffectModule.cpp index 29531f05..bfa4a4c4 100644 --- a/libsrc/effectengine/EffectModule.cpp +++ b/libsrc/effectengine/EffectModule.cpp @@ -53,9 +53,12 @@ PyObject *EffectModule::json2python(const QJsonValue &jsonData) Py_RETURN_NOTIMPLEMENTED; case QJsonValue::Double: { - if (std::round(jsonData.toDouble()) != jsonData.toDouble()) + double doubleIntegratlPart; + double doubleFractionalPart = std::modf(jsonData.toDouble(), &doubleIntegratlPart); + if (doubleFractionalPart > std::numeric_limits::epsilon()) + { return Py_BuildValue("d", jsonData.toDouble()); - + } return Py_BuildValue("i", jsonData.toInt()); } case QJsonValue::Bool: @@ -184,7 +187,8 @@ PyObject* EffectModule::wrapSetColor(PyObject *self, PyObject *args) PyObject* EffectModule::wrapSetImage(PyObject *self, PyObject *args) { // bytearray of values - int width, height; + int width = 0; + int height = 0; PyObject * bytearray = nullptr; if (PyArg_ParseTuple(args, "iiO", &width, &height, &bytearray)) { @@ -391,8 +395,10 @@ PyObject* EffectModule::wrapImageLinearGradient(PyObject *self, PyObject *args) int startRY = 0; int startX = 0; int startY = 0; - int endX, width = getEffect()->_imageSize.width(); - int endY, height = getEffect()->_imageSize.height(); + int width = getEffect()->_imageSize.width(); + int endX {width}; + int height = getEffect()->_imageSize.height(); + int endY {height}; int spread = 0; bool argsOK = false; @@ -454,7 +460,9 @@ PyObject* EffectModule::wrapImageConicalGradient(PyObject *self, PyObject *args) { int argCount = PyTuple_Size(args); PyObject * bytearray = nullptr; - int centerX, centerY, angle; + int centerX = 0; + int centerY = 0; + int angle = 0; int startX = 0; int startY = 0; int width = getEffect()->_imageSize.width(); @@ -520,7 +528,13 @@ PyObject* EffectModule::wrapImageRadialGradient(PyObject *self, PyObject *args) { int argCount = PyTuple_Size(args); PyObject * bytearray = nullptr; - int centerX, centerY, radius, focalX, focalY, focalRadius, spread; + int centerX = 0; + int centerY = 0; + int radius = 0; + int focalX = 0; + int focalY = 0; + int focalRadius =0; + int spread = 0; int startX = 0; int startY = 0; int width = getEffect()->_imageSize.width(); @@ -599,7 +613,9 @@ PyObject* EffectModule::wrapImageDrawPolygon(PyObject *self, PyObject *args) PyObject * bytearray = nullptr; int argCount = PyTuple_Size(args); - int r, g, b; + int r = 0; + int g = 0; + int b = 0; int a = 255; bool argsOK = false; @@ -658,7 +674,9 @@ PyObject* EffectModule::wrapImageDrawPie(PyObject *self, PyObject *args) QString brush; int argCount = PyTuple_Size(args); - int radius, centerX, centerY; + int radius = 0; + int centerX = 0; + int centerY = 0; int startAngle = 0; int spanAngle = 360; int r = 0; @@ -749,7 +767,9 @@ PyObject* EffectModule::wrapImageDrawPie(PyObject *self, PyObject *args) PyObject* EffectModule::wrapImageSolidFill(PyObject *self, PyObject *args) { int argCount = PyTuple_Size(args); - int r, g, b; + int r = 0; + int g = 0; + int b = 0; int a = 255; int startX = 0; int startY = 0; @@ -788,8 +808,10 @@ PyObject* EffectModule::wrapImageSolidFill(PyObject *self, PyObject *args) PyObject* EffectModule::wrapImageDrawLine(PyObject *self, PyObject *args) { int argCount = PyTuple_Size(args); - int r, g, b; - int a = 255; + int r = 0; + int g = 0; + int b = 0; + int a = 255; int startX = 0; int startY = 0; int thick = 1; @@ -826,8 +848,12 @@ PyObject* EffectModule::wrapImageDrawLine(PyObject *self, PyObject *args) PyObject* EffectModule::wrapImageDrawPoint(PyObject *self, PyObject *args) { int argCount = PyTuple_Size(args); - int r, g, b, x, y; - int a = 255; + int r = 0; + int g = 0; + int b = 0; + int x = 0; + int y = 0; + int a = 255; int thick = 1; bool argsOK = false; @@ -859,8 +885,10 @@ PyObject* EffectModule::wrapImageDrawPoint(PyObject *self, PyObject *args) PyObject* EffectModule::wrapImageDrawRect(PyObject *self, PyObject *args) { int argCount = PyTuple_Size(args); - int r, g, b; - int a = 255; + int r = 0; + int g = 0; + int b = 0; + int a = 255; int startX = 0; int startY = 0; int thick = 1; @@ -898,7 +926,11 @@ PyObject* EffectModule::wrapImageDrawRect(PyObject *self, PyObject *args) PyObject* EffectModule::wrapImageSetPixel(PyObject *self, PyObject *args) { int argCount = PyTuple_Size(args); - int r, g, b, x, y; + int r = 0; + int g = 0; + int b = 0; + int x = 0; + int y = 0; if ( argCount == 5 && PyArg_ParseTuple(args, "iiiii", &x, &y, &r, &g, &b ) ) { @@ -913,7 +945,8 @@ PyObject* EffectModule::wrapImageSetPixel(PyObject *self, PyObject *args) PyObject* EffectModule::wrapImageGetPixel(PyObject *self, PyObject *args) { int argCount = PyTuple_Size(args); - int x, y; + int x = 0; + int y = 0; if ( argCount == 2 && PyArg_ParseTuple(args, "ii", &x, &y) ) { @@ -934,7 +967,8 @@ PyObject* EffectModule::wrapImageSave(PyObject *self, PyObject *args) PyObject* EffectModule::wrapImageMinSize(PyObject *self, PyObject *args) { int argCount = PyTuple_Size(args); - int w, h; + int w = 0; + int h = 0; int width = getEffect()->_imageSize.width(); int height = getEffect()->_imageSize.height(); @@ -994,7 +1028,8 @@ PyObject* EffectModule::wrapImageCOffset(PyObject *self, PyObject *args) PyObject* EffectModule::wrapImageCShear(PyObject *self, PyObject *args) { - int sh,sv; + int sh = 0; + int sv = 0; int argCount = PyTuple_Size(args); if ( argCount == 2 && PyArg_ParseTuple(args, "ii", &sh, &sv )) diff --git a/libsrc/forwarder/MessageForwarder.cpp b/libsrc/forwarder/MessageForwarder.cpp index 0070f2e4..e9dc40f9 100644 --- a/libsrc/forwarder/MessageForwarder.cpp +++ b/libsrc/forwarder/MessageForwarder.cpp @@ -138,7 +138,7 @@ void MessageForwarder::enableTargets(bool enable, const QJsonObject& config) else { _forwarder_enabled = false; - Warning(_log,"No JSON- nor Flatbuffer-Forwarder configured -> Forwarding disabled", _forwarder_enabled); + Warning(_log,"No JSON- nor Flatbuffer-Forwarder configured -> Forwarding disabled"); } } _hyperion->setNewComponentState(hyperion::COMP_FORWARDER, _forwarder_enabled); diff --git a/libsrc/grabber/video/EncoderThread.cpp b/libsrc/grabber/video/EncoderThread.cpp index b2ece680..1de36ff7 100644 --- a/libsrc/grabber/video/EncoderThread.cpp +++ b/libsrc/grabber/video/EncoderThread.cpp @@ -223,13 +223,11 @@ void EncoderThread::processImageMjpeg() { _xform->options = TJXOPT_CROP; _xform->r = tjregion {_cropLeft,_cropTop,transformedWidth,transformedHeight}; - //qDebug() << "processImageMjpeg() | _doTransform - Image cropped: transformedWidth: " << transformedWidth << " transformedHeight: " << transformedHeight; } else { _xform->options = 0; _xform->r = tjregion {0,0,_width,_height}; - //qDebug() << "processImageMjpeg() | _doTransform - Image not cropped: _width: " << _width << " _height: " << _height; } _xform->options |= TJXOPT_TRIM; @@ -344,11 +342,9 @@ bool EncoderThread::onError(const QString context) const #if LIBJPEG_TURBO_VERSION_NUMBER > 2000000 if (tjGetErrorCode(_tjInstance) == TJERR_FATAL) { - //qDebug() << context << "Error: " << QString(tjGetErrorStr2(_tjInstance)); treatAsError = true; } #else - //qDebug() << context << "Error: " << QString(tjGetErrorStr()); treatAsError = true; #endif diff --git a/libsrc/grabber/video/v4l2/V4L2Grabber.cpp b/libsrc/grabber/video/v4l2/V4L2Grabber.cpp index 4ae3b9f2..a0caa19d 100644 --- a/libsrc/grabber/video/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/video/v4l2/V4L2Grabber.cpp @@ -1209,7 +1209,7 @@ void V4L2Grabber::setCecDetectionEnable(bool enable) { _cecDetectionEnabled = enable; if(_initialized) - Info(_log, QString("CEC detection is now %1").arg(enable ? "enabled" : "disabled").toLocal8Bit()); + Info(_log, "%s", QSTRING_CSTR(QString("CEC detection is now %1").arg(enable ? "enabled" : "disabled"))); } } @@ -1501,19 +1501,19 @@ void V4L2Grabber::enumVideoCaptureDevices() // Enumerate video control IDs QList deviceControlList; - for (auto it = _controlIDPropertyMap->constBegin(); it != _controlIDPropertyMap->constEnd(); it++) + for (auto itDeviceControls = _controlIDPropertyMap->constBegin(); itDeviceControls != _controlIDPropertyMap->constEnd(); itDeviceControls++) { struct v4l2_queryctrl queryctrl; CLEAR(queryctrl); - queryctrl.id = it.key(); + queryctrl.id = itDeviceControls.key(); if (xioctl(fd, VIDIOC_QUERYCTRL, &queryctrl) < 0) break; if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) break; DeviceControls control; - control.property = it.value(); + control.property = itDeviceControls.value(); control.minValue = queryctrl.minimum; control.maxValue = queryctrl.maximum; control.step = queryctrl.step; @@ -1524,13 +1524,13 @@ void V4L2Grabber::enumVideoCaptureDevices() CLEAR(ctrl); CLEAR(ctrls); - ctrl.id = it.key(); + ctrl.id = itDeviceControls.key(); ctrls.count = 1; ctrls.controls = &ctrl; if (xioctl(fd, VIDIOC_G_EXT_CTRLS, &ctrls) == 0) { control.currentValue = ctrl.value; - DebugIf(verbose, _log, "%s: min=%i, max=%i, step=%i, default=%i, current=%i", QSTRING_CSTR(it.value()), control.minValue, control.maxValue, control.step, control.defaultValue, control.currentValue); + DebugIf(verbose, _log, "%s: min=%i, max=%i, step=%i, default=%i, current=%i", QSTRING_CSTR(itDeviceControls.value()), control.minValue, control.maxValue, control.step, control.defaultValue, control.currentValue); } else break; diff --git a/libsrc/grabber/x11/X11Grabber.cpp b/libsrc/grabber/x11/X11Grabber.cpp index bb016480..46c7faa4 100644 --- a/libsrc/grabber/x11/X11Grabber.cpp +++ b/libsrc/grabber/x11/X11Grabber.cpp @@ -170,12 +170,12 @@ bool X11Grabber::setupDisplay() XShmQueryVersion(_x11Display, &dummy, &dummy, &pixmaps_supported); _XShmPixmapAvailable = pixmaps_supported && XShmPixmapFormat(_x11Display) == ZPixmap; - Info(_log, QString("XRandR=[%1] XRender=[%2] XShm=[%3] XPixmap=[%4]") - .arg(_XRandRAvailable ? "available" : "unavailable") - .arg(_XRenderAvailable ? "available" : "unavailable") - .arg(_XShmAvailable ? "available" : "unavailable") - .arg(_XShmPixmapAvailable ? "available" : "unavailable") - .toStdString().c_str()); + Info(_log, "%s", QSTRING_CSTR(QString("XRandR=[%1] XRender=[%2] XShm=[%3] XPixmap=[%4]") + .arg(_XRandRAvailable ? "available" : "unavailable", + _XRenderAvailable ? "available" : "unavailable", + _XShmAvailable ? "available" : "unavailable", + _XShmPixmapAvailable ? "available" : "unavailable")) + ); result = (updateScreenDimensions(true) >=0); ErrorIf(!result, _log, "X11 Grabber start failed"); diff --git a/libsrc/grabber/xcb/XcbGrabber.cpp b/libsrc/grabber/xcb/XcbGrabber.cpp index 5e791fea..a5fc97f9 100644 --- a/libsrc/grabber/xcb/XcbGrabber.cpp +++ b/libsrc/grabber/xcb/XcbGrabber.cpp @@ -242,12 +242,12 @@ bool XcbGrabber::setupDisplay() setupRender(); setupShm(); - Info(_log, QString("XcbRandR=[%1] XcbRender=[%2] XcbShm=[%3] XcbPixmap=[%4]") - .arg(_XcbRandRAvailable ? "available" : "unavailable") - .arg(_XcbRenderAvailable ? "available" : "unavailable") - .arg(_XcbShmAvailable ? "available" : "unavailable") - .arg(_XcbShmPixmapAvailable ? "available" : "unavailable") - .toStdString().c_str()); + Info(_log, "%s", QSTRING_CSTR(QString("XcbRandR=[%1] XcbRender=[%2] XcbShm=[%3] XcbPixmap=[%4]") + .arg(_XcbRandRAvailable ? "available" : "unavailable", + _XcbRenderAvailable ? "available" : "unavailable", + _XcbShmAvailable ? "available" : "unavailable", + _XcbShmPixmapAvailable ? "available" : "unavailable")) + ); result = (updateScreenDimensions(true) >= 0); ErrorIf(!result, _log, "XCB Grabber start failed"); diff --git a/libsrc/hyperion/AuthManager.cpp b/libsrc/hyperion/AuthManager.cpp index 329b387d..931443a7 100644 --- a/libsrc/hyperion/AuthManager.cpp +++ b/libsrc/hyperion/AuthManager.cpp @@ -261,26 +261,24 @@ void AuthManager::checkTimeout() void AuthManager::checkAuthBlockTimeout() { // handle user auth block - for (auto it = _userAuthAttempts.begin(); it != _userAuthAttempts.end(); it++) - { + QMutableVectorIterator itUserAuth(_userAuthAttempts); + while (itUserAuth.hasNext()) { // after 10 minutes, we remove the entry - if (*it < (uint64_t)QDateTime::currentMSecsSinceEpoch()) - { - _userAuthAttempts.erase(it--); - } + if (itUserAuth.next() < static_cast(QDateTime::currentMSecsSinceEpoch())) + itUserAuth.remove(); } // handle token auth block - for (auto it = _tokenAuthAttempts.begin(); it != _tokenAuthAttempts.end(); it++) - { + QMutableVectorIterator itTokenAuth(_tokenAuthAttempts); + while (itTokenAuth.hasNext()) { // after 10 minutes, we remove the entry - if (*it < (uint64_t)QDateTime::currentMSecsSinceEpoch()) - { - _tokenAuthAttempts.erase(it--); - } + if (itTokenAuth.next() < static_cast(QDateTime::currentMSecsSinceEpoch())) + itTokenAuth.remove(); } // if the lists are empty we stop if (_userAuthAttempts.empty() && _tokenAuthAttempts.empty()) + { _authBlockTimer->stop(); + } } diff --git a/libsrc/hyperion/Hyperion.cpp b/libsrc/hyperion/Hyperion.cpp index 52216f02..7107611e 100644 --- a/libsrc/hyperion/Hyperion.cpp +++ b/libsrc/hyperion/Hyperion.cpp @@ -233,9 +233,6 @@ void Hyperion::freeObjects() void Hyperion::handleSettingsUpdate(settings::type type, const QJsonDocument& config) { -// std::cout << "Hyperion::handleSettingsUpdate" << std::endl; -// std::cout << config.toJson().toStdString() << std::endl; - if(type == settings::COLOR) { const QJsonObject obj = config.object(); diff --git a/libsrc/hyperion/LinearColorSmoothing.cpp b/libsrc/hyperion/LinearColorSmoothing.cpp index c91309b0..4cf6d4b9 100644 --- a/libsrc/hyperion/LinearColorSmoothing.cpp +++ b/libsrc/hyperion/LinearColorSmoothing.cpp @@ -406,7 +406,6 @@ void LinearColorSmoothing::performDecay(const int64_t now) { if(microsTillNextAction > SLEEP_RES_MICROS) { const int64_t wait = std::min(microsTillNextAction - SLEEP_RES_MICROS, SLEEP_MAX_MICROS); - //usleep(wait); std::this_thread::sleep_for(std::chrono::microseconds(wait)); } } @@ -542,7 +541,6 @@ void LinearColorSmoothing::queueColors(const std::vector &ledColors) void LinearColorSmoothing::clearQueuedColors() { _timer->stop(); - //QMetaObject::invokeMethod(_timer, "stop", Qt::QueuedConnection); _previousValues.clear(); _targetValues.clear(); diff --git a/libsrc/hyperion/MultiColorAdjustment.cpp b/libsrc/hyperion/MultiColorAdjustment.cpp index dabdac0c..4c761d6a 100644 --- a/libsrc/hyperion/MultiColorAdjustment.cpp +++ b/libsrc/hyperion/MultiColorAdjustment.cpp @@ -42,11 +42,8 @@ void MultiColorAdjustment::setAdjustmentForLed(const QString& id, int startLed, // Get the identified adjustment (don't care if is nullptr) ColorAdjustment * adjustment = getAdjustment(id); - - //Debug(_log,"ColorAdjustment Profile [%s], startLed[%d], endLed[%d]", QSTRING_CSTR(id), startLed, endLed); for (int iLed=startLed; iLed<=endLed; ++iLed) { - //Debug(_log,"_ledAdjustments [%d] -> [%p]", iLed, adjustment); _ledAdjustments[iLed] = adjustment; } } diff --git a/libsrc/hyperion/schema/schema-webConfig.json b/libsrc/hyperion/schema/schema-webConfig.json index 17d9e60d..15532f78 100644 --- a/libsrc/hyperion/schema/schema-webConfig.json +++ b/libsrc/hyperion/schema/schema-webConfig.json @@ -16,7 +16,7 @@ "title" : "edt_conf_general_port_title", "minimum" : 80, "maximum" : 65535, - "default" : 8090, + "default" : 8090.3, "propertyOrder" : 3 }, "sslPort" : diff --git a/libsrc/leddevice/LedDevice.cpp b/libsrc/leddevice/LedDevice.cpp index 6e1796d9..b30dd4fd 100644 --- a/libsrc/leddevice/LedDevice.cpp +++ b/libsrc/leddevice/LedDevice.cpp @@ -234,8 +234,6 @@ void LedDevice::startRefreshTimer() connect(_refreshTimer, &QTimer::timeout, this, &LedDevice::rewriteLEDs); } _refreshTimer->setInterval(_refreshTimerInterval_ms); - - //Debug(_log, "Start refresh timer with interval = %ims", _refreshTimer->interval()); _refreshTimer->start(); } else @@ -249,11 +247,9 @@ void LedDevice::stopRefreshTimer() { if (_refreshTimer != nullptr) { - //Debug(_log, "Stopping refresh timer"); _refreshTimer->stop(); delete _refreshTimer; _refreshTimer = nullptr; - } } @@ -302,7 +298,7 @@ int LedDevice::updateLeds(std::vector ledValues) int retval = 0; if (!_isEnabled || !_isOn || !_isDeviceReady || _isDeviceInError) { - //std::cout << "LedDevice::updateLeds(), LedDevice NOT ready! "; + // LedDevice NOT ready! retval = -1; } else @@ -310,7 +306,6 @@ int LedDevice::updateLeds(std::vector ledValues) qint64 elapsedTimeMs = _lastWriteTime.msecsTo(QDateTime::currentDateTime()); if (_latchTime_ms == 0 || elapsedTimeMs >= _latchTime_ms) { - //std::cout << "LedDevice::updateLeds(), Elapsed time since last write (" << elapsedTimeMs << ") ms > _latchTime_ms (" << _latchTime_ms << ") ms" << std::endl; retval = write(ledValues); _lastWriteTime = QDateTime::currentDateTime(); @@ -323,7 +318,7 @@ int LedDevice::updateLeds(std::vector ledValues) } else { - //std::cout << "LedDevice::updateLeds(), Skip write. elapsedTime (" << elapsedTimeMs << ") ms < _latchTime_ms (" << _latchTime_ms << ") ms" << std::endl; + // Skip write as elapsedTime < latchTime if (_isRefreshEnabled) { //Stop timer to allow for next non-refresh update @@ -340,14 +335,6 @@ int LedDevice::rewriteLEDs() if (_isEnabled && _isOn && _isDeviceReady && !_isDeviceInError) { - // qint64 elapsedTimeMs = _lastWriteTime.msecsTo(QDateTime::currentDateTime()); - // std::cout << "LedDevice::rewriteLEDs(): Rewrite LEDs now, elapsedTime [" << elapsedTimeMs << "] ms" << std::endl; - // //:TESTING: Inject "white" output records to differentiate from normal writes - // _lastLedValues.clear(); - // _lastLedValues.resize(static_cast(_ledCount), ColorRgb::WHITE); - // printLedValues(_lastLedValues); - // //:TESTING: - if (!_lastLedValues.empty()) { retval = write(_lastLedValues); @@ -490,12 +477,15 @@ bool LedDevice::storeState() { bool rc{ true }; +#if 0 if (_isRestoreOrigState) { // Save device's original state // _originalStateValues = get device's state; // store original power on/off state, if available } +#endif + return rc; } @@ -503,12 +493,14 @@ bool LedDevice::restoreState() { bool rc{ true }; +#if 0 if (_isRestoreOrigState) { // Restore device's original state // update device using _originalStateValues // update original power on/off state, if supported } +#endif return rc; } @@ -699,4 +691,3 @@ QString LedDevice::getColorOrder() const bool LedDevice::componentState() const { return _isEnabled; } - diff --git a/libsrc/leddevice/LedDeviceTemplate.cpp b/libsrc/leddevice/LedDeviceTemplate.cpp index 143b7277..299569ba 100644 --- a/libsrc/leddevice/LedDeviceTemplate.cpp +++ b/libsrc/leddevice/LedDeviceTemplate.cpp @@ -69,12 +69,14 @@ int LedDeviceTemplate::close() int retval = 0; _isDeviceReady = false; +#if 0 // Test, if device requires closing if ( true /*If device is still open*/ ) { // Close device // Everything is OK -> device is closed } +#endif return retval; } diff --git a/libsrc/leddevice/dev_hid/LedDeviceHyperionUsbasp.cpp b/libsrc/leddevice/dev_hid/LedDeviceHyperionUsbasp.cpp index a5f0dfb2..d96ed85c 100644 --- a/libsrc/leddevice/dev_hid/LedDeviceHyperionUsbasp.cpp +++ b/libsrc/leddevice/dev_hid/LedDeviceHyperionUsbasp.cpp @@ -63,7 +63,9 @@ bool LedDeviceHyperionUsbasp::init(const QJsonObject &deviceConfig) else { Debug(_log, "USB context initialized"); - //libusb_set_debug(_libusbContext, 3); +#if 0 + libusb_set_debug(_libusbContext, 3); +#endif // retrieve the list of USB devices libusb_device ** deviceList; diff --git a/libsrc/leddevice/dev_hid/LedDeviceLightpack.cpp b/libsrc/leddevice/dev_hid/LedDeviceLightpack.cpp index 88b9722b..08ef0f0a 100644 --- a/libsrc/leddevice/dev_hid/LedDeviceLightpack.cpp +++ b/libsrc/leddevice/dev_hid/LedDeviceLightpack.cpp @@ -331,8 +331,6 @@ const QString &LedDeviceLightpack::getSerialNumber() const int LedDeviceLightpack::writeBytes(uint8_t *data, int size) { int rc = 0; - //Debug( _log, "[%s]", QSTRING_CSTR(uint8_t_to_hex_string(data, size, 32)) ); - int error = libusb_control_transfer(_deviceHandle, static_cast( LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE ), 0x09, diff --git a/libsrc/leddevice/dev_hid/ProviderHID.cpp b/libsrc/leddevice/dev_hid/ProviderHID.cpp index d29c0f94..6bea6e63 100644 --- a/libsrc/leddevice/dev_hid/ProviderHID.cpp +++ b/libsrc/leddevice/dev_hid/ProviderHID.cpp @@ -73,8 +73,8 @@ int ProviderHID::open() // Failed to open the device this->setInError( "Failed to open HID device. Maybe your PID/VID setting is wrong? Make sure to add a udev rule/use sudo." ); +#if 0 // http://www.signal11.us/oss/hidapi/ - /* std::cout << "Showing a list of all available HID devices:" << std::endl; auto devs = hid_enumerate(0x00, 0x00); auto cur_dev = devs; @@ -88,7 +88,8 @@ int ProviderHID::open() cur_dev = cur_dev->next; } hid_free_enumeration(devs); - */ +#endif + } else { diff --git a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp index 62ba1fe3..f895cba6 100644 --- a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp @@ -45,7 +45,6 @@ const char PANEL_NUM[] = "numPanels"; const char PANEL_ID[] = "panelId"; const char PANEL_POSITIONDATA[] = "positionData"; const char PANEL_SHAPE_TYPE[] = "shapeType"; -//const char PANEL_ORIENTATION[] = "0"; const char PANEL_POS_X[] = "x"; const char PANEL_POS_Y[] = "y"; @@ -72,7 +71,6 @@ const quint16 STREAM_CONTROL_DEFAULT_PORT = 60222; const int API_DEFAULT_PORT = 16021; const char API_BASE_PATH[] = "/api/v1/%1/"; const char API_ROOT[] = ""; -//const char API_EXT_MODE_STRING_V1[] = "{\"write\" : {\"command\" : \"display\", \"animType\" : \"extControl\"}}"; const char API_EXT_MODE_STRING_V2[] = "{\"write\" : {\"command\" : \"display\", \"animType\" : \"extControl\", \"extControlVersion\" : \"v2\"}}"; const char API_STATE[] = "state"; const char API_PANELLAYOUT[] = "panelLayout"; @@ -243,7 +241,6 @@ bool LedDeviceNanoleaf::initLedsConfiguration() int panelX = panelObj[PANEL_POS_X].toInt(); int panelY = panelObj[PANEL_POS_Y].toInt(); int panelshapeType = panelObj[PANEL_SHAPE_TYPE].toInt(); - //int panelOrientation = panelObj[PANEL_ORIENTATION].toInt(); DebugIf(verbose,_log, "Panel [%d] (%d,%d) - Type: [%d]", panelId, panelX, panelY, panelshapeType); @@ -613,16 +610,16 @@ bool LedDeviceNanoleaf::storeState() // effect _restApi->setPath(API_EFFECT); - httpResponse response = _restApi->get(); - if ( response.error() ) + httpResponse responseEffects = _restApi->get(); + if ( responseEffects.error() ) { - QString errorReason = QString("Storing device state failed with error: '%1'").arg(response.getErrorReason()); + QString errorReason = QString("Storing device state failed with error: '%1'").arg(responseEffects.getErrorReason()); setInError(errorReason); rc = false; } else { - QJsonObject effects = response.getBody().object(); + QJsonObject effects = responseEffects.getBody().object(); DebugIf(verbose, _log, "effects: [%s]", QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData() ); _originalEffect = effects[API_EFFECT_SELECT].toString(); _originalIsDynEffect = _originalEffect == "*Dynamic*" || _originalEffect == "*Solid*"; @@ -774,7 +771,7 @@ int LedDeviceNanoleaf::write(const std::vector& ledValues) } else { - // Set panels not configured to black; + // Set panels not configured to black color = ColorRgb::BLACK; DebugIf(verbose3, _log, "[%d] >= panelLedCount [%d] => Set to BLACK", panelCounter, _panelLedCount); } diff --git a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp index 4ceda587..8f73ae5f 100644 --- a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp @@ -38,7 +38,6 @@ const char CONFIG_VERBOSE[] = "verbose"; const char DEV_DATA_BRIDGEID[] = "bridgeid"; const char DEV_DATA_MODEL[] = "modelid"; const char DEV_DATA_NAME[] = "name"; -//const char DEV_DATA_MANUFACTURER[] = "manufacturer"; const char DEV_DATA_FIRMWAREVERSION[] = "swversion"; const char DEV_DATA_APIVERSION[] = "apiversion"; @@ -65,7 +64,6 @@ const char API_STREAM_RESPONSE_FORMAT[] = "/%1/%2/%3/%4"; // List of resources const char API_XY_COORDINATES[] = "xy"; const char API_BRIGHTNESS[] = "bri"; -//const char API_SATURATION[] = "sat"; const char API_TRANSITIONTIME[] = "transitiontime"; const char API_MODEID[] = "modelid"; @@ -188,7 +186,6 @@ CiColor CiColor::rgbToCiColor(double red, double green, double blue, const CiCol } if (dBC < lowest) { - //lowest = dBC; closestPoint = pBC; } // Change the xy value to a value which is within the reach of the lamp. @@ -1089,7 +1086,7 @@ bool LedDevicePhilipsHue::init(const QJsonObject &deviceConfig) if( _groupId == 0 ) { - Error(_log, "Disabling usage of HueEntertainmentAPI: Group-ID is invalid", "%d", _groupId); + Error(_log, "Disabling usage of HueEntertainmentAPI: Group-ID [%d] is invalid", _groupId); _useHueEntertainmentAPI = false; } } diff --git a/libsrc/leddevice/dev_net/LedDeviceRazer.cpp b/libsrc/leddevice/dev_net/LedDeviceRazer.cpp index 697b5c92..6f01098b 100644 --- a/libsrc/leddevice/dev_net/LedDeviceRazer.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceRazer.cpp @@ -141,10 +141,6 @@ bool LedDeviceRazer::checkApiError(const httpResponse& response) else { QString errorReason; - - QString strJson(response.getBody().toJson(QJsonDocument::Compact)); - //DebugIf(verbose, _log, "Reply: [%s]", strJson.toUtf8().constData()); - QJsonObject jsonObj = response.getBody().object(); if (!jsonObj[API_RESULT].isNull()) diff --git a/libsrc/leddevice/dev_net/LedDeviceUdpE131.cpp b/libsrc/leddevice/dev_net/LedDeviceUdpE131.cpp index 65cebf78..1be48dc3 100644 --- a/libsrc/leddevice/dev_net/LedDeviceUdpE131.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceUdpE131.cpp @@ -20,15 +20,20 @@ const ushort E131_DEFAULT_PORT = 5568; /* defined parameters from http://tsp.esta.org/tsp/documents/docs/BSR_E1-31-20xx_CP-2014-1009r2.pdf */ const uint32_t VECTOR_ROOT_E131_DATA = 0x00000004; -//#define VECTOR_ROOT_E131_EXTENDED 0x00000008 + const uint8_t VECTOR_DMP_SET_PROPERTY = 0x02; const uint32_t VECTOR_E131_DATA_PACKET = 0x00000002; -//#define VECTOR_E131_EXTENDED_SYNCHRONIZATION 0x00000001 -//#define VECTOR_E131_EXTENDED_DISCOVERY 0x00000002 -//#define VECTOR_UNIVERSE_DISCOVERY_UNIVERSE_LIST 0x00000001 -//#define E131_E131_UNIVERSE_DISCOVERY_INTERVAL 10 // seconds -//#define E131_NETWORK_DATA_LOSS_TIMEOUT 2500 // milli econds -//#define E131_DISCOVERY_UNIVERSE 64214 + +#if 0 +#define VECTOR_ROOT_E131_EXTENDED 0x00000008 +#define VECTOR_E131_EXTENDED_SYNCHRONIZATION 0x00000001 +#define VECTOR_E131_EXTENDED_DISCOVERY 0x00000002 +#define VECTOR_UNIVERSE_DISCOVERY_UNIVERSE_LIST 0x00000001 +#define E131_E131_UNIVERSE_DISCOVERY_INTERVAL 10 // seconds +#define E131_NETWORK_DATA_LOSS_TIMEOUT 2500 // milli econds +#define E131_DISCOVERY_UNIVERSE 64214 +#endif + const int DMX_MAX = 512; // 512 usable slots } diff --git a/libsrc/leddevice/dev_net/LedDeviceUdpE131.h b/libsrc/leddevice/dev_net/LedDeviceUdpE131.h index 12ec50e2..0397eeda 100644 --- a/libsrc/leddevice/dev_net/LedDeviceUdpE131.h +++ b/libsrc/leddevice/dev_net/LedDeviceUdpE131.h @@ -20,28 +20,30 @@ **/ /* E1.31 Packet Offsets */ -//#define E131_ROOT_PREAMBLE_SIZE 0 -//#define E131_ROOT_POSTAMBLE_SIZE 2 -//#define E131_ROOT_ID 4 -//#define E131_ROOT_FLENGTH 16 -//#define E131_ROOT_VECTOR 18 -//#define E131_ROOT_CID 22 +#if 0 +#define E131_ROOT_PREAMBLE_SIZE 0 +#define E131_ROOT_POSTAMBLE_SIZE 2 +#define E131_ROOT_ID 4 +#define E131_ROOT_FLENGTH 16 +#define E131_ROOT_VECTOR 18 +#define E131_ROOT_CID 22 -//#define E131_FRAME_FLENGTH 38 -//#define E131_FRAME_VECTOR 40 -//#define E131_FRAME_SOURCE 44 -//#define E131_FRAME_PRIORITY 108 -//#define E131_FRAME_RESERVED 109 -//#define E131_FRAME_SEQ 111 -//#define E131_FRAME_OPT 112 -//#define E131_FRAME_UNIVERSE 113 +#define E131_FRAME_FLENGTH 38 +#define E131_FRAME_VECTOR 40 +#define E131_FRAME_SOURCE 44 +#define E131_FRAME_PRIORITY 108 +#define E131_FRAME_RESERVED 109 +#define E131_FRAME_SEQ 111 +#define E131_FRAME_OPT 112 +#define E131_FRAME_UNIVERSE 113 -//#define E131_DMP_FLENGTH 115 -//#define E131_DMP_VECTOR 117 -//#define E131_DMP_TYPE 118 -//#define E131_DMP_ADDR_FIRST 119 -//#define E131_DMP_ADDR_INC 121 -//#define E131_DMP_COUNT 123 +#define E131_DMP_FLENGTH 115 +#define E131_DMP_VECTOR 117 +#define E131_DMP_TYPE 118 +#define E131_DMP_ADDR_FIRST 119 +#define E131_DMP_ADDR_INC 121 +#define E131_DMP_COUNT 123 +#endif const unsigned int E131_DMP_DATA=125; /* E1.31 Packet Structure */ diff --git a/libsrc/leddevice/dev_net/LedDeviceWled.cpp b/libsrc/leddevice/dev_net/LedDeviceWled.cpp index 8c772545..9483c3d4 100644 --- a/libsrc/leddevice/dev_net/LedDeviceWled.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceWled.cpp @@ -40,7 +40,6 @@ const char WLED_VERSION_DDP[] = "0.11.0"; const int API_DEFAULT_PORT = -1; //Use default port per communication scheme const char API_BASE_PATH[] = "/json/"; -//const char API_PATH_INFO[] = "info"; const char API_PATH_STATE[] = "state"; // List of State Information @@ -415,7 +414,7 @@ QJsonObject LedDeviceWled::getProperties(const QJsonObject& params) } else { - Info(_log, "DDP streaming is supported by your WLED device version [%s]. No limitation in number of LEDs.", currentVersion.getVersion().c_str(), ddpVersion.getVersion().c_str()); + Info(_log, "DDP streaming is supported by your WLED device version [%s]. No limitation in number of LEDs.", currentVersion.getVersion().c_str()); } } properties.insert("properties", propertiesDetails); diff --git a/libsrc/leddevice/dev_net/LedDeviceYeelight.cpp b/libsrc/leddevice/dev_net/LedDeviceYeelight.cpp index 55bdfdf4..8c0490ce 100644 --- a/libsrc/leddevice/dev_net/LedDeviceYeelight.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceYeelight.cpp @@ -82,7 +82,6 @@ const char API_PROP_BRIGHT[] = "bright"; // List of Result Information const char API_RESULT_ID[] = "id"; const char API_RESULT[] = "result"; -//const char API_RESULT_OK[] = "OK"; // List of Error Information const char API_ERROR[] = "error"; @@ -383,8 +382,6 @@ bool YeelightLight::streamCommand( const QJsonDocument &command ) { log ( 2, "Info:", "Skip write. Device is in error"); } - - //log (2,"streamCommand() rc","%d, isON[%d], isInMusicMode[%d]", rc, _isOn, _isInMusicMode ); return rc; } @@ -392,8 +389,6 @@ YeelightResponse YeelightLight::handleResponse(int correlationID, QByteArray con { log (3,"handleResponse()","" ); - //std::cout << _name.toStdString() <<"| Response: [" << response.toStdString() << "]" << std::endl << std::flush; - YeelightResponse yeeResponse; QString errorReason; @@ -446,8 +441,6 @@ YeelightResponse YeelightLight::handleResponse(int correlationID, QByteArray con else { int id = jsonObj[API_RESULT_ID].toInt(); - //log ( 3, "Correlation ID:", "%d", id ); - if ( id != correlationID && TEST_CORRELATION_IDS) { errorReason = QString ("%1| API is out of sync, received ID [%2], expected [%3]"). @@ -528,9 +521,6 @@ QJsonObject YeelightLight::getProperties() log (3,"getProperties()","" ); QJsonObject properties; - //Selected properties - //QJsonArray propertyList = { API_PROP_NAME, API_PROP_MODEL, API_PROP_POWER, API_PROP_RGB, API_PROP_BRIGHT, API_PROP_CT, API_PROP_FWVER }; - //All properties QJsonArray propertyList = {"power","bright","ct","rgb","hue","sat","color_mode","flowing","delayoff","music_on","name","bg_power","bg_flowing","bg_ct","bg_bright","bg_hue","bg_sat","bg_rgb","nl_br","active_mode" }; @@ -579,9 +569,6 @@ bool YeelightLight::identify() */ QJsonArray colorflowParams = { API_PROP_COLORFLOW, 6, 0, "500,1,100,100,500,1,16711696,10"}; - //Blink White - //QJsonArray colorflowParams = { API_PROP_COLORFLOW, 6, 0, "500,2,4000,1,500,2,4000,50"}; - QJsonDocument command = getCommand( API_METHOD_SETSCENE, colorflowParams ); if ( writeCommand( command ) < 0 ) @@ -819,7 +806,6 @@ bool YeelightLight::setColorRGB(const ColorRgb &color) rc = false; } } - //log (2,"setColorRGB() rc","%d, isON[%d], isInMusicMode[%d]", rc, _isOn, _isInMusicMode ); return rc; } @@ -914,7 +900,7 @@ bool YeelightLight::setColorHSV(const ColorRgb &colorRGB) } else { - //log ( 3, "setColorHSV", "Skip update. Same Color as before"); + // Skip update. Same Color as before } log( 3, "setColorHSV() rc", @@ -1471,7 +1457,6 @@ void LedDeviceYeelight::identify(const QJsonObject& params) int LedDeviceYeelight::write(const std::vector & ledValues) { - //DebugIf(verbose, _log, "enabled [%d], _isDeviceReady [%d]", _isEnabled, _isDeviceReady); int rc = -1; //Update on all Yeelights by iterating through lights and set colors. @@ -1545,8 +1530,5 @@ int LedDeviceYeelight::write(const std::vector & ledValues) // Minimum one Yeelight device is working, continue updating devices rc = 0; } - - //DebugIf(verbose, _log, "rc [%d]", rc ); - return rc; } diff --git a/libsrc/leddevice/dev_net/LedDeviceYeelight.h b/libsrc/leddevice/dev_net/LedDeviceYeelight.h index ab99d0bd..4e8f3257 100644 --- a/libsrc/leddevice/dev_net/LedDeviceYeelight.h +++ b/libsrc/leddevice/dev_net/LedDeviceYeelight.h @@ -24,7 +24,6 @@ const char API_METHOD_MUSIC_MODE[] = "set_music"; const int API_METHOD_MUSIC_MODE_ON = 1; const int API_METHOD_MUSIC_MODE_OFF = 0; -const char API_METHOD_SETRGB[] = "set_rgb"; const char API_METHOD_SETSCENE[] = "set_scene"; const char API_METHOD_GETPROP[] = "get_prop"; diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.cpp b/libsrc/leddevice/dev_net/ProviderRestApi.cpp index b1fe965e..0d318e56 100644 --- a/libsrc/leddevice/dev_net/ProviderRestApi.cpp +++ b/libsrc/leddevice/dev_net/ProviderRestApi.cpp @@ -16,10 +16,12 @@ namespace { const QChar ONE_SLASH = '/'; -const int HTTP_STATUS_NO_CONTENT = 204; -const int HTTP_STATUS_BAD_REQUEST = 400; -const int HTTP_STATUS_UNAUTHORIZED = 401; -const int HTTP_STATUS_NOT_FOUND = 404; +enum HttpStatusCode { + NoContent = 204, + BadRequest = 400, + UnAuthorized = 401, + NotFound = 404 +}; constexpr std::chrono::milliseconds DEFAULT_REST_TIMEOUT{ 400 }; @@ -303,13 +305,13 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply) { httpResponse response; - int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + HttpStatusCode httpStatusCode = static_cast(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); response.setHttpStatusCode(httpStatusCode); response.setNetworkReplyError(reply->error()); if (reply->error() == QNetworkReply::NoError) { - if ( httpStatusCode != HTTP_STATUS_NO_CONTENT ){ + if ( httpStatusCode != HttpStatusCode::NoContent ){ QByteArray replyData = reply->readAll(); if (!replyData.isEmpty()) @@ -320,13 +322,11 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply) if (error.error != QJsonParseError::NoError) { //Received not valid JSON response - //std::cout << "Response: [" << replyData.toStdString() << "]" << std::endl; response.setError(true); response.setErrorReason(error.errorString()); } else { - //std::cout << "Response: [" << QString(jsonDoc.toJson(QJsonDocument::Compact)).toStdString() << "]" << std::endl; response.setBody(jsonDoc); } } @@ -344,13 +344,13 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply) QString httpReason = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); QString advise; switch ( httpStatusCode ) { - case HTTP_STATUS_BAD_REQUEST: + case HttpStatusCode::BadRequest: advise = "Check Request Body"; break; - case HTTP_STATUS_UNAUTHORIZED: + case HttpStatusCode::UnAuthorized: advise = "Check Authentication Token (API Key)"; break; - case HTTP_STATUS_NOT_FOUND: + case HttpStatusCode::NotFound: advise = "Check Resource given"; break; default: diff --git a/libsrc/leddevice/dev_other/LedDeviceFile.cpp b/libsrc/leddevice/dev_other/LedDeviceFile.cpp index 0dba14d8..cc7ddd2d 100644 --- a/libsrc/leddevice/dev_other/LedDeviceFile.cpp +++ b/libsrc/leddevice/dev_other/LedDeviceFile.cpp @@ -92,9 +92,6 @@ int LedDeviceFile::close() int LedDeviceFile::write(const std::vector & ledValues) { QTextStream out(_file); - - //printLedValues (ledValues); - if ( _printTimeStamp ) { QDateTime now = QDateTime::currentDateTime(); diff --git a/libsrc/leddevice/dev_other/LedDevicePiBlaster.cpp b/libsrc/leddevice/dev_other/LedDevicePiBlaster.cpp index 20eaf0f7..0f46d898 100644 --- a/libsrc/leddevice/dev_other/LedDevicePiBlaster.cpp +++ b/libsrc/leddevice/dev_other/LedDevicePiBlaster.cpp @@ -179,8 +179,6 @@ int LedDevicePiBlaster::write(const std::vector & ledValues) continue; } -// fprintf(_fid, "%i=%f\n", iPins[iPin], pwmDutyCycle); - if ( (fprintf(_fid, "%i=%f\n", i, pwmDutyCycle) < 0) || (fflush(_fid) < 0)) { if (_fid != nullptr) diff --git a/libsrc/leddevice/dev_serial/LedDeviceDMX.cpp b/libsrc/leddevice/dev_serial/LedDeviceDMX.cpp index 7a9e85bc..ba7a51e6 100644 --- a/libsrc/leddevice/dev_serial/LedDeviceDMX.cpp +++ b/libsrc/leddevice/dev_serial/LedDeviceDMX.cpp @@ -42,7 +42,6 @@ bool LedDeviceDMX::init(const QJsonObject &deviceConfig) } else { - //Error(_log, "unknown dmx device type %s", QSTRING_CSTR(dmxString)); QString errortext = QString ("unknown dmx device type: %1").arg(dmxTypeString); this->setInError(errortext); return false; diff --git a/libsrc/leddevice/dev_serial/LedDeviceKarate.cpp b/libsrc/leddevice/dev_serial/LedDeviceKarate.cpp index e99ccce0..56eea4cc 100644 --- a/libsrc/leddevice/dev_serial/LedDeviceKarate.cpp +++ b/libsrc/leddevice/dev_serial/LedDeviceKarate.cpp @@ -24,7 +24,6 @@ bool LedDeviceKarate::init(const QJsonObject& deviceConfig) { if (_ledCount != 8 && _ledCount != 16) { - //Error( _log, "%d channels configured. This should always be 16!", _ledCount); QString errortext = QString("%1 channels configured. This should always be 8 or 16!").arg(_ledCount); this->setInError(errortext); isInitOK = false; diff --git a/libsrc/leddevice/dev_serial/LedDeviceSedu.cpp b/libsrc/leddevice/dev_serial/LedDeviceSedu.cpp index 9468e45e..09ac92fc 100644 --- a/libsrc/leddevice/dev_serial/LedDeviceSedu.cpp +++ b/libsrc/leddevice/dev_serial/LedDeviceSedu.cpp @@ -40,7 +40,6 @@ bool LedDeviceSedu::init(const QJsonObject &deviceConfig) if (_ledBuffer.empty()) { - //Warning(_log, "More rgb-channels required then available"); QString errortext = "More rgb-channels required then available"; this->setInError(errortext); } diff --git a/libsrc/leddevice/dev_serial/ProviderRs232.cpp b/libsrc/leddevice/dev_serial/ProviderRs232.cpp index bd4349db..9b2b1197 100644 --- a/libsrc/leddevice/dev_serial/ProviderRs232.cpp +++ b/libsrc/leddevice/dev_serial/ProviderRs232.cpp @@ -273,13 +273,6 @@ void ProviderRs232::readFeedback() { //Output as received std::cout << readData.toStdString(); - - //Output as Hex -//#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)) -// std::cout << readData.toHex(':').toStdString(); -//#else -// std::cout << readData.toHex().toStdString(); -//#endif } } diff --git a/libsrc/leddevice/dev_spi/LedDeviceSK9822.cpp b/libsrc/leddevice/dev_spi/LedDeviceSK9822.cpp index 4562ac1b..c477946f 100644 --- a/libsrc/leddevice/dev_spi/LedDeviceSK9822.cpp +++ b/libsrc/leddevice/dev_spi/LedDeviceSK9822.cpp @@ -117,10 +117,6 @@ void LedDeviceSK9822::bufferWithAdjustedCurrent(std::vector &txBuf, con txBuf[b + 1] = red; txBuf[b + 2] = green; txBuf[b + 3] = blue; - - //if(iLed == 0) { - // std::cout << std::to_string((int)rgb.red) << "," << std::to_string((int)rgb.green) << "," << std::to_string((int)rgb.blue) << ": " << std::to_string(maxValue) << (maxValue >= threshold ? " >= " : " < ") << std::to_string(threshold) << " -> " << std::to_string((int)(level&SK9822_GBC_MAX_LEVEL))<< "@" << std::to_string((int)red) << "," << std::to_string((int)green) << "," << std::to_string((int)blue) << std::endl; - //} } } diff --git a/libsrc/leddevice/dev_spi/LedDeviceSk6822SPI.cpp b/libsrc/leddevice/dev_spi/LedDeviceSk6822SPI.cpp index df478ba3..12d050dc 100644 --- a/libsrc/leddevice/dev_spi/LedDeviceSk6822SPI.cpp +++ b/libsrc/leddevice/dev_spi/LedDeviceSk6822SPI.cpp @@ -67,8 +67,6 @@ bool LedDeviceSk6822SPI::init(const QJsonObject &deviceConfig) WarningIf(( _baudRate_Hz < 2000000 || _baudRate_Hz > 2460000 ), _log, "SPI rate %d outside recommended range (2000000 -> 2460000)", _baudRate_Hz); _ledBuffer.resize( (_ledRGBCount * SPI_BYTES_PER_COLOUR) + (_ledCount * SPI_BYTES_WAIT_TIME ) + SPI_FRAME_END_LATCH_BYTES, 0x00); - // Debug(_log, "_ledBuffer.resize(_ledRGBCount:%d * SPI_BYTES_PER_COLOUR:%d) + ( _ledCount:%d * SPI_BYTES_WAIT_TIME:%d ) + SPI_FRAME_END_LATCH_BYTES:%d, 0x00)", _ledRGBCount, SPI_BYTES_PER_COLOUR, _ledCount, SPI_BYTES_WAIT_TIME, SPI_FRAME_END_LATCH_BYTES); - isInitOK = true; } @@ -95,7 +93,7 @@ int LedDeviceSk6822SPI::write(const std::vector &ledValues) spi_ptr += SPI_BYTES_WAIT_TIME; // the wait between led time is all zeros } -/* +#if 0 // debug the whole SPI packet char debug_line[2048]; int ptr=0; @@ -114,7 +112,7 @@ int LedDeviceSk6822SPI::write(const std::vector &ledValues) ptr = 0; } } -*/ +#endif return writeBytes(_ledBuffer.size(), _ledBuffer.data()); } diff --git a/libsrc/mdns/MdnsBrowser.cpp b/libsrc/mdns/MdnsBrowser.cpp index 60bd7ee4..4c0e4b9e 100644 --- a/libsrc/mdns/MdnsBrowser.cpp +++ b/libsrc/mdns/MdnsBrowser.cpp @@ -429,7 +429,7 @@ QJsonArray MdnsBrowser::getServicesDiscoveredJson(const QByteArray& serviceType, void MdnsBrowser::printCache(const QByteArray& name, quint16 type) const { - DebugIf(verboseBrowser,_log, "for type: ", QSTRING_CSTR(QMdnsEngine::typeName(type))); + DebugIf(verboseBrowser,_log, "for type: %s", QSTRING_CSTR(QMdnsEngine::typeName(type))); QList records; if (_cache.lookupRecords(name, type, records)) { @@ -466,6 +466,6 @@ void MdnsBrowser::printCache(const QByteArray& name, quint16 type) const } else { - DebugIf(verboseBrowser,_log, "Cash is empty for type: ", QSTRING_CSTR(QMdnsEngine::typeName(type))); + DebugIf(verboseBrowser,_log, "Cash is empty for type: %s", QSTRING_CSTR(QMdnsEngine::typeName(type))); } } diff --git a/libsrc/ssdp/SSDPDiscover.cpp b/libsrc/ssdp/SSDPDiscover.cpp index 3deec6f2..45401788 100644 --- a/libsrc/ssdp/SSDPDiscover.cpp +++ b/libsrc/ssdp/SSDPDiscover.cpp @@ -73,8 +73,6 @@ QString SSDPDiscover::getFirstService(const searchType& type, const QString& st, QString data(datagram); - //Debug(_log, "_data: [%s]", QSTRING_CSTR(data)); - QMap headers; QString address; // parse request @@ -107,7 +105,7 @@ QString SSDPDiscover::getFirstService(const searchType& type, const QString& st, { _usnList << headers.value("usn"); QUrl url(headers.value("location")); - //Debug(_log, "Received msearch response from '%s:%d'. Search target: %s",QSTRING_CSTR(sender.toString()), senderPort, QSTRING_CSTR(headers.value("st"))); + if(type == searchType::STY_WEBSERVER) { Debug(_log, "Found service [%s] at: %s:%d", QSTRING_CSTR(st), QSTRING_CSTR(url.host()), url.port()); @@ -191,7 +189,6 @@ void SSDPDiscover::readPendingDatagrams() if (headers.value("st") == _searchTarget) { _usnList << headers.value("usn"); - //Debug(_log, "Received msearch response from '%s:%d'. Search target: %s",QSTRING_CSTR(sender.toString()), senderPort, QSTRING_CSTR(headers.value("st"))); QUrl url(headers.value("location")); emit newService(url.host() + ":" + QString::number(url.port())); } @@ -226,8 +223,6 @@ int SSDPDiscover::discoverServices(const QString& searchTarget, const QString& k QString data(datagram); - //Debug(_log, "_data: [%s]", QSTRING_CSTR(data)); - QMap headers; // parse request QStringList entries = QStringUtils::split(data,"\n", QStringUtils::SplitBehavior::SkipEmptyParts); @@ -250,7 +245,6 @@ int SSDPDiscover::discoverServices(const QString& searchTarget, const QString& k if ( match.hasMatch() ) { Debug(_log,"Found target [%s], plus record [%s] matches [%s:%s]", QSTRING_CSTR(_searchTarget), QSTRING_CSTR(headers[_filterHeader]), QSTRING_CSTR(_filterHeader), QSTRING_CSTR(_filter) ); - //Debug(_log, "_data: [%s]", QSTRING_CSTR(data)); QString mapKey = headers[key]; @@ -303,8 +297,6 @@ QJsonArray SSDPDiscover::getServicesDiscoveredJson() const QMultiMap::const_iterator i; for (i = _services.begin(); i != _services.end(); ++i) { - //Debug(_log, "Device discovered at [%s]", QSTRING_CSTR( i.key() )); - QJsonObject obj; obj.insert("id", i.key()); @@ -363,8 +355,6 @@ QJsonArray SSDPDiscover::getServicesDiscoveredJson() const result << obj; } - - //Debug(_log, "result: [%s]", QString(QJsonDocument(result).toJson(QJsonDocument::Compact)).toUtf8().constData() ); return result; } @@ -372,7 +362,6 @@ void SSDPDiscover::sendSearch(const QString& st) { const QString msg = QString(UPNP_DISCOVER_MESSAGE).arg(_ssdpAddr.toString()).arg(_ssdpPort).arg(_ssdpMaxWaitResponseTime).arg(st); - //Debug(_log,"Search request: [%s]", QSTRING_CSTR(msg)); _udpSocket->writeDatagram(msg.toUtf8(), _ssdpAddr, _ssdpPort); } diff --git a/libsrc/ssdp/SSDPServer.cpp b/libsrc/ssdp/SSDPServer.cpp index cd8e1d08..5fba01f3 100644 --- a/libsrc/ssdp/SSDPServer.cpp +++ b/libsrc/ssdp/SSDPServer.cpp @@ -165,7 +165,6 @@ void SSDPServer::readPendingDatagrams() if (headers.value("man") == "\"ssdp:discover\"") { - //Debug(_log, "Received msearch from '%s:%d'. Search target: %s",QSTRING_CSTR(sender.toString()), senderPort, QSTRING_CSTR(headers.value("st"))); emit msearchRequestReceived(headers.value("st"), headers.value("mx"), sender.toString(), senderPort); } } diff --git a/libsrc/utils/DefaultSignalHandler.cpp b/libsrc/utils/DefaultSignalHandler.cpp index 8a06095a..e8afa969 100644 --- a/libsrc/utils/DefaultSignalHandler.cpp +++ b/libsrc/utils/DefaultSignalHandler.cpp @@ -94,8 +94,8 @@ void print_trace() * handler and print_trace functions. */ for (int i = 2; i < size; ++i) { - std::string line = "\t" + decipher_trace(symbols[i]); - Error(log, line.c_str()); + const std::string line = "\t" + decipher_trace(symbols[i]); + Error(log, "%s", line.c_str()); } free(symbols); @@ -149,8 +149,6 @@ void signal_handler(int signum, siginfo_t * /*info*/, void * /*context*/) default: /* If the signal_handler is hit before the event loop is started, * following call will do nothing. So we queue the call. */ - - // QCoreApplication::quit(); QMetaObject::invokeMethod(qApp, "quit", Qt::QueuedConnection); // Reset signal handler to default (in case this handler is not capable of stopping) diff --git a/libsrc/utils/JsonUtils.cpp b/libsrc/utils/JsonUtils.cpp index c24d0939..d43c5bba 100644 --- a/libsrc/utils/JsonUtils.cpp +++ b/libsrc/utils/JsonUtils.cpp @@ -59,8 +59,6 @@ namespace JsonUtils { { //remove Comments in data QString cleanData = data; - //cleanData .remove(QRegularExpression("([^:]?\\/\\/.*)")); - QJsonParseError error; doc = QJsonDocument::fromJson(cleanData.toUtf8(), &error); @@ -145,7 +143,6 @@ namespace JsonUtils { obj.insert(attribute, resolveRefs(attributeValue.toObject(), obj, log)); else { - //qDebug() <<"ADD ATTR:VALUE"< #include -#include +#include // Utils-Jsonschema includes #include @@ -186,7 +186,14 @@ void QJsonSchemaChecker::checkType(const QJsonValue& value, const QJsonValue& sc else if (type == "integer") { if (value.isDouble()) //check if value type not boolean (true = 1 && false = 0) - wrongType = (rint(value.toDouble()) != value.toDouble()); + { + double valueIntegratlPart; + double valueFractionalPart = std::modf(value.toDouble(), &valueIntegratlPart); + if (valueFractionalPart > std::numeric_limits::epsilon()) + { + wrongType = true; + } + } else wrongType = true; } diff --git a/libsrc/webserver/QtHttpClientWrapper.cpp b/libsrc/webserver/QtHttpClientWrapper.cpp index 3bf2bdef..4ea3bd83 100644 --- a/libsrc/webserver/QtHttpClientWrapper.cpp +++ b/libsrc/webserver/QtHttpClientWrapper.cpp @@ -77,13 +77,13 @@ void QtHttpClientWrapper::onClientDataReceived (void) else { m_parsingStatus = ParsingError; - //qWarning () << "Error : unhandled HTTP version :" << version; + // Error : unhandled HTTP version } } else { m_parsingStatus = ParsingError; - //qWarning () << "Error : incorrect HTTP command line :" << line; + // Error : incorrect HTTP command line } break; diff --git a/libsrc/webserver/StaticFileServing.cpp b/libsrc/webserver/StaticFileServing.cpp index 898d31d8..4c550280 100644 --- a/libsrc/webserver/StaticFileServing.cpp +++ b/libsrc/webserver/StaticFileServing.cpp @@ -1,5 +1,7 @@ #include "StaticFileServing.h" + +#include "QtHttpHeader.h" #include #include @@ -9,6 +11,7 @@ #include #include #include + #include StaticFileServing::StaticFileServing (QObject * parent) diff --git a/libsrc/webserver/StaticFileServing.h b/libsrc/webserver/StaticFileServing.h index c1ec6395..b328bf16 100644 --- a/libsrc/webserver/StaticFileServing.h +++ b/libsrc/webserver/StaticFileServing.h @@ -3,10 +3,8 @@ #include -//#include "QtHttpServer.h" #include "QtHttpRequest.h" #include "QtHttpReply.h" -#include "QtHttpHeader.h" #include "CgiHandler.h" #include diff --git a/libsrc/webserver/WebSocketClient.cpp b/libsrc/webserver/WebSocketClient.cpp index ac8d77ad..555fae52 100644 --- a/libsrc/webserver/WebSocketClient.cpp +++ b/libsrc/webserver/WebSocketClient.cpp @@ -58,14 +58,12 @@ void WebSocketClient::handleWebSocketFrame() if(_socket->bytesAvailable() < (qint64)_wsh.payloadLength) { - //printf("not enough data %llu %llu\n", _socket->bytesAvailable(), _wsh.payloadLength); _notEnoughData=true; return; } _notEnoughData = false; QByteArray buf = _socket->read(_wsh.payloadLength); - //printf("opcode %x payload bytes %llu avail: %llu\n", _wsh.opCode, _wsh.payloadLength, _socket->bytesAvailable()); if (OPCODE::invalid((OPCODE::value)_wsh.opCode)) { @@ -210,10 +208,10 @@ void WebSocketClient::getWsFrameHeader(WebSocketHeader* header) } /// See http://tools.ietf.org/html/rfc6455#section-5.2 for more information -void WebSocketClient::sendClose(int status, QString reason) +void WebSocketClient::sendClose(int status, const QString& reason) { Debug(_log, "Send close to %s: %d %s", QSTRING_CSTR(_socket->peerAddress().toString()), status, QSTRING_CSTR(reason)); - ErrorIf(!reason.isEmpty(), _log, QSTRING_CSTR(reason)); + ErrorIf(!reason.isEmpty(), _log, "%s", QSTRING_CSTR(reason)); _receiveBuffer.clear(); QByteArray sendBuffer; @@ -244,8 +242,6 @@ void WebSocketClient::sendClose(int status, QString reason) void WebSocketClient::handleBinaryMessage(QByteArray &data) { - //uint8_t priority = data.at(0); - //unsigned duration_s = data.at(1); unsigned imgSize = data.size() - 4; unsigned width = ((data.at(2) << 8) & 0xFF00) | (data.at(3) & 0xFF); unsigned height = imgSize / width; @@ -260,8 +256,6 @@ void WebSocketClient::handleBinaryMessage(QByteArray &data) image.resize(width, height); memcpy(image.memptr(), data.data()+4, imgSize); - //_hyperion->registerInput(); - //_hyperion->setInputImage(priority, image, duration_s*1000); } diff --git a/libsrc/webserver/WebSocketClient.h b/libsrc/webserver/WebSocketClient.h index 0b056fb1..df5342bd 100644 --- a/libsrc/webserver/WebSocketClient.h +++ b/libsrc/webserver/WebSocketClient.h @@ -31,7 +31,7 @@ private: JsonAPI* _jsonAPI; void getWsFrameHeader(WebSocketHeader* header); - void sendClose(int status, QString reason = ""); + void sendClose(int status, const QString& reason = ""); void handleBinaryMessage(QByteArray &data); qint64 sendMessage_Raw(const char* data, quint64 size); qint64 sendMessage_Raw(QByteArray &data); diff --git a/src/hyperion-framebuffer/hyperion-framebuffer.cpp b/src/hyperion-framebuffer/hyperion-framebuffer.cpp index ba82953c..8f743d48 100644 --- a/src/hyperion-framebuffer/hyperion-framebuffer.cpp +++ b/src/hyperion-framebuffer/hyperion-framebuffer.cpp @@ -21,13 +21,6 @@ #include -// Constants -namespace { - - const char SERVICE_TYPE[] = "flatbuffer"; - -} //End of constants - using namespace commandline; // save the image as screenshot diff --git a/src/hyperion-qt/hyperion-qt.cpp b/src/hyperion-qt/hyperion-qt.cpp index 29060088..2e12f2f6 100644 --- a/src/hyperion-qt/hyperion-qt.cpp +++ b/src/hyperion-qt/hyperion-qt.cpp @@ -22,13 +22,6 @@ //flatbuf sending #include -// Constants -namespace { - -const char SERVICE_TYPE[] = "flatbuffer"; - -} //End of constants - using namespace commandline; // save the image as screenshot @@ -49,7 +42,6 @@ int main(int argc, char ** argv) << "\tVersion : " << HYPERION_VERSION << " (" << HYPERION_BUILD_ID << ")" << std::endl << "\tbuild time: " << __DATE__ << " " << __TIME__ << std::endl; - //QCoreApplication app(argc, argv); QGuiApplication app(argc, argv); try diff --git a/src/hyperion-remote/hyperion-remote.cpp b/src/hyperion-remote/hyperion-remote.cpp index e1ec2c43..37d2ec03 100644 --- a/src/hyperion-remote/hyperion-remote.cpp +++ b/src/hyperion-remote/hyperion-remote.cpp @@ -29,14 +29,6 @@ #include -// Constants -namespace { - - const char SERVICE_TYPE[] = "jsonapi"; - -} //End of constants - - using namespace commandline; /// Count the number of true values in a list of booleans diff --git a/src/hyperion-v4l2/ScreenshotHandler.cpp b/src/hyperion-v4l2/ScreenshotHandler.cpp index ed2fdbb8..a1482b66 100644 --- a/src/hyperion-v4l2/ScreenshotHandler.cpp +++ b/src/hyperion-v4l2/ScreenshotHandler.cpp @@ -1,3 +1,5 @@ +#include + // Qt includes #include #include @@ -102,7 +104,6 @@ bool ScreenshotHandler::findNoSignalSettings(const Image & image) auto itG = std::max_element(std::begin(greenCounts), std::end(greenCounts)); auto itB = std::max_element(std::begin(blueCounts), std::end(blueCounts)); - //std::cout << *itR << " " << *itG << " " << *itB << std::endl; double xOffsetSuggested = xOffset; double yOffsetSuggested = yOffset; double xMaxSuggested = xMax; @@ -138,7 +139,7 @@ bool ScreenshotHandler::findNoSignalSettings(const Image & image) if (!noSignalBlack) { unsigned xMid = (xMaxSuggested + xOffsetSuggested) / 2; - for (unsigned y = yMid; y >= yOffset && yOffsetSuggested != y; --y) + for (unsigned y = yMid; y >= yOffset && (fabs(yOffsetSuggested - y) > std::numeric_limits::epsilon()); --y) { ColorRgb rgb = image(xMid, y); if (rgb <= noSignalThresholdColor) @@ -147,7 +148,7 @@ bool ScreenshotHandler::findNoSignalSettings(const Image & image) } } - for (unsigned y = yMid; y <= yMax && yMaxSuggested != y; ++y) + for (unsigned y = yMid; y <= yMax && (fabs(yMaxSuggested - y) > std::numeric_limits::epsilon()); ++y) { ColorRgb rgb = image(xMid, y); if (rgb <= noSignalThresholdColor) diff --git a/src/hyperion-v4l2/hyperion-v4l2.cpp b/src/hyperion-v4l2/hyperion-v4l2.cpp index 7a0a09b1..73195177 100644 --- a/src/hyperion-v4l2/hyperion-v4l2.cpp +++ b/src/hyperion-v4l2/hyperion-v4l2.cpp @@ -33,13 +33,6 @@ #include -// Constants -namespace { - - const char SERVICE_TYPE[] = "flatbuffer"; - -} //End of constants - using namespace commandline; int main(int argc, char** argv) diff --git a/src/hyperion-x11/hyperion-x11.cpp b/src/hyperion-x11/hyperion-x11.cpp index 4c386310..d506bf1d 100644 --- a/src/hyperion-x11/hyperion-x11.cpp +++ b/src/hyperion-x11/hyperion-x11.cpp @@ -19,13 +19,6 @@ #endif #include -// Constants -namespace { - - const char SERVICE_TYPE[] = "flatbuffer"; - -} //End of constants - using namespace commandline; // save the image as screenshot diff --git a/src/hyperion-xcb/hyperion-xcb.cpp b/src/hyperion-xcb/hyperion-xcb.cpp index 79c885c8..06a94f8d 100644 --- a/src/hyperion-xcb/hyperion-xcb.cpp +++ b/src/hyperion-xcb/hyperion-xcb.cpp @@ -19,13 +19,6 @@ #endif #include -// Constants -namespace { - - const char SERVICE_TYPE[] = "flatbuffer"; - -} //End of constants - using namespace commandline; // save the image as screenshot diff --git a/test/jsonchecks/jsonschema.py b/test/jsonchecks/jsonschema.py index f1418dc5..8b1e4cca 100644 --- a/test/jsonchecks/jsonschema.py +++ b/test/jsonchecks/jsonschema.py @@ -11,7 +11,6 @@ instance under a schema, and will create a validator for you. from __future__ import division, unicode_literals -import collections import contextlib import datetime import itertools @@ -474,10 +473,10 @@ class _Draft34CommonMixin(object): yield error else: dependencies = _list(dependency) - for dependency in dependencies: - if dependency not in instance: + for depend in dependencies: + if depend not in instance: yield ValidationError( - "%r is a dependency of %r" % (dependency, property) + "%r is a dependency of %r" % (depend, property) ) def validate_enum(self, enums, instance, schema): @@ -512,10 +511,10 @@ class Draft3Validator(ValidatorMixin, _Draft34CommonMixin, object): elif self.is_type(type, "string"): if self.is_type(instance, type): return - else: - yield ValidationError( - _types_msg(instance, types), context=all_errors, - ) + else: + yield ValidationError( + _types_msg(instance, types), context=all_errors, + ) def validate_properties(self, properties, instance, schema): if not self.is_type(instance, "object"): @@ -694,6 +693,7 @@ class Draft4Validator(ValidatorMixin, _Draft34CommonMixin, object): yield error def validate_oneOf(self, oneOf, instance, schema): + first_valid = "" subschemas = enumerate(oneOf) all_errors = [] for index, subschema in subschemas: From 09d1ef4883ded60e8e121776af378a09b8fefaf9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Jan 2023 11:26:47 +0100 Subject: [PATCH 02/23] Bump SamKirkland/FTP-Deploy-Action from 4.3.2 to 4.3.3 (#1552) Bumps [SamKirkland/FTP-Deploy-Action](https://github.com/SamKirkland/FTP-Deploy-Action) from 4.3.2 to 4.3.3. - [Release notes](https://github.com/SamKirkland/FTP-Deploy-Action/releases) - [Commits](https://github.com/SamKirkland/FTP-Deploy-Action/compare/4.3.2...4.3.3) --- updated-dependencies: - dependency-name: SamKirkland/FTP-Deploy-Action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/apt.yml | 2 +- .github/workflows/nightly.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apt.yml b/.github/workflows/apt.yml index abf3d816..c28f5761 100644 --- a/.github/workflows/apt.yml +++ b/.github/workflows/apt.yml @@ -121,7 +121,7 @@ jobs: done - name: Upload packages to APT server (DRAFT) - uses: SamKirkland/FTP-Deploy-Action@4.3.2 + uses: SamKirkland/FTP-Deploy-Action@4.3.3 with: server: apt.hyperion-project.org username: ${{ secrets.APT_USER }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 829e0304..e009245f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -171,7 +171,7 @@ jobs: done - name: Upload packages to nightly server - uses: SamKirkland/FTP-Deploy-Action@4.3.2 + uses: SamKirkland/FTP-Deploy-Action@4.3.3 with: server: nightly.apt.hyperion-project.org username: ${{ secrets.NIGHTLY_USER }} From e3a82b2890a4490b72de33cee7ef1ebc2b396341 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Jan 2023 11:26:58 +0100 Subject: [PATCH 03/23] Bump peter-evans/repository-dispatch from 2.1.0 to 2.1.1 (#1553) Bumps [peter-evans/repository-dispatch](https://github.com/peter-evans/repository-dispatch) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/peter-evans/repository-dispatch/releases) - [Commits](https://github.com/peter-evans/repository-dispatch/compare/v2.1.0...v2.1.1) --- updated-dependencies: - dependency-name: peter-evans/repository-dispatch dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 957e8dba..d4c8461a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: steps: # Dispatch event to build new HyperBian image - name: Dispatch HyperBian build - uses: peter-evans/repository-dispatch@v2.1.0 + uses: peter-evans/repository-dispatch@v2.1.1 if: ${{ github.repository_owner == 'hyperion-project'}} with: repository: hyperion-project/HyperBian From f665f1e1b667dcaf4b945b586e8b2bdee97573a3 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sun, 15 Jan 2023 21:25:39 +0000 Subject: [PATCH 04/23] Update to Protocol Buffers 3.21.12 (#1557) --- dependencies/external/protobuf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/external/protobuf b/dependencies/external/protobuf index 5ad0697c..f0dc78d7 160000 --- a/dependencies/external/protobuf +++ b/dependencies/external/protobuf @@ -1 +1 @@ -Subproject commit 5ad0697c868235f24559dc07f8a42870d160af76 +Subproject commit f0dc78d7e6e331b8c6bb2d5283e06aa26883ca7c From fa7a5b6b56813573b66ee9739d747866ea2fb18b Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Mon, 16 Jan 2023 11:01:28 +0000 Subject: [PATCH 05/23] Various Fixes/Updates (#1549) * Update FindWindowsSDK.cmake * cmake support libcec without version * Ensure Light-Ids are strings * Fix type - do not have dbus as requried * Fixing #1544 * Cleanup * CleanupFix #1551 * Consistently return instance number with JSON replies (#1504) * hyperion-remote- Fix extracting reply for configGet request * Qt 6.6 - Fix iterator finds * Fix test_image2ledsmap * Ensure window.currentHyperionInstanceName is set, cleanup system/log clipboard report * Address protobuf cmake warning * Update License * Update ChangeLog * Address CodeQL and clang findings --- CHANGELOG.md | 11 ++++ LICENSE | 2 +- assets/webconfig/js/content_index.js | 7 ++- assets/webconfig/js/content_logging.js | 46 +++++++++----- assets/webconfig/js/ui_utils.js | 4 ++ assets/webconfig/js/wizard.js | 22 +++++-- cmake/FindWindowsSDK.cmake | 8 ++- cmake/packages.cmake | 2 +- dependencies/CMakeLists.txt | 2 +- include/mdns/MdnsBrowser.h | 19 +----- include/utils/NetUtils.h | 2 +- include/utils/WeakConnect.h | 63 +++++++++++++++++++ include/utils/hyperion.h | 18 +++--- include/utils/jsonschema/QJsonUtils.h | 4 +- libsrc/api/JsonAPI.cpp | 14 ++++- .../utils/jsonschema/QJsonSchemaChecker.cpp | 11 ++-- src/hyperion-aml/hyperion-aml.cpp | 7 --- src/hyperion-dispmanx/hyperion-dispmanx.cpp | 7 --- src/hyperion-osx/hyperion-osx.cpp | 7 --- src/hyperion-remote/JsonConnection.cpp | 12 ++-- src/hyperiond/CMakeLists.txt | 2 +- test/CMakeLists.txt | 14 ++--- test/TestImage2LedsMap.cpp | 5 +- 23 files changed, 187 insertions(+), 102 deletions(-) create mode 100644 include/utils/WeakConnect.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 71a372a2..8f21a1be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Restart correctly, if running as service (#1368) +- Hue-Wizard: In case auto discovery failed, port 80 was not used as default (#1544) +- Send only one reply per Start Instance request (#1551) +- Add instance# in JSON-API replies (aligning to Add instance in websocket response to a subscription #1504 behaviour) +- hyperion-remote: Extracting reply for a configGet request correctly (#1555) + +### Technical +- Add CodeQL for GitHub code scanning +- Update to Protocol Buffers 3.21.12 +- cmake support of libcec without version in lib-name +- Qt6 alignments +- Refactor for Python 3.11 deprecated functions ## Removed diff --git a/LICENSE b/LICENSE index 0a5f4ed4..3968954e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2014-2022 Hyperion Project +Copyright (c) 2014-2023 Hyperion Project Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index 2aea52bf..5a5203ca 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -278,8 +278,9 @@ $(document).ready(function () { if (getStorage('lastSelectedInstance')) removeStorage('lastSelectedInstance') - currentHyperionInstance = 0; - currentHyperionInstanceName = getInstanceNameByIndex(0); + window.currentHyperionInstance = 0; + window.currentHyperionInstanceName = getInstanceNameByIndex(0); + requestServerConfig(); setTimeout(requestServerInfo, 100) setTimeout(requestTokenInfo, 200) @@ -293,7 +294,7 @@ $(document).ready(function () { $('#btn_hypinstanceswitch').toggle(false) // update listing for button - updateUiOnInstance(currentHyperionInstance); + updateUiOnInstance(window.currentHyperionInstance); updateHyperionInstanceListing(); }); diff --git a/assets/webconfig/js/content_logging.js b/assets/webconfig/js/content_logging.js index ac77e959..7a9c791d 100644 --- a/assets/webconfig/js/content_logging.js +++ b/assets/webconfig/js/content_logging.js @@ -35,7 +35,8 @@ $(document).ready(function () { function infoSummary() { var info = ""; - info += 'Hyperion System Summary Report (' + window.serverConfig.general.name + '), Reported instance: ' + window.currentHyperionInstanceName + '\n'; + info += 'Hyperion System Summary Report (' + window.serverConfig.general.name + ')\n'; + info += 'Reported instance: [' + window.currentHyperionInstance + '] - ' + window.currentHyperionInstanceName + '\n'; info += "\n< ----- System information -------------------- >\n"; info += getSystemInfo() + '\n'; @@ -43,22 +44,36 @@ $(document).ready(function () { info += "\n< ----- Configured Instances ------------------ >\n"; var instances = window.serverInfo.instance; for (var i = 0; i < instances.length; i++) { - info += instances[i].instance + ': ' + instances[i].friendly_name + ' Running: ' + instances[i].running + '\n'; + info += instances[i].instance + ': ' + instances[i].friendly_name + ', Running: ' + instances[i].running + '\n'; } info += "\n< ----- This instance's priorities ------------ >\n"; var prios = window.serverInfo.priorities; - for (var i = 0; i < prios.length; i++) { - info += prios[i].priority + ': '; - if (prios[i].visible) { - info += ' VISIBLE!'; + + if (prios.length > 0) { + + for (var i = 0; i < prios.length; i++) { + + var prio = prios[i].priority.toString().padStart(3, '0'); + + info += prio + ': '; + if (prios[i].visible) { + info += ' VISIBLE -'; + } + else { + info += ' INVISIBLE -'; + } + info += ' (' + prios[i].componentId + ')'; + if (prios[i].owner) { + info += ' (Owner: ' + prios[i].owner + ')'; + } + info += '\n'; + } - else { - info += ' '; - } - info += ' (' + prios[i].componentId + ') Owner: ' + prios[i].owner + '\n'; + } else { + info += 'The current priority list is empty!\n'; } - info += 'priorities_autoselect: ' + window.serverInfo.priorities_autoselect + '\n'; + info += 'Autoselect: ' + window.serverInfo.priorities_autoselect + '\n'; info += "\n< ----- This instance components' status ------->\n"; var comps = window.serverInfo.components; @@ -67,7 +82,7 @@ $(document).ready(function () { } info += "\n< ----- This instance's configuration --------- >\n"; - info += JSON.stringify(window.serverConfig) + '\n'; + info += JSON.stringify(window.serverConfig, null, 2) + '\n'; info += "\n< ----- Current Log --------------------------- >\n"; var logMsgs = document.getElementById("logmessages").textContent; @@ -193,18 +208,19 @@ $(document).ready(function () { }); // toggle fullscreen button in log output - $(".fullscreen-btn").mousedown(function(e) { + $(".fullscreen-btn").mousedown(function (e) { e.preventDefault(); }); - $(".fullscreen-btn").click(function(e) { + $(".fullscreen-btn").click(function (e) { e.preventDefault(); $(this).children('i') .toggleClass('fa-expand') .toggleClass('fa-compress'); $('#conf_cont').toggle(); - $('#logmessages').css('max-height', $('#logmessages').css('max-height') !== 'none' ? 'none' : '400px' ); + $('#logmessages').css('max-height', $('#logmessages').css('max-height') !== 'none' ? 'none' : '400px'); }); removeOverlay(); }); + diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index c20c5fc1..84dff5c5 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -171,6 +171,10 @@ function initLanguageSelection() { } function updateUiOnInstance(inst) { + + window.currentHyperionInstance = inst; + window.currentHyperionInstanceName = getInstanceNameByIndex(inst); + $("#active_instance_friendly_name").text(getInstanceNameByIndex(inst)); if (window.serverInfo.instance.filter(entry => entry.running).length > 1) { $('#btn_hypinstanceswitch').toggle(true); diff --git a/assets/webconfig/js/wizard.js b/assets/webconfig/js/wizard.js index dedd50ed..3f88f6db 100755 --- a/assets/webconfig/js/wizard.js +++ b/assets/webconfig/js/wizard.js @@ -854,11 +854,17 @@ function checkUserResult(reply, usr) { function useGroupId(id) { $('#groupId').val(id); - groupLights = hueGroups[id].lights; + + //Ensure ligthIDs are strings + groupLights = hueGroups[id].lights.map(num => { + return String(num); + }); + groupLightsLocations = hueGroups[id].locations; get_hue_lights(); } + async function discover_hue_bridges() { $('#wiz_hue_ipstate').html($.i18n('edt_dev_spec_devices_discovery_inprogress')); $('#wiz_hue_discovered').html("") @@ -1016,11 +1022,11 @@ function beginWizardHue() { $('#host').val(host); var port = eV("port"); - if (port > 0) { - $('#port').val(port); + if (port == 0) { + $('#port').val(80); } else { - $('#port').val(''); + $('#port').val(port); } hueIPs.unshift({ host: host, port: port }); @@ -1037,7 +1043,13 @@ function beginWizardHue() { hueIPs = []; hueIPsinc = 0; - hueIPs.push({ host: $('#host').val(), port: $('#port').val() }); + + var port = $('#port').val(); + if (isNaN(port) || port < 1 || port > 65535) { + port = 80; + $('#port').val(80); + } + hueIPs.push({ host: $('#host').val(), port: port }); } else { discover_hue_bridges(); diff --git a/cmake/FindWindowsSDK.cmake b/cmake/FindWindowsSDK.cmake index 747d9bc4..6db4225e 100644 --- a/cmake/FindWindowsSDK.cmake +++ b/cmake/FindWindowsSDK.cmake @@ -1,4 +1,4 @@ -# - Find the Windows SDK aka Platform SDK +# - Find the Windows SDK aka Platform SDK (from https://github.com/rpavlik/cmake-modules) # # Relevant Wikipedia article: http://en.wikipedia.org/wiki/Microsoft_Windows_SDK # @@ -49,10 +49,11 @@ # http://academic.cleardefinition.com # Iowa State University HCI Graduate Program/VRAC # -# Copyright Iowa State University 2012. +# Copyright 2012, Iowa State University # Distributed under the Boost Software License, Version 1.0. # (See accompanying file LICENSE_1_0.txt or copy at # http://www.boost.org/LICENSE_1_0.txt) +# SPDX-License-Identifier: BSL-1.0 set(_preferred_sdk_dirs) # pre-output set(_win_sdk_dirs) # pre-output @@ -76,6 +77,9 @@ endmacro() # although version numbers listed on that page don't necessarily match the directory # used by the installer. set(_winsdk_win10vers + 10.0.22000.0 + 10.0.20348.0 + 10.0.19041.0 10.0.18362.0 # Win10 1903 "19H1" 10.0.17763.0 # Win10 1809 "October 2018 Update" 10.0.17134.0 # Redstone 4 aka Win10 1803 "April 2018 Update" diff --git a/cmake/packages.cmake b/cmake/packages.cmake index bc73057d..1dd8f6a6 100644 --- a/cmake/packages.cmake +++ b/cmake/packages.cmake @@ -72,7 +72,7 @@ endif() # .deb files for apt # https://cmake.org/cmake/help/latest/cpack_gen/deb.html SET ( CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_SOURCE_DIR}/cmake/package-scripts/preinst;${CMAKE_SOURCE_DIR}/cmake/package-scripts/postinst;${CMAKE_SOURCE_DIR}/cmake/package-scripts/prerm" ) -SET ( CPACK_DEBIAN_PACKAGE_DEPENDS "libcec6 | libcec4" ) +SET ( CPACK_DEBIAN_PACKAGE_DEPENDS "libcec6 | libcec4 | libcec (>= 4.0)" ) SET ( CPACK_DEBIAN_PACKAGE_SECTION "Miscellaneous" ) # .rpm for rpm diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index f64b0d38..af2232ab 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -150,7 +150,7 @@ if(ENABLE_PROTOBUF_SERVER) set(protobuf_MSVC_STATIC_RUNTIME OFF CACHE BOOL "Build protobuf static") endif() - add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/external/protobuf/cmake") + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/external/protobuf") # define the include for the protobuf library set(PROTOBUF_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/external/protobuf/src") diff --git a/include/mdns/MdnsBrowser.h b/include/mdns/MdnsBrowser.h index b88a7f11..7ee8009f 100644 --- a/include/mdns/MdnsBrowser.h +++ b/include/mdns/MdnsBrowser.h @@ -19,6 +19,7 @@ // Utility includes #include +#include namespace { constexpr std::chrono::milliseconds DEFAULT_DISCOVER_TIMEOUT{ 500 }; @@ -103,24 +104,6 @@ private slots: void onServiceRemoved(const QMdnsEngine::Service& service); private: - - template ::value, int> = 0> - static inline QMetaObject::Connection weakConnect(typename QtPrivate::FunctionPointer::Object* sender, - Func1 signal, - Func2 slot) - { - QMetaObject::Connection conn_normal = QObject::connect(sender, signal, slot); - - QMetaObject::Connection* conn_delete = new QMetaObject::Connection(); - - *conn_delete = QObject::connect(sender, signal, [conn_normal, conn_delete]() { - QObject::disconnect(conn_normal); - QObject::disconnect(*conn_delete); - delete conn_delete; - }); - return conn_normal; - } - /// The logger instance for mDNS-Service Logger* _log; diff --git a/include/utils/NetUtils.h b/include/utils/NetUtils.h index 3b247e49..9020539b 100644 --- a/include/utils/NetUtils.h +++ b/include/utils/NetUtils.h @@ -51,7 +51,7 @@ namespace NetUtils { { if ((port <= 0 || port > MAX_PORT) && port != -1) { - Error(log, "Invalid port [%d] for host: (%s)! - Port must be in range [0 - %d]", port, QSTRING_CSTR(host), MAX_PORT); + Error(log, "Invalid port [%d] for host: (%s)! - Port must be in range [1 - %d]", port, QSTRING_CSTR(host), MAX_PORT); return false; } return true; diff --git a/include/utils/WeakConnect.h b/include/utils/WeakConnect.h new file mode 100644 index 00000000..bcfaacfe --- /dev/null +++ b/include/utils/WeakConnect.h @@ -0,0 +1,63 @@ +#ifndef WEAKCONNECT_H +#define WEAKCONNECT_H + +#include + +// Qt includes +#include + +template ::value, int> = 0> +static inline QMetaObject::Connection weakConnect(typename QtPrivate::FunctionPointer::Object* sender, + Func1 signal, + typename QtPrivate::FunctionPointer::Object* receiver, + Func2 slot) +{ + QMetaObject::Connection conn_normal = QObject::connect(sender, signal, receiver, slot); + + QMetaObject::Connection* conn_delete = new QMetaObject::Connection(); + + *conn_delete = QObject::connect(sender, signal, [conn_normal, conn_delete]() { + QObject::disconnect(conn_normal); + QObject::disconnect(*conn_delete); + delete conn_delete; + }); + return conn_normal; +} + +template ::value, int> = 0> +static inline QMetaObject::Connection weakConnect(typename QtPrivate::FunctionPointer::Object* sender, + Func1 signal, + Func2 slot) +{ + QMetaObject::Connection conn_normal = QObject::connect(sender, signal, slot); + + QMetaObject::Connection* conn_delete = new QMetaObject::Connection(); + + *conn_delete = QObject::connect(sender, signal, [conn_normal, conn_delete]() { + QObject::disconnect(conn_normal); + QObject::disconnect(*conn_delete); + delete conn_delete; + }); + return conn_normal; +} + +template ::value, int> = 0> +static inline QMetaObject::Connection weakConnect(typename QtPrivate::FunctionPointer::Object* sender, + Func1 signal, + typename QtPrivate::FunctionPointer::Object* receiver, + Func2 slot) +{ + Q_UNUSED(receiver); + QMetaObject::Connection conn_normal = QObject::connect(sender, signal, slot); + + QMetaObject::Connection* conn_delete = new QMetaObject::Connection(); + + *conn_delete = QObject::connect(sender, signal, [conn_normal, conn_delete]() { + QObject::disconnect(conn_normal); + QObject::disconnect(*conn_delete); + delete conn_delete; + }); + return conn_normal; +} + +#endif // WEAKCONNECT_H diff --git a/include/utils/hyperion.h b/include/utils/hyperion.h index 2a71c3ce..774267a8 100644 --- a/include/utils/hyperion.h +++ b/include/utils/hyperion.h @@ -19,7 +19,7 @@ /// namespace hyperion { - void handleInitialEffect(Hyperion* hyperion, const QJsonObject& FGEffectConfig) + static void handleInitialEffect(Hyperion* hyperion, const QJsonObject& FGEffectConfig) { #define FGCONFIG_ARRAY fgColorConfig.toArray() @@ -63,12 +63,12 @@ namespace hyperion { #undef FGCONFIG_ARRAY } - ColorOrder createColorOrder(const QJsonObject &deviceConfig) + static ColorOrder createColorOrder(const QJsonObject &deviceConfig) { return stringToColorOrder(deviceConfig["colorOrder"].toString("rgb")); } - RgbTransform createRgbTransform(const QJsonObject& colorConfig) + static RgbTransform createRgbTransform(const QJsonObject& colorConfig) { const double backlightThreshold = colorConfig["backlightThreshold"].toDouble(0.0); const bool backlightColored = colorConfig["backlightColored"].toBool(false); @@ -81,7 +81,7 @@ namespace hyperion { return RgbTransform(gammaR, gammaG, gammaB, backlightThreshold, backlightColored, static_cast(brightness), static_cast(brightnessComp)); } - OkhsvTransform createOkhsvTransform(const QJsonObject& colorConfig) + static OkhsvTransform createOkhsvTransform(const QJsonObject& colorConfig) { const double saturationGain = colorConfig["saturationGain"].toDouble(1.0); const double brightnessGain = colorConfig["brightnessGain"].toDouble(1.0); @@ -89,7 +89,7 @@ namespace hyperion { return OkhsvTransform(saturationGain, brightnessGain); } - RgbChannelAdjustment createRgbChannelAdjustment(const QJsonObject& colorConfig, const QString& channelName, int defaultR, int defaultG, int defaultB) + static RgbChannelAdjustment createRgbChannelAdjustment(const QJsonObject& colorConfig, const QString& channelName, int defaultR, int defaultG, int defaultB) { const QJsonArray& channelConfig = colorConfig[channelName].toArray(); return RgbChannelAdjustment( @@ -100,7 +100,7 @@ namespace hyperion { ); } - ColorAdjustment* createColorAdjustment(const QJsonObject & adjustmentConfig) + static ColorAdjustment* createColorAdjustment(const QJsonObject & adjustmentConfig) { const QString id = adjustmentConfig["id"].toString("default"); @@ -120,7 +120,7 @@ namespace hyperion { return adjustment; } - MultiColorAdjustment * createLedColorsAdjustment(int ledCnt, const QJsonObject & colorConfig) + static MultiColorAdjustment * createLedColorsAdjustment(int ledCnt, const QJsonObject & colorConfig) { // Create the result, the transforms are added to this MultiColorAdjustment * adjustment = new MultiColorAdjustment(ledCnt); @@ -184,7 +184,7 @@ namespace hyperion { * @param deviceOrder The default RGB channel ordering * @return The constructed ledstring */ - LedString createLedString(const QJsonArray& ledConfigArray, const ColorOrder deviceOrder) + static LedString createLedString(const QJsonArray& ledConfigArray, const ColorOrder deviceOrder) { LedString ledString; const QString deviceOrderStr = colorOrderToString(deviceOrder); @@ -215,7 +215,7 @@ namespace hyperion { return ledString; } - QSize getLedLayoutGridSize(const QJsonArray& ledConfigArray) + static QSize getLedLayoutGridSize(const QJsonArray& ledConfigArray) { std::vector midPointsX; std::vector midPointsY; diff --git a/include/utils/jsonschema/QJsonUtils.h b/include/utils/jsonschema/QJsonUtils.h index 63c22d34..98498a2b 100644 --- a/include/utils/jsonschema/QJsonUtils.h +++ b/include/utils/jsonschema/QJsonUtils.h @@ -57,7 +57,9 @@ public: break; } case QJsonValue::Object: - ret = getDefaultValue(value.toObject().find("default").value()); + { + ret = getDefaultValue(value.toObject().value("default")); + } break; case QJsonValue::Bool: return value.toBool() ? "True" : "False"; diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index bfa76b0a..e4418d54 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -21,6 +21,8 @@ #include #include +#include + #if defined(ENABLE_MF) #include #elif defined(ENABLE_V4L2) @@ -1453,7 +1455,7 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject &message, const QString & void JsonAPI::handleInstanceCommand(const QJsonObject &message, const QString &command, int tan) { const QString &subc = message["subcommand"].toString(); - const quint8 &inst = message["instance"].toInt(); + const quint8 &inst = static_cast(message["instance"].toInt()); const QString &name = message["name"].toString(); if (subc == "switchTo") @@ -1471,7 +1473,12 @@ void JsonAPI::handleInstanceCommand(const QJsonObject &message, const QString &c if (subc == "startInstance") { - connect(this, &API::onStartInstanceResponse, [=] (const int &tan) { sendSuccessReply(command + "-" + subc, tan); }); + //Only send update once + weakConnect(this, &API::onStartInstanceResponse, [this, command, subc] (int tan) + { + sendSuccessReply(command + "-" + subc, tan); + }); + if (!API::startInstance(inst, tan)) sendErrorReply("Can't start Hyperion instance index " + QString::number(inst), command + "-" + subc, tan); @@ -1825,6 +1832,7 @@ void JsonAPI::sendSuccessReply(const QString &command, int tan) { // create reply QJsonObject reply; + reply["instance"] = _hyperion->getInstanceIndex(); reply["success"] = true; reply["command"] = command; reply["tan"] = tan; @@ -1836,6 +1844,7 @@ void JsonAPI::sendSuccessReply(const QString &command, int tan) void JsonAPI::sendSuccessDataReply(const QJsonDocument &doc, const QString &command, int tan) { QJsonObject reply; + reply["instance"] = _hyperion->getInstanceIndex(); reply["success"] = true; reply["command"] = command; reply["tan"] = tan; @@ -1851,6 +1860,7 @@ void JsonAPI::sendErrorReply(const QString &error, const QString &command, int t { // create reply QJsonObject reply; + reply["instance"] = _hyperion->getInstanceIndex(); reply["success"] = false; reply["error"] = error; reply["command"] = command; diff --git a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp index 4158bfb2..edb49a5b 100644 --- a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp +++ b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp @@ -64,7 +64,7 @@ QJsonObject QJsonSchemaChecker::getAutoCorrectedConfig(const QJsonObject& value, _messages.clear(); _autoCorrected = value; - for (const QString& correct : sequence) + for (const QString& correct : qAsConst(sequence)) { _correct = correct; _currentPath.clear(); @@ -234,7 +234,6 @@ void QJsonSchemaChecker::checkProperties(const QJsonObject& value, const QJsonOb const QJsonValue& propertyValue = *i; _currentPath.append("." + property); - QJsonObject::const_iterator required = propertyValue.toObject().find("required"); if (value.contains(property)) { @@ -242,7 +241,8 @@ void QJsonSchemaChecker::checkProperties(const QJsonObject& value, const QJsonOb } else if (!verifyDeps(property, value, schema)) { - if (required != propertyValue.toObject().end() && propertyValue.toObject().find("required").value().toBool() && !_ignoreRequired) + bool isRequired = propertyValue.toObject().value("required").toBool(false); + if (isRequired && !_ignoreRequired) { _error = true; @@ -273,9 +273,10 @@ bool QJsonSchemaChecker::verifyDeps(const QString& property, const QJsonObject& { const QJsonObject& depends = ((schema[property].toObject())["options"].toObject())["dependencies"].toObject(); - if (depends.keys().size() > 0) + const QStringList dependsKeys = depends.keys(); + if (!dependsKeys.isEmpty()) { - QString firstName = depends.keys().first(); + const QString firstName = dependsKeys.constFirst(); if (value.contains(firstName)) { if (value[firstName] != depends[firstName]) diff --git a/src/hyperion-aml/hyperion-aml.cpp b/src/hyperion-aml/hyperion-aml.cpp index 15895447..7bcc106e 100644 --- a/src/hyperion-aml/hyperion-aml.cpp +++ b/src/hyperion-aml/hyperion-aml.cpp @@ -21,13 +21,6 @@ #include -// Constants -namespace { - - const char SERVICE_TYPE[] = "flatbuffer"; - -} //End of constants - using namespace commandline; // save the image as screenshot diff --git a/src/hyperion-dispmanx/hyperion-dispmanx.cpp b/src/hyperion-dispmanx/hyperion-dispmanx.cpp index 07ccf37f..55ccee04 100644 --- a/src/hyperion-dispmanx/hyperion-dispmanx.cpp +++ b/src/hyperion-dispmanx/hyperion-dispmanx.cpp @@ -22,13 +22,6 @@ #include -// Constants -namespace { - - const char SERVICE_TYPE[] = "flatbuffer"; - -} //End of constants - using namespace commandline; // save the image as screenshot diff --git a/src/hyperion-osx/hyperion-osx.cpp b/src/hyperion-osx/hyperion-osx.cpp index b94b2881..7a907935 100644 --- a/src/hyperion-osx/hyperion-osx.cpp +++ b/src/hyperion-osx/hyperion-osx.cpp @@ -20,13 +20,6 @@ #include -// Constants -namespace { - - const char SERVICE_TYPE[] = "flatbuffer"; - -} //End of constants - using namespace commandline; // save the image as screenshot diff --git a/src/hyperion-remote/JsonConnection.cpp b/src/hyperion-remote/JsonConnection.cpp index 5bad16a4..669a5bf4 100644 --- a/src/hyperion-remote/JsonConnection.cpp +++ b/src/hyperion-remote/JsonConnection.cpp @@ -215,7 +215,7 @@ QJsonObject JsonConnection::getServerInfo() { if (!reply.contains("info") || !reply["info"].isObject()) { - throw std::runtime_error("No info available in result"); + throw std::runtime_error("No info available in reply"); } return reply["info"].toObject(); @@ -240,7 +240,7 @@ QString JsonConnection::getSysInfo() { if (!reply.contains("info") || !reply["info"].isObject()) { - throw std::runtime_error("No info available in result"); + throw std::runtime_error("No info available in reply"); } QJsonDocument doc(reply["info"].toObject()); @@ -417,12 +417,12 @@ QString JsonConnection::getConfig(std::string type) // parse reply message if (parseReply(reply)) { - if (!reply.contains("result") || !reply["result"].isObject()) + if (!reply.contains("info") || !reply["info"].isObject()) { - throw std::runtime_error("No configuration file available in result"); + throw std::runtime_error("No configuration file available in reply"); } - QJsonDocument doc(reply["result"].toObject()); + QJsonDocument doc(reply["info"].toObject()); QString result(doc.toJson(QJsonDocument::Indented)); return result; } @@ -442,7 +442,7 @@ void JsonConnection::setConfig(const QString &jsonString) QJsonObject configObj; if(!JsonUtils::parse("hyperion-remote-args", jsonString, configObj, _log)) { - throw std::runtime_error("Error in configset arguments, abort"); + throw std::runtime_error("Error in configSet arguments, abort"); } command["config"] = configObj; diff --git a/src/hyperiond/CMakeLists.txt b/src/hyperiond/CMakeLists.txt index 282d11c0..dd0ec5b5 100644 --- a/src/hyperiond/CMakeLists.txt +++ b/src/hyperiond/CMakeLists.txt @@ -31,7 +31,7 @@ if (APPLE) endif(APPLE) if (UNIX) - find_package(Qt${QT_VERSION_MAJOR} COMPONENTS DBus REQUIRED ) + find_package(Qt${QT_VERSION_MAJOR} COMPONENTS DBus QUIET ) if (Qt${QT_VERSION_MAJOR}DBus_FOUND) set(hyperiond_POWER_MNG_DBUS "Qt${QT_VERSION_MAJOR}::DBus") endif() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9e5fe6a4..0e447f57 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -42,16 +42,16 @@ endif(ENABLE_X11) add_executable(test_versions TestVersions.cpp) target_link_libraries(test_versions Qt${QT_VERSION_MAJOR}::Core) +add_executable(test_image2ledsmap TestImage2LedsMap.cpp "${CMAKE_BINARY_DIR}/resources.qrc" ) +link_to_hyperion(test_image2ledsmap) + ######### These tests are broken. May they fix someone ########## -# add_executable(test_image2ledsmap TestImage2LedsMap.cpp) -# link_to_hyperion(test_image2ledsmap) - -# if (ENABLE_DISPMANX) +#if (ENABLE_DISPMANX) # add_subdirectory(dispmanx2png) -# endif (ENABLE_DISPMANX) +#endif (ENABLE_DISPMANX) -# add_executable(test_blackborderprocessor TestBlackBorderProcessor.cpp) -# link_to_hyperion(test_blackborderprocessor) +#add_executable(test_blackborderprocessor TestBlackBorderProcessor.cpp) +#link_to_hyperion(test_blackborderprocessor) ################################################### diff --git a/test/TestImage2LedsMap.cpp b/test/TestImage2LedsMap.cpp index aa80926c..052a21ef 100644 --- a/test/TestImage2LedsMap.cpp +++ b/test/TestImage2LedsMap.cpp @@ -9,10 +9,9 @@ int main() { - QString homeDir = getenv("RASPILIGHT_HOME"); + const QString schemaFile = ":/hyperion-schema"; + const QString configFile = ":/hyperion_default.config"; - const QString schemaFile = homeDir + "/hyperion.schema.json"; - const QString configFile = homeDir + "/hyperion.config.json"; QJsonObject config; if (QJsonFactory::load(schemaFile, configFile, config) < 0) From f327b5063ea789ef7a72b46f0b35f735206600c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Feb 2023 19:15:29 +0100 Subject: [PATCH 06/23] Bump actions/download-artifact from 3.0.1 to 3.0.2 (#1562) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v3.0.1...v3.0.2) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/apt.yml | 2 +- .github/workflows/nightly.yml | 2 +- .github/workflows/push-master.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/apt.yml b/.github/workflows/apt.yml index c28f5761..fc5eca13 100644 --- a/.github/workflows/apt.yml +++ b/.github/workflows/apt.yml @@ -108,7 +108,7 @@ jobs: reprepro -Vb apt export - name: Download artifacts - uses: actions/download-artifact@v3.0.1 + uses: actions/download-artifact@v3.0.2 - name: Include artifacts into the package source run: | diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index e009245f..75365c4c 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -158,7 +158,7 @@ jobs: reprepro -Vb nightly export - name: Download artifacts - uses: actions/download-artifact@v3.0.1 + uses: actions/download-artifact@v3.0.2 - name: Include artifacts into the package source run: | diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml index 78584474..c820a447 100644 --- a/.github/workflows/push-master.yml +++ b/.github/workflows/push-master.yml @@ -171,7 +171,7 @@ jobs: # Download artifacts from previous build process - name: Download artifacts - uses: actions/download-artifact@v3.0.1 + uses: actions/download-artifact@v3.0.2 with: path: artifacts From ef7ceb0bbfa7e6ae1f16b3158874c6da933397c8 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sun, 5 Feb 2023 14:19:25 +0000 Subject: [PATCH 07/23] Update to Mbed TLS 3.3.0 (#1558) * Convert mbedtls to subproject * Align cmake to subproject and mbedtls 3.3.0 * mdebtls 3.3.0 * Add cxx_std_20 for Windows build --- .gitmodules | 3 + dependencies/CMakeLists-mbedtls.txt.in | 37 ------------- dependencies/CMakeLists.txt | 76 +++----------------------- dependencies/external/mbedtls | 1 + libsrc/leddevice/CMakeLists.txt | 6 +- 5 files changed, 18 insertions(+), 105 deletions(-) delete mode 100644 dependencies/CMakeLists-mbedtls.txt.in create mode 160000 dependencies/external/mbedtls diff --git a/.gitmodules b/.gitmodules index 2fbcb55e..624c175c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -12,3 +12,6 @@ [submodule "dependencies/external/qmdnsengine"] path = dependencies/external/qmdnsengine url = https://github.com/nitroshare/qmdnsengine.git +[submodule "dependencies/external/mbedtls"] + path = dependencies/external/mbedtls + url = ../../Mbed-TLS/mbedtls.git diff --git a/dependencies/CMakeLists-mbedtls.txt.in b/dependencies/CMakeLists-mbedtls.txt.in deleted file mode 100644 index 76e177c7..00000000 --- a/dependencies/CMakeLists-mbedtls.txt.in +++ /dev/null @@ -1,37 +0,0 @@ -cmake_minimum_required(VERSION 3.2) - -project(mbedtls) - -set(DOWNLOAD_DIR "@MBEDTLS_DOWNLOAD_DIR@") -set(SOURCE_DIR "@MBEDTLS_SOURCE_DIR@") -set(BINARY_DIR "@MBEDTLS_BINARY_DIR@") -set(INSTALL_DIR "@MBEDTLS_INSTALL_DIR@") -set(CMAKE_ARGS "@MBEDTLS_CMAKE_ARGS@") -set(LOGGING "@MBEDTLS_LOGGING@") - -include(ExternalProject) - -ExternalProject_Add( - mbedtls - GIT_REPOSITORY "https://github.com/ARMmbed/mbedtls.git" - GIT_TAG "v3.1.0" # Bump versions manually if necessary, do not rely on origin/master to be stable - BUILD_ALWAYS OFF - DOWNLOAD_DIR "${DOWNLOAD_DIR}" - SOURCE_DIR "${SOURCE_DIR}" - BINARY_DIR "${BINARY_DIR}" - INSTALL_DIR "${INSTALL_DIR}" - CMAKE_ARGS ${CMAKE_ARGS} - CONFIGURE_COMMAND "" - UPDATE_COMMAND "" - BUILD_COMMAND "" - INSTALL_COMMAND "" - LOG_DOWNLOAD ${LOGGING} - LOG_UPDATE ${LOGGING} - LOG_PATCH ${LOGGING} - LOG_CONFIGURE ${LOGGING} - LOG_BUILD ${LOGGING} - LOG_INSTALL ${LOGGING} - LOG_TEST ${LOGGING} - LOG_MERGED_STDOUTERR ${LOGGING} - LOG_OUTPUT_ON_FAILURE ${LOGGING} -) diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index af2232ab..858454ee 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -265,15 +265,19 @@ if(ENABLE_DEV_NETWORK) set(DEFAULT_USE_SYSTEM_MBEDTLS_LIBS OFF PARENT_SCOPE) set(USE_SYSTEM_MBEDTLS_LIBS OFF) endif (NOT MBEDTLS_FOUND) - endif (USE_SYSTEM_MBEDTLS_LIBS) - - if (NOT USE_SYSTEM_MBEDTLS_LIBS) +else() cmake_minimum_required(VERSION 3.2) + + set(CMAKE_POLICY_DEFAULT_CMP0071 NEW) + set(DEFAULT_USE_SYSTEM_MBEDTLS_LIBS OFF CACHE BOOL "system mbedtls libraries not found, disable use system mbedtls libraries") set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared mbedtls libraries") set(ENABLE_TESTING OFF CACHE BOOL "Disable mbedTLS tests") + set(GEN_FILES OFF CACHE BOOL "Disable mbedTLS auto-generated files") set(ENABLE_PROGRAMS OFF CACHE BOOL "Disable mbedTLS programs") + #set(LINK_WITH_PTHREAD ON CACHE BOOL "Enable mbedTLS library linked to pthread.") + set(USE_SHARED_MBEDTLS_LIBRARY OFF CACHE BOOL "Disable mbedTLS shared libraries") set(USE_STATIC_MBEDTLS_LIBRARY ON CACHE BOOL "Enable mbedTLS static libraries") @@ -287,69 +291,7 @@ if(ENABLE_DEV_NETWORK) set(MBEDTLS_LOGGING 0) endif () - set(MBEDTLS_CMAKE_ARGS - -DCMAKE_INSTALL_PREFIX:PATH=${MBEDTLS_INSTALL_DIR} - -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} - -DUSE_SHARED_MBEDTLS_LIBRARY:BOOL=OFF - -DUSE_STATIC_MBEDTLS_LIBRARY:BOOL=ON - -DENABLE_TESTING:BOOL=OFF - -DENABLE_PROGRAMS:BOOL=OFF - -DLINK_WITH_PTHREAD:BOOL=ON - -Wno-dev - #-DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=TRUE - ) - - set(ENABLE_MBEDTLS_FETCH_CONTENT ON) - - if (ENABLE_MBEDTLS_FETCH_CONTENT AND CMAKE_VERSION VERSION_GREATER_EQUAL 3.11) - - include(FetchContent) - - FetchContent_Declare( - mbedtls - GIT_REPOSITORY https://github.com/ARMmbed/mbedtls.git - GIT_TAG "v3.1.0" # Bump versions manually if necessary, do not rely on origin/master to be stable - BUILD_ALWAYS OFF - GIT_PROGRESS 1 - DOWNLOAD_DIR "${MBEDTLS_DOWNLOAD_DIR}" - SOURCE_DIR "${MBEDTLS_SOURCE_DIR}" - BINARY_DIR "${MBEDTLS_BINARY_DIR}" - INSTALL_DIR "${MBEDTLS_INSTALL_DIR}" - CMAKE_ARGS ${MBEDTLS_CMAKE_ARGS} - BUILD_COMMAND "" - INSTALL_COMMAND "" - LOG_DOWNLOAD ${MBEDTLS_LOGGING} - LOG_UPDATE ${MBEDTLS_LOGGING} - LOG_PATCH ${MBEDTLS_LOGGING} - LOG_CONFIGURE ${MBEDTLS_LOGGING} - LOG_BUILD ${MBEDTLS_LOGGING} - LOG_INSTALL ${MBEDTLS_LOGGING} - LOG_TEST ${MBEDTLS_LOGGING} - LOG_MERGED_STDOUTERR ${MBEDTLS_LOGGING} - LOG_OUTPUT_ON_FAILURE ${MBEDTLS_LOGGING} - ) - - if (CMAKE_VERSION VERSION_LESS 3.14) - macro (FetchContent_MakeAvailable NAME) - FetchContent_GetProperties(${NAME}) - if (NOT ${NAME}_POPULATED) - FetchContent_Populate(${NAME}) - add_subdirectory(${${NAME}_SOURCE_DIR} ${${NAME}_BINARY_DIR}) - endif () - endmacro () - endif () - - FetchContent_MakeAvailable(mbedtls) - else () - set(ENABLE_MBEDTLS_FETCH_CONTENT OFF PARENT_SCOPE) - if(NOT DEFINED BUILD_MBEDTLS_ONCE) - set(BUILD_MBEDTLS_ONCE CACHE INTERNAL "Done") - configure_file(${CMAKE_SOURCE_DIR}/dependencies/CMakeLists-mbedtls.txt.in ${MBEDTLS_DOWNLOAD_DIR}/CMakeLists.txt @ONLY) - execute_process(COMMAND ${CMAKE_COMMAND} -DCMAKE_INSTALL_PREFIX:PATH=${MBEDTLS_INSTALL_DIR} -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} -G "${CMAKE_GENERATOR}" . WORKING_DIRECTORY ${MBEDTLS_DOWNLOAD_DIR}) - execute_process(COMMAND ${CMAKE_COMMAND} --build . WORKING_DIRECTORY ${MBEDTLS_DOWNLOAD_DIR}) - add_subdirectory(${MBEDTLS_SOURCE_DIR} ${MBEDTLS_BINARY_DIR}) - endif() - endif () + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/external/mbedtls) set (MBEDTLS_INCLUDE_DIR "${MBEDTLS_SOURCE_DIR}/include") set (MBEDTLS_INCLUDE_DIR ${MBEDTLS_INCLUDE_DIR} PARENT_SCOPE) @@ -387,5 +329,5 @@ if(ENABLE_DEV_NETWORK) mark_as_advanced (MBEDTLS_INCLUDE_DIR MBEDTLS_LIBRARIES MBEDTLS_SSL_LIBRARY MBEDTLS_X509_LIBRARY MBEDTLS_CRYPTO_LIBRARY) - endif (NOT USE_SYSTEM_MBEDTLS_LIBS) + endif (USE_SYSTEM_MBEDTLS_LIBS) endif(ENABLE_DEV_NETWORK) diff --git a/dependencies/external/mbedtls b/dependencies/external/mbedtls new file mode 160000 index 00000000..8c892249 --- /dev/null +++ b/dependencies/external/mbedtls @@ -0,0 +1 @@ +Subproject commit 8c89224991adff88d53cd380f42a2baa36f91454 diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 4da16a1d..233bb9fd 100644 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -118,7 +118,10 @@ if(ENABLE_DEV_NETWORK) string(REGEX MATCH "[0-9]+|-([A-Za-z0-9_.]+)" MBEDTLS_MAJOR ${MBEDTLS_VERSION}) if (MBEDTLS_MAJOR EQUAL "3") target_compile_definitions(leddevice PRIVATE USE_MBEDTLS3) - endif() + if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + target_compile_features(leddevice PRIVATE cxx_std_20) + endif() + endif() endif() if(ENABLE_DEV_SERIAL) @@ -160,3 +163,4 @@ endif() if(ENABLE_MDNS) target_link_libraries(leddevice mdns) endif() + From a57bcbc2b8719eb61d45fa8210e6776ccb3729ee Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Tue, 7 Feb 2023 07:15:22 +0000 Subject: [PATCH 08/23] WLED segment streaming support (#1556) * WLED segment streaming support * Address CodeQL findings * WLED - Remove that interim color is shown when WLED is powered off * Allow LEDDevice to stay on after streaming * Apply stayOn on segment streamed to * Fix LED-Matrix Layout: Add Cabling direction selection element again --- assets/webconfig/content/conf_leds.html | 11 + assets/webconfig/i18n/en.json | 29 +- assets/webconfig/js/content_leds.js | 297 +- assets/webconfig/js/ui_utils.js | 2737 +++++++++-------- include/leddevice/LedDevice.h | 11 +- libsrc/leddevice/LedDevice.cpp | 59 +- libsrc/leddevice/dev_net/LedDeviceWled.cpp | 361 ++- libsrc/leddevice/dev_net/LedDeviceWled.h | 25 +- libsrc/leddevice/dev_serial/ProviderRs232.cpp | 4 +- libsrc/leddevice/dev_serial/ProviderRs232.h | 5 +- libsrc/leddevice/schemas/schema-wled.json | 65 +- 11 files changed, 2078 insertions(+), 1526 deletions(-) diff --git a/assets/webconfig/content/conf_leds.html b/assets/webconfig/content/conf_leds.html index e7ff559f..3229c206 100755 --- a/assets/webconfig/content/conf_leds.html +++ b/assets/webconfig/content/conf_leds.html @@ -322,6 +322,17 @@ + + + + + + + + diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index bb3453b8..445eb0fb 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -58,11 +58,13 @@ "conf_leds_contr_label_contrtype": "Controller type:", "conf_leds_device_info_log": "In case your LEDs do not work, check here for errors:", "conf_leds_device_intro": "Hyperion supports a lot of controllers to transmit data to your target device. Select a LED controller out of the sorted list and configure it. We have chosen the best default settings for each device.", - "conf_leds_error_get_properties_text" : "Failed to get the device's properties. Please check the configuration items.", - "conf_leds_error_get_properties_title" : "Device properties", + "conf_leds_error_get_properties_text": "Failed to get the device's properties. Please check the configuration items.", + "conf_leds_error_get_properties_title": "Device properties", "conf_leds_error_hwled_gt_layout": "The hardware LED count ($1) is greater than LEDs configured via layout ($2),
    $3 {{plural:$3|LED|LEDs}} will stay black if you continue.", "conf_leds_error_hwled_lt_layout": "The hardware LED count ($1) is less than LEDs configured via layout ($2).
    The number of LEDs configured in the layout must not exceed the available LEDs", "conf_leds_error_hwled_gt_maxled": "The hardware LED count ($1) is greater than the maximum number of LEDs supported by the device ($2).
    The hardware LED count is set to ($3).", + "conf_leds_error_hwled_gt_maxled_protocol": "The hardware LED count ($1) is greater than the maximum number of LEDs supported by the streaming protocol ($2).
    The streaming protocol will be changed to ($3).", + "conf_leds_error_wled_segment_missing": "The currently configured segment ($1) is not configured at your WLED device.
    You might need to check the WLED configuration!
    The configuration page represents the current WLED setup.", "conf_leds_info_ws281x": "Hyperion must run with 'root' privileges for this controller type!", "conf_leds_layout_advanced": "Advanced Settings", "conf_leds_layout_blacklist_num_title": "Number of LEDs", @@ -558,7 +560,7 @@ "edt_dev_spec_brightnessOverwrite_title": "Overwrite brightness", "edt_dev_spec_brightnessThreshold_title": "Signal detection brightness minimum", "edt_dev_spec_brightness_title": "Brightness", - "edt_dev_spec_candyGamma_title" : "'Candy' mode (double gamma correction)", + "edt_dev_spec_candyGamma_title": "'Candy' mode (double gamma correction)", "edt_dev_spec_chanperfixture_title": "Channels per Fixture", "edt_dev_spec_cid_title": "CID", "edt_dev_spec_clientKey_title": "Clientkey", @@ -615,15 +617,22 @@ "edt_dev_spec_razer_device_title": "Razer Chroma Device", "edt_dev_spec_restoreOriginalState_title": "Restore lights' state", "edt_dev_spec_restoreOriginalState_title_info": "Restore the device's original state when device is disabled", - "edt_dev_spec_rgbw_calibration_enable" : "White channel calibration (RGBW only)", - "edt_dev_spec_rgbw_calibration_limit" : "White channel limit", - "edt_dev_spec_rgbw_calibration_red" : "Red/White channel aspect", - "edt_dev_spec_rgbw_calibration_green" : "Green/White channel aspect", - "edt_dev_spec_rgbw_calibration_blue" : "Blue/White channel aspect", + "edt_dev_spec_rgbw_calibration_enable": "White channel calibration (RGBW only)", + "edt_dev_spec_rgbw_calibration_limit": "White channel limit", + "edt_dev_spec_rgbw_calibration_red": "Red/White channel aspect", + "edt_dev_spec_rgbw_calibration_green": "Green/White channel aspect", + "edt_dev_spec_rgbw_calibration_blue": "Blue/White channel aspect", + "edt_dev_spec_segments_disabled_title": "Segment streaming disabled at WLED.", + "edt_dev_spec_segments_title": "Stream to segment", + "edt_dev_spec_segmentId_title": "Segment-ID", + "edt_dev_spec_segmentsSwitchOffOthers_title": "Switch-off other segments", + "edt_dev_spec_segmentsOverlapValidation_error": "Correct the WLED setup! The segment must not overlap with {{plural:$1| segment|segments}}: \"$2\".", "edt_dev_spec_serial_title": "Serial number", "edt_dev_spec_spipath_title": "SPI Device", "edt_dev_spec_sslHSTimeoutMax_title": "Streamer handshake timeout maximum", "edt_dev_spec_sslHSTimeoutMin_title": "Streamer handshake timeout minimum", + "edt_dev_spec_stayOnAfterStreaming_title": "Stay on after streaming", + "edt_dev_spec_stayOnAfterStreaming_title_info": "The device will stay on after streaming or restoring state.", "edt_dev_spec_switchOffOnBlack_title": "Switch off on black", "edt_dev_spec_switchOffOnbelowMinBrightness_title": "Switch-off, below minimum", "edt_dev_spec_syncOverwrite_title": "Disable synchronisation", @@ -866,11 +875,11 @@ "general_country_us": "United States", "general_disabled": "disabled", "general_enabled": "enabled", - "general_speech_ca": "Catalan", + "general_speech_ca": "Catalan", "general_speech_cs": "Czech", "general_speech_da": "Danish", "general_speech_de": "German", - "general_speech_el": "Greek", + "general_speech_el": "Greek", "general_speech_en": "English", "general_speech_es": "Spanish", "general_speech_fr": "French", diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 26ef9529..6421d6de 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -1002,6 +1002,21 @@ $(document).ready(function () { addJsonEditorHostValidation(); + JSONEditor.defaults.custom_validators.push(function (schema, value, path) { + var errors = []; + + if (path === "root.specificOptions.segments.segmentList") { + var overlapSegNames = validateWledSegmentConfig(value); + if (overlapSegNames.length > 0) { + errors.push({ + path: "root.specificOptions.segments", + message: $.i18n('edt_dev_spec_segmentsOverlapValidation_error', overlapSegNames.length, overlapSegNames.join(', ')) + }); + } + } + return errors; + }); + $("#leddevices").off().on("change", function () { var generalOptions = window.serverSchema.properties.device; @@ -1080,8 +1095,8 @@ $(document).ready(function () { $('#btn_test_controller').hide(); switch (ledType) { - case "cololight": case "wled": + case "cololight": case "nanoleaf": showAllDeviceInputOptions("hostList", false); case "apa102": @@ -1107,7 +1122,22 @@ $(document).ready(function () { if (storedAccess === 'expert') { filter.discoverAll = true; } - discover_device(ledType, filter); + + $('#btn_submit_controller').prop('disabled', true); + + discover_device(ledType, filter) + .then(discoveryResult => { + updateOutputSelectList(ledType, discoveryResult); + }) + .then(discoveryResult => { + if (ledType === "wled") { + updateElementsWled(ledType); + } + }) + .catch(error => { + showNotification('danger', "Device discovery for " + ledType + " failed with error:" + error); + }); + hwLedCountDefault = 1; colorOrderDefault = "rgb"; break; @@ -1211,8 +1241,8 @@ $(document).ready(function () { } break; - case "cololight": case "wled": + case "cololight": var hostList = conf_editor.getEditor("root.specificOptions.hostList").getValue(); if (hostList !== "SELECT") { var host = conf_editor.getEditor("root.specificOptions.host").getValue(); @@ -1339,7 +1369,9 @@ $(document).ready(function () { break; case "wled": - params = { host: host, filter: "info" }; + //Ensure that elements are defaulted for new host + updateElementsWled(ledType, host); + params = { host: host }; getProperties_device(ledType, host, params); break; @@ -1452,6 +1484,10 @@ $(document).ready(function () { } conf_editor.getEditor("root.specificOptions.rateList").setValue(rate); break; + case "wled": + var hardwareLedCount = conf_editor.getEditor("root.generalOptions.hardwareLedCount").getValue(); + validateWledLedCount(hardwareLedCount); + break; default: } }); @@ -1547,12 +1583,54 @@ $(document).ready(function () { } }); + //WLED + conf_editor.watch('root.specificOptions.segments.segmentList', () => { + + //Update hidden streamSegmentId element + var selectedSegment = conf_editor.getEditor("root.specificOptions.segments.segmentList").getValue(); + var streamSegmentId = parseInt(selectedSegment); + conf_editor.getEditor("root.specificOptions.segments.streamSegmentId").setValue(streamSegmentId); + + if (devicesProperties[ledType]) { + var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + var ledDeviceProperties = devicesProperties[ledType][host]; + + if (ledDeviceProperties) { + var hardwareLedCount = 1; + if (streamSegmentId > -1) { + // Set hardware LED count to segment length + if (ledDeviceProperties.state) { + var segments = ledDeviceProperties.state.seg; + var segmentConfig = segments.filter(seg => seg.id == streamSegmentId)[0]; + hardwareLedCount = segmentConfig.len; + } + } else { + //"Use main segment only" is disabled, i.e. stream to all LEDs + if (ledDeviceProperties.info) { + hardwareLedCount = ledDeviceProperties.info.leds.count; + } + } + conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); + } + } + }); + //Handle Hardware Led Count constraint list conf_editor.watch('root.generalOptions.hardwareLedCountList', () => { var hwLedCountSelected = conf_editor.getEditor("root.generalOptions.hardwareLedCountList").getValue(); conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(Number(hwLedCountSelected)); }); + //Handle Hardware Led update and constraints + conf_editor.watch('root.generalOptions.hardwareLedCount', () => { + var hardwareLedCount = conf_editor.getEditor("root.generalOptions.hardwareLedCount").getValue(); + switch (ledType) { + case "wled": + validateWledLedCount(hardwareLedCount); + break; + default: + } + }); }); //philipshueentertainment backward fix @@ -1798,8 +1876,8 @@ function saveLedConfig(genDefLayout = false) { location.reload(); } -// build dynamic enum -var updateSelectList = function (ledType, discoveryInfo) { +// build dynamic enum for hosts or output paths +var updateOutputSelectList = function (ledType, discoveryInfo) { // Only update, if ledType is equal of selected controller type and discovery info exists if (ledType !== $("#leddevices").val() || !discoveryInfo.devices) { return; @@ -1810,7 +1888,7 @@ var updateSelectList = function (ledType, discoveryInfo) { var key; var enumVals = []; - var enumTitelVals = []; + var enumTitleVals = []; var enumDefaultVal = ""; var addSelect = false; var addCustom = false; @@ -1835,7 +1913,7 @@ var updateSelectList = function (ledType, discoveryInfo) { if (discoveryInfo.devices.length === 0) { enumVals.push("NONE"); - enumTitelVals.push($.i18n('edt_dev_spec_devices_discovered_none')); + enumTitleVals.push($.i18n('edt_dev_spec_devices_discovered_none')); } else { var name; @@ -1876,7 +1954,7 @@ var updateSelectList = function (ledType, discoveryInfo) { } enumVals.push(host); - enumTitelVals.push(name); + enumTitleVals.push(name); } //Always allow to add custom configuration @@ -1904,7 +1982,7 @@ var updateSelectList = function (ledType, discoveryInfo) { if (discoveryInfo.devices.length == 0) { enumVals.push("NONE"); - enumTitelVals.push($.i18n('edt_dev_spec_devices_discovered_none')); + enumTitleVals.push($.i18n('edt_dev_spec_devices_discovered_none')); $('#btn_submit_controller').prop('disabled', true); showAllDeviceInputOptions(key, false); } @@ -1922,7 +2000,7 @@ var updateSelectList = function (ledType, discoveryInfo) { } else { enumVals.push(device.portName); } - enumTitelVals.push(device.portName + " (" + device.vendorIdentifier + "|" + device.productIdentifier + ") - " + device.manufacturer); + enumTitleVals.push(device.portName + " (" + device.vendorIdentifier + "|" + device.productIdentifier + ") - " + device.manufacturer); } // Select configured device @@ -1951,7 +2029,7 @@ var updateSelectList = function (ledType, discoveryInfo) { if (discoveryInfo.devices.length == 0) { enumVals.push("NONE"); - enumTitelVals.push($.i18n('edt_dev_spec_devices_discovered_none')); + enumTitleVals.push($.i18n('edt_dev_spec_devices_discovered_none')); $('#btn_submit_controller').prop('disabled', true); showAllDeviceInputOptions(key, false); } @@ -1970,7 +2048,7 @@ var updateSelectList = function (ledType, discoveryInfo) { case "piblaster": for (const device of discoveryInfo.devices) { enumVals.push(device.systemLocation); - enumTitelVals.push(device.deviceName + " (" + device.systemLocation + ")"); + enumTitleVals.push(device.deviceName + " (" + device.systemLocation + ")"); } // Select configured device @@ -1993,7 +2071,7 @@ var updateSelectList = function (ledType, discoveryInfo) { if (discoveryInfo.devices.length == 0) { enumVals.push("NONE"); - enumTitelVals.push($.i18n('edt_dev_spec_devices_discovered_none')); + enumTitleVals.push($.i18n('edt_dev_spec_devices_discovered_none')); $('#btn_submit_controller').prop('disabled', true); showAllDeviceInputOptions(key, false); @@ -2004,18 +2082,19 @@ var updateSelectList = function (ledType, discoveryInfo) { } if (enumVals.length > 0) { - updateJsonEditorSelection(conf_editor, 'root.specificOptions', key, addSchemaElements, enumVals, enumTitelVals, enumDefaultVal, addSelect, addCustom); + updateJsonEditorSelection(conf_editor, 'root.specificOptions', key, addSchemaElements, enumVals, enumTitleVals, enumDefaultVal, addSelect, addCustom); } }; async function discover_device(ledType, params) { - $('#btn_submit_controller').prop('disabled', true); - const result = await requestLedDeviceDiscovery(ledType, params); - var discoveryResult; - if (result && !result.error) { + var discoveryResult = {}; + if (result) { + if (result.error) { + throw (result.error); + } discoveryResult = result.info; } else { @@ -2024,8 +2103,7 @@ async function discover_device(ledType, params) { ledDevicetype: ledType } } - - updateSelectList(ledType, discoveryResult); + return discoveryResult; } async function getProperties_device(ledType, key, params) { @@ -2089,23 +2167,7 @@ function updateElements(ledType, key) { conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); break; case "wled": - var ledProperties = devicesProperties[ledType][key]; - - if (ledProperties && ledProperties.leds) { - hardwareLedCount = ledProperties.leds.count; - if (ledProperties.maxLedCount) { - var maxLedCount = ledProperties.maxLedCount; - if (hardwareLedCount > maxLedCount) { - showInfoDialog('warning', $.i18n("conf_leds_config_warning"), $.i18n('conf_leds_error_hwled_gt_maxled', hardwareLedCount, maxLedCount, maxLedCount)); - hardwareLedCount = maxLedCount; - conf_editor.getEditor("root.specificOptions.streamProtocol").setValue("RAW"); - //Workaround, as value seems to getting updated property when a 'getEditor("root.specificOptions").getValue()' is done during save - var editor = conf_editor.getEditor("root.specificOptions"); - editor.value["streamProtocol"] = "RAW"; - } - } - } - conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); + updateElementsWled(ledType, key); break; case "nanoleaf": @@ -2190,3 +2252,162 @@ function disableAutoResolvedGeneralOptions() { conf_editor.getEditor("root.generalOptions.colorOrder").disable(); } +function validateWledSegmentConfig(streamSegmentId) { + var overlapSegNames = []; + if (streamSegmentId > -1) { + if (!jQuery.isEmptyObject(devicesProperties)) { + var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + var ledProperties = devicesProperties['wled'][host]; + if (ledProperties && ledProperties.state) { + var segments = ledProperties.state.seg; + var segmentConfig = segments.filter(seg => seg.id == streamSegmentId)[0]; + + var overlappingSegments = segments.filter((seg) => { + if (seg.id != streamSegmentId) { + if ((segmentConfig.start >= seg.stop) || (segmentConfig.start < seg.start && segmentConfig.stop <= seg.start)) { + return false; + } + return true; + } + }); + + if (overlappingSegments.length > 0) { + var overlapSegNames = []; + for (const segment of overlappingSegments) { + if (segment.n) { + overlapSegNames.push(segment.n); + } else { + overlapSegNames.push("Segment " + segment.id); + } + } + } + } + } + } + return overlapSegNames; +} + +function validateWledLedCount(hardwareLedCount) { + + if (!jQuery.isEmptyObject(devicesProperties)) { + var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + var ledDeviceProperties = devicesProperties["wled"][host]; + + if (ledDeviceProperties) { + + var streamProtocol = conf_editor.getEditor("root.specificOptions.streamProtocol").getValue(); + if (streamProtocol === "RAW") { + var maxLedCount = 490; + if (ledDeviceProperties.maxLedCount) { + //WLED not DDP ready + maxLedCount = ledDeviceProperties.maxLedCount; + if (hardwareLedCount > maxLedCount) { + showInfoDialog('warning', $.i18n("conf_leds_config_warning"), $.i18n('conf_leds_error_hwled_gt_maxled', hardwareLedCount, maxLedCount, maxLedCount)); + hardwareLedCount = maxLedCount; + conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); + conf_editor.getEditor("root.specificOptions.streamProtocol").setValue("RAW"); + } + } else { + //WLED is DDP ready + if (hardwareLedCount > maxLedCount) { + var newStreamingProtocol = "DDP"; + showInfoDialog('warning', $.i18n("conf_leds_config_warning"), $.i18n('conf_leds_error_hwled_gt_maxled_protocol', hardwareLedCount, maxLedCount, newStreamingProtocol)); + conf_editor.getEditor("root.specificOptions.streamProtocol").setValue(newStreamingProtocol); + } + } + } + } + } +} + +function updateElementsWled(ledType, key) { + + // Get configured device's details + var configuredDeviceType = window.serverConfig.device.type; + var configuredHost = window.serverConfig.device.host; + var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + + //New segment selection list values + var enumSegSelectVals = []; + var enumSegSelectTitleVals = []; + var enumSegSelectDefaultVal = ""; + + if (devicesProperties[ledType] && devicesProperties[ledType][key]) { + var ledDeviceProperties = devicesProperties[ledType][key]; + + if (!jQuery.isEmptyObject(ledDeviceProperties)) { + + if (ledDeviceProperties.info) { + if (ledDeviceProperties.info.liveseg && ledDeviceProperties.info.liveseg < 0) { + // "Use main segment only" is disabled + var defaultSegmentId = "-1"; + enumSegSelectVals.push(defaultSegmentId); + enumSegSelectTitleVals.push($.i18n('edt_dev_spec_segments_disabled_title')); + enumSegSelectDefaultVal = defaultSegmentId; + + } else { + if (ledDeviceProperties.state) { + //Prepare new segment selection list + var segments = ledDeviceProperties.state.seg; + for (const segment of segments) { + enumSegSelectVals.push(segment.id.toString()); + if (segment.n) { + enumSegSelectTitleVals.push(segment.n); + } else { + enumSegSelectTitleVals.push("Segment " + segment.id); + } + } + var currentSegmentId = conf_editor.getEditor("root.specificOptions.segments.streamSegmentId").getValue().toString(); + enumSegSelectDefaultVal = currentSegmentId; + } + } + + // Check if currently configured segment is available at WLED + var configuredDeviceType = window.serverConfig.device.type; + var configuredHost = window.serverConfig.device.host; + + var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + if (configuredDeviceType == ledType && configuredHost == host) { + var configuredStreamSegmentId = window.serverConfig.device.segments.streamSegmentId.toString(); + var segmentIdFound = enumSegSelectVals.filter(segId => segId == configuredStreamSegmentId).length; + if (!segmentIdFound) { + showInfoDialog('warning', $.i18n("conf_leds_config_warning"), $.i18n('conf_leds_error_wled_segment_missing', configuredStreamSegmentId)); + } + } + } + } + } else { + //If failed to get properties + var hardwareLedCount; + + if (configuredDeviceType == ledType && configuredHost == host) { + // Populate elements from existing configuration + var configuredstreamSegmentId = window.serverConfig.device.segments.streamSegmentId.toString(); + enumSegSelectVals = [configuredstreamSegmentId]; + enumSegSelectTitleVals = ["Segment " + configuredstreamSegmentId]; + enumSegSelectDefaultVal = configuredstreamSegmentId; + + hardwareLedCount = window.serverConfig.device.hardwareLedCount; + } else { + // Populate elements with default values + defaultSegmentId = "-1"; + enumSegSelectVals.push(defaultSegmentId); + enumSegSelectTitleVals.push($.i18n('edt_dev_spec_segments_disabled_title')); + enumSegSelectDefaultVal = defaultSegmentId; + + hardwareLedCount = 1; + } + conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); + } + + updateJsonEditorSelection(conf_editor, 'root.specificOptions.segments', + 'segmentList', {}, enumSegSelectVals, enumSegSelectTitleVals, enumSegSelectDefaultVal, false, false); + + //Show additional configuration options, if more than one segment is available + var showAdditionalOptions = false; + if (enumSegSelectVals.length > 1) { + showAdditionalOptions = true; + } + showInputOptionForItem(conf_editor, "root.specificOptions.segments", "switchOffOtherSegments", showAdditionalOptions); +} + diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index 84dff5c5..e1a2ae5b 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -1,1344 +1,1393 @@ -var prevTag; - -function removeOverlay() { - $("#loading_overlay").removeClass("overlay"); -} - -function reload() { - location.reload(); -} - -function storageComp() { - if (typeof (Storage) !== "undefined") - return true; - return false; -} - -function getStorage(item) { - if (storageComp()) { - return localStorage.getItem(item); - } - return null; -} - -function setStorage(item, value) { - if (storageComp()) { - localStorage.setItem(item, value); - } -} - -function removeStorage(item) { - if (storageComp()) { - localStorage.removeItem(item); - } -} - -function debugMessage(msg) { - if (window.debugMessagesActive) { - console.log(msg); - } -} - -function validateDuration(d) { - if (typeof d === "undefined" || d < 0) - return ENDLESS; - else - return d *= 1000; -} - -function getHashtag() { - if (getStorage('lasthashtag') != null) - return getStorage('lasthashtag'); - else { - var tag = document.URL; - tag = tag.substr(tag.indexOf("#") + 1); - if (tag == "" || typeof tag === "undefined" || tag.startsWith("http")) - tag = "dashboard" - return tag; - } -} - -function loadContent(event, forceRefresh) { - var tag; - - var lastSelectedInstance = getStorage('lastSelectedInstance'); - - if (lastSelectedInstance && (lastSelectedInstance != window.currentHyperionInstance)) { - if (window.serverInfo.instance[lastSelectedInstance] && window.serverInfo.instance[lastSelectedInstance].running) { - instanceSwitch(lastSelectedInstance); - } else { - removeStorage('lastSelectedInstance'); - } - } - - if (typeof event != "undefined") { - tag = event.currentTarget.hash; - tag = tag.substr(tag.indexOf("#") + 1); - setStorage('lasthashtag', tag); - } - else - tag = getHashtag(); - - if (forceRefresh || prevTag != tag) { - prevTag = tag; - $("#page-content").off(); - $("#page-content").load("/content/" + tag + ".html", function (response, status, xhr) { - if (status == "error") { - tag = 'dashboard'; - console.log("Could not find page:", prevTag, ", Redirecting to:", tag); - setStorage('lasthashtag', tag); - - $("#page-content").load("/content/" + tag + ".html", function (response, status, xhr) { - if (status == "error") { - $("#page-content").html('

    ' + encode_utf8(tag) + '
    ' + $.i18n('info_404') + '

    '); - removeOverlay(); - } - }); - } - updateUiOnInstance(window.currentHyperionInstance); - }); - } -} - -function getInstanceNameByIndex(index) { - var instData = window.serverInfo.instance - for (var key in instData) { - if (instData[key].instance == index) - return instData[key].friendly_name; - } - return "unknown" -} - -function updateHyperionInstanceListing() { - if (window.serverInfo.instance) { - var data = window.serverInfo.instance.filter(entry => entry.running); - $('#hyp_inst_listing').html(""); - for (var key in data) { - var currInstMarker = (data[key].instance == window.currentHyperionInstance) ? "component-on" : ""; - - var html = '
  • \ - \ -
    \ - \ - '+ data[key].friendly_name + ' \ -
    \ -
    \ -
  • ' - - if (data.length - 1 > key) - html += '
  • ' - - $('#hyp_inst_listing').append(html); - - $('#hyperioninstance_' + data[key].instance).off().on("click", function (e) { - var inst = e.currentTarget.id.split("_")[1] - instanceSwitch(inst) - }); - } - } -} - -function initLanguageSelection() { - // Initialise language selection list with languages supported - for (var i = 0; i < availLang.length; i++) { - $("#language-select").append(''); - } - - var langLocale = storedLang; - - //Test, if language is supported by hyperion - var langIdx = availLang.indexOf(langLocale); - if (langIdx > -1) { - langText = availLangText[langIdx]; - } else { - // If language is not supported by hyperion, try fallback language - langLocale = $.i18n().options.fallbackLocale.substring(0, 2); - langIdx = availLang.indexOf(langLocale); - if (langIdx > -1) { - langText = availLangText[langIdx]; - } else { - langLocale = 'en'; - langIdx = availLang.indexOf(langLocale); - if (langIdx > -1) { - langText = availLangText[langIdx]; - } - } - } - - $('#language-select').prop('title', langText); - $("#language-select").val(langIdx); - $("#language-select").selectpicker("refresh"); -} - -function updateUiOnInstance(inst) { - - window.currentHyperionInstance = inst; - window.currentHyperionInstanceName = getInstanceNameByIndex(inst); - - $("#active_instance_friendly_name").text(getInstanceNameByIndex(inst)); - if (window.serverInfo.instance.filter(entry => entry.running).length > 1) { - $('#btn_hypinstanceswitch').toggle(true); - $('#active_instance_dropdown').prop('disabled', false); - $('#active_instance_dropdown').css('cursor', 'pointer'); - $("#active_instance_dropdown").css("pointer-events", "auto"); - } else { - $('#btn_hypinstanceswitch').toggle(false); - $('#active_instance_dropdown').prop('disabled', true); - $("#active_instance_dropdown").css('cursor', 'default'); - $("#active_instance_dropdown").css("pointer-events", "none"); - } -} - -function instanceSwitch(inst) { - requestInstanceSwitch(inst) - window.currentHyperionInstance = inst; - window.currentHyperionInstanceName = getInstanceNameByIndex(inst); - setStorage('lastSelectedInstance', inst) -} - -function loadContentTo(containerId, fileName) { - $(containerId).load("/content/" + fileName + ".html"); -} - -function toggleClass(obj, class1, class2) { - if ($(obj).hasClass(class1)) { - $(obj).removeClass(class1); - $(obj).addClass(class2); - } - else { - $(obj).removeClass(class2); - $(obj).addClass(class1); - } -} - -function setClassByBool(obj, enable, class1, class2) { - if (enable) { - $(obj).removeClass(class1); - $(obj).addClass(class2); - } - else { - $(obj).removeClass(class2); - $(obj).addClass(class1); - } -} - -function showInfoDialog(type, header, message) { - if (type == "success") { - $('#id_body').html(''); - if (header == "") - $('#id_body').append('

    ' + $.i18n('infoDialog_general_success_title') + '

    '); - $('#id_footer').html(''); - } - else if (type == "warning") { - $('#id_body').html(''); - if (header == "") - $('#id_body').append('

    ' + $.i18n('infoDialog_general_warning_title') + '

    '); - $('#id_footer').html(''); - } - else if (type == "error") { - $('#id_body').html(''); - if (header == "") - $('#id_body').append('

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

    '); - $('#id_footer').html(''); - } - else if (type == "select") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - else if (type == "iswitch") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - else if (type == "uilock") { - $('#id_body').html(''); - $('#id_footer').html('' + $.i18n('InfoDialog_nowrite_foottext') + ''); - } - else if (type == "import") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - else if (type == "delInst") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - else if (type == "renInst") { - $('#id_body_rename').html('
    '); - $('#id_body_rename').append('

    ' + header + '

    '); - $('#id_body_rename').append(''); - $('#id_footer_rename').html(''); - $('#id_footer_rename').append(''); - } - else if (type == "changePassword") { - $('#id_body_rename').html('
    '); - $('#id_body_rename').append('

    ' + header + '


    '); - $('#id_body_rename').append('

    ' + $.i18n('infoDialog_username_text') + - '


    '); - $('#id_body_rename').append('

    ' + $.i18n('infoDialog_password_current_text') + - '


    '); - $('#id_body_rename').append('

    ' + $.i18n('infoDialog_password_new_text') + - '

    '); - $('#id_body_rename').append('
    ' + $.i18n('infoDialog_password_minimum_length') + '
    '); - $('#id_footer_rename').html(''); - $('#id_footer_rename').append(''); - } - else if (type == "checklist") { - $('#id_body').html(''); - $('#id_body').append('

    ' + $.i18n('infoDialog_checklist_title') + '

    '); - $('#id_body').append(header); - $('#id_footer').html(''); - } - else if (type == "newToken") { - $('#id_body').html(''); - $('#id_footer').html(''); - } - else if (type == "grantToken") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - - if (type != "renInst") { - $('#id_body').append('

    ' + header + '

    '); - $('#id_body').append(message); - } - - if (type == "select" || type == "iswitch") - $('#id_body').append(''); - - if (getStorage("darkMode") == "on") - $('#id_logo').attr("src", 'img/hyperion/logo_negativ.png'); - - $(type == "renInst" || type == "changePassword" ? "#modal_dialog_rename" : "#modal_dialog").modal({ - backdrop: "static", - keyboard: false, - show: true - }); - - $(document).on('click', '[data-dismiss-modal]', function () { - var target = $(this).attr('data-dismiss-modal'); - $.find(target).modal('hide'); - }); -} - -function createHintH(type, text, container) { - type = String(type); - if (type == "intro") - tclass = "introd"; - - $('#' + container).prepend('

    ' + text + '


    '); -} - -function createHint(type, text, container, buttonid, buttontxt) { - var fe, tclass; - - if (type == "intro") { - fe = ''; - tclass = "intro-hint"; - } - else if (type == "info") { - fe = '
    Information
    '; - tclass = "info-hint"; - } - else if (type == "wizard") { - fe = '
    Information
    '; - tclass = "wizard-hint"; - } - else if (type == "warning") { - fe = '
    Information
    '; - tclass = "warning-hint"; - } - - if (buttonid) - buttonid = '

    '; - else - buttonid = ""; - - if (type == "intro") - $('#' + container).prepend('

    ' + $.i18n("conf_helptable_expl") + '

    ' + text + '
    '); - else if (type == "wizard") - $('#' + container).prepend('

    ' + $.i18n("wiz_wizavail") + '

    ' + $.i18n('wiz_guideyou', text) + buttonid + '
    '); - else { - createTable('', 'htb', container, true, tclass); - $('#' + container + ' .htb').append(createTableRow([fe, text], false, true)); - } -} - -function createEffHint(title, text) { - return '

    ' + title + '

    ' + text + '
    '; -} - -function valValue(id, value, min, max) { - if (typeof max === 'undefined' || max == "") - max = 999999; - - if (Number(value) > Number(max)) { - $('#' + id).val(max); - showInfoDialog("warning", "", $.i18n('edt_msg_error_maximum_incl', max)); - return max; - } - else if (Number(value) < Number(min)) { - $('#' + id).val(min); - showInfoDialog("warning", "", $.i18n('edt_msg_error_minimum_incl', min)); - return min; - } - return value; -} - -function readImg(input, cb) { - if (input.files && input.files[0]) { - var reader = new FileReader(); - // inject fileName property - reader.fileName = input.files[0].name - - reader.onload = function (e) { - cb(e.target.result, e.target.fileName); - } - reader.readAsDataURL(input.files[0]); - } -} - -function isJsonString(str) { - try { - JSON.parse(str); - } - catch (e) { - return e; - } - return ""; -} - -function createJsonEditor(container, schema, setconfig, usePanel, arrayre) { - $('#' + container).off(); - $('#' + container).html(""); - - if (typeof arrayre === 'undefined') - arrayre = true; - - var editor = new JSONEditor(document.getElementById(container), - { - theme: 'bootstrap3', - iconlib: "fontawesome4", - disable_collapse: 'true', - form_name_root: 'sa', - disable_edit_json: true, - disable_properties: true, - disable_array_reorder: arrayre, - no_additional_properties: true, - disable_array_delete_all_rows: true, - disable_array_delete_last_row: true, - access: storedAccess, - schema: { - title: '', - properties: schema - } - }); - - if (usePanel) { - $('#' + container + ' .well').first().removeClass('well well-sm'); - $('#' + container + ' h4').first().remove(); - $('#' + container + ' .well').first().removeClass('well well-sm'); - } - - if (setconfig) { - for (var key in editor.root.editors) { - editor.getEditor("root." + key).setValue(Object.assign({}, editor.getEditor("root." + key).value, window.serverConfig[key])); - } - } - - return editor; -} - -function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal, addSelect, addCustom, addCustomAsFirst, customText) { - var editor = rootEditor.getEditor(path); - var orginalProperties = editor.schema.properties[key]; - - var orginalWatchFunctions = rootEditor.watchlist[path + "." + key]; - rootEditor.unwatch(path + "." + key); - - var newSchema = []; - newSchema[key] = - { - "type": "string", - "enum": [], - "required": true, - "options": { "enum_titles": [], "infoText": "" }, - "propertyOrder": 1 - }; - - //Add additional elements to overwrite defaults - for (var item in addElements) { - newSchema[key][item] = addElements[item]; - } - - if (orginalProperties) { - if (orginalProperties["title"]) { - newSchema[key]["title"] = orginalProperties["title"]; - } - - if (orginalProperties["options"] && orginalProperties["options"]["infoText"]) { - newSchema[key]["options"]["infoText"] = orginalProperties["options"]["infoText"]; - } - - if (orginalProperties["propertyOrder"]) { - newSchema[key]["propertyOrder"] = orginalProperties["propertyOrder"]; - } - } - - if (addCustom) { - - if (newTitelVals.length === 0) { - newTitelVals = [...newEnumVals]; - } - - if (!!!customText) { - customText = "edt_conf_enum_custom"; - } - - if (addCustomAsFirst) { - newEnumVals.unshift("CUSTOM"); - newTitelVals.unshift(customText); - } else { - newEnumVals.push("CUSTOM"); - newTitelVals.push(customText); - } - - if (newSchema[key].options.infoText) { - var customInfoText = newSchema[key].options.infoText + "_custom"; - newSchema[key].options.infoText = customInfoText; - } - } - - if (addSelect) { - newEnumVals.unshift("SELECT"); - newTitelVals.unshift("edt_conf_enum_please_select"); - newDefaultVal = "SELECT"; - } - - if (newEnumVals) { - newSchema[key]["enum"] = newEnumVals; - } - - if (newTitelVals) { - newSchema[key]["options"]["enum_titles"] = newTitelVals; - } - if (newDefaultVal) { - newSchema[key]["default"] = newDefaultVal; - } - - editor.original_schema.properties[key] = orginalProperties; - editor.schema.properties[key] = newSchema[key]; - rootEditor.validator.schema.properties[editor.key].properties[key] = newSchema[key]; - - editor.removeObjectProperty(key); - delete editor.cached_editors[key]; - editor.addObjectProperty(key); - - if (orginalWatchFunctions) { - for (var i = 0; i < orginalWatchFunctions.length; i++) { - rootEditor.watch(path + "." + key, orginalWatchFunctions[i]); - } - } - rootEditor.notifyWatchers(path + "." + key); -} - -function updateJsonEditorMultiSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal) { - var editor = rootEditor.getEditor(path); - var orginalProperties = editor.schema.properties[key]; - - var orginalWatchFunctions = rootEditor.watchlist[path + "." + key]; - rootEditor.unwatch(path + "." + key); - - var newSchema = []; - newSchema[key] = - { - "type": "array", - "format": "select", - "items": { - "type": "string", - "enum": [], - "options": { "enum_titles": [] }, - }, - "options": { "infoText": "" }, - "default": [], - "propertyOrder": 1 - }; - - //Add additional elements to overwrite defaults - for (var item in addElements) { - newSchema[key][item] = addElements[item]; - } - - if (orginalProperties) { - if (orginalProperties["title"]) { - newSchema[key]["title"] = orginalProperties["title"]; - } - - if (orginalProperties["options"] && orginalProperties["options"]["infoText"]) { - newSchema[key]["options"]["infoText"] = orginalProperties["options"]["infoText"]; - } - - if (orginalProperties["propertyOrder"]) { - newSchema[key]["propertyOrder"] = orginalProperties["propertyOrder"]; - } - } - - if (newEnumVals) { - newSchema[key]["items"]["enum"] = newEnumVals; - } - - if (newTitelVals) { - newSchema[key]["items"]["options"]["enum_titles"] = newTitelVals; - } - - if (newDefaultVal) { - newSchema[key]["default"] = newDefaultVal; - } - - editor.original_schema.properties[key] = orginalProperties; - editor.schema.properties[key] = newSchema[key]; - rootEditor.validator.schema.properties[editor.key].properties[key] = newSchema[key]; - - editor.removeObjectProperty(key); - delete editor.cached_editors[key]; - editor.addObjectProperty(key); - - if (orginalWatchFunctions) { - for (var i = 0; i < orginalWatchFunctions.length; i++) { - rootEditor.watch(path + "." + key, orginalWatchFunctions[i]); - } - } - rootEditor.notifyWatchers(path + "." + key); -} - -function updateJsonEditorRange(rootEditor, path, key, minimum, maximum, defaultValue, step, clear) { - var editor = rootEditor.getEditor(path); - - //Preserve current value when updating range - var currentValue = rootEditor.getEditor(path + "." + key).getValue(); - - var orginalProperties = editor.schema.properties[key]; - var newSchema = []; - newSchema[key] = orginalProperties; - - if (clear) { - delete newSchema[key]["minimum"]; - delete newSchema[key]["maximum"]; - delete newSchema[key]["default"]; - delete newSchema[key]["step"]; - } - - if (typeof minimum !== "undefined") { - newSchema[key]["minimum"] = minimum; - } - if (typeof maximum !== "undefined") { - newSchema[key]["maximum"] = maximum; - } - if (typeof defaultValue !== "undefined") { - newSchema[key]["default"] = defaultValue; - currentValue = defaultValue; - } - - if (typeof step !== "undefined") { - newSchema[key]["step"] = step; - } - - editor.original_schema.properties[key] = orginalProperties; - editor.schema.properties[key] = newSchema[key]; - rootEditor.validator.schema.properties[editor.key].properties[key] = newSchema[key]; - - editor.removeObjectProperty(key); - delete editor.cached_editors[key]; - editor.addObjectProperty(key); - - // Restore current (new default) value for new range - rootEditor.getEditor(path + "." + key).setValue(currentValue); -} - -function addJsonEditorHostValidation() { - - JSONEditor.defaults.custom_validators.push(function (schema, value, path) { - var errors = []; - - if (!jQuery.isEmptyObject(value)) { - switch (schema.format) { - case "hostname_or_ip": - if (!isValidHostnameOrIP(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msgcust_error_hostname_ip') - }); - } - break; - case "hostname_or_ip4": - if (!isValidHostnameOrIP4(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msgcust_error_hostname_ip4') - }); - } - break; - - //Remove, when new json-editor 2.x is used - case "ipv4": - if (!isValidIPv4(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msg_error_ipv4') - }); - } - break; - case "ipv6": - if (!isValidIPv6(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msg_error_ipv6') - }); - } - break; - case "hostname": - if (!isValidHostname(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msg_error_hostname') - }); - } - break; - - default: - } - } - return errors; - }); -} - -function buildWL(link, linkt, cl) { - var baseLink = "https://docs.hyperion-project.org/"; - var lang; - - if (typeof linkt == "undefined") - linkt = "Placeholder"; - - if (storedLang == "de" || navigator.locale == "de") - lang = "de"; - else - lang = "en"; - - if (cl === true) { - linkt = $.i18n(linkt); - return '

    ' + linkt + '

    ' + $.i18n('general_wiki_moreto', linkt) + ': ' + linkt + '
    ' - } - else - return ': ' + linkt + ''; -} - -function rgbToHex(rgb) { - if (rgb.length == 3) { - return "#" + - ("0" + parseInt(rgb[0], 10).toString(16)).slice(-2) + - ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) + - ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2); - } - else - debugMessage('rgbToHex: Given rgb is no array or has wrong length'); -} - -function hexToRgb(hex) { - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } : { - r: 0, - g: 0, - b: 0 - }; -} - -/* - Show a notification - @param type Valid types are "info","success","warning","danger" - @param message The message to show - @param title A title (optional) - @param addhtml Add custom html to the notification end - */ -function showNotification(type, message, title = "", addhtml = "") { - if (title == "") { - switch (type) { - case "info": - title = $.i18n('infoDialog_general_info_title'); - break; - case "success": - title = $.i18n('infoDialog_general_success_title'); - break; - case "warning": - title = $.i18n('infoDialog_general_warning_title'); - break; - case "danger": - title = $.i18n('infoDialog_general_error_title'); - break; - } - } - - $.notify({ - // options - title: title, - message: message - }, { - // settings - type: type, - animate: { - enter: 'animate__animated animate__fadeInDown', - exit: 'animate__animated animate__fadeOutUp' - }, - placement: { - align: 'center' - }, - mouse_over: 'pause', - template: '' - }); -} - -function createCP(id, color, cb) { - if (Array.isArray(color)) - color = rgbToHex(color); - else if (color == "undefined") - color = "#AA3399"; - - if (color.startsWith("#")) { - $('#' + id).colorpicker({ - format: 'rgb', - customClass: 'colorpicker-2x', - color: color, - sliders: { - saturation: { - maxLeft: 200, - maxTop: 200 - }, - hue: { - maxTop: 200 - }, - } - }); - $('#' + id).colorpicker().on('changeColor', function (e) { - var rgb = e.color.toRGB(); - var hex = e.color.toHex(); - cb(rgb, hex, e); - }); - } - else - debugMessage('createCP: Given color is not legit'); -} - -// Creates a table with thead and tbody ids -// @param string hid : a class for thead -// @param string bid : a class for tbody -// @param string cont : a container id to html() the table -// @param string bless: if true the table is borderless -function createTable(hid, bid, cont, bless, tclass) { - var table = document.createElement('table'); - var thead = document.createElement('thead'); - var tbody = document.createElement('tbody'); - - table.className = "table"; - if (bless === true) - table.className += " borderless"; - if (typeof tclass !== "undefined") - table.className += " " + tclass; - table.style.marginBottom = "0px"; - if (hid != "") - thead.className = hid; - tbody.className = bid; - if (hid != "") - table.appendChild(thead); - table.appendChild(tbody); - - $('#' + cont).append(table); -} - -// Creates a table row -// @param array list :innerHTML content for / -// @param bool head :if null or false it's body -// @param bool align :if null or false no alignment -// -// @return : with or as child(s) -function createTableRow(list, head, align) { - var row = document.createElement('tr'); - - for (var i = 0; i < list.length; i++) { - if (head === true) - var el = document.createElement('th'); - else - var el = document.createElement('td'); - - if (align) - el.style.verticalAlign = "middle"; - - var purifyConfig = { - ADD_TAGS: ['button'], - ADD_ATTR: ['onclick'] - }; - el.innerHTML = DOMPurify.sanitize(list[i], purifyConfig); - row.appendChild(el); - } - return row; -} - -function createRow(id) { - var el = document.createElement('div'); - el.className = "row"; - el.setAttribute('id', id); - return el; -} - -function createOptPanel(phicon, phead, bodyid, footerid, css, panelId) { - phead = '' + phead; - - var pfooter = document.createElement('button'); - pfooter.className = "btn btn-primary"; - pfooter.setAttribute("id", footerid); - pfooter.innerHTML = '' + $.i18n('general_button_savesettings'); - - return createPanel(phead, "", pfooter, "panel-default", bodyid, css, panelId); -} - -function compareTwoValues(key1, key2, order = 'asc') { - return function innerSort(a, b) { - if (!a.hasOwnProperty(key1) || !b.hasOwnProperty(key1)) { - // property key1 doesn't exist on either object - return 0; - } - - const varA1 = (typeof a[key1] === 'string') - ? a[key1].toUpperCase() : a[key1]; - const varB1 = (typeof b[key1] === 'string') - ? b[key1].toUpperCase() : b[key1]; - - let comparison = 0; - if (varA1 > varB1) { - comparison = 1; - } else { - if (varA1 < varB1) { - comparison = -1; - } else { - if (!a.hasOwnProperty(key2) || !b.hasOwnProperty(key2)) { - // property key2 doesn't exist on either object - return 0; - } - - const varA2 = (typeof a[key2] === 'string') - ? a[key2].toUpperCase() : a[key2]; - const varB2 = (typeof b[key1] === 'string') - ? b[key2].toUpperCase() : b[key2]; - - if (varA2 > varB2) { - comparison = 1; - } else { - comparison = -1; - } - } - } - return ( - (order === 'desc') ? (comparison * -1) : comparison - ); - }; -} - -function sortProperties(list) { - for (var key in list) { - list[key].key = key; - } - list = $.map(list, function (value, index) { - return [value]; - }); - return list.sort(function (a, b) { - return a.propertyOrder - b.propertyOrder; - }); -} - -function createHelpTable(list, phead, panelId) { - var table = document.createElement('table'); - var thead = document.createElement('thead'); - var tbody = document.createElement('tbody'); - list = sortProperties(list); - - phead = '' + phead + ' ' + $.i18n("conf_helptable_expl"); - - table.className = 'table table-hover borderless'; - - thead.appendChild(createTableRow([$.i18n('conf_helptable_option'), $.i18n('conf_helptable_expl')], true, false)); - - for (var key in list) { - if (list[key].access != 'system') { - // break one iteration (in the loop), if the schema has the entry hidden=true - if ("options" in list[key] && "hidden" in list[key].options && (list[key].options.hidden)) - continue; - if ("access" in list[key] && ((list[key].access == "advanced" && storedAccess == "default") || (list[key].access == "expert" && storedAccess != "expert"))) - continue; - var text = list[key].title.replace('title', 'expl'); - tbody.appendChild(createTableRow([$.i18n(list[key].title), $.i18n(text)], false, false)); - - if (list[key].items && list[key].items.properties) { - var ilist = sortProperties(list[key].items.properties); - for (var ikey in ilist) { - // break one iteration (in the loop), if the schema has the entry hidden=true - if ("options" in ilist[ikey] && "hidden" in ilist[ikey].options && (ilist[ikey].options.hidden)) - continue; - if ("access" in ilist[ikey] && ((ilist[ikey].access == "advanced" && storedAccess == "default") || (ilist[ikey].access == "expert" && storedAccess != "expert"))) - continue; - var itext = ilist[ikey].title.replace('title', 'expl'); - tbody.appendChild(createTableRow([$.i18n(ilist[ikey].title), $.i18n(itext)], false, false)); - } - } - } - } - table.appendChild(thead); - table.appendChild(tbody); - - return createPanel(phead, table, undefined, undefined, undefined, undefined, panelId); -} - -function createPanel(head, body, footer, type, bodyid, css, panelId) { - var cont = document.createElement('div'); - var p = document.createElement('div'); - var phead = document.createElement('div'); - var pbody = document.createElement('div'); - var pfooter = document.createElement('div'); - - cont.className = "col-lg-6"; - - if (typeof type == 'undefined') - type = 'panel-default'; - - p.className = 'panel ' + type; - if (typeof panelId != 'undefined') { - p.setAttribute("id", panelId); - } - - phead.className = 'panel-heading ' + css; - pbody.className = 'panel-body'; - pfooter.className = 'panel-footer'; - - phead.innerHTML = head; - - if (typeof bodyid != 'undefined') { - pfooter.style.textAlign = 'right'; - pbody.setAttribute("id", bodyid); - } - - if (typeof body != 'undefined' && body != "") - pbody.appendChild(body); - - if (typeof footer != 'undefined') - pfooter.appendChild(footer); - - p.appendChild(phead); - p.appendChild(pbody); - - if (typeof footer != 'undefined') { - pfooter.style.textAlign = "right"; - p.appendChild(pfooter); - } - - cont.appendChild(p); - - return cont; -} - -function createSelGroup(group) { - var el = document.createElement('optgroup'); - el.setAttribute('label', group); - return el; -} - -function createSelOpt(opt, title) { - var el = document.createElement('option'); - el.setAttribute('value', opt); - if (typeof title == 'undefined') - el.innerHTML = opt; - else - el.innerHTML = title; - return el; -} - -function createSel(array, group, split) { - if (array.length != 0) { - var el = createSelGroup(group); - for (var i = 0; i < array.length; i++) { - var opt; - if (split) { - opt = array[i].split(":") - opt = createSelOpt(opt[0], opt[1]) - } - else - opt = createSelOpt(array[i]) - el.appendChild(opt); - } - return el; - } -} - -function performTranslation() { - $('[data-i18n]').i18n(); -} - -function encode_utf8(s) { - return unescape(encodeURIComponent(s)); -} - -function getReleases(callback) { - $.ajax({ - url: window.gitHubReleaseApiUrl, - method: 'get', - error: function (XMLHttpRequest, textStatus, errorThrown) { - callback(false); - }, - success: function (releases) { - window.gitHubVersionList = releases; - var highestRelease = { - tag_name: '0.0.0' - }; - var highestAlphaRelease = { - tag_name: '0.0.0' - }; - var highestBetaRelease = { - tag_name: '0.0.0' - }; - var highestRcRelease = { - tag_name: '0.0.0' - }; - - for (var i in releases) { - //drafts will be ignored - if (releases[i].draft) - continue; - - if (releases[i].tag_name.includes('alpha')) { - if (sem = semverLite.gt(releases[i].tag_name, highestAlphaRelease.tag_name)) - highestAlphaRelease = releases[i]; - } - else if (releases[i].tag_name.includes('beta')) { - if (sem = semverLite.gt(releases[i].tag_name, highestBetaRelease.tag_name)) - highestBetaRelease = releases[i]; - } - else if (releases[i].tag_name.includes('rc')) { - if (semverLite.gt(releases[i].tag_name, highestRcRelease.tag_name)) - highestRcRelease = releases[i]; - } - else { - if (semverLite.gt(releases[i].tag_name, highestRelease.tag_name)) - highestRelease = releases[i]; - } - } - window.latestStableVersion = highestRelease; - window.latestBetaVersion = highestBetaRelease; - window.latestAlphaVersion = highestAlphaRelease; - window.latestRcVersion = highestRcRelease; - - if (window.serverConfig.general.watchedVersionBranch == "Beta" && semverLite.gt(highestBetaRelease.tag_name, highestRelease.tag_name)) - window.latestVersion = highestBetaRelease; - else - window.latestVersion = highestRelease; - - if (window.serverConfig.general.watchedVersionBranch == "Alpha" && semverLite.gt(highestAlphaRelease.tag_name, highestBetaRelease.tag_name)) - window.latestVersion = highestAlphaRelease; - - if (window.serverConfig.general.watchedVersionBranch == "Alpha" && semverLite.lt(highestAlphaRelease.tag_name, highestBetaRelease.tag_name)) - window.latestVersion = highestBetaRelease; - - //next two if statements are only necessary if we don't have a beta or stable release. We need one alpha release at least - if (window.latestVersion.tag_name == '0.0.0' && highestBetaRelease.tag_name != '0.0.0') - window.latestVersion = highestBetaRelease; - - if (window.latestVersion.tag_name == '0.0.0' && highestAlphaRelease.tag_name != '0.0.0') - window.latestVersion = highestAlphaRelease; - - callback(true); - } - }); -} - -function getSystemInfo() { - var sys = window.sysInfo.system; - var shy = window.sysInfo.hyperion; - - var info = "Hyperion Server:\n"; - info += '- Build: ' + shy.build + '\n'; - info += '- Build time: ' + shy.time + '\n'; - info += '- Git Remote: ' + shy.gitremote + '\n'; - info += '- Version: ' + shy.version + '\n'; - info += '- UI Lang: ' + storedLang + ' (BrowserLang: ' + navigator.language + ')\n'; - info += '- UI Access: ' + storedAccess + '\n'; - //info += '- Log lvl: ' + window.serverConfig.logger.level + '\n'; - info += '- Avail Screen Cap.: ' + window.serverInfo.grabbers.screen.available + '\n'; - info += '- Avail Video Cap.: ' + window.serverInfo.grabbers.video.available + '\n'; - info += '- Avail Services: ' + window.serverInfo.services + '\n'; - info += '- Config path: ' + shy.rootPath + '\n'; - info += '- Database: ' + (shy.readOnlyMode ? "ready-only" : "read/write") + '\n'; - - info += '\n'; - - info += 'Hyperion Server OS:\n'; - info += '- Distribution: ' + sys.prettyName + '\n'; - info += '- Architecture: ' + sys.architecture + '\n'; - - if (sys.cpuModelName) - info += '- CPU Model: ' + sys.cpuModelName + '\n'; - if (sys.cpuModelType) - info += '- CPU Type: ' + sys.cpuModelType + '\n'; - if (sys.cpuRevision) - info += '- CPU Revision: ' + sys.cpuRevision + '\n'; - if (sys.cpuHardware) - info += '- CPU Hardware: ' + sys.cpuHardware + '\n'; - - info += '- Kernel: ' + sys.kernelType + ' (' + sys.kernelVersion + ' (WS: ' + sys.wordSize + '))\n'; - info += '- Root/Admin: ' + sys.isUserAdmin + '\n'; - info += '- Qt Version: ' + sys.qtVersion + '\n'; - if (jQuery.inArray("effectengine", window.serverInfo.services) !== -1) { - info += '- Python Version: ' + sys.pyVersion + '\n'; - } - info += '- Browser: ' + navigator.userAgent; - return info; -} - -function handleDarkMode() { - $("", { - rel: "stylesheet", - type: "text/css", - href: "../css/darkMode.css" - }).appendTo("head"); - - setStorage("darkMode", "on"); - $('#btn_darkmode_icon').removeClass('fa fa-moon-o'); - $('#btn_darkmode_icon').addClass('mdi mdi-white-balance-sunny'); - $('#navbar_brand_logo').attr("src", 'img/hyperion/logo_negativ.png'); -} - -function isAccessLevelCompliant(accessLevel) { - var isOK = true; - if (accessLevel) { - if (accessLevel === 'system') { - isOK = false; - } - else if (accessLevel === 'advanced' && storedAccess === 'default') { - isOK = false; - } - else if (accessLevel === 'expert' && storedAccess !== 'expert') { - isOK = false; - } - } - return isOK -} - -function showInputOptions(path, elements, state) { - for (var i = 0; i < elements.length; i++) { - $('[data-schemapath="root.' + path + '.' + elements[i] + '"]').toggle(state); - } -} - -function showInputOptionForItem(editor, path, item, state) { - var accessLevel = editor.schema.properties[path].properties[item].access; - // Enable element only, if access level compliant - if (!state || isAccessLevelCompliant(accessLevel)) { - showInputOptions(path, [item], state); - } -} - -function showInputOptionsForKey(editor, item, showForKeys, state) { - var elements = []; - var keysToshow = []; - - if (Array.isArray(showForKeys)) { - keysToshow = showForKeys; - } else { - if (typeof showForKeys === 'string') { - keysToshow.push(showForKeys); - } else { - return - } - } - - for (var key in editor.schema.properties[item].properties) { - if ($.inArray(key, keysToshow) === -1) { - var accessLevel = editor.schema.properties[item].properties[key].access; - - //Always disable all elements, but only enable elements, if access level compliant - if (!state || isAccessLevelCompliant(accessLevel)) { - elements.push(key); - } - } - } - showInputOptions(item, elements, state); -} - -function encodeHTML(s) { - return s.replace(/&/g, '&').replace(/ 255) { - return false; - } - } - return true; -} - -function isValidIPv6(value) { - if (value.match( - '^(?:(?:(?:[a-fA-F0-9]{1,4}:){6}|(?=(?:[a-fA-F0-9]{0,4}:){2,6}(?:[0-9]{1,3}.){3}[0-9]{1,3}$)(([0-9a-fA-F]{1,4}:){1,5}|:)((:[0-9a-fA-F]{1,4}){1,5}:|:)|::(?:[a-fA-F0-9]{1,4}:){5})(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]).){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?=(?:[a-fA-F0-9]{0,4}:){0,7}[a-fA-F0-9]{0,4}$)(([0-9a-fA-F]{1,4}:){1,7}|:)((:[0-9a-fA-F]{1,4}){1,7}|:)|(?:[a-fA-F0-9]{1,4}:){7}:|:(:[a-fA-F0-9]{1,4}){7})$' - )) - return true; - else - return false; -} - -function isValidHostname(value) { - if (value.match( - '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])(.([a-zA-Z0-9]|[_a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$' - )) - return true; - else - return false; -} - -function isValidServicename(value) { - if (value.match( - '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9 -]{0,61}[a-zA-Z0-9])(.([a-zA-Z0-9]|[_a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$' - )) - return true; - else - return false; -} - -function isValidHostnameOrIP4(value) { - return (isValidHostname(value) || isValidIPv4(value)); -} - -function isValidHostnameOrIP(value) { - return (isValidHostnameOrIP4(value) || isValidIPv6(value) || isValidServicename(value)); -} - +var prevTag; + +function removeOverlay() { + $("#loading_overlay").removeClass("overlay"); +} + +function reload() { + location.reload(); +} + +function storageComp() { + if (typeof (Storage) !== "undefined") + return true; + return false; +} + +function getStorage(item) { + if (storageComp()) { + return localStorage.getItem(item); + } + return null; +} + +function setStorage(item, value) { + if (storageComp()) { + localStorage.setItem(item, value); + } +} + +function removeStorage(item) { + if (storageComp()) { + localStorage.removeItem(item); + } +} + +function debugMessage(msg) { + if (window.debugMessagesActive) { + console.log(msg); + } +} + +function validateDuration(d) { + if (typeof d === "undefined" || d < 0) + return ENDLESS; + else + return d *= 1000; +} + +function getHashtag() { + if (getStorage('lasthashtag') != null) + return getStorage('lasthashtag'); + else { + var tag = document.URL; + tag = tag.substr(tag.indexOf("#") + 1); + if (tag == "" || typeof tag === "undefined" || tag.startsWith("http")) + tag = "dashboard" + return tag; + } +} + +function loadContent(event, forceRefresh) { + var tag; + + var lastSelectedInstance = getStorage('lastSelectedInstance'); + + if (lastSelectedInstance && (lastSelectedInstance != window.currentHyperionInstance)) { + if (window.serverInfo.instance[lastSelectedInstance] && window.serverInfo.instance[lastSelectedInstance].running) { + instanceSwitch(lastSelectedInstance); + } else { + removeStorage('lastSelectedInstance'); + } + } + + if (typeof event != "undefined") { + tag = event.currentTarget.hash; + tag = tag.substr(tag.indexOf("#") + 1); + setStorage('lasthashtag', tag); + } + else + tag = getHashtag(); + + if (forceRefresh || prevTag != tag) { + prevTag = tag; + $("#page-content").off(); + $("#page-content").load("/content/" + tag + ".html", function (response, status, xhr) { + if (status == "error") { + tag = 'dashboard'; + console.log("Could not find page:", prevTag, ", Redirecting to:", tag); + setStorage('lasthashtag', tag); + + $("#page-content").load("/content/" + tag + ".html", function (response, status, xhr) { + if (status == "error") { + $("#page-content").html('

    ' + encode_utf8(tag) + '
    ' + $.i18n('info_404') + '

    '); + removeOverlay(); + } + }); + } + updateUiOnInstance(window.currentHyperionInstance); + }); + } +} + +function getInstanceNameByIndex(index) { + var instData = window.serverInfo.instance + for (var key in instData) { + if (instData[key].instance == index) + return instData[key].friendly_name; + } + return "unknown" +} + +function updateHyperionInstanceListing() { + if (window.serverInfo.instance) { + var data = window.serverInfo.instance.filter(entry => entry.running); + $('#hyp_inst_listing').html(""); + for (var key in data) { + var currInstMarker = (data[key].instance == window.currentHyperionInstance) ? "component-on" : ""; + + var html = '
  • \ + \ +
    \ + \ + '+ data[key].friendly_name + ' \ +
    \ +
    \ +
  • ' + + if (data.length - 1 > key) + html += '
  • ' + + $('#hyp_inst_listing').append(html); + + $('#hyperioninstance_' + data[key].instance).off().on("click", function (e) { + var inst = e.currentTarget.id.split("_")[1] + instanceSwitch(inst) + }); + } + } +} + +function initLanguageSelection() { + // Initialise language selection list with languages supported + for (var i = 0; i < availLang.length; i++) { + $("#language-select").append(''); + } + + var langLocale = storedLang; + + //Test, if language is supported by hyperion + var langIdx = availLang.indexOf(langLocale); + if (langIdx > -1) { + langText = availLangText[langIdx]; + } else { + // If language is not supported by hyperion, try fallback language + langLocale = $.i18n().options.fallbackLocale.substring(0, 2); + langIdx = availLang.indexOf(langLocale); + if (langIdx > -1) { + langText = availLangText[langIdx]; + } else { + langLocale = 'en'; + langIdx = availLang.indexOf(langLocale); + if (langIdx > -1) { + langText = availLangText[langIdx]; + } + } + } + + $('#language-select').prop('title', langText); + $("#language-select").val(langIdx); + $("#language-select").selectpicker("refresh"); +} + +function updateUiOnInstance(inst) { + + window.currentHyperionInstance = inst; + window.currentHyperionInstanceName = getInstanceNameByIndex(inst); + + $("#active_instance_friendly_name").text(getInstanceNameByIndex(inst)); + if (window.serverInfo.instance.filter(entry => entry.running).length > 1) { + $('#btn_hypinstanceswitch').toggle(true); + $('#active_instance_dropdown').prop('disabled', false); + $('#active_instance_dropdown').css('cursor', 'pointer'); + $("#active_instance_dropdown").css("pointer-events", "auto"); + } else { + $('#btn_hypinstanceswitch').toggle(false); + $('#active_instance_dropdown').prop('disabled', true); + $("#active_instance_dropdown").css('cursor', 'default'); + $("#active_instance_dropdown").css("pointer-events", "none"); + } +} + +function instanceSwitch(inst) { + requestInstanceSwitch(inst) + window.currentHyperionInstance = inst; + window.currentHyperionInstanceName = getInstanceNameByIndex(inst); + setStorage('lastSelectedInstance', inst) +} + +function loadContentTo(containerId, fileName) { + $(containerId).load("/content/" + fileName + ".html"); +} + +function toggleClass(obj, class1, class2) { + if ($(obj).hasClass(class1)) { + $(obj).removeClass(class1); + $(obj).addClass(class2); + } + else { + $(obj).removeClass(class2); + $(obj).addClass(class1); + } +} + +function setClassByBool(obj, enable, class1, class2) { + if (enable) { + $(obj).removeClass(class1); + $(obj).addClass(class2); + } + else { + $(obj).removeClass(class2); + $(obj).addClass(class1); + } +} + +function showInfoDialog(type, header, message) { + if (type == "success") { + $('#id_body').html(''); + if (header == "") + $('#id_body').append('

    ' + $.i18n('infoDialog_general_success_title') + '

    '); + $('#id_footer').html(''); + } + else if (type == "warning") { + $('#id_body').html(''); + if (header == "") + $('#id_body').append('

    ' + $.i18n('infoDialog_general_warning_title') + '

    '); + $('#id_footer').html(''); + } + else if (type == "error") { + $('#id_body').html(''); + if (header == "") + $('#id_body').append('

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

    '); + $('#id_footer').html(''); + } + else if (type == "select") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + else if (type == "iswitch") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + else if (type == "uilock") { + $('#id_body').html(''); + $('#id_footer').html('' + $.i18n('InfoDialog_nowrite_foottext') + ''); + } + else if (type == "import") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + else if (type == "delInst") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + else if (type == "renInst") { + $('#id_body_rename').html('
    '); + $('#id_body_rename').append('

    ' + header + '

    '); + $('#id_body_rename').append(''); + $('#id_footer_rename').html(''); + $('#id_footer_rename').append(''); + } + else if (type == "changePassword") { + $('#id_body_rename').html('
    '); + $('#id_body_rename').append('

    ' + header + '


    '); + $('#id_body_rename').append('

    ' + $.i18n('infoDialog_username_text') + + '


    '); + $('#id_body_rename').append('

    ' + $.i18n('infoDialog_password_current_text') + + '


    '); + $('#id_body_rename').append('

    ' + $.i18n('infoDialog_password_new_text') + + '

    '); + $('#id_body_rename').append('
    ' + $.i18n('infoDialog_password_minimum_length') + '
    '); + $('#id_footer_rename').html(''); + $('#id_footer_rename').append(''); + } + else if (type == "checklist") { + $('#id_body').html(''); + $('#id_body').append('

    ' + $.i18n('infoDialog_checklist_title') + '

    '); + $('#id_body').append(header); + $('#id_footer').html(''); + } + else if (type == "newToken") { + $('#id_body').html(''); + $('#id_footer').html(''); + } + else if (type == "grantToken") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + + if (type != "renInst") { + $('#id_body').append('

    ' + header + '

    '); + $('#id_body').append(message); + } + + if (type == "select" || type == "iswitch") + $('#id_body').append(''); + + if (getStorage("darkMode") == "on") + $('#id_logo').attr("src", 'img/hyperion/logo_negativ.png'); + + $(type == "renInst" || type == "changePassword" ? "#modal_dialog_rename" : "#modal_dialog").modal({ + backdrop: "static", + keyboard: false, + show: true + }); + + $(document).on('click', '[data-dismiss-modal]', function () { + var target = $(this).attr('data-dismiss-modal'); + $.find(target).modal('hide'); + }); +} + +function createHintH(type, text, container) { + type = String(type); + if (type == "intro") + tclass = "introd"; + + $('#' + container).prepend('

    ' + text + '


    '); +} + +function createHint(type, text, container, buttonid, buttontxt) { + var fe, tclass; + + if (type == "intro") { + fe = ''; + tclass = "intro-hint"; + } + else if (type == "info") { + fe = '
    Information
    '; + tclass = "info-hint"; + } + else if (type == "wizard") { + fe = '
    Information
    '; + tclass = "wizard-hint"; + } + else if (type == "warning") { + fe = '
    Information
    '; + tclass = "warning-hint"; + } + + if (buttonid) + buttonid = '

    '; + else + buttonid = ""; + + if (type == "intro") + $('#' + container).prepend('

    ' + $.i18n("conf_helptable_expl") + '

    ' + text + '
    '); + else if (type == "wizard") + $('#' + container).prepend('

    ' + $.i18n("wiz_wizavail") + '

    ' + $.i18n('wiz_guideyou', text) + buttonid + '
    '); + else { + createTable('', 'htb', container, true, tclass); + $('#' + container + ' .htb').append(createTableRow([fe, text], false, true)); + } +} + +function createEffHint(title, text) { + return '

    ' + title + '

    ' + text + '
    '; +} + +function valValue(id, value, min, max) { + if (typeof max === 'undefined' || max == "") + max = 999999; + + if (Number(value) > Number(max)) { + $('#' + id).val(max); + showInfoDialog("warning", "", $.i18n('edt_msg_error_maximum_incl', max)); + return max; + } + else if (Number(value) < Number(min)) { + $('#' + id).val(min); + showInfoDialog("warning", "", $.i18n('edt_msg_error_minimum_incl', min)); + return min; + } + return value; +} + +function readImg(input, cb) { + if (input.files && input.files[0]) { + var reader = new FileReader(); + // inject fileName property + reader.fileName = input.files[0].name + + reader.onload = function (e) { + cb(e.target.result, e.target.fileName); + } + reader.readAsDataURL(input.files[0]); + } +} + +function isJsonString(str) { + try { + JSON.parse(str); + } + catch (e) { + return e; + } + return ""; +} + +const getObjectProperty = (obj, path) => path.split(".").reduce((o, key) => o && typeof o[key] !== 'undefined' ? o[key] : undefined, obj); + +const setObjectProperty = (object, path, value) => { + const parts = path.split('.'); + const limit = parts.length - 1; + for (let i = 0; i < limit; ++i) { + const key = parts[i]; + if (key === "__proto__" || key === "constructor") continue; + object = object[key] ?? (object[key] = {}); + } + const key = parts[limit]; + object[key] = value; +}; + +function getLongPropertiesPath(path) { + if (path) { + var path = path.replace('root.', ''); + const parts = path.split('.'); + parts.forEach(function (part, index) { + this[index] += ".properties"; + }, parts); + path = parts.join('.') + '.'; + } + return path; +} + +function createJsonEditor(container, schema, setconfig, usePanel, arrayre) { + $('#' + container).off(); + $('#' + container).html(""); + + if (typeof arrayre === 'undefined') + arrayre = true; + + var editor = new JSONEditor(document.getElementById(container), + { + theme: 'bootstrap3', + iconlib: "fontawesome4", + disable_collapse: 'true', + form_name_root: 'sa', + disable_edit_json: true, + disable_properties: true, + disable_array_reorder: arrayre, + no_additional_properties: true, + disable_array_delete_all_rows: true, + disable_array_delete_last_row: true, + access: storedAccess, + schema: { + title: '', + properties: schema + } + }); + + if (usePanel) { + $('#' + container + ' .well').first().removeClass('well well-sm'); + $('#' + container + ' h4').first().remove(); + $('#' + container + ' .well').first().removeClass('well well-sm'); + } + + if (setconfig) { + for (var key in editor.root.editors) { + editor.getEditor("root." + key).setValue(Object.assign({}, editor.getEditor("root." + key).value, window.serverConfig[key])); + } + } + + return editor; +} + +function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal, addSelect, addCustom, addCustomAsFirst, customText) { + var editor = rootEditor.getEditor(path); + var orginalProperties = editor.schema.properties[key]; + + var orginalWatchFunctions = rootEditor.watchlist[path + "." + key]; + rootEditor.unwatch(path + "." + key); + + var newSchema = []; + newSchema[key] = + { + "type": "string", + "enum": [], + "required": true, + "options": { "enum_titles": [], "infoText": "" }, + "propertyOrder": 1 + }; + + //Add additional elements to overwrite defaults + for (var item in addElements) { + newSchema[key][item] = addElements[item]; + } + + if (orginalProperties) { + if (orginalProperties["title"]) { + newSchema[key]["title"] = orginalProperties["title"]; + } + + if (orginalProperties["options"] && orginalProperties["options"]["infoText"]) { + newSchema[key]["options"]["infoText"] = orginalProperties["options"]["infoText"]; + } + + if (orginalProperties["propertyOrder"]) { + newSchema[key]["propertyOrder"] = orginalProperties["propertyOrder"]; + } + } + + if (addCustom) { + + if (newTitelVals.length === 0) { + newTitelVals = [...newEnumVals]; + } + + if (!!!customText) { + customText = "edt_conf_enum_custom"; + } + + if (addCustomAsFirst) { + newEnumVals.unshift("CUSTOM"); + newTitelVals.unshift(customText); + } else { + newEnumVals.push("CUSTOM"); + newTitelVals.push(customText); + } + + if (newSchema[key].options.infoText) { + var customInfoText = newSchema[key].options.infoText + "_custom"; + newSchema[key].options.infoText = customInfoText; + } + } + + if (addSelect) { + newEnumVals.unshift("SELECT"); + newTitelVals.unshift("edt_conf_enum_please_select"); + newDefaultVal = "SELECT"; + } + + if (newEnumVals) { + newSchema[key]["enum"] = newEnumVals; + } + + if (newTitelVals) { + newSchema[key]["options"]["enum_titles"] = newTitelVals; + } + if (newDefaultVal) { + newSchema[key]["default"] = newDefaultVal; + } + + editor.original_schema.properties[key] = orginalProperties; + editor.schema.properties[key] = newSchema[key]; + //Update schema properties for validator + setObjectProperty(rootEditor.validator.schema.properties, getLongPropertiesPath(path) + key, newSchema[key]); + + editor.removeObjectProperty(key); + delete editor.cached_editors[key]; + editor.addObjectProperty(key); + + if (orginalWatchFunctions) { + for (var i = 0; i < orginalWatchFunctions.length; i++) { + rootEditor.watch(path + "." + key, orginalWatchFunctions[i]); + } + } + rootEditor.notifyWatchers(path + "." + key); +} + +function updateJsonEditorMultiSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal) { + var editor = rootEditor.getEditor(path); + var orginalProperties = editor.schema.properties[key]; + + var orginalWatchFunctions = rootEditor.watchlist[path + "." + key]; + rootEditor.unwatch(path + "." + key); + + var newSchema = []; + newSchema[key] = + { + "type": "array", + "format": "select", + "items": { + "type": "string", + "enum": [], + "options": { "enum_titles": [] }, + }, + "options": { "infoText": "" }, + "default": [], + "propertyOrder": 1 + }; + + //Add additional elements to overwrite defaults + for (var item in addElements) { + newSchema[key][item] = addElements[item]; + } + + if (orginalProperties) { + if (orginalProperties["title"]) { + newSchema[key]["title"] = orginalProperties["title"]; + } + + if (orginalProperties["options"] && orginalProperties["options"]["infoText"]) { + newSchema[key]["options"]["infoText"] = orginalProperties["options"]["infoText"]; + } + + if (orginalProperties["propertyOrder"]) { + newSchema[key]["propertyOrder"] = orginalProperties["propertyOrder"]; + } + } + + if (newEnumVals) { + newSchema[key]["items"]["enum"] = newEnumVals; + } + + if (newTitelVals) { + newSchema[key]["items"]["options"]["enum_titles"] = newTitelVals; + } + + if (newDefaultVal) { + newSchema[key]["default"] = newDefaultVal; + } + + editor.original_schema.properties[key] = orginalProperties; + editor.schema.properties[key] = newSchema[key]; + //Update schema properties for validator + setObjectProperty(rootEditor.validator.schema.properties, getLongPropertiesPath(path) + key, newSchema[key]); + + editor.removeObjectProperty(key); + delete editor.cached_editors[key]; + editor.addObjectProperty(key); + + if (orginalWatchFunctions) { + for (var i = 0; i < orginalWatchFunctions.length; i++) { + rootEditor.watch(path + "." + key, orginalWatchFunctions[i]); + } + } + rootEditor.notifyWatchers(path + "." + key); +} + +function updateJsonEditorRange(rootEditor, path, key, minimum, maximum, defaultValue, step, clear) { + var editor = rootEditor.getEditor(path); + + //Preserve current value when updating range + var currentValue = rootEditor.getEditor(path + "." + key).getValue(); + + var orginalProperties = editor.schema.properties[key]; + var newSchema = []; + newSchema[key] = orginalProperties; + + if (clear) { + delete newSchema[key]["minimum"]; + delete newSchema[key]["maximum"]; + delete newSchema[key]["default"]; + delete newSchema[key]["step"]; + } + + if (typeof minimum !== "undefined") { + newSchema[key]["minimum"] = minimum; + } + if (typeof maximum !== "undefined") { + newSchema[key]["maximum"] = maximum; + } + if (typeof defaultValue !== "undefined") { + newSchema[key]["default"] = defaultValue; + currentValue = defaultValue; + } + + if (typeof step !== "undefined") { + newSchema[key]["step"] = step; + } + + editor.original_schema.properties[key] = orginalProperties; + editor.schema.properties[key] = newSchema[key]; + //Update schema properties for validator + setObjectProperty(rootEditor.validator.schema.properties, getLongPropertiesPath(path) + key, newSchema[key]); + + editor.removeObjectProperty(key); + delete editor.cached_editors[key]; + editor.addObjectProperty(key); + + // Restore current (new default) value for new range + rootEditor.getEditor(path + "." + key).setValue(currentValue); +} + +function addJsonEditorHostValidation() { + + JSONEditor.defaults.custom_validators.push(function (schema, value, path) { + var errors = []; + + if (!jQuery.isEmptyObject(value)) { + switch (schema.format) { + case "hostname_or_ip": + if (!isValidHostnameOrIP(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msgcust_error_hostname_ip') + }); + } + break; + case "hostname_or_ip4": + if (!isValidHostnameOrIP4(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msgcust_error_hostname_ip4') + }); + } + break; + + //Remove, when new json-editor 2.x is used + case "ipv4": + if (!isValidIPv4(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msg_error_ipv4') + }); + } + break; + case "ipv6": + if (!isValidIPv6(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msg_error_ipv6') + }); + } + break; + case "hostname": + if (!isValidHostname(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msg_error_hostname') + }); + } + break; + + default: + } + } + return errors; + }); +} + +function buildWL(link, linkt, cl) { + var baseLink = "https://docs.hyperion-project.org/"; + var lang; + + if (typeof linkt == "undefined") + linkt = "Placeholder"; + + if (storedLang == "de" || navigator.locale == "de") + lang = "de"; + else + lang = "en"; + + if (cl === true) { + linkt = $.i18n(linkt); + return '

    ' + linkt + '

    ' + $.i18n('general_wiki_moreto', linkt) + ': ' + linkt + '
    ' + } + else + return ': ' + linkt + ''; +} + +function rgbToHex(rgb) { + if (rgb.length == 3) { + return "#" + + ("0" + parseInt(rgb[0], 10).toString(16)).slice(-2) + + ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) + + ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2); + } + else + debugMessage('rgbToHex: Given rgb is no array or has wrong length'); +} + +function hexToRgb(hex) { + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : { + r: 0, + g: 0, + b: 0 + }; +} + +/* + Show a notification + @param type Valid types are "info","success","warning","danger" + @param message The message to show + @param title A title (optional) + @param addhtml Add custom html to the notification end + */ +function showNotification(type, message, title = "", addhtml = "") { + if (title == "") { + switch (type) { + case "info": + title = $.i18n('infoDialog_general_info_title'); + break; + case "success": + title = $.i18n('infoDialog_general_success_title'); + break; + case "warning": + title = $.i18n('infoDialog_general_warning_title'); + break; + case "danger": + title = $.i18n('infoDialog_general_error_title'); + break; + } + } + + $.notify({ + // options + title: title, + message: message + }, { + // settings + type: type, + animate: { + enter: 'animate__animated animate__fadeInDown', + exit: 'animate__animated animate__fadeOutUp' + }, + placement: { + align: 'center' + }, + mouse_over: 'pause', + template: '' + }); +} + +function createCP(id, color, cb) { + if (Array.isArray(color)) + color = rgbToHex(color); + else if (color == "undefined") + color = "#AA3399"; + + if (color.startsWith("#")) { + $('#' + id).colorpicker({ + format: 'rgb', + customClass: 'colorpicker-2x', + color: color, + sliders: { + saturation: { + maxLeft: 200, + maxTop: 200 + }, + hue: { + maxTop: 200 + }, + } + }); + $('#' + id).colorpicker().on('changeColor', function (e) { + var rgb = e.color.toRGB(); + var hex = e.color.toHex(); + cb(rgb, hex, e); + }); + } + else + debugMessage('createCP: Given color is not legit'); +} + +// Creates a table with thead and tbody ids +// @param string hid : a class for thead +// @param string bid : a class for tbody +// @param string cont : a container id to html() the table +// @param string bless: if true the table is borderless +function createTable(hid, bid, cont, bless, tclass) { + var table = document.createElement('table'); + var thead = document.createElement('thead'); + var tbody = document.createElement('tbody'); + + table.className = "table"; + if (bless === true) + table.className += " borderless"; + if (typeof tclass !== "undefined") + table.className += " " + tclass; + table.style.marginBottom = "0px"; + if (hid != "") + thead.className = hid; + tbody.className = bid; + if (hid != "") + table.appendChild(thead); + table.appendChild(tbody); + + $('#' + cont).append(table); +} + +// Creates a table row +// @param array list :innerHTML content for / +// @param bool head :if null or false it's body +// @param bool align :if null or false no alignment +// +// @return : with or as child(s) +function createTableRow(list, head, align) { + var row = document.createElement('tr'); + + for (var i = 0; i < list.length; i++) { + if (head === true) + var el = document.createElement('th'); + else + var el = document.createElement('td'); + + if (align) + el.style.verticalAlign = "middle"; + + var purifyConfig = { + ADD_TAGS: ['button'], + ADD_ATTR: ['onclick'] + }; + el.innerHTML = DOMPurify.sanitize(list[i], purifyConfig); + row.appendChild(el); + } + return row; +} + +function createRow(id) { + var el = document.createElement('div'); + el.className = "row"; + el.setAttribute('id', id); + return el; +} + +function createOptPanel(phicon, phead, bodyid, footerid, css, panelId) { + phead = '' + phead; + + var pfooter = document.createElement('button'); + pfooter.className = "btn btn-primary"; + pfooter.setAttribute("id", footerid); + pfooter.innerHTML = '' + $.i18n('general_button_savesettings'); + + return createPanel(phead, "", pfooter, "panel-default", bodyid, css, panelId); +} + +function compareTwoValues(key1, key2, order = 'asc') { + return function innerSort(a, b) { + if (!a.hasOwnProperty(key1) || !b.hasOwnProperty(key1)) { + // property key1 doesn't exist on either object + return 0; + } + + const varA1 = (typeof a[key1] === 'string') + ? a[key1].toUpperCase() : a[key1]; + const varB1 = (typeof b[key1] === 'string') + ? b[key1].toUpperCase() : b[key1]; + + let comparison = 0; + if (varA1 > varB1) { + comparison = 1; + } else { + if (varA1 < varB1) { + comparison = -1; + } else { + if (!a.hasOwnProperty(key2) || !b.hasOwnProperty(key2)) { + // property key2 doesn't exist on either object + return 0; + } + + const varA2 = (typeof a[key2] === 'string') + ? a[key2].toUpperCase() : a[key2]; + const varB2 = (typeof b[key1] === 'string') + ? b[key2].toUpperCase() : b[key2]; + + if (varA2 > varB2) { + comparison = 1; + } else { + comparison = -1; + } + } + } + return ( + (order === 'desc') ? (comparison * -1) : comparison + ); + }; +} + +function sortProperties(list) { + for (var key in list) { + list[key].key = key; + } + list = $.map(list, function (value, index) { + return [value]; + }); + return list.sort(function (a, b) { + return a.propertyOrder - b.propertyOrder; + }); +} + +function createHelpTable(list, phead, panelId) { + var table = document.createElement('table'); + var thead = document.createElement('thead'); + var tbody = document.createElement('tbody'); + list = sortProperties(list); + + phead = '' + phead + ' ' + $.i18n("conf_helptable_expl"); + + table.className = 'table table-hover borderless'; + + thead.appendChild(createTableRow([$.i18n('conf_helptable_option'), $.i18n('conf_helptable_expl')], true, false)); + + for (var key in list) { + if (list[key].access != 'system') { + // break one iteration (in the loop), if the schema has the entry hidden=true + if ("options" in list[key] && "hidden" in list[key].options && (list[key].options.hidden)) + continue; + if ("access" in list[key] && ((list[key].access == "advanced" && storedAccess == "default") || (list[key].access == "expert" && storedAccess != "expert"))) + continue; + var text = list[key].title.replace('title', 'expl'); + tbody.appendChild(createTableRow([$.i18n(list[key].title), $.i18n(text)], false, false)); + + if (list[key].items && list[key].items.properties) { + var ilist = sortProperties(list[key].items.properties); + for (var ikey in ilist) { + // break one iteration (in the loop), if the schema has the entry hidden=true + if ("options" in ilist[ikey] && "hidden" in ilist[ikey].options && (ilist[ikey].options.hidden)) + continue; + if ("access" in ilist[ikey] && ((ilist[ikey].access == "advanced" && storedAccess == "default") || (ilist[ikey].access == "expert" && storedAccess != "expert"))) + continue; + var itext = ilist[ikey].title.replace('title', 'expl'); + tbody.appendChild(createTableRow([$.i18n(ilist[ikey].title), $.i18n(itext)], false, false)); + } + } + } + } + table.appendChild(thead); + table.appendChild(tbody); + + return createPanel(phead, table, undefined, undefined, undefined, undefined, panelId); +} + +function createPanel(head, body, footer, type, bodyid, css, panelId) { + var cont = document.createElement('div'); + var p = document.createElement('div'); + var phead = document.createElement('div'); + var pbody = document.createElement('div'); + var pfooter = document.createElement('div'); + + cont.className = "col-lg-6"; + + if (typeof type == 'undefined') + type = 'panel-default'; + + p.className = 'panel ' + type; + if (typeof panelId != 'undefined') { + p.setAttribute("id", panelId); + } + + phead.className = 'panel-heading ' + css; + pbody.className = 'panel-body'; + pfooter.className = 'panel-footer'; + + phead.innerHTML = head; + + if (typeof bodyid != 'undefined') { + pfooter.style.textAlign = 'right'; + pbody.setAttribute("id", bodyid); + } + + if (typeof body != 'undefined' && body != "") + pbody.appendChild(body); + + if (typeof footer != 'undefined') + pfooter.appendChild(footer); + + p.appendChild(phead); + p.appendChild(pbody); + + if (typeof footer != 'undefined') { + pfooter.style.textAlign = "right"; + p.appendChild(pfooter); + } + + cont.appendChild(p); + + return cont; +} + +function createSelGroup(group) { + var el = document.createElement('optgroup'); + el.setAttribute('label', group); + return el; +} + +function createSelOpt(opt, title) { + var el = document.createElement('option'); + el.setAttribute('value', opt); + if (typeof title == 'undefined') + el.innerHTML = opt; + else + el.innerHTML = title; + return el; +} + +function createSel(array, group, split) { + if (array.length != 0) { + var el = createSelGroup(group); + for (var i = 0; i < array.length; i++) { + var opt; + if (split) { + opt = array[i].split(":") + opt = createSelOpt(opt[0], opt[1]) + } + else + opt = createSelOpt(array[i]) + el.appendChild(opt); + } + return el; + } +} + +function performTranslation() { + $('[data-i18n]').i18n(); +} + +function encode_utf8(s) { + return unescape(encodeURIComponent(s)); +} + +function getReleases(callback) { + $.ajax({ + url: window.gitHubReleaseApiUrl, + method: 'get', + error: function (XMLHttpRequest, textStatus, errorThrown) { + callback(false); + }, + success: function (releases) { + window.gitHubVersionList = releases; + var highestRelease = { + tag_name: '0.0.0' + }; + var highestAlphaRelease = { + tag_name: '0.0.0' + }; + var highestBetaRelease = { + tag_name: '0.0.0' + }; + var highestRcRelease = { + tag_name: '0.0.0' + }; + + for (var i in releases) { + //drafts will be ignored + if (releases[i].draft) + continue; + + if (releases[i].tag_name.includes('alpha')) { + if (sem = semverLite.gt(releases[i].tag_name, highestAlphaRelease.tag_name)) + highestAlphaRelease = releases[i]; + } + else if (releases[i].tag_name.includes('beta')) { + if (sem = semverLite.gt(releases[i].tag_name, highestBetaRelease.tag_name)) + highestBetaRelease = releases[i]; + } + else if (releases[i].tag_name.includes('rc')) { + if (semverLite.gt(releases[i].tag_name, highestRcRelease.tag_name)) + highestRcRelease = releases[i]; + } + else { + if (semverLite.gt(releases[i].tag_name, highestRelease.tag_name)) + highestRelease = releases[i]; + } + } + window.latestStableVersion = highestRelease; + window.latestBetaVersion = highestBetaRelease; + window.latestAlphaVersion = highestAlphaRelease; + window.latestRcVersion = highestRcRelease; + + if (window.serverConfig.general.watchedVersionBranch == "Beta" && semverLite.gt(highestBetaRelease.tag_name, highestRelease.tag_name)) + window.latestVersion = highestBetaRelease; + else + window.latestVersion = highestRelease; + + if (window.serverConfig.general.watchedVersionBranch == "Alpha" && semverLite.gt(highestAlphaRelease.tag_name, highestBetaRelease.tag_name)) + window.latestVersion = highestAlphaRelease; + + if (window.serverConfig.general.watchedVersionBranch == "Alpha" && semverLite.lt(highestAlphaRelease.tag_name, highestBetaRelease.tag_name)) + window.latestVersion = highestBetaRelease; + + //next two if statements are only necessary if we don't have a beta or stable release. We need one alpha release at least + if (window.latestVersion.tag_name == '0.0.0' && highestBetaRelease.tag_name != '0.0.0') + window.latestVersion = highestBetaRelease; + + if (window.latestVersion.tag_name == '0.0.0' && highestAlphaRelease.tag_name != '0.0.0') + window.latestVersion = highestAlphaRelease; + + callback(true); + } + }); +} + +function getSystemInfo() { + var sys = window.sysInfo.system; + var shy = window.sysInfo.hyperion; + + var info = "Hyperion Server:\n"; + info += '- Build: ' + shy.build + '\n'; + info += '- Build time: ' + shy.time + '\n'; + info += '- Git Remote: ' + shy.gitremote + '\n'; + info += '- Version: ' + shy.version + '\n'; + info += '- UI Lang: ' + storedLang + ' (BrowserLang: ' + navigator.language + ')\n'; + info += '- UI Access: ' + storedAccess + '\n'; + //info += '- Log lvl: ' + window.serverConfig.logger.level + '\n'; + info += '- Avail Screen Cap.: ' + window.serverInfo.grabbers.screen.available + '\n'; + info += '- Avail Video Cap.: ' + window.serverInfo.grabbers.video.available + '\n'; + info += '- Avail Services: ' + window.serverInfo.services + '\n'; + info += '- Config path: ' + shy.rootPath + '\n'; + info += '- Database: ' + (shy.readOnlyMode ? "ready-only" : "read/write") + '\n'; + + info += '\n'; + + info += 'Hyperion Server OS:\n'; + info += '- Distribution: ' + sys.prettyName + '\n'; + info += '- Architecture: ' + sys.architecture + '\n'; + + if (sys.cpuModelName) + info += '- CPU Model: ' + sys.cpuModelName + '\n'; + if (sys.cpuModelType) + info += '- CPU Type: ' + sys.cpuModelType + '\n'; + if (sys.cpuRevision) + info += '- CPU Revision: ' + sys.cpuRevision + '\n'; + if (sys.cpuHardware) + info += '- CPU Hardware: ' + sys.cpuHardware + '\n'; + + info += '- Kernel: ' + sys.kernelType + ' (' + sys.kernelVersion + ' (WS: ' + sys.wordSize + '))\n'; + info += '- Root/Admin: ' + sys.isUserAdmin + '\n'; + info += '- Qt Version: ' + sys.qtVersion + '\n'; + if (jQuery.inArray("effectengine", window.serverInfo.services) !== -1) { + info += '- Python Version: ' + sys.pyVersion + '\n'; + } + info += '- Browser: ' + navigator.userAgent; + return info; +} + +function handleDarkMode() { + $("", { + rel: "stylesheet", + type: "text/css", + href: "../css/darkMode.css" + }).appendTo("head"); + + setStorage("darkMode", "on"); + $('#btn_darkmode_icon').removeClass('fa fa-moon-o'); + $('#btn_darkmode_icon').addClass('mdi mdi-white-balance-sunny'); + $('#navbar_brand_logo').attr("src", 'img/hyperion/logo_negativ.png'); +} + +function isAccessLevelCompliant(accessLevel) { + var isOK = true; + if (accessLevel) { + if (accessLevel === 'system') { + isOK = false; + } + else if (accessLevel === 'advanced' && storedAccess === 'default') { + isOK = false; + } + else if (accessLevel === 'expert' && storedAccess !== 'expert') { + isOK = false; + } + } + return isOK +} + +function showInputOptions(path, elements, state) { + + if (!path.startsWith("root.")) { + path = ["root", path].join('.'); + } + + for (var i = 0; i < elements.length; i++) { + $('[data-schemapath="' + path + '.' + elements[i] + '"]').toggle(state); + } +} + +function showInputOptionForItem(editor, path, item, state) { + //Get access level for full path and item + var accessLevel = getObjectProperty(editor.schema.properties, getLongPropertiesPath(path) + item + ".access"); + // Enable element only, if access level compliant + if (!state || isAccessLevelCompliant(accessLevel)) { + + if (!path) { + debugger; + path = editor.path; + } + showInputOptions(path, [item], state); + } +} + +function showInputOptionsForKey(editor, item, showForKeys, state) { + var elements = []; + var keysToshow = []; + + if (Array.isArray(showForKeys)) { + keysToshow = showForKeys; + } else { + if (typeof showForKeys === 'string') { + keysToshow.push(showForKeys); + } else { + return; + } + } + + for (var key in editor.schema.properties[item].properties) { + if ($.inArray(key, keysToshow) === -1) { + var accessLevel = editor.schema.properties[item].properties[key].access; + + var hidden = false; + if (editor.schema.properties[item].properties[key].options) { + hidden = editor.schema.properties[item].properties[key].options.hidden; + if (typeof hidden === 'undefined') { + hidden = false; + } + } + //Always disable all elements, but only enable elements, if access level compliant + if (!state || isAccessLevelCompliant(accessLevel)) { + if (!hidden) { + elements.push(key); + } + } + } + } + showInputOptions(item, elements, state); +} + +function encodeHTML(s) { + return s.replace(/&/g, '&').replace(/ 255) { + return false; + } + } + return true; +} + +function isValidIPv6(value) { + if (value.match( + '^(?:(?:(?:[a-fA-F0-9]{1,4}:){6}|(?=(?:[a-fA-F0-9]{0,4}:){2,6}(?:[0-9]{1,3}.){3}[0-9]{1,3}$)(([0-9a-fA-F]{1,4}:){1,5}|:)((:[0-9a-fA-F]{1,4}){1,5}:|:)|::(?:[a-fA-F0-9]{1,4}:){5})(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]).){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?=(?:[a-fA-F0-9]{0,4}:){0,7}[a-fA-F0-9]{0,4}$)(([0-9a-fA-F]{1,4}:){1,7}|:)((:[0-9a-fA-F]{1,4}){1,7}|:)|(?:[a-fA-F0-9]{1,4}:){7}:|:(:[a-fA-F0-9]{1,4}){7})$' + )) + return true; + else + return false; +} + +function isValidHostname(value) { + if (value.match( + '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])(.([a-zA-Z0-9]|[_a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$' + )) + return true; + else + return false; +} + +function isValidServicename(value) { + if (value.match( + '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9 -]{0,61}[a-zA-Z0-9])(.([a-zA-Z0-9]|[_a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$' + )) + return true; + else + return false; +} + +function isValidHostnameOrIP4(value) { + return (isValidHostname(value) || isValidIPv4(value)); +} + +function isValidHostnameOrIP(value) { + return (isValidHostnameOrIP4(value) || isValidIPv6(value) || isValidServicename(value)); +} + diff --git a/include/leddevice/LedDevice.h b/include/leddevice/LedDevice.h index f43ee2d9..b17a0fc9 100644 --- a/include/leddevice/LedDevice.h +++ b/include/leddevice/LedDevice.h @@ -438,7 +438,10 @@ protected: uint _ledRGBWCount; /// Does the device allow restoring the original state? - bool _isRestoreOrigState; + bool _isRestoreOrigState; + + /// Does the device should be kept on after streaming + bool _isStayOnAfterStreaming; /// Device, lights state before streaming via hyperion QJsonObject _orignalStateValues; @@ -460,6 +463,9 @@ protected: /// Is the device in error state and stopped? bool _isDeviceInError; + /// Is the device in error state, but is retries might resolve the situation? + bool _isDeviceRecoverable; + /// Timestamp of last write QDateTime _lastWriteTime; @@ -476,8 +482,9 @@ protected slots: /// @brief Set device in error state /// /// @param[in] errorMsg The error message to be logged + /// @param[in] isRecoverable If False, no further retries will be done /// - virtual void setInError( const QString& errorMsg); + virtual void setInError( const QString& errorMsg, bool isRecoverable=true); private: diff --git a/libsrc/leddevice/LedDevice.cpp b/libsrc/leddevice/LedDevice.cpp index b30dd4fd..f49878c4 100644 --- a/libsrc/leddevice/LedDevice.cpp +++ b/libsrc/leddevice/LedDevice.cpp @@ -49,11 +49,13 @@ LedDevice::LedDevice(const QJsonObject& deviceConfig, QObject* parent) , _latchTime_ms(0) , _ledCount(0) , _isRestoreOrigState(false) + , _isStayOnAfterStreaming(false) , _isEnabled(false) , _isDeviceInitialised(false) , _isDeviceReady(false) , _isOn(false) , _isDeviceInError(false) + , _isDeviceRecoverable(false) , _lastWriteTime(QDateTime::currentDateTime()) , _enableAttemptsTimer(nullptr) , _enableAttemptTimerInterval(DEFAULT_ENABLE_ATTEMPTS_INTERVAL) @@ -117,7 +119,7 @@ int LedDevice::close() return retval; } -void LedDevice::setInError(const QString& errorMsg) +void LedDevice::setInError(const QString& errorMsg, bool isRecoverable) { _isOn = false; _isDeviceInError = true; @@ -125,6 +127,10 @@ void LedDevice::setInError(const QString& errorMsg) _isEnabled = false; this->stopRefreshTimer(); + if (isRecoverable) + { + _isDeviceRecoverable = isRecoverable; + } Error(_log, "Device disabled, device '%s' signals error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(errorMsg)); emit enableStateChanged(_isEnabled); } @@ -170,7 +176,7 @@ void LedDevice::enable() { emit enableStateChanged(false); - if (_maxEnableAttempts > 0) + if (_maxEnableAttempts > 0 && _isDeviceRecoverable) { Debug(_log, "Device's enablement failed - Start retry timer. Retried already done [%d], isEnabled: [%d]", _enableAttempts, _isEnabled); startEnableAttemptsTimer(); @@ -257,27 +263,30 @@ void LedDevice::startEnableAttemptsTimer() { ++_enableAttempts; - if (_enableAttempts <= _maxEnableAttempts) + if (_isDeviceRecoverable) { - if (_enableAttemptTimerInterval.count() > 0) + if (_enableAttempts <= _maxEnableAttempts) { - // setup enable retry timer - if (_enableAttemptsTimer == nullptr) + if (_enableAttemptTimerInterval.count() > 0) { - _enableAttemptsTimer = new QTimer(this); - _enableAttemptsTimer->setTimerType(Qt::PreciseTimer); - connect(_enableAttemptsTimer, &QTimer::timeout, this, &LedDevice::enable); - } - _enableAttemptsTimer->setInterval(static_cast(_enableAttemptTimerInterval.count() * 1000)); //NOLINT + // setup enable retry timer + if (_enableAttemptsTimer == nullptr) + { + _enableAttemptsTimer = new QTimer(this); + _enableAttemptsTimer->setTimerType(Qt::PreciseTimer); + connect(_enableAttemptsTimer, &QTimer::timeout, this, &LedDevice::enable); + } + _enableAttemptsTimer->setInterval(static_cast(_enableAttemptTimerInterval.count() * 1000)); //NOLINT - Info(_log, "Start %d. attempt of %d to enable the device in %d seconds", _enableAttempts, _maxEnableAttempts, _enableAttemptTimerInterval.count()); - _enableAttemptsTimer->start(); + Info(_log, "Start %d. attempt of %d to enable the device in %d seconds", _enableAttempts, _maxEnableAttempts, _enableAttemptTimerInterval.count()); + _enableAttemptsTimer->start(); + } + } + else + { + Error(_log, "Device disabled. Maximum number of %d attempts enabling the device reached. Tried for %d seconds.", _maxEnableAttempts, _enableAttempts * _enableAttemptTimerInterval.count()); + _enableAttempts = 0; } - } - else - { - Error(_log, "Device disabled. Maximum number of %d attempts enabling the device reached. Tried for %d seconds.", _maxEnableAttempts, _enableAttempts * _enableAttemptTimerInterval.count()); - _enableAttempts = 0; } } @@ -452,14 +461,16 @@ bool LedDevice::switchOff() bool LedDevice::powerOff() { - bool rc{ false }; + bool rc{ true }; - Debug(_log, "Power Off: %s", QSTRING_CSTR(_activeDeviceType)); - - // Simulate power-off by writing a final "Black" to have a defined outcome - if (writeBlack() >= 0) + if (!_isStayOnAfterStreaming) { - rc = true; + Debug(_log, "Power Off: %s", QSTRING_CSTR(_activeDeviceType)); + // Simulate power-off by writing a final "Black" to have a defined outcome + if (writeBlack() < 0) + { + rc = false; + } } return rc; } diff --git a/libsrc/leddevice/dev_net/LedDeviceWled.cpp b/libsrc/leddevice/dev_net/LedDeviceWled.cpp index 9483c3d4..7b4fed2a 100644 --- a/libsrc/leddevice/dev_net/LedDeviceWled.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceWled.cpp @@ -23,35 +23,61 @@ const bool verbose = false; const char CONFIG_HOST[] = "host"; const char CONFIG_STREAM_PROTOCOL[] = "streamProtocol"; const char CONFIG_RESTORE_STATE[] = "restoreOriginalState"; +const char CONFIG_STAY_ON_AFTER_STREAMING[] = "stayOnAfterStreaming"; + const char CONFIG_BRIGHTNESS[] = "brightness"; const char CONFIG_BRIGHTNESS_OVERWRITE[] = "overwriteBrightness"; const char CONFIG_SYNC_OVERWRITE[] = "overwriteSync"; +const char CONFIG_STREAM_SEGMENTS[] = "segments"; +const char CONFIG_STREAM_SEGMENT_ID[] = "streamSegmentId"; +const char CONFIG_SWITCH_OFF_OTHER_SEGMENTS[] = "switchOffOtherSegments"; + const char DEFAULT_STREAM_PROTOCOL[] = "DDP"; // UDP-RAW const int UDP_STREAM_DEFAULT_PORT = 19446; const int UDP_MAX_LED_NUM = 490; -// DDP +// Version constraints const char WLED_VERSION_DDP[] = "0.11.0"; +const char WLED_VERSION_SEGMENT_STREAMING[] = "0.13.3"; // WLED JSON-API elements const int API_DEFAULT_PORT = -1; //Use default port per communication scheme const char API_BASE_PATH[] = "/json/"; const char API_PATH_STATE[] = "state"; +const char API_PATH_INFO[] = "info"; -// List of State Information +// List of State keys const char STATE_ON[] = "on"; -const char STATE_VALUE_TRUE[] = "true"; -const char STATE_VALUE_FALSE[] = "false"; +const char STATE_BRI[] = "bri"; const char STATE_LIVE[] = "live"; +const char STATE_LOR[] = "lor"; +const char STATE_SEG[] = "seg"; +const char STATE_SEG_ID[] = "id"; +const char STATE_SEG_LEN[] = "len"; +const char STATE_SEG_FX[] = "fx"; +const char STATE_SEG_SX[] = "sx"; +const char STATE_MAINSEG[] = "mainseg"; +const char STATE_UDPN[] = "udpn"; +const char STATE_UDPN_SEND[] = "send"; +const char STATE_UDPN_RECV[] = "recv"; +const char STATE_TRANSITIONTIME_CURRENTCALL[] = "tt"; +// List of Info keys +const char INFO_VER[] = "ver"; +const char INFO_LIVESEG[] = "liveseg"; + +//Default state values const bool DEFAULT_IS_RESTORE_STATE = false; +const bool DEFAULT_IS_STAY_ON_AFTER_STREAMING = false; const bool DEFAULT_IS_BRIGHTNESS_OVERWRITE = true; const int BRI_MAX = 255; const bool DEFAULT_IS_SYNC_OVERWRITE = true; +const int DEFAULT_SEGMENT_ID = -1; +const bool DEFAULT_IS_SWITCH_OFF_OTHER_SEGMENTS = true; constexpr std::chrono::milliseconds DEFAULT_IDENTIFY_TIME{ 2000 }; @@ -61,12 +87,16 @@ LedDeviceWled::LedDeviceWled(const QJsonObject &deviceConfig) : ProviderUdp(deviceConfig), LedDeviceUdpDdp(deviceConfig), LedDeviceUdpRaw(deviceConfig) ,_restApi(nullptr) ,_apiPort(API_DEFAULT_PORT) + ,_currentVersion("") ,_isBrightnessOverwrite(DEFAULT_IS_BRIGHTNESS_OVERWRITE) ,_brightness (BRI_MAX) ,_isSyncOverwrite(DEFAULT_IS_SYNC_OVERWRITE) ,_originalStateUdpnSend(false) ,_originalStateUdpnRecv(true) ,_isStreamDDP(true) + ,_streamSegmentId(DEFAULT_SEGMENT_ID) + ,_isSwitchOffOtherSegments(DEFAULT_IS_SWITCH_OFF_OTHER_SEGMENTS) + ,_isStreamToSegment(false) { #ifdef ENABLE_MDNS QMetaObject::invokeMethod(&MdnsBrowser::getInstance(), "browseForServiceType", @@ -112,6 +142,7 @@ bool LedDeviceWled::init(const QJsonObject &deviceConfig) { _apiPort = API_DEFAULT_PORT; _isRestoreOrigState = _devConfig[CONFIG_RESTORE_STATE].toBool(DEFAULT_IS_RESTORE_STATE); + _isStayOnAfterStreaming = _devConfig[CONFIG_STAY_ON_AFTER_STREAMING].toBool(DEFAULT_IS_STAY_ON_AFTER_STREAMING); _isSyncOverwrite = _devConfig[CONFIG_SYNC_OVERWRITE].toBool(DEFAULT_IS_SYNC_OVERWRITE); _isBrightnessOverwrite = _devConfig[CONFIG_BRIGHTNESS_OVERWRITE].toBool(DEFAULT_IS_BRIGHTNESS_OVERWRITE); _brightness = _devConfig[CONFIG_BRIGHTNESS].toInt(BRI_MAX); @@ -121,6 +152,23 @@ bool LedDeviceWled::init(const QJsonObject &deviceConfig) Debug(_log, "Overwrite Brightn.: %d", _isBrightnessOverwrite); Debug(_log, "Set Brightness to : %d", _brightness); + + QJsonObject segments = _devConfig[CONFIG_STREAM_SEGMENTS].toObject(); + _streamSegmentId = segments[CONFIG_STREAM_SEGMENT_ID].toInt(DEFAULT_SEGMENT_ID); + + if (_streamSegmentId > DEFAULT_SEGMENT_ID) + { + _isStreamToSegment = true; + } + _isSwitchOffOtherSegments = segments[CONFIG_SWITCH_OFF_OTHER_SEGMENTS].toBool(DEFAULT_IS_SWITCH_OFF_OTHER_SEGMENTS); + + Debug(_log, "Stream to one segment: %s", _isStreamToSegment ? "Yes" : "No"); + if (_isStreamToSegment ) + { + Debug(_log, "Stream to segment [%d]", _streamSegmentId); + Debug(_log, "Switch-off other segments: %s", _isSwitchOffOtherSegments ? "Yes" : "No"); + } + isInitOK = true; } @@ -193,79 +241,187 @@ int LedDeviceWled::close() return retval; } -QString LedDeviceWled::getOnOffRequest(bool isOn) const +QJsonObject LedDeviceWled::getUdpnObject(bool isSendOn, bool isRecvOn) const { - QString state = isOn ? STATE_VALUE_TRUE : STATE_VALUE_FALSE; - return QString( "\"%1\":%2,\"%3\":%4" ).arg( STATE_ON, state).arg( STATE_LIVE, state); + QJsonObject udpnObj + { + {STATE_UDPN_SEND, isSendOn}, + {STATE_UDPN_RECV, isRecvOn} + }; + return udpnObj; } -QString LedDeviceWled::getBrightnessRequest(int bri) const +QJsonObject LedDeviceWled::getSegmentObject(int segmentId, bool isOn, int brightness) const { - return QString( "\"bri\":%1" ).arg(bri); + QJsonObject segmentObj + { + {STATE_SEG_ID, segmentId}, + {STATE_ON, isOn} + }; + + if ( brightness > -1) + { + segmentObj.insert(STATE_BRI, brightness); + } + return segmentObj; } -QString LedDeviceWled::getEffectRequest(int effect, int speed) const -{ - return QString( "\"seg\":{\"fx\":%1,\"sx\":%2}" ).arg(effect).arg(speed); -} - -QString LedDeviceWled::getLorRequest(int lor) const -{ - return QString( "\"lor\":%1" ).arg(lor); -} - -QString LedDeviceWled::getUdpnRequest(bool isSendOn, bool isRecvOn) const -{ - QString send = isSendOn ? STATE_VALUE_TRUE : STATE_VALUE_FALSE; - QString recv = isRecvOn ? STATE_VALUE_TRUE : STATE_VALUE_FALSE; - return QString( "\"udpn\":{\"send\":%1,\"recv\":%2}" ).arg(send, recv); -} - -bool LedDeviceWled::sendStateUpdateRequest(const QString &request) +bool LedDeviceWled::sendStateUpdateRequest(const QJsonObject &request, const QString requestType) { bool rc = true; _restApi->setPath(API_PATH_STATE); - httpResponse response1 = _restApi->put(QString("{%1}").arg(request)); - if ( response1.error() ) + httpResponse response = _restApi->put(request); + if ( response.error() ) { + QString errorReason = QString("%1 request failed with error: '%2'").arg(requestType, response.getErrorReason()); + this->setInError ( errorReason ); rc = false; } return rc; } + +bool LedDeviceWled::isReadyForSegmentStreaming(semver::version& version) const +{ + bool isReady{false}; + + if (version.isValid()) + { + semver::version segmentStreamingVersion{WLED_VERSION_SEGMENT_STREAMING}; + if (version < segmentStreamingVersion) + { + Warning(_log, "Segment streaming not supported by your WLED device version [%s], minimum version expected [%s].", _currentVersion.getVersion().c_str(), segmentStreamingVersion.getVersion().c_str()); + } + else + { + Debug(_log, "Segment streaming is supported by your WLED device version [%s].", _currentVersion.getVersion().c_str()); + isReady = true; + } + } + else + { + Error(_log, "Version provided to test for streaming readiness is not valid "); + } + return isReady; +} + +bool LedDeviceWled::isReadyForDDPStreaming(semver::version& version) const +{ + bool isReady{false}; + + if (version.isValid()) + { + semver::version ddpVersion{WLED_VERSION_DDP}; + if (version < ddpVersion) + { + Warning(_log, "DDP streaming not supported by your WLED device version [%s], minimum version expected [%s]. Fall back to UDP-Streaming (%d LEDs max)", _currentVersion.getVersion().c_str(), ddpVersion.getVersion().c_str(), UDP_MAX_LED_NUM); + } + else + { + Debug(_log, "DDP streaming is supported by your WLED device version [%s]. No limitation in number of LEDs.", _currentVersion.getVersion().c_str()); + isReady = true; + } + } + else + { + Error(_log, "Version provided to test for streaming readiness is not valid "); + } + return isReady; +} + bool LedDeviceWled::powerOn() { bool on = false; if ( _isDeviceReady) { //Power-on WLED device - _restApi->setPath(API_PATH_STATE); - - QString cmd = getOnOffRequest(true); - - if ( _isBrightnessOverwrite) + QJsonObject cmd; + if (_isStreamToSegment) { - cmd += "," + getBrightnessRequest(_brightness); + if (!isReadyForSegmentStreaming(_currentVersion)) + { + return false; + } + + if (_wledInfo[INFO_LIVESEG].toInt() == -1) + { + stopEnableAttemptsTimer(); + this->setInError( "Segment streaming configured, but \"Use main segment only\" in WLED Sync Interface configuration is not enabled!", false); + return false; + } + else + { + QJsonArray propertiesSegments = _originalStateProperties[STATE_SEG].toArray(); + + bool isStreamSegmentIdFound { false }; + + QJsonArray segments; + for (const auto& segmentItem : qAsConst(propertiesSegments)) + { + QJsonObject segmentObj = segmentItem.toObject(); + + int segmentID = segmentObj.value(STATE_SEG_ID).toInt(); + if (segmentID == _streamSegmentId) + { + isStreamSegmentIdFound = true; + int len = segmentObj.value(STATE_SEG_LEN).toInt(); + if (getLedCount() > len) + { + QString errorReason = QString("Too many LEDs [%1] configured for segment [%2], which supports maximum [%3] LEDs. Check your WLED setup!").arg(getLedCount()).arg(_streamSegmentId).arg(len); + this->setInError(errorReason, false); + return false; + } + else + { + int brightness{ -1 }; + if (_isBrightnessOverwrite) + { + brightness = _brightness; + } + segments.append(getSegmentObject(segmentID, true, brightness)); + } + } + else + { + if (_isSwitchOffOtherSegments) + { + segments.append(getSegmentObject(segmentID, false)); + } + } + } + + if (!isStreamSegmentIdFound) + { + QString errorReason = QString("Segment streaming to segment [%1] configured, but segment does not exist on WLED. Check your WLED setup!").arg(_streamSegmentId); + this->setInError(errorReason, false); + return false; + } + + cmd.insert(STATE_SEG, segments); + + //Set segment to be streamed to + cmd.insert(STATE_MAINSEG, _streamSegmentId); + } } + else + { + if (_isBrightnessOverwrite) + { + cmd.insert(STATE_BRI, _brightness); + } + } + + cmd.insert(STATE_LIVE, true); + cmd.insert(STATE_ON, true); if (_isSyncOverwrite) { Debug( _log, "Disable synchronisation with other WLED devices"); - cmd += "," + getUdpnRequest(false, false); + cmd.insert(STATE_UDPN, getUdpnObject(false, false)); } - httpResponse response = _restApi->put(QString("{%1}").arg(cmd)); - if ( response.error() ) - { - QString errorReason = QString("Power-on request failed with error: '%1'").arg(response.getErrorReason()); - this->setInError ( errorReason ); - on = false; - } - else - { - on = true; - } + on = sendStateUpdateRequest(cmd,"Power-on"); } return on; } @@ -279,23 +435,25 @@ bool LedDeviceWled::powerOff() writeBlack(); //Power-off the WLED device physically - _restApi->setPath(API_PATH_STATE); + QJsonObject cmd; + if (_isStreamToSegment) + { + QJsonArray segments; + segments.append(getSegmentObject(_streamSegmentId, _isStayOnAfterStreaming)); + cmd.insert(STATE_SEG, segments); + } - QString cmd = getOnOffRequest(false); + cmd.insert(STATE_LIVE, false); + cmd.insert(STATE_TRANSITIONTIME_CURRENTCALL, 0); + cmd.insert(STATE_ON, _isStayOnAfterStreaming); if (_isSyncOverwrite) { Debug( _log, "Restore synchronisation with other WLED devices"); - cmd += "," + getUdpnRequest(_originalStateUdpnSend, _originalStateUdpnRecv); + cmd.insert(STATE_UDPN, getUdpnObject(_originalStateUdpnSend, _originalStateUdpnRecv)); } - httpResponse response = _restApi->put(QString("{%1}").arg(cmd)); - if ( response.error() ) - { - QString errorReason = QString("Power-off request failed with error: '%1'").arg(response.getErrorReason()); - this->setInError ( errorReason ); - off = false; - } + off = sendStateUpdateRequest(cmd,"Power-off"); } return off; } @@ -304,28 +462,33 @@ bool LedDeviceWled::storeState() { bool rc = true; - if ( _isRestoreOrigState || _isSyncOverwrite ) + if ( _isRestoreOrigState || _isSyncOverwrite || _isStreamToSegment) { - _restApi->setPath(API_PATH_STATE); + _restApi->setPath(""); httpResponse response = _restApi->get(); if ( response.error() ) { - QString errorReason = QString("Storing device state failed with error: '%1'").arg(response.getErrorReason()); + QString errorReason = QString("Retrieving device properties failed with error: '%1'").arg(response.getErrorReason()); setInError(errorReason); rc = false; } else { - _originalStateProperties = response.getBody().object(); + _originalStateProperties = response.getBody().object().value(API_PATH_STATE).toObject(); DebugIf(verbose, _log, "state: [%s]", QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData() ); - QJsonObject udpn = _originalStateProperties.value("udpn").toObject(); + QJsonObject udpn = _originalStateProperties.value(STATE_UDPN).toObject(); if (!udpn.isEmpty()) { - _originalStateUdpnSend = udpn["send"].toBool(false); - _originalStateUdpnRecv = udpn["recv"].toBool(true); + _originalStateUdpnSend = udpn[STATE_UDPN_SEND].toBool(false); + _originalStateUdpnRecv = udpn[STATE_UDPN_RECV].toBool(true); } + + _wledInfo = response.getBody().object().value(API_PATH_INFO).toObject(); + DebugIf(verbose, _log, "info: [%s]", QString(QJsonDocument(_wledInfo).toJson(QJsonDocument::Compact)).toUtf8().constData() ); + + _currentVersion.setVersion(_wledInfo.value(INFO_VER).toString().toStdString()); } } @@ -340,10 +503,29 @@ bool LedDeviceWled::restoreState() { _restApi->setPath(API_PATH_STATE); + if (_isStreamToSegment) + { + QJsonArray propertiesSegments = _originalStateProperties[STATE_SEG].toArray(); + QJsonArray segments; + for (const auto& segmentItem : qAsConst(propertiesSegments)) + { + QJsonObject segmentObj = segmentItem.toObject(); + + int segmentID = segmentObj.value(STATE_SEG_ID).toInt(); + if (segmentID == _streamSegmentId) + { + segmentObj[STATE_ON] = _isStayOnAfterStreaming; + } + segments.append(segmentObj); + } + _originalStateProperties[STATE_SEG] = segments; + } + _originalStateProperties[STATE_LIVE] = false; + _originalStateProperties[STATE_TRANSITIONTIME_CURRENTCALL] = 0; + _originalStateProperties[STATE_ON] = _isStayOnAfterStreaming; - httpResponse response = _restApi->put(QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData()); - + httpResponse response = _restApi->put(_originalStateProperties); if ( response.error() ) { Warning (_log, "%s restoring state failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); @@ -363,10 +545,10 @@ QJsonObject LedDeviceWled::discover(const QJsonObject& /*params*/) #ifdef ENABLE_MDNS QString discoveryMethod("mDNS"); deviceList = MdnsBrowser::getInstance().getServicesDiscoveredJson( - MdnsServiceRegister::getServiceType(_activeDeviceType), - MdnsServiceRegister::getServiceNameFilter(_activeDeviceType), - DEFAULT_DISCOVER_TIMEOUT - ); + MdnsServiceRegister::getServiceType(_activeDeviceType), + MdnsServiceRegister::getServiceNameFilter(_activeDeviceType), + DEFAULT_DISCOVER_TIMEOUT + ); devicesDiscovered.insert("discoveryMethod", discoveryMethod); #endif devicesDiscovered.insert("devices", deviceList); @@ -397,27 +579,21 @@ QJsonObject LedDeviceWled::getProperties(const QJsonObject& params) { Warning (_log, "%s get properties failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); } - - QJsonObject propertiesDetails = response.getBody().object(); - - semver::version currentVersion {""}; - if (currentVersion.setVersion(propertiesDetails.value("ver").toString().toStdString())) + else { - semver::version ddpVersion{WLED_VERSION_DDP}; - if (currentVersion < ddpVersion) + QJsonObject propertiesDetails = response.getBody().object(); + + _wledInfo = propertiesDetails.value(API_PATH_INFO).toObject(); + _currentVersion.setVersion(_wledInfo.value(INFO_VER).toString().toStdString()); + if (!isReadyForDDPStreaming(_currentVersion)) { - Warning(_log, "DDP streaming not supported by your WLED device version [%s], minimum version expected [%s]. Fall back to UDP-Streaming (%d LEDs max)", currentVersion.getVersion().c_str(), ddpVersion.getVersion().c_str(), UDP_MAX_LED_NUM); if (!propertiesDetails.isEmpty()) { propertiesDetails.insert("maxLedCount", UDP_MAX_LED_NUM); } } - else - { - Info(_log, "DDP streaming is supported by your WLED device version [%s]. No limitation in number of LEDs.", currentVersion.getVersion().c_str()); - } + properties.insert("properties", propertiesDetails); } - properties.insert("properties", propertiesDetails); } DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData() ); @@ -441,8 +617,23 @@ void LedDeviceWled::identify(const QJsonObject& params) _isRestoreOrigState = true; storeState(); - QString request = getOnOffRequest(true) + "," + getLorRequest(1) + "," + getEffectRequest(25); - sendStateUpdateRequest(request); + QJsonObject cmd; + + cmd.insert(STATE_ON, true); + cmd.insert(STATE_LOR, 1); + + _streamSegmentId = params[CONFIG_STREAM_SEGMENT_ID].toInt(0); + + QJsonObject segment; + segment = getSegmentObject(_streamSegmentId, true, BRI_MAX); + segment.insert(STATE_SEG_FX, 25); + segment.insert(STATE_SEG_SX, 128); + + QJsonArray segments; + segments.append(segment); + cmd.insert(STATE_SEG, segments); + + sendStateUpdateRequest(cmd,"Identify"); wait(DEFAULT_IDENTIFY_TIME); diff --git a/libsrc/leddevice/dev_net/LedDeviceWled.h b/libsrc/leddevice/dev_net/LedDeviceWled.h index 5b5f9940..7f6b25ed 100644 --- a/libsrc/leddevice/dev_net/LedDeviceWled.h +++ b/libsrc/leddevice/dev_net/LedDeviceWled.h @@ -7,6 +7,7 @@ #include "LedDeviceUdpDdp.h" #include "LedDeviceUdpRaw.h" +#include /// /// Implementation of a WLED-device /// @@ -146,20 +147,13 @@ private: /// bool openRestAPI(); - /// - /// @brief Get command to power WLED-device on or off - /// - /// @param isOn True, if to switch on device - /// @return Command to switch device on/off - /// - QString getOnOffRequest (bool isOn ) const; + QJsonObject getUdpnObject(bool send, bool recv) const; + QJsonObject getSegmentObject(int segmentId, bool isOn, int brightness=-1) const; - QString getBrightnessRequest (int bri ) const; - QString getEffectRequest(int effect, int speed=128) const; - QString getLorRequest(int lor) const; - QString getUdpnRequest(bool send, bool recv) const; + bool sendStateUpdateRequest(const QJsonObject &request, const QString requestType = ""); - bool sendStateUpdateRequest(const QString &request); + bool isReadyForSegmentStreaming(semver::version& version) const; + bool isReadyForDDPStreaming(semver::version& version) const; QString resolveAddress (const QString& hostName); @@ -169,8 +163,11 @@ private: QString _hostAddress; int _apiPort; + QJsonObject _wledInfo; QJsonObject _originalStateProperties; + semver::version _currentVersion; + bool _isBrightnessOverwrite; int _brightness; @@ -179,6 +176,10 @@ private: bool _originalStateUdpnRecv; bool _isStreamDDP; + + int _streamSegmentId; + bool _isSwitchOffOtherSegments; + bool _isStreamToSegment; }; #endif // LEDDEVICEWLED_H diff --git a/libsrc/leddevice/dev_serial/ProviderRs232.cpp b/libsrc/leddevice/dev_serial/ProviderRs232.cpp index 9b2b1197..ea4f99b9 100644 --- a/libsrc/leddevice/dev_serial/ProviderRs232.cpp +++ b/libsrc/leddevice/dev_serial/ProviderRs232.cpp @@ -207,12 +207,12 @@ bool ProviderRs232::tryOpen(int delayAfterConnect_ms) return _rs232Port.isOpen(); } -void ProviderRs232::setInError(const QString& errorMsg) +void ProviderRs232::setInError(const QString& errorMsg, bool isRecoverable) { _rs232Port.clearError(); this->close(); - LedDevice::setInError( errorMsg ); + LedDevice::setInError( errorMsg, isRecoverable ); } int ProviderRs232::writeBytes(const qint64 size, const uint8_t *data) diff --git a/libsrc/leddevice/dev_serial/ProviderRs232.h b/libsrc/leddevice/dev_serial/ProviderRs232.h index 36348b55..2b02a815 100644 --- a/libsrc/leddevice/dev_serial/ProviderRs232.h +++ b/libsrc/leddevice/dev_serial/ProviderRs232.h @@ -119,9 +119,10 @@ protected slots: /// /// @brief Set device in error state /// - /// @param errorMsg The error message to be logged + /// @param[in] errorMsg The error message to be logged + /// @param[in] isRecoverable If False, no further retries will be done /// - void setInError( const QString& errorMsg) override; + void setInError( const QString& errorMsg, bool isRecoverable=true) override; /// /// @brief Handle any feedback provided by the device diff --git a/libsrc/leddevice/schemas/schema-wled.json b/libsrc/leddevice/schemas/schema-wled.json index 125b7d99..b8ea52fe 100644 --- a/libsrc/leddevice/schemas/schema-wled.json +++ b/libsrc/leddevice/schemas/schema-wled.json @@ -35,6 +35,46 @@ "access": "expert", "propertyOrder": 3 }, + "segments": { + "type": "object", + "title": "Segment streaming", + "required": false, + "properties": { + "segmentList": { + "type": "string", + "title": "edt_dev_spec_segments_title", + "enum": [ "-1" ], + "default": "-1", + "options": { + "enum_titles": [ "edt_dev_spec_segments_disabled_title" ] + }, + "propertyOrder": 1 + }, + "streamSegmentId": { + "type": "integer", + "title": "edt_dev_spec_segmentId_title", + "default": -1, + "minimum": -1, + "maximum": 16, + "options": { + "hidden": true + }, + "access": "expert", + "propertyOrder": 2 + }, + "switchOffOtherSegments": { + "type": "boolean", + "format": "checkbox", + "title": "edt_dev_spec_segmentsSwitchOffOthers_title", + "default": true, + "required": true, + "access": "advanced", + "propertyOrder": 3 + } + }, + "propertyOrder": 4, + "additionalProperties": false + }, "restoreOriginalState": { "type": "boolean", "format": "checkbox", @@ -44,7 +84,18 @@ "options": { "infoText": "edt_dev_spec_restoreOriginalState_title_info" }, - "propertyOrder": 4 + "propertyOrder": 7 + }, + "stayOnAfterStreaming": { + "type": "boolean", + "format": "checkbox", + "title": "edt_dev_spec_stayOnAfterStreaming_title", + "default": false, + "required": true, + "options": { + "infoText": "edt_dev_spec_stayOnAfterStreaming_title_info" + }, + "propertyOrder": 8 }, "overwriteSync": { "type": "boolean", @@ -53,7 +104,7 @@ "default": true, "required": true, "access": "advanced", - "propertyOrder": 5 + "propertyOrder": 9 }, "overwriteBrightness": { "type": "boolean", @@ -62,7 +113,7 @@ "default": true, "required": true, "access": "advanced", - "propertyOrder": 6 + "propertyOrder": 10 }, "brightness": { "type": "integer", @@ -76,7 +127,7 @@ } }, "access": "advanced", - "propertyOrder": 7 + "propertyOrder": 11 }, "latchTime": { "type": "integer", @@ -89,8 +140,8 @@ "options": { "infoText": "edt_dev_spec_latchtime_title_info" }, - "propertyOrder": 8 + "propertyOrder": 12 } }, - "additionalProperties": true -} + "additionalProperties": true + } From bf418686e38f674754e0639f158a8ccaf6e40688 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sun, 12 Feb 2023 21:20:50 +0100 Subject: [PATCH 09/23] Fix WLED & Smoothing (#1567) * WLED - Fix empty segment element in DB * WLED - Fix to not overwrite on state when not isStayOnAfterStreaming * Refactor ProviderRestApi, increase default timeout * Fix Smoothing - Fix empty updates, consider smoothing configs for effects * UI - Fix not removed priority * Add missing header and code updates * setRedirectPolicy was only introduced in Qt 5.9 * Adalight - Align to HyperSerial v9.0.0 * HyperSerial Hyperion with awa protocol v8.0.0 * Correct line-endings --- .../HyperSerialESP32_Neopixel.ino | 655 ++++++++++++++++++ .../HyperSerialESP8266_Neopixel.ino | 646 +++++++++++++++++ assets/webconfig/js/content_leds.js | 16 +- assets/webconfig/js/content_remote.js | 2 +- include/hyperion/LinearColorSmoothing.h | 3 + libsrc/effectengine/EffectEngine.cpp | 29 +- libsrc/hyperion/LinearColorSmoothing.cpp | 87 ++- libsrc/leddevice/dev_net/LedDeviceWled.cpp | 5 +- libsrc/leddevice/dev_net/ProviderRestApi.cpp | 252 +++---- libsrc/leddevice/dev_net/ProviderRestApi.h | 92 ++- .../dev_serial/LedDeviceAdalight.cpp | 10 +- 11 files changed, 1594 insertions(+), 203 deletions(-) create mode 100644 assets/firmware/HyperSerial/HyperSerialESP32_Neopixel/HyperSerialESP32_Neopixel.ino create mode 100644 assets/firmware/HyperSerial/HyperSerialESP8266_Neopixel/HyperSerialESP8266_Neopixel.ino diff --git a/assets/firmware/HyperSerial/HyperSerialESP32_Neopixel/HyperSerialESP32_Neopixel.ino b/assets/firmware/HyperSerial/HyperSerialESP32_Neopixel/HyperSerialESP32_Neopixel.ino new file mode 100644 index 00000000..89a039bb --- /dev/null +++ b/assets/firmware/HyperSerial/HyperSerialESP32_Neopixel/HyperSerialESP32_Neopixel.ino @@ -0,0 +1,655 @@ +#include +//////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////// CONFIG SECTION STARTS ///////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#define THIS_IS_RGBW // RGBW SK6812, otherwise comment it +#define COLD_WHITE // for RGBW (THIS_IS_RGBW enabled) select COLD version, comment it if NEUTRAL + +const bool skipFirstLed = false; // if set the first led in the strip will be set to black (for level shifters using sacrifice LED) +const int serialSpeed = 2000000; // serial port speed +#define DATA_PIN 2 // PIN: data output for LED strip + +const bool reportStats = false; // Send back processing statistics +const int reportStatInterval_s = 10; // Send back processing every interval in seconds + +/* Statistics breakdown: + FPS: Updates to the LEDs per second + F-FPS: Frames identified per second + S: Shown (Done) updates to the LEDs per given interval + F: Frames identified per interval (garbled grames cannot be counted) + G: Good frames identified per interval + B: Total bad frames of all types identified per interval + BF: Bad frames identified per interval + BS: Skipped incomplete frames + BC: Frames failing CRC check per interval + BFL Frames failing Fletcher content validation per interval +*/ + +//Developer configs +#define ENABLE_STRIP +#define ENABLE_CHECK_FLETCHER + +const int SERIAL_SIZE_RX = 4096; + +#ifndef ENABLE_STRIP +const int serial2Speed = 460800; +const bool reportInput = false; +#endif +//////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////// CONFIG SECTION ENDS ///////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +const String version = "8.0"; + +#ifdef THIS_IS_RGBW +float whiteLimit = 1.0f; +#ifdef COLD_WHITE +uint8_t rCorrection = 0xA0; // adjust red -> white in 0-0xFF range +uint8_t gCorrection = 0xA0; // adjust green -> white in 0-0xFF range +uint8_t bCorrection = 0xA0; // adjust blue -> white in 0-0xFF range +#else +uint8_t rCorrection = 0xB0; // adjust red -> white in 0-0xFF range +uint8_t gCorrection = 0xB0; // adjust green -> white in 0-0xFF range +uint8_t bCorrection = 0x70; // adjust blue -> white in 0-0xFF range +#endif +#endif + +int ledCount = 0; // This is dynamic, don't change it +int pixelCount = 0; // This is dynamic, don't change it + +#ifdef THIS_IS_RGBW +#define LED_TYPE NeoGrbwFeature + #if defined(ARDUINO_LOLIN_S2_MINI) + #define LED_METHOD NeoEsp32I2s0Sk6812Method + #else + #define LED_METHOD NeoEsp32I2s1Sk6812Method + #endif +#else +#define LED_TYPE NeoGrbFeature + #if defined(ARDUINO_LOLIN_S2_MINI) + #define LED_METHOD NeoEsp32I2s0Ws2812xMethod + #else + #define LED_METHOD NeoEsp32I2s1Ws2812xMethod + #endif +#endif + +#define LED_DRIVER NeoPixelBus + +uint8_t* ledBuffer; +int ledBufferSize; + +#ifdef ENABLE_STRIP +LED_DRIVER* strip = NULL; +#endif + +enum class AwaProtocol +{ + HEADER_A, + HEADER_w, + HEADER_a, + HEADER_HI, + HEADER_LO, + HEADER_CRC, + CHANNELCALIB_GAIN, + CHANNELCALIB_RED, + CHANNELCALIB_GREEN, + CHANNELCALIB_BLUE, + PIXEL, + FLETCHER1, + FLETCHER2, + FLETCHER_EXT +}; + +AwaProtocol state = AwaProtocol::HEADER_A; + +const int headerSize = 6; +const int trailerSize = 3; +const int calibInfoSize = 4; +int bytesRead = 0; + +bool isVersion2 = false; +bool isChannelCalib = false; +uint8_t CRC = 0; +int count = 0; +int currentPixel = 0; +uint16_t fletcher1 = 0; +uint16_t fletcher2 = 0; +uint16_t fletcherExt = 0; + +#ifdef THIS_IS_RGBW +RgbwColor inputColor; +uint8_t wChannel[256]; +uint8_t rChannel[256]; +uint8_t gChannel[256]; +uint8_t bChannel[256]; +#else +RgbColor inputColor; +#endif + +bool ledsComplete = false; + +// statistics +const int reportStatInterval_ms = reportStatInterval_s * 1000; +unsigned long curTime; +unsigned long stat_start = 0; +uint16_t stat_shown = 0; +uint16_t stat_frames = 0; +uint16_t stat_good = 0; +uint16_t stat_bad = 0; + +uint16_t stat_bad_frame = 0; +uint16_t stat_bad_skip = 0; +uint16_t stat_bad_crc = 0; +uint16_t stat_bad_fletcher = 0; + +uint16_t stat_final_shown = 0; +uint16_t stat_final_frames = 0; +uint16_t stat_final_good = 0; +uint16_t stat_final_bad = 0; + +uint16_t stat_final_bad_frame = 0; +uint16_t stat_final_bad_skip = 0; +uint16_t stat_final_bad_crc = 0; +uint16_t stat_final_bad_fletcher = 0; + +//Debugging +String inputString; +String inputErrorString; +String debugString; + +void printStringHex(String string) +{ +#ifndef ENABLE_STRIP + Serial2.println(string.length()); + for (int i = 0; i < string.length(); ++i) + { + if (i % 36 == 0) + { + Serial2.println(); + Serial2.print("["); + Serial2.print(i); + Serial2.print("] "); + } + + if (string[i] < 16) + Serial2.print("0"); + Serial2.print(string[i], HEX); + Serial2.print(":"); + } +#endif +} + +inline void showMe() +{ +#ifdef ENABLE_STRIP + if (strip != NULL && strip->CanShow()) + { + stat_shown++; + strip->Show(); + } +#endif +} + +// statistics +inline void showStats() +{ + if (reportStats) + { + if (stat_frames > 0) + { + stat_final_shown = stat_shown; + stat_final_frames = stat_frames; + stat_final_good = stat_good; + stat_final_bad = stat_bad; + + stat_final_bad_frame = stat_bad_frame; + stat_final_bad_skip = stat_bad_skip; + stat_final_bad_crc = stat_bad_crc; + stat_final_bad_fletcher = stat_bad_fletcher; + } + + stat_start = curTime; + stat_shown = 0; + stat_frames = 0; + stat_good = 0; + stat_bad = 0; + + stat_bad_frame = 0; + stat_bad_skip = 0; + stat_bad_crc = 0; + stat_bad_fletcher = 0; + + String summary = String("FPS: ") + (stat_final_shown / reportStatInterval_s) + + " F-FPS: " + (stat_final_frames / reportStatInterval_s) + + " S: " + stat_final_shown + + " F: " + stat_final_frames + + " G: " + stat_final_good + + " B: " + stat_final_bad + + " (BF: " + stat_final_bad_frame + + " BS: " + stat_final_bad_skip + + " BC: " + stat_final_bad_crc + + " BFL: " + stat_final_bad_fletcher + + ")"; +#ifdef ENABLE_STRIP + Serial.println(summary); +#else + Serial2.println(summary); +#endif + } +} + +void InitLeds(uint16_t ledCount, int pixelCount, bool channelCalibration = false) +{ + if (ledBuffer != NULL) + delete ledBuffer; + + ledBufferSize = pixelCount + (channelCalibration ? calibInfoSize : 0); + ledBuffer = new uint8_t[ledBufferSize]; + +#ifdef ENABLE_STRIP + if (strip != NULL) + delete strip; + + strip = new LED_DRIVER(ledCount, DATA_PIN); + strip->Begin(); +#endif +} + +inline void processSerialData() +{ + while (Serial.available()) { + + char input = Serial.read(); + ++bytesRead; + +#ifndef ENABLE_STRIP + if (reportInput) + inputString += input; +#endif + + switch (state) + { + case AwaProtocol::HEADER_A: + if (input == 'A') + { + state = AwaProtocol::HEADER_w; + } + break; + + case AwaProtocol::HEADER_w: + if (input == 'w') + state = AwaProtocol::HEADER_a; + else + { + state = AwaProtocol::HEADER_A; + } + break; + + case AwaProtocol::HEADER_a: + if (input == 'a') + { + isVersion2 = false; + state = AwaProtocol::HEADER_HI; + } + else if (input == 'A') + { + state = AwaProtocol::HEADER_HI; + isVersion2 = true; + } + else + { + state = AwaProtocol::HEADER_A; + } + break; + + case AwaProtocol::HEADER_HI: + + stat_frames++; + + count = input << 8; + + CRC = input; + fletcher1 = 0; + fletcher2 = 0; + fletcherExt = 0; + state = AwaProtocol::HEADER_LO; + break; + + case AwaProtocol::HEADER_LO: + count += input + 1; + + if (ledCount != count || isChannelCalib != isVersion2) + { + ledCount = count; + isChannelCalib = isVersion2; + pixelCount = ledCount * 3; + + if (isChannelCalib) + prepareCalibration(); + + InitLeds(ledCount, pixelCount, isChannelCalib); + } + + CRC = CRC ^ input ^ 0x55; + + state = AwaProtocol::HEADER_CRC; + + break; + + case AwaProtocol::HEADER_CRC: + + // Check, if incomplete package information was skipped, set bytesread to headersize and skip wrong input + if (bytesRead != headerSize) + { + stat_bad_skip++; + bytesRead = headerSize; + } + + currentPixel = 0; + if (CRC == input) + { + state = AwaProtocol::PIXEL; + } + else + { + // CRC failure + stat_bad++; + stat_bad_crc++; + + state = AwaProtocol::HEADER_A; + } + break; + + case AwaProtocol::PIXEL: + ledBuffer[currentPixel++] = input; + if (currentPixel == pixelCount) + { + if (isChannelCalib) + state = AwaProtocol::CHANNELCALIB_GAIN; + else + state = AwaProtocol::FLETCHER1; + } + break; + + case AwaProtocol::CHANNELCALIB_GAIN: + ledBuffer[currentPixel++] = input; + state = AwaProtocol::CHANNELCALIB_RED; + break; + + case AwaProtocol::CHANNELCALIB_RED: + ledBuffer[currentPixel++] = input; + + state = AwaProtocol::CHANNELCALIB_GREEN; + break; + + case AwaProtocol::CHANNELCALIB_GREEN: + ledBuffer[currentPixel++] = input; + + state = AwaProtocol::CHANNELCALIB_BLUE; + break; + + case AwaProtocol::CHANNELCALIB_BLUE: + ledBuffer[currentPixel++] = input; + + state = AwaProtocol::FLETCHER1; + break; + + case AwaProtocol::FLETCHER1: + fletcher1 = input; + + state = AwaProtocol::FLETCHER2; + break; + + case AwaProtocol::FLETCHER2: + fletcher2 = input; + + state = AwaProtocol::FLETCHER_EXT; + break; + + case AwaProtocol::FLETCHER_EXT: + fletcherExt = input; + ledsComplete = true; + + state = AwaProtocol::HEADER_A; + break; + } + } +} + +void setup() +{ + // Init serial port + int bufSize = Serial.setRxBufferSize(SERIAL_SIZE_RX); + Serial.begin(serialSpeed); + Serial.setTimeout(50); + +#ifndef ENABLE_STRIP + Serial2.begin(serial2Speed); + + Serial2.println(); + Serial2.println("Welcome!"); + Serial2.println("Hyperion Awa driver " + version); + Serial2.println("!!! Debug Output !!!"); +#endif + + // Display config + Serial.println(); + Serial.println("Welcome!"); + Serial.println("Hyperion Awa driver " + version); + Serial.print("(Build: "); + Serial.print(__DATE__); + Serial.print(" "); + Serial.print(__TIME__); + Serial.println(")"); + + // first LED info + if (skipFirstLed) + Serial.println("First LED: disabled"); + else + Serial.println("First LED: enabled"); + + // RGBW claibration info +#ifdef THIS_IS_RGBW +#ifdef COLD_WHITE + Serial.println("Default color mode: RGBW cold"); +#else + Serial.println("Default color mode: RGBW neutral"); +#endif + prepareCalibration(); +#else + Serial.println("Color mode: RGB"); +#endif + + InitLeds(ledCount, pixelCount); +} + +void prepareCalibration() +{ +#ifdef THIS_IS_RGBW + // prepare LUT calibration table, cold white is much better than "neutral" white + for (uint32_t i = 0; i < 256; i++) + { + // color calibration + float red = rCorrection * i; // adjust red + float green = gCorrection * i; // adjust green + float blue = bCorrection * i; // adjust blue + + wChannel[i] = (uint8_t)round(min(whiteLimit * i, 255.0f)); + rChannel[i] = (uint8_t)round(min(red / 0xFF, 255.0f)); + gChannel[i] = (uint8_t)round(min(green / 0xFF, 255.0f)); + bChannel[i] = (uint8_t)round(min(blue / 0xFF, 255.0f)); + } + + Serial.write("RGBW calibration. White limit(%): "); + Serial.print(whiteLimit * 100.0f); + Serial.write(" %, red: "); + Serial.print(rCorrection); + Serial.write(" , green: "); + Serial.print(gCorrection); + Serial.write(" , blue: "); + Serial.print(bCorrection); + Serial.println(); +#endif +} + +void loop() +{ + curTime = millis(); + +#ifdef __AVR__ + // nothing , USART Interrupt is implemented + ESPserialEvent(); +#else + // ESP8266 polling + ESPserialEvent(); +#endif + + if (ledsComplete) + { +#ifndef ENABLE_STRIP + if (reportInput) + { + Serial2.println(); + Serial2.print(" L: "); + printStringHex(inputString); + Serial2.println("<\input>"); + inputString = ""; + + Serial2.print("bytesRead: "); + Serial2.print(bytesRead); + Serial2.print(" , currentPixel: "); + Serial2.print(currentPixel); + Serial2.print(" ,pixelCount: "); + Serial2.print(pixelCount); + Serial2.println(); + } +#endif + + int frameSize = headerSize + ledBufferSize + trailerSize; + + if (bytesRead > frameSize) + { + //Add number of frames ignored on top of frame + int frames = bytesRead / frameSize; + stat_frames += frames; + + //Count frame plus frames ignored as bad frames + int badFrames = frames + 1; + stat_bad += badFrames; + stat_bad_frame += badFrames; + } + else + { + +#ifdef ENABLE_CHECK_FLETCHER + //Test if content is valid + uint16_t item = 0; + uint16_t fletch1 = 0; + uint16_t fletch2 = 0; + uint16_t fletchExt = 0; + + while (item < ledBufferSize) + { + fletch1 = (fletch1 + (uint16_t)ledBuffer[item]) % 255; + fletch2 = (fletch2 + fletch1) % 255; + fletcherExt = (fletcherExt + ((uint16_t)ledBuffer[item] ^ (item))) % 255; + ++item; + } + if ((fletch1 == fletcher1) && (fletch2 == fletcher2) && (ledBuffer[item-1] == (fletcherExt != 0x41) ? fletcherExt : 0xaa)) + { +#endif + stat_good++; + + uint16_t startLed = 0; + if (skipFirstLed) + { +#ifdef ENABLE_STRIP + #ifdef THIS_IS_RGBW + strip->SetPixelColor(startLed, RgbwColor(0, 0, 0, 0)); + #else + strip->SetPixelColor(startLed, RgbColor(0, 0, 0)); + #endif +#endif + startLed = 1; + } + + for (uint16_t led = startLed; led < ledCount; ++led) + { + inputColor.R = ledBuffer[led * 3]; + inputColor.G = ledBuffer[led * 3 + 1]; + inputColor.B = ledBuffer[led * 3 + 2]; + + #ifdef THIS_IS_RGBW + inputColor.W = min(rChannel[inputColor.R], + min(gChannel[inputColor.G], + bChannel[inputColor.B])); + inputColor.R -= rChannel[inputColor.W]; + inputColor.G -= gChannel[inputColor.W]; + inputColor.B -= bChannel[inputColor.W]; + inputColor.W = wChannel[inputColor.W]; + #endif +#ifdef ENABLE_STRIP + strip->SetPixelColor(led, inputColor); +#endif + } + + showMe(); + yield(); + + #ifdef THIS_IS_RGBW + if (isChannelCalib) + { + uint8_t incoming_gain = ledBuffer[pixelCount]; + uint8_t incoming_red = ledBuffer[pixelCount + 1]; + uint8_t incoming_green = ledBuffer[pixelCount + 2]; + uint8_t incoming_blue = ledBuffer[pixelCount + 3]; + + float final_limit = (incoming_gain != 255) ? incoming_gain / 255.0f : 1.0f; + if (rCorrection != incoming_red || gCorrection != incoming_green || bCorrection != incoming_blue || whiteLimit != final_limit) + { + rCorrection = incoming_red; + gCorrection = incoming_green; + bCorrection = incoming_blue; + whiteLimit = final_limit; + prepareCalibration(); + } + } + #endif + +#ifdef ENABLE_CHECK_FLETCHER + } + else + { + stat_bad++; + stat_bad_fletcher++; + } +#endif + } + + bytesRead = 0; + state = AwaProtocol::HEADER_A; + + ledsComplete = false; + } + + if ((curTime - stat_start > reportStatInterval_ms)) + { + if (stat_frames > 0) + { + showStats(); + } + } +} + +#ifdef __AVR__ +void serialEvent() +{ + processSerialData(); +} +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) +void ESPserialEvent() +{ + processSerialData(); +} +#endif diff --git a/assets/firmware/HyperSerial/HyperSerialESP8266_Neopixel/HyperSerialESP8266_Neopixel.ino b/assets/firmware/HyperSerial/HyperSerialESP8266_Neopixel/HyperSerialESP8266_Neopixel.ino new file mode 100644 index 00000000..33aa5988 --- /dev/null +++ b/assets/firmware/HyperSerial/HyperSerialESP8266_Neopixel/HyperSerialESP8266_Neopixel.ino @@ -0,0 +1,646 @@ +#include +//////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////// CONFIG SECTION STARTS ///////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +#define THIS_IS_RGBW // RGBW SK6812, otherwise comment it +#define COLD_WHITE // for RGBW (THIS_IS_RGBW enabled) select COLD version, comment it if NEUTRAL + +const bool skipFirstLed = false; // if set the first led in the strip will be set to black (for level shifters using sacrifice LED) +const int serialSpeed = 2000000; // serial port speed + +const bool reportStats = false; // Send back processing statistics +const int reportStatInterval_s = 10; // Send back processing every interval in seconds + +/* Statistics breakdown: + FPS: Updates to the LEDs per second + F-FPS: Frames identified per second + S: Shown (Done) updates to the LEDs per given interval + F: Frames identified per interval (garbled grames cannot be counted) + G: Good frames identified per interval + B: Total bad frames of all types identified per interval + BF: Bad frames identified per interval + BS: Skipped incomplete frames + BC: Frames failing CRC check per interval + BFL Frames failing Fletcher content validation per interval +*/ + +//Developer configs +#define ENABLE_STRIP +#define ENABLE_CHECK_FLETCHER + +const int SERIAL_SIZE_RX = 4096; + +#ifndef ENABLE_STRIP +const int serial2Speed = 460800; +const bool reportInput = false; +#endif +//////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////// CONFIG SECTION ENDS ///////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +const String version = "8.0"; + +#ifdef THIS_IS_RGBW +float whiteLimit = 1.0f; +#ifdef COLD_WHITE +uint8_t rCorrection = 0xA0; // adjust red -> white in 0-0xFF range +uint8_t gCorrection = 0xA0; // adjust green -> white in 0-0xFF range +uint8_t bCorrection = 0xA0; // adjust blue -> white in 0-0xFF range +#else +uint8_t rCorrection = 0xB0; // adjust red -> white in 0-0xFF range +uint8_t gCorrection = 0xB0; // adjust green -> white in 0-0xFF range +uint8_t bCorrection = 0x70; // adjust blue -> white in 0-0xFF range +#endif +#endif + +int ledCount = 0; // This is dynamic, don't change it +int pixelCount = 0; // This is dynamic, don't change it + +#ifdef THIS_IS_RGBW +#define LED_TYPE NeoGrbwFeature + #define LED_METHOD NeoEsp8266Uart1Sk6812Method +#else +#define LED_TYPE NeoGrbFeature + #define LED_METHOD NeoEsp8266Uart1Ws2812xMethod +#endif + +#define LED_DRIVER NeoPixelBus + +uint8_t* ledBuffer; +int ledBufferSize; + +#ifdef ENABLE_STRIP +LED_DRIVER* strip = NULL; +#endif + +enum class AwaProtocol +{ + HEADER_A, + HEADER_w, + HEADER_a, + HEADER_HI, + HEADER_LO, + HEADER_CRC, + CHANNELCALIB_GAIN, + CHANNELCALIB_RED, + CHANNELCALIB_GREEN, + CHANNELCALIB_BLUE, + PIXEL, + FLETCHER1, + FLETCHER2, + FLETCHER_EXT +}; + +AwaProtocol state = AwaProtocol::HEADER_A; + +const int headerSize = 6; +const int trailerSize = 3; +const int calibInfoSize = 4; +int bytesRead = 0; + +bool isVersion2 = false; +bool isChannelCalib = false; +uint8_t CRC = 0; +int count = 0; +int currentPixel = 0; +uint16_t fletcher1 = 0; +uint16_t fletcher2 = 0; +uint16_t fletcherExt = 0; + +#ifdef THIS_IS_RGBW +RgbwColor inputColor; +uint8_t wChannel[256]; +uint8_t rChannel[256]; +uint8_t gChannel[256]; +uint8_t bChannel[256]; +#else +RgbColor inputColor; +#endif + +bool ledsComplete = false; + +// statistics +const int reportStatInterval_ms = reportStatInterval_s * 1000; +unsigned long curTime; +unsigned long stat_start = 0; +uint16_t stat_shown = 0; +uint16_t stat_frames = 0; +uint16_t stat_good = 0; +uint16_t stat_bad = 0; + +uint16_t stat_bad_frame = 0; +uint16_t stat_bad_skip = 0; +uint16_t stat_bad_crc = 0; +uint16_t stat_bad_fletcher = 0; + +uint16_t stat_final_shown = 0; +uint16_t stat_final_frames = 0; +uint16_t stat_final_good = 0; +uint16_t stat_final_bad = 0; + +uint16_t stat_final_bad_frame = 0; +uint16_t stat_final_bad_skip = 0; +uint16_t stat_final_bad_crc = 0; +uint16_t stat_final_bad_fletcher = 0; + +//Debugging +String inputString; +String inputErrorString; +String debugString; + +void printStringHex(String string) +{ +#ifndef ENABLE_STRIP + Serial2.println(string.length()); + for (int i = 0; i < string.length(); ++i) + { + if (i % 36 == 0) + { + Serial2.println(); + Serial2.print("["); + Serial2.print(i); + Serial2.print("] "); + } + + if (string[i] < 16) + Serial2.print("0"); + Serial2.print(string[i], HEX); + Serial2.print(":"); + } +#endif +} + +inline void showMe() +{ +#ifdef ENABLE_STRIP + if (strip != NULL && strip->CanShow()) + { + stat_shown++; + strip->Show(); + } +#endif +} + +// statistics +inline void showStats() +{ + if (reportStats) + { + if (stat_frames > 0) + { + stat_final_shown = stat_shown; + stat_final_frames = stat_frames; + stat_final_good = stat_good; + stat_final_bad = stat_bad; + + stat_final_bad_frame = stat_bad_frame; + stat_final_bad_skip = stat_bad_skip; + stat_final_bad_crc = stat_bad_crc; + stat_final_bad_fletcher = stat_bad_fletcher; + } + + stat_start = curTime; + stat_shown = 0; + stat_frames = 0; + stat_good = 0; + stat_bad = 0; + + stat_bad_frame = 0; + stat_bad_skip = 0; + stat_bad_crc = 0; + stat_bad_fletcher = 0; + + String summary = String("FPS: ") + (stat_final_shown / reportStatInterval_s) + + " F-FPS: " + (stat_final_frames / reportStatInterval_s) + + " S: " + stat_final_shown + + " F: " + stat_final_frames + + " G: " + stat_final_good + + " B: " + stat_final_bad + + " (BF: " + stat_final_bad_frame + + " BS: " + stat_final_bad_skip + + " BC: " + stat_final_bad_crc + + " BFL: " + stat_final_bad_fletcher + + ")"; +#ifdef ENABLE_STRIP + Serial.println(summary); +#else + Serial2.println(summary); +#endif + } +} + +void InitLeds(uint16_t ledCount, int pixelCount, bool channelCalibration = false) +{ + if (ledBuffer != NULL) + delete ledBuffer; + + ledBufferSize = pixelCount + (channelCalibration ? calibInfoSize : 0); + ledBuffer = new uint8_t[ledBufferSize]; + +#ifdef ENABLE_STRIP + if (strip != NULL) + delete strip; + + strip = new LED_DRIVER(ledCount); + strip->Begin(); +#endif +} + +inline void processSerialData() +{ + while (Serial.available()) { + + char input = Serial.read(); + ++bytesRead; + +#ifndef ENABLE_STRIP + if (reportInput) + inputString += input; +#endif + + switch (state) + { + case AwaProtocol::HEADER_A: + if (input == 'A') + { + state = AwaProtocol::HEADER_w; + } + break; + + case AwaProtocol::HEADER_w: + if (input == 'w') + state = AwaProtocol::HEADER_a; + else + { + state = AwaProtocol::HEADER_A; + } + break; + + case AwaProtocol::HEADER_a: + if (input == 'a') + { + isVersion2 = false; + state = AwaProtocol::HEADER_HI; + } + else if (input == 'A') + { + state = AwaProtocol::HEADER_HI; + isVersion2 = true; + } + else + { + state = AwaProtocol::HEADER_A; + } + break; + + case AwaProtocol::HEADER_HI: + + stat_frames++; + + count = input << 8; + + CRC = input; + fletcher1 = 0; + fletcher2 = 0; + fletcherExt = 0; + state = AwaProtocol::HEADER_LO; + break; + + case AwaProtocol::HEADER_LO: + count += input + 1; + + if (ledCount != count || isChannelCalib != isVersion2) + { + ledCount = count; + isChannelCalib = isVersion2; + pixelCount = ledCount * 3; + + if (isChannelCalib) + prepareCalibration(); + + InitLeds(ledCount, pixelCount, isChannelCalib); + } + + CRC = CRC ^ input ^ 0x55; + + state = AwaProtocol::HEADER_CRC; + + break; + + case AwaProtocol::HEADER_CRC: + + // Check, if incomplete package information was skipped, set bytesread to headersize and skip wrong input + if (bytesRead != headerSize) + { + stat_bad_skip++; + bytesRead = headerSize; + } + + currentPixel = 0; + if (CRC == input) + { + state = AwaProtocol::PIXEL; + } + else + { + // CRC failure + stat_bad++; + stat_bad_crc++; + + state = AwaProtocol::HEADER_A; + } + break; + + case AwaProtocol::PIXEL: + ledBuffer[currentPixel++] = input; + if (currentPixel == pixelCount) + { + if (isChannelCalib) + state = AwaProtocol::CHANNELCALIB_GAIN; + else + state = AwaProtocol::FLETCHER1; + } + break; + + case AwaProtocol::CHANNELCALIB_GAIN: + ledBuffer[currentPixel++] = input; + state = AwaProtocol::CHANNELCALIB_RED; + break; + + case AwaProtocol::CHANNELCALIB_RED: + ledBuffer[currentPixel++] = input; + + state = AwaProtocol::CHANNELCALIB_GREEN; + break; + + case AwaProtocol::CHANNELCALIB_GREEN: + ledBuffer[currentPixel++] = input; + + state = AwaProtocol::CHANNELCALIB_BLUE; + break; + + case AwaProtocol::CHANNELCALIB_BLUE: + ledBuffer[currentPixel++] = input; + + state = AwaProtocol::FLETCHER1; + break; + + case AwaProtocol::FLETCHER1: + fletcher1 = input; + + state = AwaProtocol::FLETCHER2; + break; + + case AwaProtocol::FLETCHER2: + fletcher2 = input; + + state = AwaProtocol::FLETCHER_EXT; + break; + + case AwaProtocol::FLETCHER_EXT: + fletcherExt = input; + ledsComplete = true; + + state = AwaProtocol::HEADER_A; + break; + } + } +} + +void setup() +{ + // Init serial port + int bufSize = Serial.setRxBufferSize(SERIAL_SIZE_RX); + Serial.begin(serialSpeed); + Serial.setTimeout(50); + +#ifndef ENABLE_STRIP + Serial2.begin(serial2Speed); + + Serial2.println(); + Serial2.println("Welcome!"); + Serial2.println("Hyperion Awa driver " + version); + Serial2.println("!!! Debug Output !!!"); +#endif + + // Display config + Serial.println(); + Serial.println("Welcome!"); + Serial.println("Hyperion Awa driver " + version); + Serial.print("(Build: "); + Serial.print(__DATE__); + Serial.print(" "); + Serial.print(__TIME__); + Serial.println(")"); + + // first LED info + if (skipFirstLed) + Serial.println("First LED: disabled"); + else + Serial.println("First LED: enabled"); + + // RGBW claibration info +#ifdef THIS_IS_RGBW +#ifdef COLD_WHITE + Serial.println("Default color mode: RGBW cold"); +#else + Serial.println("Default color mode: RGBW neutral"); +#endif + prepareCalibration(); +#else + Serial.println("Color mode: RGB"); +#endif + + InitLeds(ledCount, pixelCount); +} + +void prepareCalibration() +{ +#ifdef THIS_IS_RGBW + // prepare LUT calibration table, cold white is much better than "neutral" white + for (uint32_t i = 0; i < 256; i++) + { + // color calibration + float red = rCorrection * i; // adjust red + float green = gCorrection * i; // adjust green + float blue = bCorrection * i; // adjust blue + + wChannel[i] = (uint8_t)round(min(whiteLimit * i, 255.0f)); + rChannel[i] = (uint8_t)round(min(red / 0xFF, 255.0f)); + gChannel[i] = (uint8_t)round(min(green / 0xFF, 255.0f)); + bChannel[i] = (uint8_t)round(min(blue / 0xFF, 255.0f)); + } + + Serial.write("RGBW calibration. White limit(%): "); + Serial.print(whiteLimit * 100.0f); + Serial.write(" %, red: "); + Serial.print(rCorrection); + Serial.write(" , green: "); + Serial.print(gCorrection); + Serial.write(" , blue: "); + Serial.print(bCorrection); + Serial.println(); +#endif +} + +void loop() +{ + curTime = millis(); + +#ifdef __AVR__ + // nothing , USART Interrupt is implemented + ESPserialEvent(); +#else + // ESP8266 polling + ESPserialEvent(); +#endif + + if (ledsComplete) + { +#ifndef ENABLE_STRIP + if (reportInput) + { + Serial2.println(); + Serial2.print(" L: "); + printStringHex(inputString); + Serial2.println("<\input>"); + inputString = ""; + + Serial2.print("bytesRead: "); + Serial2.print(bytesRead); + Serial2.print(" , currentPixel: "); + Serial2.print(currentPixel); + Serial2.print(" ,pixelCount: "); + Serial2.print(pixelCount); + Serial2.println(); + } +#endif + + int frameSize = headerSize + ledBufferSize + trailerSize; + + if (bytesRead > frameSize) + { + //Add number of frames ignored on top of frame + int frames = bytesRead / frameSize; + stat_frames += frames; + + //Count frame plus frames ignored as bad frames + int badFrames = frames + 1; + stat_bad += badFrames; + stat_bad_frame += badFrames; + } + else + { + +#ifdef ENABLE_CHECK_FLETCHER + //Test if content is valid + uint16_t item = 0; + uint16_t fletch1 = 0; + uint16_t fletch2 = 0; + uint16_t fletchExt = 0; + + while (item < ledBufferSize) + { + fletch1 = (fletch1 + (uint16_t)ledBuffer[item]) % 255; + fletch2 = (fletch2 + fletch1) % 255; + fletcherExt = (fletcherExt + ((uint16_t)ledBuffer[item] ^ (item))) % 255; + ++item; + } + if ((fletch1 == fletcher1) && (fletch2 == fletcher2) && (ledBuffer[item-1] == (fletcherExt != 0x41) ? fletcherExt : 0xaa)) + { +#endif + stat_good++; + + uint16_t startLed = 0; + if (skipFirstLed) + { +#ifdef ENABLE_STRIP + #ifdef THIS_IS_RGBW + strip->SetPixelColor(startLed, RgbwColor(0, 0, 0, 0)); + #else + strip->SetPixelColor(startLed, RgbColor(0, 0, 0)); + #endif +#endif + startLed = 1; + } + + for (uint16_t led = startLed; led < ledCount; ++led) + { + inputColor.R = ledBuffer[led * 3]; + inputColor.G = ledBuffer[led * 3 + 1]; + inputColor.B = ledBuffer[led * 3 + 2]; + + #ifdef THIS_IS_RGBW + inputColor.W = min(rChannel[inputColor.R], + min(gChannel[inputColor.G], + bChannel[inputColor.B])); + inputColor.R -= rChannel[inputColor.W]; + inputColor.G -= gChannel[inputColor.W]; + inputColor.B -= bChannel[inputColor.W]; + inputColor.W = wChannel[inputColor.W]; + #endif +#ifdef ENABLE_STRIP + strip->SetPixelColor(led, inputColor); +#endif + } + + showMe(); + yield(); + + #ifdef THIS_IS_RGBW + if (isChannelCalib) + { + uint8_t incoming_gain = ledBuffer[pixelCount]; + uint8_t incoming_red = ledBuffer[pixelCount + 1]; + uint8_t incoming_green = ledBuffer[pixelCount + 2]; + uint8_t incoming_blue = ledBuffer[pixelCount + 3]; + + float final_limit = (incoming_gain != 255) ? incoming_gain / 255.0f : 1.0f; + if (rCorrection != incoming_red || gCorrection != incoming_green || bCorrection != incoming_blue || whiteLimit != final_limit) + { + rCorrection = incoming_red; + gCorrection = incoming_green; + bCorrection = incoming_blue; + whiteLimit = final_limit; + prepareCalibration(); + } + } + #endif + +#ifdef ENABLE_CHECK_FLETCHER + } + else + { + stat_bad++; + stat_bad_fletcher++; + } +#endif + } + + bytesRead = 0; + state = AwaProtocol::HEADER_A; + + ledsComplete = false; + } + + if ((curTime - stat_start > reportStatInterval_ms)) + { + if (stat_frames > 0) + { + showStats(); + } + } +} + +#ifdef __AVR__ +void serialEvent() +{ + processSerialData(); +} +#elif defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) +void ESPserialEvent() +{ + processSerialData(); +} +#endif diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 6421d6de..5cff7fd3 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -2379,23 +2379,29 @@ function updateElementsWled(ledType, key) { } else { //If failed to get properties var hardwareLedCount; + var segmentConfig = false; if (configuredDeviceType == ledType && configuredHost == host) { // Populate elements from existing configuration + if (window.serverConfig.device.segments) { + segmentConfig = true; + } + hardwareLedCount = window.serverConfig.device.hardwareLedCount; + } else { + // Populate elements with default values + hardwareLedCount = 1; + } + + if (segmentConfig) { var configuredstreamSegmentId = window.serverConfig.device.segments.streamSegmentId.toString(); enumSegSelectVals = [configuredstreamSegmentId]; enumSegSelectTitleVals = ["Segment " + configuredstreamSegmentId]; enumSegSelectDefaultVal = configuredstreamSegmentId; - - hardwareLedCount = window.serverConfig.device.hardwareLedCount; } else { - // Populate elements with default values defaultSegmentId = "-1"; enumSegSelectVals.push(defaultSegmentId); enumSegSelectTitleVals.push($.i18n('edt_dev_spec_segments_disabled_title')); enumSegSelectDefaultVal = defaultSegmentId; - - hardwareLedCount = 1; } conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); } diff --git a/assets/webconfig/js/content_remote.js b/assets/webconfig/js/content_remote.js index d0c1d9c2..e689c885 100644 --- a/assets/webconfig/js/content_remote.js +++ b/assets/webconfig/js/content_remote.js @@ -97,7 +97,7 @@ $(document).ready(function () { } function updateInputSelect() { - $('.sstbody').html(""); + $('.sstbody').empty(); var prios = window.serverInfo.priorities; var clearAll = false; diff --git a/include/hyperion/LinearColorSmoothing.h b/include/hyperion/LinearColorSmoothing.h index 5636c278..d22c5da5 100644 --- a/include/hyperion/LinearColorSmoothing.h +++ b/include/hyperion/LinearColorSmoothing.h @@ -289,6 +289,9 @@ private: int _currentConfigId; bool _enabled; + //The system enable state, to restore smoothing state after effect with smoothing ran + bool _enabledSystemCfg; + /// The type of smoothing to perform SmoothingType _smoothingType; diff --git a/libsrc/effectengine/EffectEngine.cpp b/libsrc/effectengine/EffectEngine.cpp index f192df99..2c5ab521 100644 --- a/libsrc/effectengine/EffectEngine.cpp +++ b/libsrc/effectengine/EffectEngine.cpp @@ -121,11 +121,17 @@ void EffectEngine::handleUpdatedEffectList() // add smoothing configurations to Hyperion if (def.args["smoothing-custom-settings"].toBool()) { + int settlingTime_ms = def.args["smoothing-time_ms"].toInt(); + double ledUpdateFrequency_hz = def.args["smoothing-updateFrequency"].toDouble(); + unsigned updateDelay {0}; + + Debug(_log, "Effect \"%s\": Add custom smoothing settings [%d]. Type: Linear, Settling time: %dms, Interval: %.fHz ", QSTRING_CSTR(def.name), specificId, settlingTime_ms, ledUpdateFrequency_hz); + def.smoothCfg = _hyperion->updateSmoothingConfig( - ++specificId, - def.args["smoothing-time_ms"].toInt(), - def.args["smoothing-updateFrequency"].toDouble(), - 0 ); + ++specificId, + settlingTime_ms, + ledUpdateFrequency_hz, + updateDelay ); } else { @@ -155,11 +161,18 @@ int EffectEngine::runEffect(const QString &effectName, const QJsonObject &args, //In case smoothing information is provided dynamically use temp smoothing config item (2) if (smoothCfg == SmoothingConfigID::SYSTEM && args["smoothing-custom-settings"].toBool()) { + int settlingTime_ms = args["smoothing-time_ms"].toInt(); + double ledUpdateFrequency_hz = args["smoothing-updateFrequency"].toDouble(); + unsigned updateDelay {0}; + + Debug(_log, "Effect \"%s\": Apply dynamic smoothing settings, if smoothing. Type: Linear, Settling time: %dms, Interval: %.fHz ", QSTRING_CSTR(effectName), settlingTime_ms, ledUpdateFrequency_hz); + smoothCfg = _hyperion->updateSmoothingConfig( - SmoothingConfigID::EFFECT_DYNAMIC, - args["smoothing-time_ms"].toInt(), - args["smoothing-updateFrequency"].toDouble(), - 0 ); + SmoothingConfigID::EFFECT_DYNAMIC, + settlingTime_ms, + ledUpdateFrequency_hz, + updateDelay + ); } if (pythonScript.isEmpty()) diff --git a/libsrc/hyperion/LinearColorSmoothing.cpp b/libsrc/hyperion/LinearColorSmoothing.cpp index 4cf6d4b9..c8d4d657 100644 --- a/libsrc/hyperion/LinearColorSmoothing.cpp +++ b/libsrc/hyperion/LinearColorSmoothing.cpp @@ -70,6 +70,7 @@ LinearColorSmoothing::LinearColorSmoothing(const QJsonDocument &config, Hyperion , _pause(false) , _currentConfigId(SmoothingConfigID::SYSTEM) , _enabled(false) + , _enabledSystemCfg(false) , _smoothingType(SmoothingType::Linear) , tempValues(std::vector(0, 0L)) { @@ -114,11 +115,12 @@ void LinearColorSmoothing::handleSettingsUpdate(settings::type type, const QJson QJsonObject obj = config.object(); setEnable(obj["enable"].toBool(_enabled)); + _enabledSystemCfg = _enabled; - SmoothingCfg cfg(false, - static_cast(obj[SETTINGS_KEY_SETTLING_TIME].toInt(DEFAULT_SETTLINGTIME)), - static_cast(MS_PER_MICRO / obj[SETTINGS_KEY_UPDATE_FREQUENCY].toDouble(DEFAULT_UPDATEFREQUENCY)) - ); + int64_t settlingTime_ms = static_cast(obj[SETTINGS_KEY_SETTLING_TIME].toInt(DEFAULT_SETTLINGTIME)); + int _updateInterval_ms =static_cast(MS_PER_MICRO / obj[SETTINGS_KEY_UPDATE_FREQUENCY].toDouble(DEFAULT_UPDATEFREQUENCY)); + + SmoothingCfg cfg(false, settlingTime_ms, _updateInterval_ms); const QString typeString = obj[SETTINGS_KEY_SMOOTHING_TYPE].toString(); @@ -162,7 +164,10 @@ int LinearColorSmoothing::write(const std::vector &ledValues) _previousValues = ledValues; _previousInterpolationTime = micros(); - _timer->start(_updateInterval); + if (!_pause) + { + _timer->start(_updateInterval); + } } return 0; @@ -510,6 +515,8 @@ void LinearColorSmoothing::clearRememberedFrames() void LinearColorSmoothing::queueColors(const std::vector &ledColors) { + assert (ledColors.size() > 0); + if (_outputDelay == 0) { // No output delay => immediate write @@ -558,13 +565,16 @@ void LinearColorSmoothing::componentStateChange(hyperion::Components component, void LinearColorSmoothing::setEnable(bool enable) { - _enabled = enable; - if (!_enabled) + if ( _enabled != enable) { - clearQueuedColors(); + _enabled = enable; + if (!_enabled) + { + clearQueuedColors(); + } + // update comp register + _hyperion->setNewComponentState(hyperion::COMP_SMOOTHING, enable); } - // update comp register - _hyperion->setNewComponentState(hyperion::COMP_SMOOTHING, enable); } void LinearColorSmoothing::setPause(bool pause) @@ -603,7 +613,7 @@ unsigned LinearColorSmoothing::updateConfig(int cfgID, int settlingTime_ms, doub updateDelay }; _cfgList[updatedCfgID] = cfg; - DebugIf(verbose && _enabled, _log,"%s", QSTRING_CSTR(getConfig(updatedCfgID))); + Debug(_log,"%s", QSTRING_CSTR(getConfig(updatedCfgID))); } else { @@ -660,6 +670,19 @@ bool LinearColorSmoothing::selectConfig(int cfgID, bool force) _interpolationCounter = 0; _interpolationStatCounter = 0; + //Enable smoothing for effects with smoothing + if (cfgID >= SmoothingConfigID::EFFECT_DYNAMIC) + { + Debug(_log,"Run Effect with Smoothing enabled"); + _enabledSystemCfg = _enabled; + setEnable(true); + } + else + { + // Restore enabled state after running an effect with smoothing + setEnable(_enabledSystemCfg); + } + if (_cfgList[cfgID]._updateInterval != _updateInterval) { @@ -667,7 +690,10 @@ bool LinearColorSmoothing::selectConfig(int cfgID, bool force) _updateInterval = _cfgList[cfgID]._updateInterval; if (this->enabled()) { - _timer->start(_updateInterval); + if (!_pause && !_targetValues.empty()) + { + _timer->start(_updateInterval); + } } } _currentConfigId = cfgID; @@ -689,30 +715,36 @@ QString LinearColorSmoothing::getConfig(int cfgID) { SmoothingCfg cfg = _cfgList[cfgID]; - configText = QString ("[%1] - type: %2, pause: %3, settlingTime: %4ms, interval: %5ms (%6Hz), delay: %7 frames") - .arg(cfgID) - .arg(SmoothingCfg::EnumToString(cfg._type),(cfg._pause) ? "true" : "false") - .arg(cfg._settlingTime) - .arg(cfg._updateInterval) - .arg(int(MS_PER_MICRO/cfg._updateInterval)) - .arg(cfg._outputDelay); + configText = QString ("[%1] - Type: %2, Pause: %3") + .arg(cfgID) + .arg(SmoothingCfg::EnumToString(cfg._type),(cfg._pause) ? "true" : "false") ; switch (cfg._type) { - case SmoothingType::Linear: - break; - case SmoothingType::Decay: { const double thalf = (1.0-std::pow(1.0/2, 1.0/_decay))*_settlingTime; - configText += QString (", interpolationRate: %1Hz, dithering: %2, decay: %3 -> halftime: %4ms") - .arg(cfg._interpolationRate,0,'f',2) - .arg((cfg._dithering) ? "true" : "false") - .arg(cfg._decay,0,'f',2) - .arg(thalf,0,'f',2); + configText += QString (", Interpolation rate: %1Hz, Dithering: %2, decay: %3 -> Halftime: %4ms") + .arg(cfg._interpolationRate,0,'f',2) + .arg((cfg._dithering) ? "true" : "false") + .arg(cfg._decay,0,'f',2) + .arg(thalf,0,'f',2); + [[fallthrough]]; + } + + case SmoothingType::Linear: + { + configText += QString (", Settling time: %1ms, Interval: %2ms (%3Hz)") + .arg(cfg._settlingTime) + .arg(cfg._updateInterval) + .arg(int(MS_PER_MICRO/cfg._updateInterval)); break; } } + + configText += QString (", delay: %1 frames") + .arg(cfg._outputDelay); } + return configText; } @@ -736,7 +768,6 @@ LinearColorSmoothing::SmoothingCfg::SmoothingCfg(bool pause, int64_t settlingTim { } - QString LinearColorSmoothing::SmoothingCfg::EnumToString(SmoothingType type) { if (type == SmoothingType::Linear) { diff --git a/libsrc/leddevice/dev_net/LedDeviceWled.cpp b/libsrc/leddevice/dev_net/LedDeviceWled.cpp index 7b4fed2a..3aa4662f 100644 --- a/libsrc/leddevice/dev_net/LedDeviceWled.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceWled.cpp @@ -523,7 +523,10 @@ bool LedDeviceWled::restoreState() _originalStateProperties[STATE_LIVE] = false; _originalStateProperties[STATE_TRANSITIONTIME_CURRENTCALL] = 0; - _originalStateProperties[STATE_ON] = _isStayOnAfterStreaming; + if (_isStayOnAfterStreaming) + { + _originalStateProperties[STATE_ON] = true; + } httpResponse response = _restApi->put(_originalStateProperties); if ( response.error() ) diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.cpp b/libsrc/leddevice/dev_net/ProviderRestApi.cpp index 0d318e56..d2732f8e 100644 --- a/libsrc/leddevice/dev_net/ProviderRestApi.cpp +++ b/libsrc/leddevice/dev_net/ProviderRestApi.cpp @@ -20,25 +20,34 @@ enum HttpStatusCode { NoContent = 204, BadRequest = 400, UnAuthorized = 401, + Forbidden = 403, NotFound = 404 }; -constexpr std::chrono::milliseconds DEFAULT_REST_TIMEOUT{ 400 }; - } //End of constants -ProviderRestApi::ProviderRestApi(const QString& host, int port, const QString& basePath) - :_log(Logger::getInstance("LEDDEVICE")) - , _networkManager(nullptr) +ProviderRestApi::ProviderRestApi(const QString& scheme, const QString& host, int port, const QString& basePath) + : _log(Logger::getInstance("LEDDEVICE")) + , _networkManager(nullptr) + , _requestTimeout(DEFAULT_REST_TIMEOUT) { _networkManager = new QNetworkAccessManager(); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)) + _networkManager->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); +#endif - _apiUrl.setScheme("http"); + _apiUrl.setScheme(scheme); _apiUrl.setHost(host); _apiUrl.setPort(port); _basePath = basePath; } +ProviderRestApi::ProviderRestApi(const QString& scheme, const QString& host, int port) + : ProviderRestApi(scheme, host, port, "") {} + +ProviderRestApi::ProviderRestApi(const QString& host, int port, const QString& basePath) +: ProviderRestApi("http", host, port, basePath) {} + ProviderRestApi::ProviderRestApi(const QString& host, int port) : ProviderRestApi(host, port, "") {} @@ -62,6 +71,12 @@ void ProviderRestApi::setBasePath(const QString& basePath) appendPath(_basePath, basePath); } +void ProviderRestApi::setPath(const QStringList& pathElements) +{ + _path.clear(); + appendPath(_path, pathElements.join(ONE_SLASH)); +} + void ProviderRestApi::setPath(const QString& path) { _path.clear(); @@ -73,6 +88,11 @@ void ProviderRestApi::appendPath(const QString& path) appendPath(_path, path); } +void ProviderRestApi::appendPath(const QStringList& pathElements) +{ + appendPath(_path, pathElements.join(ONE_SLASH)); +} + void ProviderRestApi::appendPath ( QString& path, const QString &appendPath) { if (!appendPath.isEmpty() && appendPath != ONE_SLASH) @@ -132,40 +152,7 @@ httpResponse ProviderRestApi::get() httpResponse ProviderRestApi::get(const QUrl& url) { - // Perform request - QNetworkRequest request(_networkRequestHeaders); - request.setUrl(url); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - _networkManager->setTransferTimeout(DEFAULT_REST_TIMEOUT.count()); -#endif - - QNetworkReply* reply = _networkManager->get(request); - - // Connect requestFinished signal to quit slot of the loop. - QEventLoop loop; - QEventLoop::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) - ReplyTimeout::set(reply, DEFAULT_REST_TIMEOUT.count()); -#endif - - // Go into the loop until the request is finished. - loop.exec(); - - httpResponse response; - if (reply->operation() == QNetworkAccessManager::GetOperation) - { - if(reply->error() != QNetworkReply::NoError) - { - Debug(_log, "GET: [%s]", QSTRING_CSTR( url.toString() )); - } - response = getResponse(reply ); - } - // Free space. - reply->deleteLater(); - // Return response - return response; + return executeOperation(QNetworkAccessManager::GetOperation, url); } httpResponse ProviderRestApi::put(const QJsonObject &body) @@ -180,40 +167,7 @@ httpResponse ProviderRestApi::put(const QString &body) httpResponse ProviderRestApi::put(const QUrl &url, const QByteArray &body) { - // Perform request - QNetworkRequest request(_networkRequestHeaders); - request.setUrl(url); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - _networkManager->setTransferTimeout(DEFAULT_REST_TIMEOUT.count()); -#endif - - QNetworkReply* reply = _networkManager->put(request, body); - // Connect requestFinished signal to quit slot of the loop. - QEventLoop loop; - QEventLoop::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) - ReplyTimeout::set(reply, DEFAULT_REST_TIMEOUT.count()); -#endif - - // Go into the loop until the request is finished. - loop.exec(); - - httpResponse response; - if (reply->operation() == QNetworkAccessManager::PutOperation) - { - if(reply->error() != QNetworkReply::NoError) - { - Debug(_log, "PUT: [%s] [%s]", QSTRING_CSTR( url.toString() ),body.constData() ); - } - response = getResponse(reply); - } - // Free space. - reply->deleteLater(); - - // Return response - return response; + return executeOperation(QNetworkAccessManager::PutOperation, url, body); } httpResponse ProviderRestApi::post(const QJsonObject& body) @@ -228,76 +182,69 @@ httpResponse ProviderRestApi::post(const QString& body) httpResponse ProviderRestApi::post(const QUrl& url, const QByteArray& body) { - // Perform request - QNetworkRequest request(_networkRequestHeaders); - request.setUrl(url); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - _networkManager->setTransferTimeout(DEFAULT_REST_TIMEOUT.count()); -#endif - - QNetworkReply* reply = _networkManager->post(request, body); - // Connect requestFinished signal to quit slot of the loop. - QEventLoop loop; - QEventLoop::connect(reply,&QNetworkReply::finished,&loop,&QEventLoop::quit); - -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) - ReplyTimeout::set(reply, DEFAULT_REST_TIMEOUT.count()); -#endif - - // Go into the loop until the request is finished. - loop.exec(); - - httpResponse response; - if (reply->operation() == QNetworkAccessManager::PostOperation) - { - if(reply->error() != QNetworkReply::NoError) - { - Debug(_log, "POST: [%s] [%s]", QSTRING_CSTR( url.toString() ),body.constData() ); - } - response = getResponse(reply); - } - // Free space. - reply->deleteLater(); - - // Return response - return response; + return executeOperation(QNetworkAccessManager::PostOperation, url, body); } httpResponse ProviderRestApi::deleteResource(const QUrl& url) +{ + return executeOperation(QNetworkAccessManager::DeleteOperation, url); +} + +httpResponse ProviderRestApi::executeOperation(QNetworkAccessManager::Operation operation, const QUrl& url, const QByteArray& body) { // Perform request QNetworkRequest request(_networkRequestHeaders); request.setUrl(url); + request.setOriginatingObject(this); #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - _networkManager->setTransferTimeout(DEFAULT_REST_TIMEOUT.count()); + _networkManager->setTransferTimeout(_requestTimeout.count()); #endif - QNetworkReply* reply = _networkManager->deleteResource(request); + QDateTime start = QDateTime::currentDateTime(); + QString opCode; + QNetworkReply* reply; + switch (operation) { + case QNetworkAccessManager::GetOperation: + opCode = "GET"; + reply = _networkManager->get(request); + break; + case QNetworkAccessManager::PutOperation: + opCode = "PUT"; + reply = _networkManager->put(request, body); + break; + case QNetworkAccessManager::PostOperation: + opCode = "POST"; + reply = _networkManager->post(request, body); + break; + case QNetworkAccessManager::DeleteOperation: + opCode = "DELETE"; + reply = _networkManager->deleteResource(request); + break; + default: + Error(_log, "Unsupported operation"); + return httpResponse(); + } + // Connect requestFinished signal to quit slot of the loop. QEventLoop loop; QEventLoop::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - // Go into the loop until the request is finished. - loop.exec(); #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) - ReplyTimeout::set(reply, DEFAULT_REST_TIMEOUT.count()); + ReplyTimeout* timeout = ReplyTimeout::set(reply, _requestTimeout.count()); #endif - httpResponse response; - if (reply->operation() == QNetworkAccessManager::DeleteOperation) - { - if(reply->error() != QNetworkReply::NoError) - { - Debug(_log, "DELETE: [%s]", QSTRING_CSTR(url.toString())); - } - response = getResponse(reply); - } + // Go into the loop until the request is finished. + loop.exec(); + QDateTime end = QDateTime::currentDateTime(); + + httpResponse response = (reply->operation() == operation) ? getResponse(reply) : httpResponse(); + + Debug(_log, "%s took %lldms, HTTP %d: [%s] [%s]", QSTRING_CSTR(opCode), start.msecsTo(end), response.getHttpStatusCode(), QSTRING_CSTR(url.toString()), body.constData()); + // Free space. reply->deleteLater(); - // Return response return response; } @@ -311,34 +258,31 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply) if (reply->error() == QNetworkReply::NoError) { - if ( httpStatusCode != HttpStatusCode::NoContent ){ - QByteArray replyData = reply->readAll(); + QByteArray replyData = reply->readAll(); - if (!replyData.isEmpty()) + if (!replyData.isEmpty()) + { + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(replyData, &error); + + if (error.error != QJsonParseError::NoError) { - QJsonParseError error; - QJsonDocument jsonDoc = QJsonDocument::fromJson(replyData, &error); - - if (error.error != QJsonParseError::NoError) - { - //Received not valid JSON response - response.setError(true); - response.setErrorReason(error.errorString()); - } - else - { - response.setBody(jsonDoc); - } + //Received not valid JSON response + response.setError(true); + response.setErrorReason(error.errorString()); } else - { // Create valid body which is empty - response.setBody(QJsonDocument()); + { + response.setBody(jsonDoc); } } + else + { // Create valid body which is empty + response.setBody(QJsonDocument()); + } } else { - Debug(_log, "Reply.httpStatusCode [%d]", httpStatusCode ); QString errorReason; if (httpStatusCode > 0) { QString httpReason = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); @@ -350,25 +294,32 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply) case HttpStatusCode::UnAuthorized: advise = "Check Authentication Token (API Key)"; break; + case HttpStatusCode::Forbidden: + advise = "No permission to access the given resource"; + break; case HttpStatusCode::NotFound: advise = "Check Resource given"; break; default: + advise = httpReason; break; } errorReason = QString ("[%3 %4] - %5").arg(httpStatusCode).arg(httpReason, advise); } else { - errorReason = reply->errorString(); + if (reply->error() == QNetworkReply::OperationCanceledError) { - response.setError(true); - response.setErrorReason(errorReason); + errorReason = "Network request timeout error"; + } + else + { + errorReason = reply->errorString(); } - } - // Create valid body which is empty - response.setBody(QJsonDocument()); + } + response.setError(true); + response.setErrorReason(errorReason); } return response; } @@ -388,3 +339,8 @@ void ProviderRestApi::setHeader(QNetworkRequest::KnownHeaders header, const QVar } } } + +void ProviderRestApi::setHeader(const QByteArray &headerName, const QByteArray &headerValue) +{ + _networkRequestHeaders.setRawHeader(headerName, headerValue); +} diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.h b/libsrc/leddevice/dev_net/ProviderRestApi.h index a87c5f2c..8d8e0a83 100644 --- a/libsrc/leddevice/dev_net/ProviderRestApi.h +++ b/libsrc/leddevice/dev_net/ProviderRestApi.h @@ -13,15 +13,22 @@ #include #include +#include + +constexpr std::chrono::milliseconds DEFAULT_REST_TIMEOUT{ 1000 }; + //Set QNetworkReply timeout without external timer //https://stackoverflow.com/questions/37444539/how-to-set-qnetworkreply-timeout-without-external-timer -class ReplyTimeout : public QObject { +class ReplyTimeout : public QObject +{ Q_OBJECT + public: enum HandleMethod { Abort, Close }; + ReplyTimeout(QNetworkReply* reply, const int timeout, HandleMethod method = Abort) : - QObject(reply), m_method(method) + QObject(reply), m_method(method), m_timedout(false) { Q_ASSERT(reply); if (reply && reply->isRunning()) { @@ -29,20 +36,30 @@ public: connect(reply, &QNetworkReply::finished, this, &QObject::deleteLater); } } - static void set(QNetworkReply* reply, const int timeout, HandleMethod method = Abort) + + bool isTimedout() const { - new ReplyTimeout(reply, timeout, method); + return m_timedout; } + static ReplyTimeout * set(QNetworkReply* reply, const int timeout, HandleMethod method = Abort) + { + return new ReplyTimeout(reply, timeout, method); + } + +signals: + void timedout(); + protected: - QBasicTimer m_timer; - HandleMethod m_method; + void timerEvent(QTimerEvent * ev) override { if (!m_timer.isActive() || ev->timerId() != m_timer.timerId()) return; auto reply = static_cast(parent()); if (reply->isRunning()) { + m_timedout = true; + emit timedout(); if (m_method == Close) reply->close(); else if (m_method == Abort) @@ -50,6 +67,10 @@ protected: m_timer.stop(); } } + + QBasicTimer m_timer; + HandleMethod m_method; + bool m_timedout; }; /// @@ -104,11 +125,12 @@ private: /// ///@endcode /// -class ProviderRestApi +class ProviderRestApi : public QObject { + Q_OBJECT + public: - /// /// @brief Constructor of the REST-API wrapper /// ProviderRestApi(); @@ -121,6 +143,15 @@ public: /// explicit ProviderRestApi(const QString& host, int port); + /// + /// @brief Constructor of the REST-API wrapper + /// + /// @param[in] scheme + /// @param[in] host + /// @param[in] port + /// + explicit ProviderRestApi(const QString& scheme, const QString& host, int port); + /// /// @brief Constructor of the REST-API wrapper /// @@ -130,10 +161,20 @@ public: /// explicit ProviderRestApi(const QString& host, int port, const QString& basePath); + /// + /// @brief Constructor of the REST-API wrapper + /// + /// @param[in] scheme + /// @param[in] host + /// @param[in] port + /// @param[in] API base-path + /// + explicit ProviderRestApi(const QString& scheme, const QString& host, int port, const QString& basePath); + /// /// @brief Destructor of the REST-API wrapper /// - virtual ~ProviderRestApi(); + virtual ~ProviderRestApi() override; /// /// @brief Set an API's host @@ -177,6 +218,12 @@ public: /// void setPath(const QString& path); + /// @brief Set an API's path to address resources + /// + /// @param[in] pathElements to form a path, e.g. (lights,1,state) results in "/lights/1/state/" + /// + void setPath(const QStringList& pathElements); + /// /// @brief Append an API's path element to path set before /// @@ -184,6 +231,13 @@ public: /// void appendPath(const QString& appendPath); + /// + /// @brief Append API's path elements to path set before + /// + /// @param[in] pathElements + /// + void appendPath(const QStringList& pathElements); + /// /// @brief Set an API's fragment /// @@ -283,14 +337,28 @@ public: /// @param[in] The type of the header field. /// @param[in] The value of the header field. /// If the header field exists, the value will be combined as comma separated string. - void setHeader(QNetworkRequest::KnownHeaders header, const QVariant& value); + /// + /// Set a header field. + /// + /// @param[in] The type of the header field. + /// @param[in] The value of the header field. + /// If the header field exists, the value will override the previous setting. + void setHeader(const QByteArray &headerName, const QByteArray &headerValue); + /// /// Remove all header fields. /// void removeAllHeaders() { _networkRequestHeaders = QNetworkRequest(); } + /// + /// Sets the timeout time frame after a request is aborted + /// Zero means no timer is set. + /// + /// @param[in] timeout in milliseconds. + void setTransferTimeout(std::chrono::milliseconds timeout = DEFAULT_REST_TIMEOUT) { _requestTimeout = timeout; } + /// /// @brief Set the common logger for LED-devices. /// @@ -308,10 +376,14 @@ private: /// static void appendPath (QString &path, const QString &appendPath) ; + + httpResponse executeOperation(QNetworkAccessManager::Operation op, const QUrl& url, const QByteArray& body = {}); + Logger* _log; // QNetworkAccessManager object for sending REST-requests. QNetworkAccessManager* _networkManager; + std::chrono::milliseconds _requestTimeout; QUrl _apiUrl; diff --git a/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp b/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp index d3b9dcce..8c45e233 100644 --- a/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp +++ b/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp @@ -94,7 +94,7 @@ void LedDeviceAdalight::prepareHeader() break; case Adalight::AWA: - _bufferLength += 7; + _bufferLength += 8; [[fallthrough]]; case Adalight::ADA: [[fallthrough]]; @@ -162,14 +162,20 @@ int LedDeviceAdalight::write(const std::vector & ledValues) { whiteChannelExtension(writer); - uint16_t fletcher1 = 0, fletcher2 = 0; + uint16_t fletcher1 = 0; + uint16_t fletcher2 = 0; + uint16_t fletcherExt = 0; + uint8_t position = 0; + while (hasher < writer) { + fletcherExt = (fletcherExt + (*(hasher) ^ (position++))) % 255; fletcher1 = (fletcher1 + *(hasher++)) % 255; fletcher2 = (fletcher2 + fletcher1) % 255; } *(writer++) = static_cast(fletcher1); *(writer++) = static_cast(fletcher2); + *(writer++) = static_cast((fletcherExt != 0x41) ? fletcherExt : 0xaa); } _bufferLength = writer - _ledBuffer.data(); } From 22fa008c4b5a36f6e80acd672ec2b4e289245137 Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Fri, 17 Feb 2023 15:19:03 +0100 Subject: [PATCH 10/23] Update fr.json (POEditor.com) --- assets/webconfig/i18n/fr.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/webconfig/i18n/fr.json b/assets/webconfig/i18n/fr.json index 0b87ecae..ebaf1731 100644 --- a/assets/webconfig/i18n/fr.json +++ b/assets/webconfig/i18n/fr.json @@ -398,9 +398,11 @@ "edt_conf_v4l2_greenSignalThreshold_expl": "Assombrie les valeurs vertes faibles (reconnues comme noires)", "edt_conf_v4l2_greenSignalThreshold_title": "Seuil de signal vert", "edt_conf_v4l2_hardware_brightness_expl": "Définie la luminosité matérielle", + "edt_conf_v4l2_hardware_brightness_title": "Contrôle matériel de la luminosité", "edt_conf_v4l2_hardware_contrast_expl": "Définie le contraste matériel", "edt_conf_v4l2_hardware_contrast_title": "Contrôle le contraste matériel", "edt_conf_v4l2_hardware_hue_expl": "Définie le hue matériel", + "edt_conf_v4l2_hardware_hue_title": "Contrôle matériel de la teinte", "edt_conf_v4l2_hardware_saturation_expl": "Définie la saturation matérielle", "edt_conf_v4l2_hardware_saturation_title": "Contrôle la saturation matériel", "edt_conf_v4l2_heading_title": "Capture USB", From dea0a17df4e600a7adc117fb789fd5bc6f50e88c Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Fri, 17 Feb 2023 15:19:04 +0100 Subject: [PATCH 11/23] Update it.json (POEditor.com) --- assets/webconfig/i18n/it.json | 158 ++++++++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 7 deletions(-) diff --git a/assets/webconfig/i18n/it.json b/assets/webconfig/i18n/it.json index dd3a4bdf..3314a912 100644 --- a/assets/webconfig/i18n/it.json +++ b/assets/webconfig/i18n/it.json @@ -5,6 +5,8 @@ "InfoDialog_changePassword_title": "Cambia Password", "InfoDialog_iswitch_text": "Se esegui più di un Hyperion nella tua rete locale puoi passare tra configurazioni web. Seleziona l'istanza di Hyperion qui sotto e cambia!", "InfoDialog_iswitch_title": "Cambia Hyperion", + "InfoDialog_nostorage_text": "Il tuo browser non supporta localStorage. Non è possibile salvare un'impostazione di lingua specifica (fallback su \"rilevamento automatico\") e livello di accesso (fallback su \"predefinito\"). Alcuni maghi potrebbero essere nascosti. Puoi ancora utilizzare l'interfaccia web senza ulteriori problemi", + "InfoDialog_nostorage_title": "Impossibile memorizzare le impostazioni", "InfoDialog_nowrite_foottext": "WebUI sarà sbloccata automaticamente appena avrai risolto il problema!", "InfoDialog_nowrite_text": "Hyperion non può scrivere sull'attuale file di configurazione caricato. Riparare i permessi del file per procedere", "InfoDialog_nowrite_title": "errore dei permessi di scrittura!", @@ -40,14 +42,29 @@ "conf_general_intro": "Impostazioni base di Hyperion e WebUI che non rientrano in altre categorie.", "conf_general_label_title": "Impostazioni generali", "conf_grabber_fg_intro": "Cattura di sistema usa la cattura locale del sistema su cui è installato Hyperion come sorgente di input.", + "conf_grabber_inst_grabber_config_info": "Configura in anticipo i dispositivi hardware di acquisizione in modo che vengano utilizzati dall'istanza", "conf_grabber_v4l_intro": "Cattura USB è un dispositivo (di cattura) connesso via USB usato come sorgente di immagini per l'elaborazione.", "conf_helptable_expl": "Spiegazione", "conf_helptable_option": "Opzioni", + "conf_leds_config_error": "Errore nella configurazione del layout LED/LED", + "conf_leds_config_warning": "Controlla la configurazione del layout LED/LED", "conf_leds_contr_label_contrtype": "Tipo di Controller", + "conf_leds_device_info_log": "Nel caso in cui i tuoi LED non funzionino, controlla qui per gli errori:", "conf_leds_device_intro": "Hyperion supporta molti controller per trasmettere dati al tuo dispositivo. Seleziona un controller LED dalla lista e configuralo. Abbiamo scelto le impostazioni di default migliori per ogni dispositivo.", + "conf_leds_error_get_properties_text": "Impossibile ottenere le proprietà del dispositivo. Si prega di controllare gli elementi di configurazione.", + "conf_leds_error_get_properties_title": "Proprietà del dispositivo", + "conf_leds_error_hwled_gt_layout": "Il conteggio dei LED hardware ($1) è maggiore dei LED configurati tramite layout ($2),
    $3 {{plural:$3|LED|LED}} rimarranno neri se continui.", + "conf_leds_error_hwled_gt_maxled": "Il numero di LED hardware ($ 1) è superiore al numero massimo di LED supportati dal dispositivo ($ 2).
    Il conteggio dei LED hardware è impostato su ($ 3)", + "conf_leds_error_hwled_lt_layout": "Il numero di LED hardware ($ 1) è inferiore ai LED configurati tramite layout ($ 2).
    Il numero di LED configurati nel layout non deve superare i LED disponibili", + "conf_leds_info_ws281x": "Hyperion deve essere eseguito con privilegi di 'root' per questo tipo di controller!", "conf_leds_layout_advanced": "Impostazioni Avanzate", "conf_leds_layout_blacklist_num_title": "Numero di LEDs", + "conf_leds_layout_blacklist_rule_title": "Regole della lista nera", + "conf_leds_layout_blacklist_rules_title": "Regole delle liste nere", + "conf_leds_layout_blacklist_start_title": "LED Iniziale", + "conf_leds_layout_blacklistleds_title": "I LED della lista nera", "conf_leds_layout_btn_checklist": "Mostra lista", + "conf_leds_layout_btn_keystone": "Correzione trapezoidale", "conf_leds_layout_button_savelay": "Salva Layout", "conf_leds_layout_button_updsim": "Aggiorna Anteprima", "conf_leds_layout_checkp1": "Il led nero è il tuo primo led, il primo led è il punto in cui c'è l'input del segnale dati", @@ -67,6 +84,16 @@ "conf_leds_layout_cl_leftbottom": "Sinistra 0% - 100% Sotto", "conf_leds_layout_cl_leftmiddle": "Sinistra 0% - 75% Centro", "conf_leds_layout_cl_lefttop": "Sinistra 0% - 50% Sopra", + "conf_leds_layout_cl_lightPosBottomLeft11": "In basso: 75 - 100% da sinistra", + "conf_leds_layout_cl_lightPosBottomLeft112": "In basso: 0 - 50% da sinistra", + "conf_leds_layout_cl_lightPosBottomLeft12": "In basso: 25 - 50% da sinistra", + "conf_leds_layout_cl_lightPosBottomLeft121": "In basso: 50 - 100% da sinistra", + "conf_leds_layout_cl_lightPosBottomLeft14": "In basso: 0 - 25% da sinistra", + "conf_leds_layout_cl_lightPosBottomLeft34": "In basso: 50 - 75% da sinistra", + "conf_leds_layout_cl_lightPosBottomLeftNewMid": "In basso: 25 - 75% da sinistra", + "conf_leds_layout_cl_lightPosTopLeft112": "In alto: 0 - 50% da sinistra", + "conf_leds_layout_cl_lightPosTopLeft121": "In alto: 50 - 100% da sinistra", + "conf_leds_layout_cl_lightPosTopLeftNewMid": "In alto: 25 - 75% da sinistra", "conf_leds_layout_cl_overlap": "Overlap", "conf_leds_layout_cl_reversdir": "Inverti direzione", "conf_leds_layout_cl_right": "Destra", @@ -81,6 +108,7 @@ "conf_leds_layout_generatedconf": "Configurazione LED Generata/Attuale", "conf_leds_layout_intro": "è necessario anche un layout dei led che rifletta la disposizione dei tuoi led. Il layout classico è il comune bordo della tv, ma supportiamo anche la creazione di matrici led (parete led). La visuale sul layout è SEMPRE DI FRONTE alla TV.", "conf_leds_layout_ma_cabling": "Cablaggio", + "conf_leds_layout_ma_direction": "Direzione", "conf_leds_layout_ma_horiz": "Orizzontale", "conf_leds_layout_ma_optbottomleft": "Basso a sinistra", "conf_leds_layout_ma_optbottomright": "Basso a destra", @@ -112,17 +140,21 @@ "conf_leds_layout_textf1": "Questo campo testo mostra di default il layout attualmente caricato e viene sovrascritto se ne generi uno nuovo con le opzioni sopra. Se vuoi puoi fare ulteriori modifiche.", "conf_leds_nav_label_ledcontroller": "Controller LED", "conf_leds_nav_label_ledlayout": "Layout LED", + "conf_leds_note_layout_overwrite": "Nota: Overwrite crea un layout predefinito per {{plural:$1| un LED| tutti i LED $1}} dati dal conteggio dei LED hardware", "conf_leds_optgroup_RPiGPIO": "RPi GPIO", "conf_leds_optgroup_RPiPWM": "RPi PWM", "conf_leds_optgroup_RPiSPI": "RPi SPI", + "conf_leds_optgroup_debug": "Debug", "conf_leds_optgroup_network": "Rete", "conf_leds_optgroup_other": "Altro", "conf_leds_optgroup_usb": "USB/Seriale", "conf_logging_btn_autoscroll": "Auto scrolling", + "conf_logging_btn_clipboard": "Copia nel registro Appunti", "conf_logging_btn_pbupload": "Invia segnalazione per richieste di supporto", "conf_logging_contpolicy": "Policy per la Privacy delle segnalazioni", "conf_logging_label_intro": "Area per controllare i messaggi di log, a seconda dell'impostazione di loglevel vedrai più o meno informazioni.", "conf_logging_lastreports": "Segnalazioni precedenti", + "conf_logging_logoutput": "Log output", "conf_logging_nomessage": "Nessun messaggio di log disponibile.", "conf_logging_report": "Segnala", "conf_logging_uplfailed": "Upload fallito! Controlla la tua connessione a internet!", @@ -163,7 +195,12 @@ "dashboard_infobox_label_instance": "Istanza:", "dashboard_infobox_label_latesthyp": "L'ultima versione di Hyperion:", "dashboard_infobox_label_platform": "Piattaforma:", + "dashboard_infobox_label_port_boblight": "Server Boblight:", + "dashboard_infobox_label_port_flat": "Flatbuffer:", + "dashboard_infobox_label_port_json": "JSON-Server:", + "dashboard_infobox_label_port_proto": "Protobuffer:", "dashboard_infobox_label_ports": "Porte", + "dashboard_infobox_label_ports_websocket": "WebSocket (ws|wss):", "dashboard_infobox_label_smartacc": "Accesso Smart", "dashboard_infobox_label_statush": "Status Hyperion:", "dashboard_infobox_label_title": "Informazioni", @@ -181,6 +218,7 @@ "dashboard_newsbox_readmore": "Leggi ancora", "dashboard_newsbox_visitblog": "Visita Hyperion-Blog", "edt_append_degree": "°", + "edt_append_frames": "frames", "edt_append_hz": "Hz", "edt_append_leds": "LEDs", "edt_append_ms": "ms", @@ -216,6 +254,8 @@ "edt_conf_color_blue_title": "Blu", "edt_conf_color_brightnessComp_expl": "Compensa la differenza di luminosità tra rosso verde blu, ciano magneta giallo e bianco. 100 significa massima compensazione, 0 nessuna compensazione", "edt_conf_color_brightnessComp_title": "Compensazione luminosità", + "edt_conf_color_brightnessGain_expl": "Regola la luminosità dei colori. 1.0 significa nessun cambiamento, oltre 1.0 aumenta la luminosità, sotto 1.0 diminuisce la luminosità", + "edt_conf_color_brightnessGain_title": "Intensità della luminosità", "edt_conf_color_brightness_expl": "Imposta la luminosità complessiva dei leds", "edt_conf_color_brightness_title": "Luminosità", "edt_conf_color_channelAdjustment_header_expl": "Crea profilo di colore che può essere assegnato a un componente specifico. Regola colore, gamma, luminosità, compensazione e altro.", @@ -242,6 +282,8 @@ "edt_conf_color_magenta_title": "Magenta", "edt_conf_color_red_expl": "Il valore calibrato del rosso.", "edt_conf_color_red_title": "Rosso", + "edt_conf_color_saturationGain_expl": "Regola la saturazione dei colori. 1.0 significa nessun cambiamento, oltre 1.0 aumenta la saturazione, sotto 1.0 desatura.", + "edt_conf_color_saturationGain_title": "Intensità della saturazione", "edt_conf_color_white_expl": "Il valore calibrato del bianco.", "edt_conf_color_white_title": "Bianco", "edt_conf_color_yellow_expl": "Il valore calibrato del giallo.", @@ -253,10 +295,13 @@ "edt_conf_effp_paths_expl": "Puoi definire altre cartelle che contengono effetti aggiuntivi. Il configuratore di effetti salva sempre all'interno della prima cartella.", "edt_conf_effp_paths_itemtitle": "Percorso", "edt_conf_effp_paths_title": "Percorso Effetti", + "edt_conf_enum_BOTH": "Orizzontale e Verticale", + "edt_conf_enum_HORIZONTAL": "Orizzontale", "edt_conf_enum_NO_CHANGE": "Auto", "edt_conf_enum_NTSC": "NTSC", "edt_conf_enum_PAL": "PAL", "edt_conf_enum_SECAM": "SECAM", + "edt_conf_enum_VERTICAL": "Verticale", "edt_conf_enum_automatic": "Automatico", "edt_conf_enum_bbclassic": "Classico", "edt_conf_enum_bbdefault": "Default", @@ -287,12 +332,15 @@ "edt_conf_enum_logverbose": "Verboso", "edt_conf_enum_logwarn": "Avvertimento", "edt_conf_enum_multicolor_mean": "Multicolore", + "edt_conf_enum_please_select": "Seleziona", "edt_conf_enum_rbg": "RBG", "edt_conf_enum_rgb": "RGB", "edt_conf_enum_right_left": "Da destra a sinistra", "edt_conf_enum_top_down": "Dall'alto verso il basso", "edt_conf_enum_transeffect_smooth": "Dolce", "edt_conf_enum_transeffect_sudden": "Repentina", + "edt_conf_enum_udp_ddp": "DDP", + "edt_conf_enum_udp_raw": "RAW", "edt_conf_enum_unicolor_mean": "Monocromatico", "edt_conf_fbs_heading_title": "Server Flatbuffers", "edt_conf_fbs_timeout_expl": "Se nessuna informazione viene ricevuta per un dato periodo, il componente verrà disabilitato (soft).", @@ -321,11 +369,19 @@ "edt_conf_fge_type_title": "Tipo", "edt_conf_fw_flat_expl": "Una destinazione flatbuffer per riga. Contiene IP:PORTA:(Esempio: 127.0.0.1:19401)", "edt_conf_fw_flat_itemtitle": "Destinatario flatbuffer", + "edt_conf_fw_flat_services_discovered_expl": "Obiettivi flatbuffer scoperti", + "edt_conf_fw_flat_services_discovered_title": "Obiettivi flatbuffer scoperti", "edt_conf_fw_flat_title": "Lista dei client flatbuffer", "edt_conf_fw_heading_title": "Forwarder", "edt_conf_fw_json_expl": "Una destinazione json per riga. Contiene IP:PORTA:(Esempio: 127.0.0.1:19446)", "edt_conf_fw_json_itemtitle": "Destinatario json", + "edt_conf_fw_json_services_discovered_expl": "Server Hyperion scoperti che forniscono servizi JSON-API", + "edt_conf_fw_json_services_discovered_title": "Target JSON rilevati", "edt_conf_fw_json_title": "Lista dei client json", + "edt_conf_fw_remote_service_discovered_none": "Nessun servizio remoto rilevato", + "edt_conf_fw_service_name_expl": "Nome del fornitore di servizi", + "edt_conf_fw_service_name_title": "Nome di Servizio", + "edt_conf_gen_configVersion_title": "Versione di configurazione", "edt_conf_gen_heading_title": "impostazioni Generali", "edt_conf_gen_name_expl": "Un nome definito dall'utente che viene utilizzato per riconoscere Hyperion. (Utile con più di un'istanza di Hyperion)", "edt_conf_gen_name_title": "Nome configurazione", @@ -339,10 +395,19 @@ "edt_conf_general_port_title": "Porta", "edt_conf_general_priority_expl": "Priorità di questo componente", "edt_conf_general_priority_title": "Priorità canale.", + "edt_conf_grabber_discovered_expl": "Seleziona il tuo dispositivo di acquisizione scoperto", + "edt_conf_grabber_discovered_none": "Nessun dispositivo di acquisizione rilevato", + "edt_conf_grabber_discovered_title": "Dispositivo scoperto", + "edt_conf_grabber_discovered_title_info": "Seleziona il tuo dispositivo di acquisizione scoperto", + "edt_conf_grabber_discovery_inprogress": "ricerca in corso", + "edt_conf_instC_screen_grabber_device_expl": "l dispositivo di cattura dello schermo utilizzato", + "edt_conf_instC_screen_grabber_device_title": "Dispositivo di cattura dello schermo", "edt_conf_instC_systemEnable_expl": "Abilita la cattura della piattaforma per questa istanza di hardware led", "edt_conf_instC_systemEnable_title": "Abilita cattura piattaforma", "edt_conf_instC_v4lEnable_expl": "Abilita la cattura USB per questa istanza di hardware led", "edt_conf_instC_v4lEnable_title": "Abilita cattura USB", + "edt_conf_instC_video_grabber_device_expl": "Il dispositivo di acquisizione video utilizzato", + "edt_conf_instC_video_grabber_device_title": "Dispositivo di acquisizione video", "edt_conf_instCapture_heading_title": "Cattura Istanza", "edt_conf_js_heading_title": "Server JSON", "edt_conf_log_heading_title": "Logging", @@ -374,8 +439,6 @@ "edt_conf_smooth_heading_title": "Sfumatura", "edt_conf_smooth_interpolationRate_expl": "Velocità di calcolo dei regolari frame intermedi", "edt_conf_smooth_interpolationRate_title": "Tasso di interpolazione", - "edt_conf_smooth_outputRate_expl": "Velocità di uscita al tuo controller LED.", - "edt_conf_smooth_outputRate_title": "Velocità di uscita", "edt_conf_smooth_time_ms_expl": "Quanto a lungo la sfumatura dovrebbe raggiungere le immagini?", "edt_conf_smooth_time_ms_title": "Durata", "edt_conf_smooth_type_expl": "Tipo di sfumatura.", @@ -390,21 +453,41 @@ "edt_conf_v4l2_cecDetection_title": "Rilevamento CEC", "edt_conf_v4l2_cropBottom_expl": "Numero di pixels in basso che vengono rimossi dall'immagine.", "edt_conf_v4l2_cropBottom_title": "Ritaglia in basso", + "edt_conf_v4l2_cropHeightValidation_error": "Ritaglia in alto + Ritaglia in basso non può essere maggiore di Altezza ($ 1)", "edt_conf_v4l2_cropLeft_expl": "Numero di pixels sulla sinistra che vengono rimossi dall'immagine.", "edt_conf_v4l2_cropLeft_title": "Ritaglia sinistra", "edt_conf_v4l2_cropRight_expl": "Numero di pixels sulla destra che vengono rimossi dall'immagine.", "edt_conf_v4l2_cropRight_title": "Ritaglia destra", "edt_conf_v4l2_cropTop_expl": "Numero di pixels in alto che vengono rimossi dall'immagine.", "edt_conf_v4l2_cropTop_title": "Ritaglia in alto", + "edt_conf_v4l2_cropWidthValidation_error": "Ritaglia a sinistra + Ritaglia a destra non può essere maggiore di Larghezza ($1)", "edt_conf_v4l2_device_expl": "Percorso del dispositivo di cattura USB. Imposta su 'Automatico' per riconoscimento automatico. Esempio: '/dev/video0'", "edt_conf_v4l2_device_title": "Dispositivo", + "edt_conf_v4l2_encoding_expl": "Forza la codifica video per i grabber multiformato", + "edt_conf_v4l2_encoding_title": "Formato di codifica", + "edt_conf_v4l2_flip_expl": "Ciò consente di capovolgere l'immagine orizzontalmente, verticalmente o entrambi.", + "edt_conf_v4l2_flip_title": "Capovolgimento dell'immagine", + "edt_conf_v4l2_fpsSoftwareDecimation_expl": "Per risparmiare risorse verrà elaborato solo ogni n-esimo frame. Per es. se il grabber è impostato a 30fps con questa opzione impostata a 5 il risultato finale sarà di circa 6fps", + "edt_conf_v4l2_fpsSoftwareDecimation_title": "Salto del frame del software", "edt_conf_v4l2_framerate_expl": "Frame al secondo supportati per il dispositivo attivo", "edt_conf_v4l2_framerate_title": "Frame al secondo", "edt_conf_v4l2_greenSignalThreshold_expl": "Scurisce valori bassi di verde (riconosciuti come nero)", "edt_conf_v4l2_greenSignalThreshold_title": "Soglia segnale verde", + "edt_conf_v4l2_hardware_brightness_expl": "Imposta la luminosità dell'hardware", + "edt_conf_v4l2_hardware_brightness_title": "Controllo hardware della luminosità", + "edt_conf_v4l2_hardware_contrast_expl": "Imposta il contrasto hardware", + "edt_conf_v4l2_hardware_contrast_title": "Controllo hardware del contrasto", + "edt_conf_v4l2_hardware_hue_expl": "Imposta la tonalità dell'hardware", + "edt_conf_v4l2_hardware_hue_title": "Controllo della tonalità hardware", + "edt_conf_v4l2_hardware_saturation_expl": "Imposta la saturazione hardware", + "edt_conf_v4l2_hardware_saturation_title": "Controllo della saturazione hardware", + "edt_conf_v4l2_hardware_set_defaults": "Controlli hardware predefiniti", + "edt_conf_v4l2_hardware_set_defaults_tip": "Imposta i valori predefiniti del dispositivo per luminosità, contrasto, tonalità e saturazione", "edt_conf_v4l2_heading_title": "Cattura USB", "edt_conf_v4l2_input_expl": "Seleziona l'input video per il tuo dispositivo. 'Automatico' mantiene il Valero scelto dall'interfaccia v4l2.", "edt_conf_v4l2_input_title": "Input", + "edt_conf_v4l2_noSignalCounterThreshold_expl": "Conteggio dei fotogrammi (controllalo con l'attuale modalità FPS del grabber) dopo il quale viene attivato il segnale di assenza", + "edt_conf_v4l2_noSignalCounterThreshold_title": "Soglia del contatore di segnale", "edt_conf_v4l2_redSignalThreshold_expl": "Scurisce valori bassi di rosso (riconosciuti come nero)", "edt_conf_v4l2_redSignalThreshold_title": "Soglia segnale rosso", "edt_conf_v4l2_resolution_expl": "Lista Risoluzioni supportate per il dispositivo attivo", @@ -435,12 +518,21 @@ "edt_conf_webc_sslport_expl": "Porta del webserver HTTPS", "edt_conf_webc_sslport_title": "Porta HTTPS", "edt_dev_auth_key_title": "Token di autenticazione", + "edt_dev_auth_key_title_info": "Token di autenticazione necessario per accedere al dispositivo", "edt_dev_enum_sub_min_cool_adjust": "Regolazione freddo min", "edt_dev_enum_sub_min_warm_adjust": "Regolazione calore min", "edt_dev_enum_subtract_minimum": "Sottrai minimo", "edt_dev_enum_white_off": "Bianco off", + "edt_dev_general_autostart_title": "avvio automatico", + "edt_dev_general_autostart_title_info": "Il dispositivo LED è acceso durante l'avvio oppure no", "edt_dev_general_colorOrder_title": "Ordine byte RGB", + "edt_dev_general_colorOrder_title_info": "L'ordine dei colori del dispositivo", + "edt_dev_general_enableAttemptsInterval_title": "Intervallo tra tentativi", + "edt_dev_general_enableAttemptsInterval_title_info": "Intervallo tra due tentativi di connessione.", + "edt_dev_general_enableAttempts_title": "Tentativi di connessione", + "edt_dev_general_enableAttempts_title_info": "Numero di tentativi di connessione di un dispositivo prima che entri in uno stato di errore.", "edt_dev_general_hardwareLedCount_title": "Numero di LED Hardware", + "edt_dev_general_hardwareLedCount_title_info": "Il numero di LED fisici disponibili per il dispositivo specificato", "edt_dev_general_heading_title": "Impostazioni Generali", "edt_dev_general_name_title": "Nome configurazione", "edt_dev_general_rewriteTime_title": "Tempo di ricarica", @@ -449,21 +541,28 @@ "edt_dev_spec_FCsetConfig_title": "Imposta configurazione Fadecandy", "edt_dev_spec_LBap102Mode_title": "Modalità LightBerry APA102", "edt_dev_spec_PBFiFo_title": "Pi-Blaster FiFo", + "edt_dev_spec_ada_mode_title": "Adalight - Standard", + "edt_dev_spec_awa_mode_title": "HyperSerial - Alta velocità", "edt_dev_spec_baudrate_title": "Baudrate", "edt_dev_spec_blackLightsTimeout_title": "Timeout rilevamento segnale su nero", "edt_dev_spec_brightnessFactor_title": "Fattore luminosità", "edt_dev_spec_brightnessMax_title": "Luminosità massima", - "edt_dev_spec_brightnessMin_title": "Luminosità minima", + "edt_dev_spec_brightnessMin_title": "Luminosità minima", "edt_dev_spec_brightnessOverwrite_title": "Sovrascrivi luminosità", "edt_dev_spec_brightnessThreshold_title": "Luminosità minima per rilevamento segnale", "edt_dev_spec_brightness_title": "Luminosità", + "edt_dev_spec_candyGamma_title": "Modalità \"Candy\" (doppia correzione gamma)", "edt_dev_spec_chanperfixture_title": "Canali per dispositivo", "edt_dev_spec_cid_title": "CID", "edt_dev_spec_clientKey_title": "Clientkey", "edt_dev_spec_colorComponent_title": "Componente colore", "edt_dev_spec_debugLevel_title": "Livello Debug", - "edt_dev_spec_debugStreamer_title": "Debug Streamer", "edt_dev_spec_delayAfterConnect_title": "Ritardo dopo la connessione", + "edt_dev_spec_devices_discovered_none": "Nessun dispositivo rilevato", + "edt_dev_spec_devices_discovered_title": "Dispositivi trovato", + "edt_dev_spec_devices_discovered_title_info": "Seleziona il tuo dispositivo LED trovato", + "edt_dev_spec_devices_discovered_title_info_custom": "Seleziona il tuo dispositivo LED trovato o configurane uno personalizzato", + "edt_dev_spec_devices_discovery_inprogress": "Ricerca in corso", "edt_dev_spec_dithering_title": "Dithering", "edt_dev_spec_dmaNumber_title": "Canale DMA", "edt_dev_spec_gamma_title": "Gamma", @@ -478,6 +577,7 @@ "edt_dev_spec_intervall_title": "Intervallo", "edt_dev_spec_invert_title": "Inverti segnale", "edt_dev_spec_latchtime_title": "Tempo di latch", + "edt_dev_spec_latchtime_title_info": "Il tempo di latch è l'intervallo di tempo richiesto da un dispositivo fino all'elaborazione del successivo aggiornamento. Durante tale periodo di tempo, tutti gli aggiornamenti effettuati vengono ignorati.", "edt_dev_spec_ledIndex_title": "Indice LED", "edt_dev_spec_ledType_title": "Tipo LED", "edt_dev_spec_lightid_itemtitle": "ID", @@ -491,6 +591,8 @@ "edt_dev_spec_networkDeviceName_title": "Nome dispositivo di rete", "edt_dev_spec_networkDevicePort_title": "Porta", "edt_dev_spec_numberOfLeds_title": "Numero di LEDs", + "edt_dev_spec_onBlackTimeToPowerOff": "È ora di spegnere la lampada se viene attivato il livello di nero", + "edt_dev_spec_onBlackTimeToPowerOn": "È ora di accendere la lampada se il segnale viene ripristinato", "edt_dev_spec_orbIds_title": "Orb ID", "edt_dev_spec_order_left_right_title": "2.", "edt_dev_spec_order_top_down_title": "1.", @@ -498,18 +600,29 @@ "edt_dev_spec_panel_start_position": "Pannello d'inizio [0-max panels]", "edt_dev_spec_panelorganisation_title": "Sequenza numerazione pannelli", "edt_dev_spec_pid_title": "PID", + "edt_dev_spec_port_expl": "Porta di servizio [1-65535]", "edt_dev_spec_port_title": "Porta", "edt_dev_spec_printTimeStamp_title": "Aggiungi timestamp", "edt_dev_spec_pwmChannel_title": "Canale PWM", + "edt_dev_spec_razer_device_title": "Dispositivo Razer Chroma", "edt_dev_spec_restoreOriginalState_title": "Ripristina lo stato delle luci", + "edt_dev_spec_restoreOriginalState_title_info": "Ripristina lo stato originale del dispositivo quando il dispositivo è disabilitato", + "edt_dev_spec_rgbw_calibration_blue": "Aspetto del canale blu/bianco", + "edt_dev_spec_rgbw_calibration_enable": "Calibrazione del canale del bianco (solo RGBW)", + "edt_dev_spec_rgbw_calibration_green": "Aspetto canale verde/bianco", + "edt_dev_spec_rgbw_calibration_limit": "Limite canale bianco", + "edt_dev_spec_rgbw_calibration_red": "Aspetto del canale rosso/bianco", "edt_dev_spec_serial_title": "Numero seriale", "edt_dev_spec_spipath_title": "Percorso SPI", "edt_dev_spec_sslHSTimeoutMax_title": "Timeout massimo handkshake streamer", "edt_dev_spec_sslHSTimeoutMin_title": "Timeout minimo handkshake streamer", - "edt_dev_spec_sslReadTimeout_title": "Timeout lettura streamer", + "edt_dev_spec_stream_protocol_title": "Protocollo di streaming", "edt_dev_spec_switchOffOnBlack_title": "Spegni o accendi il nero", "edt_dev_spec_switchOffOnbelowMinBrightness_title": "Spegni, sotto il minimo", + "edt_dev_spec_syncOverwrite_title": "Disabilita la sincronizzazione", + "edt_dev_spec_targetIpHost_expl": "Nome host (DNS/mDNS) o indirizzo IP (IPv4 o IPv6)", "edt_dev_spec_targetIpHost_title": "IP di destinazione/nome host", + "edt_dev_spec_targetIpHost_title_info": "Il nome host o l'indirizzo IP del dispositivo", "edt_dev_spec_targetIp_title": "IP di destinazione", "edt_dev_spec_transeffect_title": "Effetto Transizione", "edt_dev_spec_transistionTimeExtra_title": "Tempo extra di buio", @@ -573,9 +686,14 @@ "edt_eff_frequency": "Frequenza", "edt_eff_gif_header": "GIF", "edt_eff_gif_header_desc": "Questo effetto riproduce file .gif. Fornisci un semplice loop video come effetto.", + "edt_eff_grayscale": "Scala di grigi", "edt_eff_height": "Altezza", "edt_eff_huechange": "Cambiamento colore", "edt_eff_image": "file immagine", + "edt_eff_image_source": "Fonte immagine", + "edt_eff_image_source_file": "File locale", + "edt_eff_image_source_url": "URL", + "edt_eff_initial_blink": "Flash per attirare l'attenzione", "edt_eff_interval": "Intervallo", "edt_eff_knightrider_header": "Supercar", "edt_eff_knightrider_header_desc": "K.I.T.T. è tornato! Lo scanner frontale della nota automobile, questa volta non solo in rosso.", @@ -613,6 +731,7 @@ "edt_eff_reversedirection": "Inverti direzione", "edt_eff_rotationtime": "Tempo di rotazione", "edt_eff_saturation": "Saturazione", + "edt_eff_set_post_color": "Imposta il colore del post dopo l'allarme", "edt_eff_showseconds": "Mostra secondi", "edt_eff_sleeptime": "Tempo di riposo", "edt_eff_smooth_custom": "Abilita sfumatura", @@ -631,6 +750,7 @@ "edt_eff_traces_header_desc": "Necessita redesign", "edt_eff_trails_header": "Scie", "edt_eff_trails_header_desc": "Stelle colorate che cadono dall'alto verso il basso", + "edt_eff_url": "Indirizzo dell'immagine", "edt_eff_waves_header": "Onde", "edt_eff_waves_header_desc": "Onde di colore! Scegli i colori, tempo di rotazione, direzione e altro.", "edt_eff_whichleds": "Quali Leds", @@ -655,6 +775,10 @@ "edt_msg_error_disallow": "Il valore non deve essere di tipo $1", "edt_msg_error_disallow_union": "Il valore non deve essere di uno dei tipi forniti non ammessi", "edt_msg_error_enum": "Il valore deve essere uno tra quelli enumerati", + "edt_msg_error_hostname": "Il nome host ha il formato errato", + "edt_msg_error_invalid_epoch": "La data deve essere successiva al 1° gennaio 1970", + "edt_msg_error_ipv4": "Il valore deve essere un indirizzo IPv4 valido sotto forma di 4 numeri compresi tra 0 e 255, separati da punti", + "edt_msg_error_ipv6": "Il valore deve essere un indirizzo IPv6 valido", "edt_msg_error_maxItems": "Il valore deve contenere al massimo $1 elementi", "edt_msg_error_maxLength": "Il valore deve essere al massimo lungo $1 caratteri", "edt_msg_error_maxProperties": "L'oggetto deve avere al massimo $1 proprietà", @@ -675,6 +799,8 @@ "edt_msg_error_type": "Il valore deve essere di tipo $1", "edt_msg_error_type_union": "Il valore deve essere di uno dei tipi forniti", "edt_msg_error_uniqueItems": "Il vettore deve contenere elementi diversi", + "edt_msgcust_error_hostname_ip": "Non è un nome host valido né IPv4 né IPv6", + "edt_msgcust_error_hostname_ip4": "Non è un nome host valido né IPv4", "effectsconfigurator_button_conttest": "Test continuo", "effectsconfigurator_button_deleffect": "Cancella Effetto", "effectsconfigurator_button_editeffect": "Carica Effetto", @@ -686,7 +812,7 @@ "effectsconfigurator_label_effectname": "Nome Effetto", "effectsconfigurator_label_intro": "Crea nuovi effetti calibrati a tuo piacimento a partire dagli effetti base. A seconda dell'effetto sono disponibili opzioni come colore, velocità, direzione e altri.", "general_access_advanced": "Avanzate", - "general_access_default": "Default", + "general_access_default": "Predefinito", "general_access_expert": "Esperto", "general_btn_back": "Indietro", "general_btn_cancel": "Annulla", @@ -699,6 +825,7 @@ "general_btn_off": "Off", "general_btn_ok": "OK", "general_btn_on": "On", + "general_btn_overwrite": "Sovrascrivi", "general_btn_rename": "Rinomina", "general_btn_restarthyperion": "Riavvia Hyperion", "general_btn_save": "Salva", @@ -718,7 +845,7 @@ "general_comp_FORWARDER": "Forwarder", "general_comp_GRABBER": "Cattura di Sistema", "general_comp_LEDDEVICE": "Dispositivo LED", - "general_comp_PROTOSERVER": "Server Protocol Buffers", + "general_comp_PROTOSERVER": "Buffer del protocollo del server", "general_comp_SMOOTHING": "Sfumatura", "general_comp_V4L": "Cattura USB", "general_country_cn": "Cina", @@ -730,14 +857,23 @@ "general_country_ru": "Russia", "general_country_uk": "Regno Unito", "general_country_us": "Stati Uniti", + "general_disabled": "disabilitato", + "general_enabled": "abilitato", + "general_speech_ca": "Catalano", "general_speech_cs": "Czech", + "general_speech_da": "Danimarca", "general_speech_de": "Tedesco", + "general_speech_el": "Greco", "general_speech_en": "Inglese", "general_speech_es": "Spagnolo", "general_speech_fr": "Francese", + "general_speech_hu": "Ungheria", "general_speech_it": "Italiano", + "general_speech_ja": "Giapponese", + "general_speech_nb": "Norvegese (Bokmål)", "general_speech_nl": "Olandese", "general_speech_pl": "Polacco", + "general_speech_pt": "Portoghese", "general_speech_ro": "Rumeno", "general_speech_ru": "Russo", "general_speech_sv": "Svedese", @@ -757,6 +893,10 @@ "infoDialog_import_confirm_title": "Conferma import", "infoDialog_import_hyperror_text": "Il file di configurazione selezionato \"$1\" non può essere importato. Non è compatibile con Hyperion 2.0 e superiori!", "infoDialog_import_jsonerror_text": "Il file di configurazione selezionato \"$1\" non è un file .json o è corrotto. Messaggio di errore: ($2)", + "infoDialog_password_current_text": "Password attuale", + "infoDialog_password_minimum_length": "Le password devono contenere almeno 8 caratteri.", + "infoDialog_password_new_text": "Nuova Password", + "infoDialog_username_text": "Nome utente", "infoDialog_wizrgb_text": "L'ordine dei Byte RGB è già impostato correttamente.", "infoDialog_writeconf_error_text": "Salvataggio della configurazione fallito.", "infoDialog_writeimage_error_text": "Il file selezionato \"$1\" non è un immagine o è corrotto! Seleziona un altro file.", @@ -776,6 +916,7 @@ "main_ledsim_btn_togglelednumber": "Numeri LED", "main_ledsim_btn_toggleleds": "Mostra LEDs", "main_ledsim_btn_togglelivevideo": "Video live", + "main_ledsim_btn_togglesigdetect": "Area di rilevamento del segnale", "main_ledsim_text": "Visualizzazione live dei colori dei led e, opzionalmente, lo stream video dal dispositivo di cattura.", "main_ledsim_title": "Visualizzazione LED", "main_menu_about_token": "Info su Hyperion", @@ -787,6 +928,7 @@ "main_menu_general_conf_token": "Generale", "main_menu_grabber_conf_token": "Hardware di cattura", "main_menu_input_selection_token": "Selezione Input", + "main_menu_instcapture_conf_token": "Fonti", "main_menu_leds_conf_token": "Hardware LED", "main_menu_logging_token": "Log", "main_menu_network_conf_token": "Servizi di Rete", @@ -886,6 +1028,7 @@ "wiz_cc_testintrok": "Premi il bottone qui sotto per iniziare un test video.", "wiz_cc_testintrowok": "Guarda il link di seguito per il download dei video di test", "wiz_cc_title": "Assistente calibrazione colore", + "wiz_cc_try_connect": "Connessione...", "wiz_cololight_desc2": "Ora scegli quali Cololights dovrebbero essere aggiunti. Per identificare le singole luci, premere il pulsante a destra.", "wiz_cololight_intro1": "Questa procedura guidata configura Hyperion per il sistema Cololight. Le caratteristiche sono il rilevamento automatico di Cololight e la regolazione automatica delle impostazioni di Hyperion! In breve: bastano pochi clic e il gioco è fatto!
    Nota: in caso di una Strip Cololight, potrebbe essere necessario correggere manualmente il numero e il layout dei LED", "wiz_cololight_noprops": "Impossibile ottenere le proprietà del dispositivo: definire manualmente il numero dei LED", @@ -921,6 +1064,7 @@ "wiz_hue_username": "ID Utente", "wiz_identify": "Identifica", "wiz_identify_light": "Identifica $1", + "wiz_identify_tip": "Identifica il dispositivo configurato accendendolo", "wiz_ids_disabled": "Disattivato", "wiz_ids_entire": "Immagine intera", "wiz_noLights": "Nessun $1 trovato! Per favore connetti le luci alla rete o configurale manualmente", From 6251b58ff96a758b2beac9d2d1e31562b8ac8e6e Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Fri, 17 Feb 2023 15:19:06 +0100 Subject: [PATCH 12/23] Update pl.json (POEditor.com) --- assets/webconfig/i18n/pl.json | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/assets/webconfig/i18n/pl.json b/assets/webconfig/i18n/pl.json index f8d771c4..30613155 100644 --- a/assets/webconfig/i18n/pl.json +++ b/assets/webconfig/i18n/pl.json @@ -10,6 +10,9 @@ "InfoDialog_nowrite_foottext": "Interfejs WebUI zostanie odblokowany automatycznie po rozwiązaniu problemu!", "InfoDialog_nowrite_text": "Hyperion nie może zapisać do aktualnie załadowanego pliku konfiguracyjnego. zmień uprawnienia zapisu plików, aby kontynuować.", "InfoDialog_nowrite_title": "Nie masz uprawnień do zapisu!", + "InfoDialog_systemRestart_title": "Restart", + "InfoDialog_systemResume_title": "Wznów", + "InfoDialog_systemSuspend_title": "Zawieś", "about_3rd_party_licenses": "Licencje stron trzecich", "about_3rd_party_licenses_error": "Mieliśmy problem z gromadzeniem informacji o licencjach stron trzecich z sieci.
    Kliknij ten link do zasobu GitHub.", "about_build": "Build:", @@ -254,6 +257,8 @@ "edt_conf_color_blue_title": "Niebieski", "edt_conf_color_brightnessComp_expl": "Kompensuje różnice jasności między czerwonym, zielonym, niebieskim, cyjanowym, purpurowym, żółtym i białym. 100 oznacza pełną kompensację, 0 oznacza brak kompensacji", "edt_conf_color_brightnessComp_title": "Kompensacja jasności", + "edt_conf_color_brightnessGain_expl": "Reguluje jasność kolorów. 1.0 oznacza brak zmian, powyżej 1.0 zwiększa jasność, poniżej 1.0 zmniejsza jasność.", + "edt_conf_color_brightnessGain_title": "Wzmocnienie jasności", "edt_conf_color_brightness_expl": "Ustaw jasność LEDów", "edt_conf_color_brightness_title": "Jasność", "edt_conf_color_channelAdjustment_header_expl": "Utwórz profile kolorów, które można przypisać do określonego profilu. Dostosuj kolor, gamma, jasność, kompensację i więcej", @@ -280,6 +285,8 @@ "edt_conf_color_magenta_title": "Magenta", "edt_conf_color_red_expl": "Skalibrowana wartość koloru czerwonego", "edt_conf_color_red_title": "Czerwony", + "edt_conf_color_saturationGain_expl": "Reguluje nasycenie kolorów. 1.0 oznacza brak zmian, powyżej 1.0 zwiększa nasycenie, poniżej 1.0 zmniejsza nasycenie.", + "edt_conf_color_saturationGain_title": "Wzmocnienie nasycenia", "edt_conf_color_white_expl": "Skalibrowana wartość koloru białego.", "edt_conf_color_white_title": "Biały", "edt_conf_color_yellow_expl": "Skalibrowana wartość koloru żółtego", @@ -393,7 +400,7 @@ "edt_conf_general_priority_title": "Kanał priorytetowy", "edt_conf_grabber_discovered_expl": "Wybierz wykryte urządzenie do przechwytywania", "edt_conf_grabber_discovered_none": "Nie wykryto urządzenia do przechwytywania", - "edt_conf_grabber_discovered_title": "Wykryto urządzenie", + "edt_conf_grabber_discovered_title": "Wykryte urządzenia", "edt_conf_grabber_discovered_title_info": "Wybierz wykryte urządzenie do przechwytywania", "edt_conf_grabber_discovery_inprogress": "Wykrywanie w toku", "edt_conf_instC_screen_grabber_device_expl": "Używane urządzenie do przechwytywania ekranu", @@ -435,8 +442,6 @@ "edt_conf_smooth_heading_title": "Wygładzanie", "edt_conf_smooth_interpolationRate_expl": "Szybkość obliczania wygładzanych ramek pośrednich.", "edt_conf_smooth_interpolationRate_title": "Współczynnik interpolacji", - "edt_conf_smooth_outputRate_expl": "Prędkość wyjściowa do twojego kontrolera led.", - "edt_conf_smooth_outputRate_title": "Szybkość wyjściowa", "edt_conf_smooth_time_ms_expl": "Jak długo wygładzanie powinno zbierać obrazy?", "edt_conf_smooth_time_ms_title": "Czas", "edt_conf_smooth_type_expl": "Rodzaj wygładzania.", @@ -539,9 +544,13 @@ "edt_dev_spec_FCsetConfig_title": "Ustaw konfigurację zanikania", "edt_dev_spec_LBap102Mode_title": "Tryb \"LightBerry APA102\"", "edt_dev_spec_PBFiFo_title": "Pi-Blaster FiFo", + "edt_dev_spec_ada_mode_title": "Adalight - Standard", + "edt_dev_spec_awa_mode_title": "HyperSerial - High speed", "edt_dev_spec_baudrate_title": "Prędkość (Baudrate)", "edt_dev_spec_blackLightsTimeout_title": "Limit czasu wykrywania sygnału na czarno", "edt_dev_spec_brightnessFactor_title": "Współczynnik jasności", + "edt_dev_spec_brightnessMax_title": "Maksymalna jasność", + "edt_dev_spec_brightnessMin_title": "Minimalna jasność", "edt_dev_spec_brightnessOverwrite_title": "Nadpisz jasność", "edt_dev_spec_brightnessThreshold_title": "Minimalna jasność wykrywania sygnału", "edt_dev_spec_brightness_title": "Jasność", @@ -601,6 +610,11 @@ "edt_dev_spec_razer_device_title": "Urządzenie Razer Chroma", "edt_dev_spec_restoreOriginalState_title": "Przywróć oryginalny stan świateł, gdy są wyłączone", "edt_dev_spec_restoreOriginalState_title_info": "Przywróć pierwotny stan urządzenia, gdy jest ono wyłączone", + "edt_dev_spec_rgbw_calibration_blue": "Wygląd kanału niebieskiego/białego", + "edt_dev_spec_rgbw_calibration_enable": "Kalibracja kanału bieli (tylko RGBW)", + "edt_dev_spec_rgbw_calibration_green": "Wygląd kanału zielonego/białego", + "edt_dev_spec_rgbw_calibration_limit": "Limit kanału białego", + "edt_dev_spec_rgbw_calibration_red": "Wygląd kanału czerwonego/białego", "edt_dev_spec_serial_title": "Numer seryjny", "edt_dev_spec_spipath_title": "SPI path", "edt_dev_spec_sslHSTimeoutMax_title": "Maksymalny limit czasu uzgadniania streamera", @@ -848,9 +862,11 @@ "general_country_us": "Stany Zjednoczone (United States)", "general_disabled": "wyłączony", "general_enabled": "włączony", + "general_speech_ca": "Kataloński", "general_speech_cs": "Czeski (Czech)", "general_speech_da": "Duński", "general_speech_de": "Niemiecki (German)", + "general_speech_el": "Grecki", "general_speech_en": "Angielski (English)", "general_speech_es": "Hiszpański (Spanish)", "general_speech_fr": "Francuski (French)", From f062aa71c5d9bb878dbe20ae68f8bdcedf5ca7ba Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Fri, 17 Feb 2023 15:19:07 +0100 Subject: [PATCH 13/23] Update ru.json (POEditor.com) --- assets/webconfig/i18n/ru.json | 64 ++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/assets/webconfig/i18n/ru.json b/assets/webconfig/i18n/ru.json index aaf891d2..0b959039 100644 --- a/assets/webconfig/i18n/ru.json +++ b/assets/webconfig/i18n/ru.json @@ -10,6 +10,9 @@ "InfoDialog_nowrite_foottext": "Веб-интерфейс будет разблокирован автоматически после того, как вы решите проблему!", "InfoDialog_nowrite_text": "Hyperion не может выполнять запись в ваш текущий загруженный файл конфигурации. Чтобы продолжить, восстановите права доступа к файлу.", "InfoDialog_nowrite_title": "Ошибка разрешения записи!", + "InfoDialog_systemRestart_title": "Перезапустить", + "InfoDialog_systemResume_title": "Продолжить", + "InfoDialog_systemSuspend_title": "Отключить", "about_3rd_party_licenses": "Сторонние лицензии", "about_3rd_party_licenses_error": "У нас возникли проблемы со сбором информации о сторонних лицензиях из Интернета.
    Перейдите по этой ссылке на GitHub.", "about_build": "Сборка", @@ -51,6 +54,8 @@ "conf_leds_contr_label_contrtype": "Тип контроллера:", "conf_leds_device_info_log": "Если ваши светодиоды не работают, проверьте здесь наличие ошибок:", "conf_leds_device_intro": "Hyperion поддерживает множество контроллеров для передачи данных на целевое устройство. Выберите светодиодный контроллер из списка и настройте его. Мы выбрали лучшие настройки по умолчанию для каждого устройства.", + "conf_leds_error_get_properties_text": "Ошибка при работе с устройством. Проверьте настройки.", + "conf_leds_error_get_properties_title": "Настройки устройства", "conf_leds_error_hwled_gt_layout": "Количество светодиодов оборудования ($1) больше, чем количество светодиодов, настроенных с помощью макета ($2),
    $3 {{plural:$3|Светодиод|Светодиоды}} останутся черными, если вы продолжите.", "conf_leds_error_hwled_gt_maxled": "Количество светодиодов оборудования ($1) превышает максимальное количество светодиодов, поддерживаемое устройством ($2).
    Счетчик аппаратных светодиодов установлен на ($3).", "conf_leds_error_hwled_lt_layout": "Количество светодиодных индикаторов оборудования ($1) меньше, чем количество светодиодов, настроенных с помощью макета ($2).
    Количество светодиодов, настроенных в макете, не должно превышать количество доступных светодиодов", @@ -62,6 +67,7 @@ "conf_leds_layout_blacklist_start_title": "Начальный светодиод", "conf_leds_layout_blacklistleds_title": "Светодиоды из черного списка", "conf_leds_layout_btn_checklist": "Показать сверку", + "conf_leds_layout_btn_keystone": "Коррекция трапеции", "conf_leds_layout_button_savelay": "Сохранить раскладку", "conf_leds_layout_button_updsim": "Просмотр обновления", "conf_leds_layout_checkp1": "Черный светодиод — это ваш первый светодиод, первый светодиод — это точка, в которую вы вводите сигнал данных.", @@ -81,6 +87,16 @@ "conf_leds_layout_cl_leftbottom": "Левый 50% - 100% снизу", "conf_leds_layout_cl_leftmiddle": "Левый 25% - 75% посередине", "conf_leds_layout_cl_lefttop": "Слева 0% - 50% сверху", + "conf_leds_layout_cl_lightPosBottomLeft11": "Низ: 75 - 100% слева", + "conf_leds_layout_cl_lightPosBottomLeft112": "Низ: 0 - 50% слева", + "conf_leds_layout_cl_lightPosBottomLeft12": "Низ: 25 - 50% слева", + "conf_leds_layout_cl_lightPosBottomLeft121": "Низ: 50 - 100% слева", + "conf_leds_layout_cl_lightPosBottomLeft14": "Низ: 0 - 25% слева", + "conf_leds_layout_cl_lightPosBottomLeft34": "Низ: 50 - 75% слева", + "conf_leds_layout_cl_lightPosBottomLeftNewMid": "Низ: 25 - 75% слева", + "conf_leds_layout_cl_lightPosTopLeft112": "Верх: 0 - 50% слева", + "conf_leds_layout_cl_lightPosTopLeft121": "Верх: 50 - 100% слева", + "conf_leds_layout_cl_lightPosTopLeftNewMid": "Верх: 25 - 75% слева", "conf_leds_layout_cl_overlap": "Нахлёст", "conf_leds_layout_cl_reversdir": "Обратное направление", "conf_leds_layout_cl_right": "Справа", @@ -95,6 +111,7 @@ "conf_leds_layout_generatedconf": "Сгенерированная/Текущая Конфигурация LED", "conf_leds_layout_intro": "Вам также нужна LED-раскладка, которая отражает положение ваших светодиодов. Классическая раскладка обычно представляет ТВ-рамку, но поддерживается и LED-матрица (LED-стена). Вид на этой раскладке ВСЕГДА СПЕРЕДИ вашего ТВ.", "conf_leds_layout_ma_cabling": "Подключение", + "conf_leds_layout_ma_direction": "Направление", "conf_leds_layout_ma_horiz": "Горизонтально", "conf_leds_layout_ma_optbottomleft": "Низ слева", "conf_leds_layout_ma_optbottomright": "Низ справа", @@ -181,6 +198,7 @@ "dashboard_infobox_label_instance": "Пример:", "dashboard_infobox_label_latesthyp": "Последняя версия Hyperion:", "dashboard_infobox_label_platform": "Платформа:", + "dashboard_infobox_label_port_boblight": "Boblight сервер:", "dashboard_infobox_label_port_flat": "Плоский буфер:", "dashboard_infobox_label_port_json": "JSON-сервер", "dashboard_infobox_label_port_proto": "Протобуфер:", @@ -213,6 +231,7 @@ "edt_append_percent_v": "% верт.", "edt_append_pixel": "Пиксель", "edt_append_s": "сек", + "edt_append_sdegree": "с/градус", "edt_conf_bb_blurRemoveCnt_expl": "Количество пикселей, которые удаляются с обнаруженной границы, чтобы убрать размытие.", "edt_conf_bb_blurRemoveCnt_title": "Размытие пикселя", "edt_conf_bb_borderFrameCnt_expl": "Количество кадров до установки согласованной обнаруженной границы.", @@ -238,6 +257,8 @@ "edt_conf_color_blue_title": "Синий", "edt_conf_color_brightnessComp_expl": "Компенсирует разницу в яркости между красным, зеленым, синим, голубым, пурпурным, жёлтым и белым. 100 означает полную компенсацию, 0 без компенсации", "edt_conf_color_brightnessComp_title": "Компенсация яркости", + "edt_conf_color_brightnessGain_expl": "Настройка яркости. 1.0 - без коррекции, больше 1.0 - повышает, а меньше 1.0 уменьшает яркость.", + "edt_conf_color_brightnessGain_title": "Яркость", "edt_conf_color_brightness_expl": "установить общую яркость светодиодов", "edt_conf_color_brightness_title": "Яркость", "edt_conf_color_channelAdjustment_header_expl": "Создавайте цветовые профили, которые можно назначить конкретному компоненту. Отрегулируйте цвет, гамму, яркость, компенсацию и многое другое.", @@ -264,6 +285,8 @@ "edt_conf_color_magenta_title": "Пурпурный", "edt_conf_color_red_expl": "Откалиброванное значение красного.", "edt_conf_color_red_title": "Красный", + "edt_conf_color_saturationGain_expl": "Настройка насыщенности цветов. 1.0 - без коррекции, больше 1.0 - повышает, а меньше 1.0 уменьшает насыщенность.", + "edt_conf_color_saturationGain_title": "Насыщенность", "edt_conf_color_white_expl": "Калиброванное значение белого.", "edt_conf_color_white_title": "Белый", "edt_conf_color_yellow_expl": "Откалиброванное значение жёлтого цвета.", @@ -319,6 +342,8 @@ "edt_conf_enum_top_down": "Сверху вниз", "edt_conf_enum_transeffect_smooth": "Сглаживание", "edt_conf_enum_transeffect_sudden": "Внезапный", + "edt_conf_enum_udp_ddp": "DDP", + "edt_conf_enum_udp_raw": "RAW", "edt_conf_enum_unicolor_mean": "Одноцветный", "edt_conf_fbs_heading_title": "Сервер Flatbuffers", "edt_conf_fbs_timeout_expl": "Если данные за указанный период не поступают, компонент будет (мягко) отключён.", @@ -347,11 +372,18 @@ "edt_conf_fge_type_title": "Тип", "edt_conf_fw_flat_expl": "Одна цель плоского буфера на строку. Содержит IP: ПОРТ (Пример: 127.0.0.1:19401)", "edt_conf_fw_flat_itemtitle": "цель плоского буфера", + "edt_conf_fw_flat_services_discovered_expl": "Обнаруженные Hyperion сервера с flatbuffer сервисами", + "edt_conf_fw_flat_services_discovered_title": "Найденные Flatbuffer цели", "edt_conf_fw_flat_title": "Список целей плоского буфера", "edt_conf_fw_heading_title": "Экспедитор", "edt_conf_fw_json_expl": "Одна json цель на строку. Содержит IP:PORT (Пример: 127.0.0.1:19446)", - "edt_conf_fw_json_itemtitle": "Цель Json", + "edt_conf_fw_json_itemtitle": "Цель JSON", + "edt_conf_fw_json_services_discovered_expl": "Обнаруженные Hyperion сервера с JSON-API сервисами", + "edt_conf_fw_json_services_discovered_title": "Найденные JSON цели", "edt_conf_fw_json_title": "Список целей json", + "edt_conf_fw_remote_service_discovered_none": "Не найдено никаких сервисов", + "edt_conf_fw_service_name_expl": "Название провайдера", + "edt_conf_fw_service_name_title": "Название сервиса", "edt_conf_gen_configVersion_title": "Версия конфигурации", "edt_conf_gen_heading_title": "Общие настройки", "edt_conf_gen_name_expl": "Пользовательское имя, которое используется для обнаружения Hyperion. (Полезно с более чем одним экземпляром Hyperion)", @@ -410,8 +442,6 @@ "edt_conf_smooth_heading_title": "Сглаживание", "edt_conf_smooth_interpolationRate_expl": "Скорость расчета плавных промежуточных кадров.", "edt_conf_smooth_interpolationRate_title": "Скорость интерполяции", - "edt_conf_smooth_outputRate_expl": "Скорость вывода на ваш светодиодный контроллер.", - "edt_conf_smooth_outputRate_title": "Выходная скорость", "edt_conf_smooth_time_ms_expl": "Как долго сглаживание должно собирать картинки?", "edt_conf_smooth_time_ms_title": "Время", "edt_conf_smooth_type_expl": "Тип сглаживания.", @@ -497,8 +527,13 @@ "edt_dev_enum_subtract_minimum": "Уменьшить минимум", "edt_dev_enum_white_off": "Выключить белый ", "edt_dev_general_autostart_title": "Автозапуск", + "edt_dev_general_autostart_title_info": "Включать LED устройство при загрузке или нет", "edt_dev_general_colorOrder_title": "Порядок байтов RGB", "edt_dev_general_colorOrder_title_info": "Порядок цвета устройства", + "edt_dev_general_enableAttemptsInterval_title": "Задержка", + "edt_dev_general_enableAttemptsInterval_title_info": "Задержка между попытками подключения", + "edt_dev_general_enableAttempts_title": "Количество попыток", + "edt_dev_general_enableAttempts_title_info": "Количество попыток подключения к устройству до аварийного состояния.", "edt_dev_general_hardwareLedCount_title": "Количество светодиодных индикаторов оборудования", "edt_dev_general_hardwareLedCount_title_info": "Количество физических светодиодов, доступных для данного устройства", "edt_dev_general_heading_title": "Общие настройки", @@ -509,12 +544,17 @@ "edt_dev_spec_FCsetConfig_title": "Установить конфигурацию fadecandy", "edt_dev_spec_LBap102Mode_title": "Режим LightBerry APA102", "edt_dev_spec_PBFiFo_title": "Pi-Blaster FiFo", + "edt_dev_spec_ada_mode_title": "Adalight - Стандартно", + "edt_dev_spec_awa_mode_title": "HyperSerial - Высокая скорость", "edt_dev_spec_baudrate_title": "Скорость", "edt_dev_spec_blackLightsTimeout_title": "Тайм-аут обнаружения сигнала на черном", "edt_dev_spec_brightnessFactor_title": "Фактор яркости", + "edt_dev_spec_brightnessMax_title": "Максимальная яркость", + "edt_dev_spec_brightnessMin_title": "Минимальная яркость", "edt_dev_spec_brightnessOverwrite_title": "Перезаписать яркость", "edt_dev_spec_brightnessThreshold_title": "Минимальная яркость обнаружения сигнала", "edt_dev_spec_brightness_title": "Яркость", + "edt_dev_spec_candyGamma_title": "'Candy' режим (двойная гамма коррекция)", "edt_dev_spec_chanperfixture_title": "Каналов на прибор", "edt_dev_spec_cid_title": "CID", "edt_dev_spec_clientKey_title": "Клиентский ключ", @@ -530,6 +570,7 @@ "edt_dev_spec_dmaNumber_title": "Канал DMA", "edt_dev_spec_gamma_title": "Гамма", "edt_dev_spec_globalBrightnessControlMaxLevel_title": "Максимальный текущий уровень", + "edt_dev_spec_globalBrightnessControlThreshold_title": "Адаптивный контроль тока", "edt_dev_spec_gpioBcm_title": "Вывод GPIO", "edt_dev_spec_gpioMap_title": "Отображение GPIO", "edt_dev_spec_gpioNumber_title": "Номер GPIO", @@ -553,6 +594,8 @@ "edt_dev_spec_networkDeviceName_title": "Сетевое имя устройства", "edt_dev_spec_networkDevicePort_title": "Порт", "edt_dev_spec_numberOfLeds_title": "Количество светодиодов", + "edt_dev_spec_onBlackTimeToPowerOff": "Время до отключения подсветки если сработала проверка уровня черного", + "edt_dev_spec_onBlackTimeToPowerOn": "Время до включения подсветки после восстановления сигнала", "edt_dev_spec_orbIds_title": "ID сфер", "edt_dev_spec_order_left_right_title": "2.", "edt_dev_spec_order_top_down_title": "1.", @@ -560,19 +603,27 @@ "edt_dev_spec_panel_start_position": "Стартовая панель [0-макс панели]", "edt_dev_spec_panelorganisation_title": "Последовательность нумерации панелей", "edt_dev_spec_pid_title": "PID", + "edt_dev_spec_port_expl": "Сервисный порт [1-65535]", "edt_dev_spec_port_title": "Порт", "edt_dev_spec_printTimeStamp_title": "Добавить отметку времени", "edt_dev_spec_pwmChannel_title": "Канал ШИМ (PWM)", "edt_dev_spec_razer_device_title": "Устройство Razer Chroma", "edt_dev_spec_restoreOriginalState_title": "Восстановить состояние огней", "edt_dev_spec_restoreOriginalState_title_info": "Восстановить исходное состояние устройства, когда устройство отключено", + "edt_dev_spec_rgbw_calibration_blue": "Соотношение Синего/Белого каналов", + "edt_dev_spec_rgbw_calibration_enable": "Калибровка белого (только для RGBW)", + "edt_dev_spec_rgbw_calibration_green": "Соотношение Зеленого/Белого каналов", + "edt_dev_spec_rgbw_calibration_limit": "Ограничение белого", + "edt_dev_spec_rgbw_calibration_red": "Соотношение Красного/Белого каналов", "edt_dev_spec_serial_title": "Серийный номер", "edt_dev_spec_spipath_title": "Устройство SPI", "edt_dev_spec_sslHSTimeoutMax_title": "Максимальное время ожидания подтверждения стримером", "edt_dev_spec_sslHSTimeoutMin_title": "Минимальное время ожидания подтверждения стримером", + "edt_dev_spec_stream_protocol_title": "Протокол", "edt_dev_spec_switchOffOnBlack_title": "Выключить черный", "edt_dev_spec_switchOffOnbelowMinBrightness_title": "Отключение, ниже минимального", "edt_dev_spec_syncOverwrite_title": "Выключить синхронизацию", + "edt_dev_spec_targetIpHost_expl": "Имя хоста (DNS/mDNS) или IP адрес (IPv4 or IPv6)", "edt_dev_spec_targetIpHost_title": "Целевое имя хоста/IP-адрес", "edt_dev_spec_targetIpHost_title_info": "Имя хоста или IP-адрес устройства", "edt_dev_spec_targetIp_title": "Целевой IP-адрес", @@ -811,14 +862,17 @@ "general_country_us": "Соединённые Штаты Америки", "general_disabled": "отключено", "general_enabled": "включено", + "general_speech_ca": "Каталонский", "general_speech_cs": "Чешский", - "general_speech_da": "Danish", + "general_speech_da": "Датский", "general_speech_de": "Немецкий", + "general_speech_el": "Греческий", "general_speech_en": "Английский", "general_speech_es": "Испанский", "general_speech_fr": "французский", - "general_speech_hu": "Hungarian", + "general_speech_hu": "Венгерский", "general_speech_it": "Итальянский", + "general_speech_ja": "Японский", "general_speech_nb": "норвежский", "general_speech_nl": "Dutch", "general_speech_pl": "Polish", From f4391a512b6cb745f57d0fd2d63d0f7d2cef208b Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Fri, 17 Feb 2023 15:19:09 +0100 Subject: [PATCH 14/23] Update es.json (POEditor.com) --- assets/webconfig/i18n/es.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/assets/webconfig/i18n/es.json b/assets/webconfig/i18n/es.json index 2e751dbf..9fe26371 100644 --- a/assets/webconfig/i18n/es.json +++ b/assets/webconfig/i18n/es.json @@ -10,6 +10,9 @@ "InfoDialog_nowrite_foottext": "¡El WebUI se desbloqueará automáticamente después de haber resuelto el problema!", "InfoDialog_nowrite_text": "Hyperion no puede escribir en el archivo de configuración cargado actual. Repara los permisos de archivo para continuar.", "InfoDialog_nowrite_title": "¡Error de permiso de escritura!", + "InfoDialog_systemRestart_title": "Reiniciar", + "InfoDialog_systemResume_title": "Retomar", + "InfoDialog_systemSuspend_title": "Suspender", "about_3rd_party_licenses": "Licencias de terceros", "about_3rd_party_licenses_error": "Tuvimos problemas para recopilar información de licencias de terceros a través de la Web.
    Por favor, sigue este enlace para acceder al Recurso GitHub.", "about_build": "Build", @@ -55,7 +58,9 @@ "conf_leds_error_get_properties_title": "Propiedades del dispositivo", "conf_leds_error_hwled_gt_layout": "El recuento de LEDs por hardware ($1) es mayor que los LEDs configurados a través de la disposición ($2),
    $3 {{plural:$1|LED|LEDs}} se quedará en negro si continúas.", "conf_leds_error_hwled_gt_maxled": "El recuento de LEDs por hardware ($1) es mayor que el número máximo de LEDs soportado por el dispositivo ($2).
    El recuento de LEDs de hardware se establece en ($3).", + "conf_leds_error_hwled_gt_maxled_protocol": "El recuento de LEDs por hardware ($1) es mayor que el número máximo de LEDs soportado por el protocolo de streaming ($2).
    El protocolo de streaming cambiará a ($3).", "conf_leds_error_hwled_lt_layout": "El recuento de LEDs por hardware ($1) es menor que los LEDs configurados a través del diseño ($2).
    El número de LEDs configurados en el esquema no debe superar los LEDs disponibles.", + "conf_leds_error_wled_segment_missing": "El segmento configurado actualmente ($1) no está configurado en tu dispositivo WLED.
    ¡Es posible que tengas que comprobar la configuración del WLED!
    La página de configuración representa la configuración actual del WLED.", "conf_leds_info_ws281x": "Hyperion debe ejecutarse con privilegios 'root' para este tipo de controlador.", "conf_leds_layout_advanced": "Ajustes Avanzados", "conf_leds_layout_blacklist_num_title": "Número de LEDs", @@ -439,8 +444,6 @@ "edt_conf_smooth_heading_title": "Suavizado", "edt_conf_smooth_interpolationRate_expl": "Velocidad de cálculo de los fotogramas intermedios suaves.", "edt_conf_smooth_interpolationRate_title": "Tasa de interpolación", - "edt_conf_smooth_outputRate_expl": "La velocidad de salida a tu controlador de leds.", - "edt_conf_smooth_outputRate_title": "Tasa de salida", "edt_conf_smooth_time_ms_expl": "¿Cuánto tiempo debe recoger las imágenes el suavizado?", "edt_conf_smooth_time_ms_title": "Tiempo", "edt_conf_smooth_type_expl": "Tipo de suavizado", @@ -614,10 +617,17 @@ "edt_dev_spec_rgbw_calibration_green": "Aspecto del canal Verde/Blanco", "edt_dev_spec_rgbw_calibration_limit": "Límite del canal blanco", "edt_dev_spec_rgbw_calibration_red": "Aspecto del canal Rojo/Blanco", + "edt_dev_spec_segmentId_title": "Segmento-ID", + "edt_dev_spec_segmentsOverlapValidation_error": "¡Corrige la configuración de WLED! El segmento no debe solaparse con {{plural:$1| segmento|segmentos}}: \"$2\".", + "edt_dev_spec_segmentsSwitchOffOthers_title": "Desconectar otros segmentos", + "edt_dev_spec_segments_disabled_title": "Transmisión de segmentos desactivada en WLED.", + "edt_dev_spec_segments_title": "Transmisión a segmento", "edt_dev_spec_serial_title": "Número de serie", "edt_dev_spec_spipath_title": "Dispositivo SPI", "edt_dev_spec_sslHSTimeoutMax_title": "Máximo tiempo de espera para el contacto con el Streamer", "edt_dev_spec_sslHSTimeoutMin_title": "Tiempo mínimo de espera para el contacto con el Streamer", + "edt_dev_spec_stayOnAfterStreaming_title": "Permanecer encendido después del streaming", + "edt_dev_spec_stayOnAfterStreaming_title_info": "El dispositivo permanecerá encendido después de transmitir o restaurar el estado.", "edt_dev_spec_stream_protocol_title": "Protocolo de streaming", "edt_dev_spec_switchOffOnBlack_title": "Apagar en negro", "edt_dev_spec_switchOffOnbelowMinBrightness_title": "Apagado, por debajo del mínimo", @@ -861,9 +871,11 @@ "general_country_us": "Estados Unidos", "general_disabled": "Deshabilitado", "general_enabled": "Habilitado", + "general_speech_ca": "Catalán", "general_speech_cs": "Czech", "general_speech_da": "Danés", "general_speech_de": "Alemán", + "general_speech_el": "Griego", "general_speech_en": "Inglés", "general_speech_es": "Español", "general_speech_fr": "Francés", From 093361d3e4cc8a4d1de182541cf613057587fd45 Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Fri, 17 Feb 2023 15:19:10 +0100 Subject: [PATCH 15/23] Update sv.json (POEditor.com) --- assets/webconfig/i18n/sv.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/assets/webconfig/i18n/sv.json b/assets/webconfig/i18n/sv.json index d9c41718..9f242cf7 100644 --- a/assets/webconfig/i18n/sv.json +++ b/assets/webconfig/i18n/sv.json @@ -10,6 +10,9 @@ "InfoDialog_nowrite_foottext": "Webbkonfigurationen kommer att släppas igen automatiskt så snart problemet har åtgärdats!", "InfoDialog_nowrite_text": "Hyperion har inte skrivbehörighet till den för närvarande inlästa konfigurationen. Korrigera filbehörigheterna för att fortsätta.", "InfoDialog_nowrite_title": "Skrivåtkomstfel!", + "InfoDialog_systemRestart_title": "Starta om", + "InfoDialog_systemResume_title": "Återuppta", + "InfoDialog_systemSuspend_title": "Stäng av", "about_3rd_party_licenses": "Tredjepartslicenser", "about_3rd_party_licenses_error": "Vi hade problem med att ladda tredjepartslicenserna från internet.
    Klicka här för att komma åt filen på GitHub.", "about_build": "Bygge", @@ -55,7 +58,9 @@ "conf_leds_error_get_properties_title": "Enhetsegenskaper", "conf_leds_error_hwled_gt_layout": "Antalet givna hårdvarulysdioder ($1) är större än antalet definierade i LED-layouten ($2), $3 {{plural:$3|LEDs kommer|LEDs kommer att vara}} förblir svarta.", "conf_leds_error_hwled_gt_maxled": "Antalet LED-lampor för hårdvara ($1) är större än det maximala antalet lysdioder som stöds av enheten ($2).
    Antalet LED-lampor för hårdvara är inställt på ($3).", + "conf_leds_error_hwled_gt_maxled_protocol": "Antalet LED-lampor för hårdvara ($1) är större än det maximala antalet lysdioder som stöds av streamingprotokollet ($2).
    Strömningsprotokollet kommer att ändras till ($3).", "conf_leds_error_hwled_lt_layout": "Antalet givna hårdvarulysdioder ($1) är mindre än antalet definierade i LED-layouten ($2).
    I LED-layouten får inte fler lysdioder konfigureras än vad som är tillgängligt.", + "conf_leds_error_wled_segment_missing": "Det för närvarande konfigurerade segmentet ($1) är inte konfigurerat på din WLED-enhet.
    Du kan behöva kontrollera WLED-konfigurationen!
    Konfigurationssidan representerar den aktuella WLED-inställningen.", "conf_leds_info_ws281x": "Hyperion måste köras med 'root'-privilegier för denna styrenhetstyp!", "conf_leds_layout_advanced": "Utökade alternativ", "conf_leds_layout_blacklist_num_title": "Antal lysdioder", @@ -439,8 +444,6 @@ "edt_conf_smooth_heading_title": "Utjämning", "edt_conf_smooth_interpolationRate_expl": "Frekvens i vilken mellanliggande utjämningssteg beräknas.", "edt_conf_smooth_interpolationRate_title": "Interpolationsfrekvens", - "edt_conf_smooth_outputRate_expl": "Utgångsfrekvensen till LED-enheten", - "edt_conf_smooth_outputRate_title": "Utfrekvens", "edt_conf_smooth_time_ms_expl": "Hur länge ska utjämningen samla bilder?", "edt_conf_smooth_time_ms_title": "Tid", "edt_conf_smooth_type_expl": "Utjämningsalgoritm.", @@ -614,10 +617,17 @@ "edt_dev_spec_rgbw_calibration_green": "Grön/Vit kanalförhållande", "edt_dev_spec_rgbw_calibration_limit": "Vit kanalgräns", "edt_dev_spec_rgbw_calibration_red": "Röd/Vit kanalförhållande", + "edt_dev_spec_segmentId_title": "Segment-ID", + "edt_dev_spec_segmentsOverlapValidation_error": "Korrigera WLED-inställningen! Segmentet får inte överlappa med {{plural:$1| segment|segments}}: \"$2\".", + "edt_dev_spec_segmentsSwitchOffOthers_title": "Stäng av övriga segment", + "edt_dev_spec_segments_disabled_title": "Segmentströmning inaktiverad vid WLED.", + "edt_dev_spec_segments_title": "Streama till segment", "edt_dev_spec_serial_title": "Serienummer", "edt_dev_spec_spipath_title": "SPI Pfad", "edt_dev_spec_sslHSTimeoutMax_title": "Streamer handskakning maximal timeout", "edt_dev_spec_sslHSTimeoutMin_title": "Minsta timeout för Streamerhandslag", + "edt_dev_spec_stayOnAfterStreaming_title": "Förbli på efter streaming", + "edt_dev_spec_stayOnAfterStreaming_title_info": "Enheten förblir på efter streaming eller återställning.", "edt_dev_spec_stream_protocol_title": "Strömningsprotokoll", "edt_dev_spec_switchOffOnBlack_title": "Av på svart", "edt_dev_spec_switchOffOnbelowMinBrightness_title": "Av vid lägsta", @@ -861,9 +871,11 @@ "general_country_us": "USA", "general_disabled": "Inaktiverad", "general_enabled": "Aktiverad", + "general_speech_ca": "Katalanska", "general_speech_cs": "Tjeckiska", "general_speech_da": "Danska", "general_speech_de": "Tyska", + "general_speech_el": "Grekiska", "general_speech_en": "Engelska", "general_speech_es": "Spanska", "general_speech_fr": "Franska", From 1ae37d151ede2d21a0fcb58ce282c7a0293fda56 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Fri, 17 Feb 2023 16:02:51 +0100 Subject: [PATCH 16/23] Dominant Color support (#1569) * Dominant Color and Mean Color Squared * Workaround - Suppress empty LED updates * Add missing text * Dominant Colors advanced * Test with fixed initial colors * Test with fixed initial colors * Support new processing values via API * ImageToLED - Add reduced pixel processing, make dominant color advanced configurable * Updates on Grabber fps setting * ImageToLedMap - Remove maptype and update test * Update dynamic cluster array allocation --- assets/webconfig/i18n/en.json | 25 +- include/hyperion/GrabberWrapper.h | 10 +- include/hyperion/ImageProcessor.h | 99 +++- include/hyperion/ImageToLedsMap.h | 544 ++++++++++++++++-- include/utils/ColorRgb.h | 13 + include/utils/ColorRgbScalar.h | 203 +++++++ include/utils/ColorRgba.h | 4 +- include/utils/ColorSys.h | 13 + .../api/JSONRPC_schema/schema-processing.json | 2 +- libsrc/grabber/video/VideoWrapper.cpp | 8 +- libsrc/hyperion/Grabber.cpp | 2 + libsrc/hyperion/GrabberWrapper.cpp | 3 +- libsrc/hyperion/ImageProcessor.cpp | 143 ++++- libsrc/hyperion/ImageToLedsMap.cpp | 98 +++- libsrc/hyperion/schema/schema-color.json | 30 +- test/TestImage2LedsMap.cpp | 6 +- 16 files changed, 1048 insertions(+), 155 deletions(-) create mode 100644 include/utils/ColorRgbScalar.h diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 445eb0fb..9f56ef26 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -253,6 +253,8 @@ "edt_conf_bb_unknownFrameCnt_title": "Unknown frames", "edt_conf_bge_heading_title": "Background Effect/Color", "edt_conf_bobls_heading_title": "Boblight Server", + "edt_conf_color_accuracyLevel_expl": "Level how accurate dominat colors are evaluated. A higher level creates more accurate results, but also requries more processing power. Should to be combined with reduced pixel processing.", + "edt_conf_color_accuracyLevel_title": "Accuracy level", "edt_conf_color_backlightColored_expl": "Add some color to your backlight.", "edt_conf_color_backlightColored_title": "Colored backlight", "edt_conf_color_backlightThreshold_expl": "The minimum amount of brightness (backlight). Disabled during effects, colors and in status \"Off\"", @@ -281,7 +283,7 @@ "edt_conf_color_heading_title": "Color Calibration", "edt_conf_color_id_expl": "User given name", "edt_conf_color_id_title": "ID", - "edt_conf_color_imageToLedMappingType_expl": "Overwrites the LED area assignment of your LED layout if it's not \"multicolor\"", + "edt_conf_color_imageToLedMappingType_expl": "Overwrites the LED area assignment of your LED layout if it's not \"Mean Color Simple\"", "edt_conf_color_imageToLedMappingType_title": "LED area assignment", "edt_conf_color_leds_expl": "Assign this adjustment to all LEDs (*) or just some (0-24).", "edt_conf_color_leds_title": "LED index", @@ -293,6 +295,8 @@ "edt_conf_color_saturationGain_title": "Saturation gain", "edt_conf_color_brightnessGain_expl": "Adjusts the brightness of colors. 1.0 means no change, over 1.0 increases brightness, under 1.0 decreases brightness.", "edt_conf_color_brightnessGain_title": "Brightness gain", + "edt_conf_color_reducedPixelSetFactorFactor_expl": "Evaluate only a set of pixels per LED area defined, Low ~25%, Medium ~10%, High ~6%", + "edt_conf_color_reducedPixelSetFactorFactor_title": "Reduced pixel processing", "edt_conf_color_white_expl": "The calibrated white value.", "edt_conf_color_white_title": "White", "edt_conf_color_yellow_expl": "The calibrated yellow value.", @@ -322,6 +326,8 @@ "edt_conf_enum_color": "Color", "edt_conf_enum_custom": "Custom", "edt_conf_enum_decay": "Decay", + "edt_conf_enum_delay": "Delay only", + "edt_conf_enum_disabled": "Disabled", "edt_conf_enum_dl_error": "Error", "edt_conf_enum_dl_informational": "Informational", "edt_conf_enum_dl_nodebug": "No Debug output", @@ -330,9 +336,12 @@ "edt_conf_enum_dl_verbose1": "Verbosity level 1", "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_effect": "Effect", "edt_conf_enum_gbr": "GBR", "edt_conf_enum_grb": "GRB", + "edt_conf_enum_high": "High", "edt_conf_enum_hsv": "HSV", "edt_conf_enum_left_right": "Left to right", "edt_conf_enum_linear": "Linear", @@ -340,7 +349,10 @@ "edt_conf_enum_logsilent": "Silent", "edt_conf_enum_logverbose": "Verbose", "edt_conf_enum_logwarn": "Warning", - "edt_conf_enum_multicolor_mean": "Multicolor", + "edt_conf_enum_low": "Low", + "edt_conf_enum_medium": "Medium", + "edt_conf_enum_multicolor_mean": "Mean Color Simple - per LED", + "edt_conf_enum_multicolor_mean_squared": "Mean Color Squared - per LED", "edt_conf_enum_please_select": "Please Select", "edt_conf_enum_rbg": "RBG", "edt_conf_enum_rgb": "RGB", @@ -350,7 +362,7 @@ "edt_conf_enum_transeffect_sudden": "Sudden", "edt_conf_enum_udp_ddp": "DDP", "edt_conf_enum_udp_raw": "RAW", - "edt_conf_enum_unicolor_mean": "Unicolor", + "edt_conf_enum_unicolor_mean": "Mean Color Image - applied to all LEDs", "edt_conf_fbs_heading_title": "Flatbuffers Server", "edt_conf_fbs_timeout_expl": "If no data is received for the given period, the component will be (soft) disabled.", "edt_conf_fbs_timeout_title": "Timeout", @@ -974,8 +986,11 @@ "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_multicolor_mean": "Multicolor", - "remote_maptype_label_unicolor_mean": "Unicolor", + "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_optgroup_syseffets": "System Effects", "remote_optgroup_templates_custom": "User Templates", "remote_optgroup_templates_system": "System Templates", diff --git a/include/hyperion/GrabberWrapper.h b/include/hyperion/GrabberWrapper.h index 99cfa4c6..fd58449c 100644 --- a/include/hyperion/GrabberWrapper.h +++ b/include/hyperion/GrabberWrapper.h @@ -147,10 +147,7 @@ private slots: void handleSourceRequest(hyperion::Components component, int hyperionInd, bool listen); /// - /// @brief Update Update capture rate - /// @param type interval between frames in milliseconds - /// - void updateTimer(int interval); + protected: @@ -168,6 +165,11 @@ protected: /// virtual bool close() { return true; } + /// @brief Update Update capture rate + /// @param type interval between frames in milliseconds + /// + void updateTimer(int interval); + QString _grabberName; diff --git a/include/hyperion/ImageProcessor.h b/include/hyperion/ImageProcessor.h index bc192668..84f4e26c 100644 --- a/include/hyperion/ImageProcessor.h +++ b/include/hyperion/ImageProcessor.h @@ -1,6 +1,7 @@ #pragma once #include +#include // Utils includes #include @@ -46,7 +47,7 @@ public: /// @param[in] width The new width of the buffer-image /// @param[in] height The new height of the buffer-image /// - void setSize(unsigned width, unsigned height); + void setSize(int width, int height); /// /// @brief Update the led string (eg on settings change) @@ -56,6 +57,19 @@ public: /// Returns state of black border detector bool blackBorderDetectorEnabled() const; + /// + /// Factor to reduce the number of pixels evaluated during processing + /// + /// @param[in] count Use every "count" pixel + void setReducedPixelSetFactorFactor(int count); + + /// + /// Set the accuracy used during processing + /// (only for selected types) + /// + /// @param[in] level The accuracy level (0-4) + void setAccuracyLevel(int level); + /// Returns the current _userMappingType, this may not be the current applied type! int getUserLedMappingType() const { return _userMappingType; } @@ -98,30 +112,45 @@ public: } /// - /// Processes the image to a list of led colors. This will update the size of the buffer-image - /// if required and call the image-to-leds mapping to determine the mean color per led. + /// Processes the image to a list of LED colors. This will update the size of the buffer-image + /// if required and call the image-to-LEDs mapping to determine the color per LED. /// - /// @param[in] image The image to translate to led values + /// @param[in] image The image to translate to LED values /// - /// @return The color value per led + /// @return The color value per LED /// template std::vector process(const Image& image) { std::vector colors; + if (image.width()>0 && image.height()>0) { // Ensure that the buffer-image is the proper size setSize(image); + assert(!_imageToLedColors.isNull()); + // Check black border detection verifyBorder(image); // Create a result vector and call the 'in place' function switch (_mappingType) { - case 1: colors = _imageToLeds->getUniLedColor(image); break; - default: colors = _imageToLeds->getMeanLedColor(image); + case 1: + colors = _imageToLedColors->getUniLedColor(image); + break; + case 2: + colors = _imageToLedColors->getMeanLedColorSqrt(image); + break; + case 3: + colors = _imageToLedColors->getDominantLedColor(image); + break; + case 4: + colors = _imageToLedColors->getDominantLedColorAdv(image); + break; + default: + colors = _imageToLedColors->getMeanLedColor(image); } } else @@ -136,8 +165,8 @@ public: /// /// Determines the led colors of the image in the buffer. /// - /// @param[in] image The image to translate to led values - /// @param[out] ledColors The color value per led + /// @param[in] image The image to translate to LED values + /// @param[out] ledColors The color value per LED /// template void process(const Image& image, std::vector& ledColors) @@ -153,8 +182,20 @@ public: // Determine the mean or uni colors of each led (using the existing mapping) switch (_mappingType) { - case 1: _imageToLeds->getUniLedColor(image, ledColors); break; - default: _imageToLeds->getMeanLedColor(image, ledColors); + case 1: + _imageToLedColors->getUniLedColor(image, ledColors); + break; + case 2: + _imageToLedColors->getMeanLedColorSqrt(image, ledColors); + break; + case 3: + _imageToLedColors->getDominantLedColor(image, ledColors); + break; + case 4: + _imageToLedColors->getDominantLedColorAdv(image, ledColors); + break; + default: + _imageToLedColors->getMeanLedColor(image, ledColors); } } else @@ -164,9 +205,9 @@ public: } /// - /// Get the hscan and vscan parameters for a single led + /// Get the hscan and vscan parameters for a single LED /// - /// @param[in] led Index of the led + /// @param[in] led Index of the LED /// @param[out] hscanBegin begin of the hscan /// @param[out] hscanEnd end of the hscan /// @param[out] vscanBegin begin of the hscan @@ -175,6 +216,13 @@ public: bool getScanParameters(size_t led, double & hscanBegin, double & hscanEnd, double & vscanBegin, double & vscanEnd) const; private: + + void registerProcessingUnit( + int width, + int height, + int horizontalBorder, + int verticalBorder); + /// /// Performs black-border detection (if enabled) on the given image /// @@ -183,34 +231,25 @@ private: template void verifyBorder(const Image & image) { - if (!_borderProcessor->enabled() && ( _imageToLeds->horizontalBorder()!=0 || _imageToLeds->verticalBorder()!=0 )) + if (!_borderProcessor->enabled() && ( _imageToLedColors->horizontalBorder()!=0 || _imageToLedColors->verticalBorder()!=0 )) { Debug(_log, "Reset border"); _borderProcessor->process(image); - delete _imageToLeds; - _imageToLeds = new hyperion::ImageToLedsMap(image.width(), image.height(), 0, 0, _ledString.leds()); + registerProcessingUnit(image.width(), image.height(), 0, 0); } if(_borderProcessor->enabled() && _borderProcessor->process(image)) { const hyperion::BlackBorder border = _borderProcessor->getCurrentBorder(); - // Clean up the old mapping - delete _imageToLeds; - if (border.unknown) { - // Construct a new buffer and mapping - _imageToLeds = new hyperion::ImageToLedsMap(image.width(), image.height(), 0, 0, _ledString.leds()); + registerProcessingUnit(image.width(), image.height(), 0, 0); } else { - // Construct a new buffer and mapping - _imageToLeds = new hyperion::ImageToLedsMap(image.width(), image.height(), border.horizontalSize, border.verticalSize, _ledString.leds()); + registerProcessingUnit(image.width(), image.height(), border.horizontalSize, border.verticalSize); } - - //Debug(Logger::getInstance("BLACKBORDER"), "CURRENT BORDER TYPE: unknown=%d hor.size=%d vert.size=%d", - // border.unknown, border.horizontalSize, border.verticalSize ); } } @@ -218,6 +257,7 @@ private slots: void handleSettingsUpdate(settings::type type, const QJsonDocument& config); private: + Logger * _log; /// The Led-string specification LedString _ledString; @@ -226,15 +266,18 @@ private: hyperion::BlackBorderProcessor * _borderProcessor; /// The mapping of image-pixels to LEDs - hyperion::ImageToLedsMap* _imageToLeds; + QSharedPointer _imageToLedColors; - /// Type of image 2 led mapping + /// Type of image to LED mapping int _mappingType; /// Type of last requested user type int _userMappingType; /// Type of last requested hard type int _hardMappingType; + int _accuraryLevel; + int _reducedPixelSetFactorFactor; + /// Hyperion instance pointer Hyperion* _hyperion; }; diff --git a/include/hyperion/ImageToLedsMap.h b/include/hyperion/ImageToLedsMap.h index 779082ab..17662f28 100644 --- a/include/hyperion/ImageToLedsMap.h +++ b/include/hyperion/ImageToLedsMap.h @@ -1,72 +1,90 @@ - -#pragma once +#ifndef IMAGETOLEDSMAP_H +#define IMAGETOLEDSMAP_H // STL includes #include +#include #include +#include // hyperion-utils includes #include #include +#include +#include // hyperion includes #include namespace hyperion { - /// - /// The ImageToLedsMap holds a mapping of indices into an image to leds. It can be used to - /// calculate the average (or mean) color per led for a specific region. + /// The ImageToLedsMap holds a mapping of indices into an image to LEDs. It can be used to + /// calculate the average (aka mean) or dominant color per LED for a given region. /// - class ImageToLedsMap + class ImageToLedsMap : public QObject { + Q_OBJECT + public: /// - /// Constructs an mapping from the absolute indices in an image to each led based on the border - /// definition given in the list of leds. The map holds absolute indices to any given image, + /// Constructs an mapping from the absolute indices in an image to each LED based on the border + /// definition given in the list of LEDs. The map holds absolute indices to any given image, /// provided that it is row-oriented. /// The mapping is created purely on size (width and height). The given borders are excluded /// from indexing. /// + /// @param[in] log Logger /// @param[in] width The width of the indexed image /// @param[in] height The width of the indexed image /// @param[in] horizontalBorder The size of the horizontal border (0=no border) /// @param[in] verticalBorder The size of the vertical border (0=no border) /// @param[in] leds The list with led specifications + /// @param[in] reducedProcessingFactor Factor to reduce the number of pixels evaluated during processing + /// @param[in] accuraryLevel The accuracy used during processing (only for selected types) /// ImageToLedsMap( - const unsigned width, - const unsigned height, - const unsigned horizontalBorder, - const unsigned verticalBorder, - const std::vector & leds); + Logger* log, + int width, + int height, + int horizontalBorder, + int verticalBorder, + const std::vector & leds, + int reducedProcessingFactor = 0, + int accuraryLevel = 0); /// /// Returns the width of the indexed image /// /// @return The width of the indexed image [pixels] /// - unsigned width() const; + int width() const; /// /// Returns the height of the indexed image /// /// @return The height of the indexed image [pixels] /// - unsigned height() const; + int height() const; - unsigned horizontalBorder() const { return _horizontalBorder; } - unsigned verticalBorder() const { return _verticalBorder; } + int horizontalBorder() const { return _horizontalBorder; } + int verticalBorder() const { return _verticalBorder; } /// - /// Determines the mean color for each led using the mapping the image given + /// Set the accuracy used during processing + /// (only for selected types) + /// + /// @param[in] level The accuracy level (0-4) + void setAccuracyLevel (int level); + + /// + /// Determines the mean color for each LED using the LED area mapping given /// at construction. /// /// @param[in] image The image from which to extract the led colors /// - /// @return ledColors The vector containing the output + /// @return The vector containing the output /// template std::vector getMeanLedColor(const Image & image) const @@ -77,20 +95,18 @@ namespace hyperion } /// - /// Determines the mean color for each led using the mapping the image given + /// Determines the mean color for each LED using the LED area mapping given /// at construction. /// - /// @param[in] image The image from which to extract the led colors + /// @param[in] image The image from which to extract the LED colors /// @param[out] ledColors The vector containing the output /// template void getMeanLedColor(const Image & image, std::vector & ledColors) const { - // Sanity check for the number of leds - //assert(_colorsMap.size() == ledColors.size()); if(_colorsMap.size() != ledColors.size()) { - Debug(Logger::getInstance("HYPERION"), "ImageToLedsMap: colorsMap.size != ledColors.size -> %d != %d", _colorsMap.size(), ledColors.size()); + Debug(_log, "ImageToLedsMap: colorsMap.size != ledColors.size -> %d != %d", _colorsMap.size(), ledColors.size()); return; } @@ -104,12 +120,52 @@ namespace hyperion } /// - /// Determines the uni color for each led using the mapping the image given + /// Determines the mean color squared for each LED using the LED area mapping given /// at construction. /// /// @param[in] image The image from which to extract the led colors /// - /// @return ledColors The vector containing the output + /// @return The vector containing the output + /// + template + std::vector getMeanLedColorSqrt(const Image & image) const + { + std::vector colors(_colorsMap.size(), ColorRgb{0,0,0}); + getMeanLedColorSqrt(image, colors); + return colors; + } + + /// + /// Determines the mean color squared for each LED using the LED area mapping given + /// at construction. + /// + /// @param[in] image The image from which to extract the LED colors + /// @param[out] ledColors The vector containing the output + /// + template + void getMeanLedColorSqrt(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; + } + + // Iterate each led and compute the mean + auto led = ledColors.begin(); + for (auto colors = _colorsMap.begin(); colors != _colorsMap.end(); ++colors, ++led) + { + const ColorRgb color = calcMeanColorSqrt(image, *colors); + *led = color; + } + } + + /// + /// Determines the mean 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 getUniLedColor(const Image & image) const @@ -120,57 +176,145 @@ namespace hyperion } /// - /// Determines the uni color for each led using the mapping the image given - /// at construction. + /// Determines the mean color of the image and assigns it to all LEDs /// - /// @param[in] image The image from which to extract the led colors + /// @param[in] image The image from which to extract the LED colors /// @param[out] ledColors The vector containing the output /// template void getUniLedColor(const Image & image, std::vector & ledColors) const { - // Sanity check for the number of leds - // assert(_colorsMap.size() == ledColors.size()); if(_colorsMap.size() != ledColors.size()) { - Debug(Logger::getInstance("HYPERION"), "ImageToLedsMap: colorsMap.size != ledColors.size -> %d != %d", _colorsMap.size(), ledColors.size()); + Debug(_log, "ImageToLedsMap: colorsMap.size != ledColors.size -> %d != %d", _colorsMap.size(), ledColors.size()); return; } - // calculate uni color const ColorRgb color = calcMeanColor(image); + //Update all LEDs with same color std::fill(ledColors.begin(),ledColors.end(), color); } - private: - /// The width of the indexed image - const unsigned _width; - /// The height of the indexed image - const unsigned _height; - - const unsigned _horizontalBorder; - - const unsigned _verticalBorder; - - /// The absolute indices into the image for each led - std::vector> _colorsMap; + /// + /// Determines the dominant color for each LED using the LED area mapping given + /// at construction. + /// + /// @param[in] image The image from which to extract the LED color + /// + /// @return The vector containing the output + /// + template + std::vector getDominantLedColor(const Image & image) const + { + std::vector colors(_colorsMap.size(), ColorRgb{0,0,0}); + getDominantLedColor(image, colors); + return colors; + } /// - /// Calculates the 'mean color' of the given list. This is the mean over each color-channel + /// Determines the dominant color for each LED using the LED area mapping given + /// at construction. + /// + /// @param[in] image The image from which to extract the LED colors + /// @param[out] ledColors The vector containing the output + /// + template + void getDominantLedColor(const Image & image, std::vector & ledColors) const + { + // Sanity check for the number of LEDs + if(_colorsMap.size() != ledColors.size()) + { + Debug(_log, "ImageToLedsMap: colorsMap.size != ledColors.size -> %d != %d", _colorsMap.size(), ledColors.size()); + return; + } + + // Iterate each led and compute the dominant color + auto led = ledColors.begin(); + for (auto colors = _colorsMap.begin(); colors != _colorsMap.end(); ++colors, ++led) + { + const ColorRgb color = calculateDominantColor(image, *colors); + *led = 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 + /// + /// @return The vector containing the output + /// + template + std::vector getDominantLedColorAdv(const Image & image) const + { + std::vector colors(_colorsMap.size(), ColorRgb{0,0,0}); + getDominantLedColorAdv(image, colors); + return colors; + } + + /// + /// 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 colors + /// @param[out] ledColors The vector containing the output + /// + template + void getDominantLedColorAdv(const Image & image, std::vector & ledColors) const + { + // Sanity check for the number of LEDs + if(_colorsMap.size() != ledColors.size()) + { + Debug(_log, "ImageToLedsMap: colorsMap.size != ledColors.size -> %d != %d", _colorsMap.size(), ledColors.size()); + return; + } + + // Iterate each led and compute the dominant color + auto led = ledColors.begin(); + for (auto colors = _colorsMap.begin(); colors != _colorsMap.end(); ++colors, ++led) + { + const ColorRgb color = calculateDominantColorAdv(image, *colors); + *led = color; + } + } + + private: + + Logger* _log; + + /// The width of the indexed image + const int _width; + /// The height of the indexed image + const int _height; + + const int _horizontalBorder; + const int _verticalBorder; + + /// Evaluate every "count" pixel + int _nextPixelCount; + + /// Number of clusters used during dominant color advanced processing (k-means) + int _clusterCount; + + /// The absolute indices into the image for each led + std::vector> _colorsMap; + + /// + /// Calculates the 'mean color' over the given image. This is the mean over each color-channel /// (red, green, blue) /// /// @param[in] image The image a section from which an average color must be computed - /// @param[in] colors The list with colors + /// @param[in] pixels The list of pixel indices for the given image to be evaluated/// /// /// @return The mean of the given list of colors (or black when empty) /// template - ColorRgb calcMeanColor(const Image & image, const std::vector & colors) const + ColorRgb calcMeanColor(const Image & image, const std::vector & pixels) const { - const auto colorVecSize = colors.size(); - - if (colorVecSize == 0) + const auto pixelNum = pixels.size(); + if (pixelNum == 0) { return ColorRgb::BLACK; } @@ -179,20 +323,20 @@ namespace hyperion uint_fast32_t cummRed = 0; uint_fast32_t cummGreen = 0; uint_fast32_t cummBlue = 0; - const auto& imgData = image.memptr(); - for (const unsigned colorOffset : colors) + const auto& imgData = image.memptr(); + for (const int pixelOffset : pixels) { - const auto& pixel = imgData[colorOffset]; + const auto& pixel = imgData[pixelOffset]; cummRed += pixel.red; cummGreen += pixel.green; cummBlue += pixel.blue; } // Compute the average of each color channel - const uint8_t avgRed = uint8_t(cummRed/colorVecSize); - const uint8_t avgGreen = uint8_t(cummGreen/colorVecSize); - const uint8_t avgBlue = uint8_t(cummBlue/colorVecSize); + const uint8_t avgRed = uint8_t(cummRed/pixelNum); + const uint8_t avgGreen = uint8_t(cummGreen/pixelNum); + const uint8_t avgBlue = uint8_t(cummBlue/pixelNum); // Return the computed color return {avgRed, avgGreen, avgBlue}; @@ -213,11 +357,11 @@ namespace hyperion uint_fast32_t cummRed = 0; uint_fast32_t cummGreen = 0; uint_fast32_t cummBlue = 0; - const unsigned imageSize = image.width() * image.height(); + const unsigned pixelNum = image.width() * image.height(); const auto& imgData = image.memptr(); - for (unsigned idx=0; idx + ColorRgb calcMeanColorSqrt(const Image & image, const std::vector & pixels) const + { + const auto pixelNum = pixels.size(); + if (pixelNum == 0) + { + return ColorRgb::BLACK; + } + + // Accumulate the squared sum of each separate color channel + uint_fast32_t cummRed = 0; + uint_fast32_t cummGreen = 0; + uint_fast32_t cummBlue = 0; + + const auto& imgData = image.memptr(); + + for (const int colorOffset : pixels) + { + const auto& pixel = imgData[colorOffset]; + + cummRed += pixel.red * pixel.red; + cummGreen += pixel.green * pixel.green; + cummBlue += pixel.blue * pixel.blue; + } + + // Compute the average of each color channel + const uint8_t avgRed = uint8_t(std::min(std::lround(sqrt(static_cast(cummRed/pixelNum))), 255L)); + const uint8_t avgGreen = uint8_t(std::min(std::lround(sqrt(static_cast(cummGreen/pixelNum))), 255L)); + const uint8_t avgBlue = uint8_t(std::min(std::lround(sqrt(static_cast(cummBlue/pixelNum))), 255L)); + + // Return the computed color + return {avgRed, avgGreen, avgBlue}; + } + + /// + /// Calculates the 'mean color' squared over the given image. This is the mean over each color-channel + /// (red, green, blue) + /// + /// @param[in] image The image a section from which an average color must be computed + /// + /// @return The mean of the given list of colors (or black when empty) + /// + template + ColorRgb calcMeanColorSqrt(const Image & image) const + { + // Accumulate the squared sum of each separate color channel + uint_fast32_t cummRed = 0; + uint_fast32_t cummGreen = 0; + uint_fast32_t cummBlue = 0; + + const unsigned pixelNum = image.width() * image.height(); + const auto& imgData = image.memptr(); + + for (int idx=0; idx(cummRed/pixelNum)))); + const uint8_t avgGreen = uint8_t(std::lround(sqrt(static_cast(cummGreen/pixelNum)))); + const uint8_t avgBlue = uint8_t(std::lround(sqrt(static_cast(cummBlue/pixelNum)))); + + // Return the computed color + return {avgRed, avgGreen, avgBlue}; + } + + /// + /// Calculates the 'dominant color' of an image area defined by a list of pixel indices + /// + /// @param[in] image The image for which a dominant color is to be computed + /// @param[in] pixels The list of pixel indices for the given image to be evaluated + /// + /// @return The image area's dominant color or black, if no pixel indices provided + /// + template + ColorRgb calculateDominantColor(const Image & image, const std::vector & pixels) const + { + ColorRgb dominantColor {ColorRgb::BLACK}; + + const auto pixelNum = pixels.size(); + if (pixelNum > 0) + { + const auto& imgData = image.memptr(); + + QMap colorDistributionMap; + int count = 0; + for (const int pixelOffset : pixels) + { + QRgb color = imgData[pixelOffset].rgb(); + if (colorDistributionMap.contains(color)) { + colorDistributionMap[color] = colorDistributionMap[color] + 1; + } + else { + colorDistributionMap[color] = 1; + } + + int colorsFound = colorDistributionMap[color]; + if (colorsFound > count) { + dominantColor.setRgb(color); + count = colorsFound; + } + } + } + return dominantColor; + } + + /// + /// Calculates the 'dominant color' of an image + /// + /// @param[in] image The image for which a dominant color is to be computed + /// + /// @return The image's dominant color + /// + template + ColorRgb calculateDominantColor(const Image & image) const + { + const unsigned pixelNum = image.width() * image.height(); + + std::vector pixels(pixelNum); + std::iota(pixels.begin(), pixels.end(), 0); + + return calculateDominantColor(image, pixels); + } + + template + struct ColorCluster { + + ColorCluster():count(0) {} + ColorCluster(Pixel_T color):count(0),color(color) {} + + Pixel_T color; + Pixel_T newColor; + int count; + }; + + const ColorRgb DEFAULT_CLUSTER_COLORS[5] { + {ColorRgb::BLACK}, + {ColorRgb::GREEN}, + {ColorRgb::WHITE}, + {ColorRgb::RED}, + {ColorRgb::YELLOW} + }; + + /// + /// Calculates the 'dominant color' of an image area defined by a list of pixel indices + /// using a k-means algorithm (https://robocraft.ru/computervision/1063) + /// + /// @param[in] image The image for which a dominant color is to be computed + /// @param[in] pixels The list of pixel indices for the given image to be evaluated + /// + /// @return The image area's dominant color or black, if no pixel indices provided + /// + template + ColorRgb calculateDominantColorAdv(const Image & image, const std::vector & pixels) const + { + ColorRgb dominantColor {ColorRgb::BLACK}; + const auto pixelNum = pixels.size(); + if (pixelNum > 0) + { + // initial cluster with different colors + auto clusters = std::unique_ptr< ColorCluster >(new ColorCluster[_clusterCount]); + for(int k = 0; k < _clusterCount; ++k) + { + clusters.get()[k].newColor = DEFAULT_CLUSTER_COLORS[k]; + } + + // k-means + double min_rgb_euclidean {0}; + double old_rgb_euclidean {0}; + + while(1) + { + for(int k = 0; k < _clusterCount; ++k) + { + clusters.get()[k].count = 0; + clusters.get()[k].color = clusters.get()[k].newColor; + clusters.get()[k].newColor.setRgb(ColorRgb::BLACK); + } + + const auto& imgData = image.memptr(); + for (const int pixelOffset : pixels) + { + const auto& pixel = imgData[pixelOffset]; + + min_rgb_euclidean = 255 * 255 * 255; + int clusterIndex = -1; + for(int k = 0; k < _clusterCount; ++k) + { + double euclid = ColorSys::rgb_euclidean(ColorRgbScalar(pixel), clusters.get()[k].color); + + if( euclid < min_rgb_euclidean ) { + min_rgb_euclidean = euclid; + clusterIndex = k; + } + } + + clusters.get()[clusterIndex].count++; + clusters.get()[clusterIndex].newColor += ColorRgbScalar(pixel); + } + + min_rgb_euclidean = 0; + for(int k = 0; k < _clusterCount; ++k) + { + if (clusters.get()[k].count > 0) + { + // new color + clusters.get()[k].newColor /= clusters.get()[k].count; + double ecli = ColorSys::rgb_euclidean(clusters.get()[k].newColor, clusters.get()[k].color); + if(ecli > min_rgb_euclidean) + { + min_rgb_euclidean = ecli; + } + } + } + + if( fabs(min_rgb_euclidean - old_rgb_euclidean) < 1) + { + break; + } + + old_rgb_euclidean = min_rgb_euclidean; + } + + int colorsFoundMax = 0; + int dominantClusterIdx {0}; + + for(int clusterIdx=0; clusterIdx < _clusterCount; ++clusterIdx){ + int colorsFoundinCluster = clusters.get()[clusterIdx].count; + if (colorsFoundinCluster > colorsFoundMax) { + colorsFoundMax = colorsFoundinCluster; + dominantClusterIdx = clusterIdx; + } + } + + dominantColor.red = static_cast(clusters.get()[dominantClusterIdx].newColor.red); + dominantColor.green = static_cast(clusters.get()[dominantClusterIdx].newColor.green); + dominantColor.blue = static_cast(clusters.get()[dominantClusterIdx].newColor.blue); + } + + return dominantColor; + } + + /// + /// Calculates the 'dominant color' of an image area defined by a list of pixel indices + /// using a k-means algorithm (https://robocraft.ru/computervision/1063) + /// + /// @param[in] image The image for which a dominant color is to be computed + /// + /// @return The image's dominant color + /// + template + ColorRgb calculateDominantColorAdv(const Image & image) const + { + const unsigned pixelNum = image.width() * image.height(); + + std::vector pixels(pixelNum); + std::iota(pixels.begin(), pixels.end(), 0); + + return calculateDominantColorAdv(image, pixels); + } }; } // end namespace hyperion + +#endif // IMAGETOLEDSMAP_H diff --git a/include/utils/ColorRgb.h b/include/utils/ColorRgb.h index 901b000c..b9a91038 100644 --- a/include/utils/ColorRgb.h +++ b/include/utils/ColorRgb.h @@ -6,6 +6,7 @@ #include #include +#include /// /// Plain-Old-Data structure containing the red-green-blue color specification. Size of the @@ -52,6 +53,18 @@ struct ColorRgb return a; } + QRgb rgb() const + { + return qRgb(red,green,blue); + } + + void setRgb(QRgb rgb) + { + red = static_cast(qRed(rgb)); + green = static_cast(qGreen(rgb)); + blue = static_cast(qBlue(rgb)); + } + QString toQString() const { return QString("(%1,%2,%3)").arg(red).arg(green).arg(blue); diff --git a/include/utils/ColorRgbScalar.h b/include/utils/ColorRgbScalar.h new file mode 100644 index 00000000..3b605a2f --- /dev/null +++ b/include/utils/ColorRgbScalar.h @@ -0,0 +1,203 @@ +#ifndef COLORRGBSCALAR_H +#define COLORRGBSCALAR_H + +// STL includes +#include +#include + +#include +#include +#include +#include + +/// +/// Plain-Old-Data structure containing the red-green-blue color specification. Size of the +/// structure is exactly 3 times int for easy writing to led-device +/// +struct ColorRgbScalar +{ + /// The red color channel + int red; + /// The green color channel + int green; + /// The blue color channel + int blue; + + /// 'Black' RgbColor (0, 0, 0) + static const ColorRgbScalar BLACK; + /// 'Red' RgbColor (255, 0, 0) + static const ColorRgbScalar RED; + /// 'Green' RgbColor (0, 255, 0) + static const ColorRgbScalar GREEN; + /// 'Blue' RgbColor (0, 0, 255) + static const ColorRgbScalar BLUE; + /// 'Yellow' RgbColor (255, 255, 0) + static const ColorRgbScalar YELLOW; + /// 'White' RgbColor (255, 255, 255) + static const ColorRgbScalar WHITE; + + ColorRgbScalar() = default; + + ColorRgbScalar(int _red, int _green,int _blue): + red(_red), + green(_green), + blue(_blue) + { + + } + + ColorRgbScalar(ColorRgb rgb): + red(rgb.red), + green(rgb.green), + blue(rgb.blue) + { + + } + + ColorRgbScalar operator-(const ColorRgbScalar& b) const + { + ColorRgbScalar a(*this); + a.red -= b.red; + a.green -= b.green; + a.blue -= b.blue; + return a; + } + + void setRgb(QRgb rgb) + { + red = qRed(rgb); + green = qGreen(rgb); + blue = qBlue(rgb); + } + + void setRgb(ColorRgb rgb) + { + red = rgb.red; + green = rgb.green; + blue = rgb.blue; + } + + QString toQString() const + { + return QString("(%1,%2,%3)").arg(red).arg(green).arg(blue); + } +}; +/// Assert to ensure that the size of the structure is 'only' 3 times int +static_assert(sizeof(ColorRgbScalar) == 3 * sizeof(int), "Incorrect size of ColorRgbInt"); + + +/// +/// Stream operator to write ColorRgbInt to an outputstream (format "'{'[red]','[green]','[blue]'}'") +/// +/// @param os The output stream +/// @param color The color to write +/// @return The output stream (with the color written to it) +/// +inline std::ostream& operator<<(std::ostream& os, const ColorRgbScalar& color) +{ + os << "{" + << static_cast(color.red) << "," + << static_cast(color.green) << "," + << static_cast(color.blue) + << "}"; + + return os; +} + +/// +/// Stream operator to write ColorRgbInt to a QTextStream (format "'{'[red]','[green]','[blue]'}'") +/// +/// @param os The output stream +/// @param color The color to write +/// @return The output stream (with the color written to it) +/// +inline QTextStream& operator<<(QTextStream &os, const ColorRgbScalar& color) +{ + os << "{" + << static_cast(color.red) << "," + << static_cast(color.green) << "," + << static_cast(color.blue) + << "}"; + + return os; +} + +/// Compare operator to check if a color is 'equal' to another color +inline bool operator==(const ColorRgbScalar & lhs, const ColorRgbScalar & rhs) +{ + return lhs.red == rhs.red && + lhs.green == rhs.green && + lhs.blue == rhs.blue; +} + +/// Compare operator to check if a color is 'smaller' than another color +inline bool operator<(const ColorRgbScalar & lhs, const ColorRgbScalar & rhs) +{ + return lhs.red < rhs.red && + lhs.green < rhs.green && + lhs.blue < rhs.blue; +} + +/// Compare operator to check if a color is 'not equal' to another color +inline bool operator!=(const ColorRgbScalar & lhs, const ColorRgbScalar & rhs) +{ + return !(lhs == rhs); +} + +/// Compare operator to check if a color is 'smaller' than or 'equal' to another color +inline bool operator<=(const ColorRgbScalar & lhs, const ColorRgbScalar & rhs) +{ + return lhs.red <= rhs.red && + lhs.green <= rhs.green && + lhs.blue <= rhs.blue; +} + +/// Compare operator to check if a color is 'greater' to another color +inline bool operator>(const ColorRgbScalar & lhs, const ColorRgbScalar & rhs) +{ + return lhs.red > rhs.red && + lhs.green > rhs.green && + lhs.blue > rhs.blue; +} + +/// Compare operator to check if a color is 'greater' than or 'equal' to another color +inline bool operator>=(const ColorRgbScalar & lhs, const ColorRgbScalar & rhs) +{ + return lhs.red >= rhs.red && + lhs.green >= rhs.green && + lhs.blue >= rhs.blue; +} + +inline ColorRgbScalar& operator+=(ColorRgbScalar& lhs, const ColorRgbScalar& rhs) +{ + lhs.red += rhs.red; + lhs.green += rhs.green; + lhs.blue += rhs.blue; + + return lhs; +} + +inline ColorRgbScalar operator+(ColorRgbScalar lhs, const ColorRgbScalar rhs) +{ + lhs += rhs; + return lhs; +} + +inline ColorRgbScalar& operator/=(ColorRgbScalar& lhs, int count) +{ + if (count > 0) + { + lhs.red /= count; + lhs.green /= count; + lhs.blue /= count; + } + return lhs; +} + +inline ColorRgbScalar operator/(ColorRgbScalar lhs, int count) +{ + lhs /= count; + return lhs; +} + +#endif // COLORRGBSCALAR_H diff --git a/include/utils/ColorRgba.h b/include/utils/ColorRgba.h index 63afdc5a..648cebb7 100644 --- a/include/utils/ColorRgba.h +++ b/include/utils/ColorRgba.h @@ -30,11 +30,11 @@ struct ColorRgba static const ColorRgba WHITE; }; -/// Assert to ensure that the size of the structure is 'only' 3 bytes +/// Assert to ensure that the size of the structure is 'only' 4 bytes static_assert(sizeof(ColorRgba) == 4, "Incorrect size of ColorARGB"); /// -/// Stream operator to write ColorRgb to an outputstream (format "'{'[alpha]', '[red]','[green]','[blue]'}'") +/// Stream operator to write ColorRgba to an outputstream (format "'{'[alpha]', '[red]','[green]','[blue]'}'") /// /// @param os The output stream /// @param color The color to write diff --git a/include/utils/ColorSys.h b/include/utils/ColorSys.h index 63fb74cb..8a83ef7a 100644 --- a/include/utils/ColorSys.h +++ b/include/utils/ColorSys.h @@ -105,6 +105,19 @@ public: /// @note See https://bottosson.github.io/posts/colorpicker/#okhsv /// static void okhsv2rgb(double hue, double saturation, double value, uint8_t & red, uint8_t & green, uint8_t & blue); + + template + static double rgb_euclidean(Pixel_T p1, Pixel_T p2) + { + double val = sqrt( + (p1.red - p2.red) * (p1.red - p2.red) + + (p1.green - p2.green) * (p1.green - p2.green) + + (p1.blue - p2.blue) * (p1.blue - p2.blue) + ); + + return val; + } + }; #endif // COLORSYS_H diff --git a/libsrc/api/JSONRPC_schema/schema-processing.json b/libsrc/api/JSONRPC_schema/schema-processing.json index ddd04da1..d67828f0 100644 --- a/libsrc/api/JSONRPC_schema/schema-processing.json +++ b/libsrc/api/JSONRPC_schema/schema-processing.json @@ -12,7 +12,7 @@ }, "mappingType": { "type" : "string", - "enum" : ["multicolor_mean", "unicolor_mean"] + "enum" : ["multicolor_mean", "unicolor_mean", "multicolor_mean_squared", "dominant_color", "dominant_color_advanced"] } }, "additionalProperties": false diff --git a/libsrc/grabber/video/VideoWrapper.cpp b/libsrc/grabber/video/VideoWrapper.cpp index bd5ef76c..7a3ed201 100644 --- a/libsrc/grabber/video/VideoWrapper.cpp +++ b/libsrc/grabber/video/VideoWrapper.cpp @@ -74,9 +74,6 @@ void VideoWrapper::handleSettingsUpdate(settings::type type, const QJsonDocument // Device resolution _grabber.setWidthHeight(obj["width"].toInt(0), obj["height"].toInt(0)); - // Device framerate - _grabber.setFramerate(obj["fps"].toInt(15)); - // Device encoding format _grabber.setEncoding(obj["encoding"].toString("NO_CHANGE")); @@ -124,6 +121,11 @@ void VideoWrapper::handleSettingsUpdate(settings::type type, const QJsonDocument obj["blueSignalThreshold"].toDouble(0.0)/100.0, obj["noSignalCounterThreshold"].toInt(50)); + // Device framerate + _grabber.setFramerate(obj["fps"].toInt(15)); + + updateTimer(_ggrabber->getUpdateInterval()); + // Reload the Grabber if any settings have been changed that require it _grabber.reload(getV4lGrabberState()); } diff --git a/libsrc/hyperion/Grabber.cpp b/libsrc/hyperion/Grabber.cpp index 376da7ff..aefdb142 100644 --- a/libsrc/hyperion/Grabber.cpp +++ b/libsrc/hyperion/Grabber.cpp @@ -149,6 +149,8 @@ bool Grabber::setWidthHeight(int width, int height) bool Grabber::setFramerate(int fps) { + Debug(_log,"Set new frames per second to: %i fps, current fps: %i", fps, _fps); + if((fps > 0) && (_fps != fps)) { Info(_log,"Set new frames per second to: %i fps", fps); diff --git a/libsrc/hyperion/GrabberWrapper.cpp b/libsrc/hyperion/GrabberWrapper.cpp index a31e0339..1c846aa4 100644 --- a/libsrc/hyperion/GrabberWrapper.cpp +++ b/libsrc/hyperion/GrabberWrapper.cpp @@ -186,7 +186,8 @@ void GrabberWrapper::updateTimer(int interval) } void GrabberWrapper::handleSettingsUpdate(settings::type type, const QJsonDocument& config) -{ if(type == settings::SYSTEMCAPTURE && !_grabberName.startsWith("V4L")) +{ + if(type == settings::SYSTEMCAPTURE && !_grabberName.startsWith("V4L")) { // extract settings const QJsonObject& obj = config.object(); diff --git a/libsrc/hyperion/ImageProcessor.cpp b/libsrc/hyperion/ImageProcessor.cpp index f527a3a9..6f9c6222 100644 --- a/libsrc/hyperion/ImageProcessor.cpp +++ b/libsrc/hyperion/ImageProcessor.cpp @@ -4,26 +4,83 @@ #include #include -// Blacborder includes +// Blackborder includes #include +#include +#include + using namespace hyperion; +void ImageProcessor::registerProcessingUnit( + int width, + int height, + int horizontalBorder, + int verticalBorder) +{ + if (width > 0 && height > 0) + { + _imageToLedColors = QSharedPointer(new ImageToLedsMap( + _log, + width, + height, + horizontalBorder, + verticalBorder, + _ledString.leds(), + _reducedPixelSetFactorFactor, + _accuraryLevel + )); + } + else + { + _imageToLedColors = QSharedPointer(nullptr); + } +} + // global transform method int ImageProcessor::mappingTypeToInt(const QString& mappingType) { if (mappingType == "unicolor_mean" ) + { return 1; - + } + else if (mappingType == "multicolor_mean_squared" ) + { + return 2; + } + else if (mappingType == "dominant_color" ) + { + return 3; + } + else if (mappingType == "dominant_color_advanced" ) + { + return 4; + } return 0; } // global transform method QString ImageProcessor::mappingTypeToStr(int mappingType) { - if (mappingType == 1 ) - return "unicolor_mean"; + QString typeText; + switch (mappingType) { + case 1: + typeText = "unicolor_mean"; + break; + case 2: + typeText = "multicolor_mean_squared"; + break; + case 3: + typeText = "dominant_color"; + break; + case 4: + typeText = "dominant_color_advanced"; + break; + default: + typeText = "multicolor_mean"; + break; + } - return "multicolor_mean"; + return typeText; } ImageProcessor::ImageProcessor(const LedString& ledString, Hyperion* hyperion) @@ -31,10 +88,12 @@ ImageProcessor::ImageProcessor(const LedString& ledString, Hyperion* hyperion) , _log(nullptr) , _ledString(ledString) , _borderProcessor(new BlackBorderProcessor(hyperion, this)) - , _imageToLeds(nullptr) + , _imageToLedColors(nullptr) , _mappingType(0) , _userMappingType(0) - , _hardMappingType(0) + , _hardMappingType(-1) + , _accuraryLevel(0) + , _reducedPixelSetFactorFactor(1) , _hyperion(hyperion) { QString subComponent = hyperion->property("instance").toString(); @@ -48,7 +107,6 @@ ImageProcessor::ImageProcessor(const LedString& ledString, Hyperion* hyperion) ImageProcessor::~ImageProcessor() { - delete _imageToLeds; } void ImageProcessor::handleSettingsUpdate(settings::type type, const QJsonDocument& config) @@ -61,39 +119,40 @@ void ImageProcessor::handleSettingsUpdate(settings::type type, const QJsonDocume { setLedMappingType(newType); } + + int reducedPixelSetFactorFactor = obj["reducedPixelSetFactorFactor"].toString().toInt(); + setReducedPixelSetFactorFactor(reducedPixelSetFactorFactor); + + int accuracyLevel = obj["accuracyLevel"].toInt(); + setAccuracyLevel(accuracyLevel); } } -void ImageProcessor::setSize(unsigned width, unsigned height) +void ImageProcessor::setSize(int width, int height) { // Check if the existing buffer-image is already the correct dimensions - if (_imageToLeds && _imageToLeds->width() == width && _imageToLeds->height() == height) + if (!_imageToLedColors.isNull() && _imageToLedColors->width() == width && _imageToLedColors->height() == height) { return; } - // Clean up the old buffer and mapping - delete _imageToLeds; - // Construct a new buffer and mapping - _imageToLeds = (width>0 && height>0) ? (new ImageToLedsMap(width, height, 0, 0, _ledString.leds())) : nullptr; + registerProcessingUnit(width, height, 0, 0); } void ImageProcessor::setLedString(const LedString& ledString) { - if ( _imageToLeds != nullptr) + Debug(_log,""); + if ( !_imageToLedColors.isNull() ) { _ledString = ledString; // get current width/height - unsigned width = _imageToLeds->width(); - unsigned height = _imageToLeds->height(); - - // Clean up the old buffer and mapping - delete _imageToLeds; + int width = _imageToLedColors->width(); + int height = _imageToLedColors->height(); // Construct a new buffer and mapping - _imageToLeds = new ImageToLedsMap(width, height, 0, 0, _ledString.leds()); + registerProcessingUnit(width, height, 0, 0); } } @@ -107,15 +166,55 @@ bool ImageProcessor::blackBorderDetectorEnabled() const return _borderProcessor->enabled(); } +void ImageProcessor::setReducedPixelSetFactorFactor(int count) +{ + int currentReducedPixelSetFactor= _reducedPixelSetFactorFactor; + + _reducedPixelSetFactorFactor = count; + Debug(_log, "Set reduced pixel set factor to %d", _reducedPixelSetFactorFactor); + + if (currentReducedPixelSetFactor != _reducedPixelSetFactorFactor && !_imageToLedColors.isNull()) + { + int width = _imageToLedColors->width(); + int height = _imageToLedColors->height(); + + // Construct a new buffer and mapping + registerProcessingUnit(width, height, 0, 0); + } +} + +void ImageProcessor::setAccuracyLevel(int level) +{ + _accuraryLevel = level; + Debug(_log, "Set processing accuracy level to %d", _accuraryLevel); + + if (!_imageToLedColors.isNull()) + { + _imageToLedColors->setAccuracyLevel(_accuraryLevel); + } +} + void ImageProcessor::setLedMappingType(int mapType) { + int currentMappingType = _mappingType; + // if the _hardMappingType is >-1 we aren't allowed to overwrite it _userMappingType = mapType; - Debug(_log, "set user led mapping to %s", QSTRING_CSTR(mappingTypeToStr(mapType))); + + Debug(_log, "Set user LED mapping to %s", QSTRING_CSTR(mappingTypeToStr(mapType))); + if(_hardMappingType == -1) { _mappingType = mapType; } + + if (currentMappingType != _mappingType && !_imageToLedColors.isNull()) + { + int width = _imageToLedColors->width(); + int height = _imageToLedColors->height(); + + registerProcessingUnit(width, height, 0, 0); + } } void ImageProcessor::setHardLedMappingType(int mapType) diff --git a/libsrc/hyperion/ImageToLedsMap.cpp b/libsrc/hyperion/ImageToLedsMap.cpp index 783fdedd..f3e3ac24 100644 --- a/libsrc/hyperion/ImageToLedsMap.cpp +++ b/libsrc/hyperion/ImageToLedsMap.cpp @@ -3,17 +3,26 @@ using namespace hyperion; ImageToLedsMap::ImageToLedsMap( - unsigned width, - unsigned height, - unsigned horizontalBorder, - unsigned verticalBorder, - const std::vector& leds) - : _width(width) + Logger* log, + int width, + int height, + int horizontalBorder, + int verticalBorder, + const std::vector& leds, + int reducedPixelSetFactor, + int accuracyLevel) + : _log(log) + , _width(width) , _height(height) , _horizontalBorder(horizontalBorder) , _verticalBorder(verticalBorder) + , _nextPixelCount(reducedPixelSetFactor) + , _clusterCount() , _colorsMap() { + _nextPixelCount = reducedPixelSetFactor + 1; + setAccuracyLevel(accuracyLevel); + // Sanity check of the size of the borders (and width and height) Q_ASSERT(_width > 2*_verticalBorder); Q_ASSERT(_height > 2*_horizontalBorder); @@ -23,10 +32,14 @@ ImageToLedsMap::ImageToLedsMap( // Reserve enough space in the map for the leds _colorsMap.reserve(leds.size()); - const unsigned xOffset = _verticalBorder; - const unsigned actualWidth = _width - 2 * _verticalBorder; - const unsigned yOffset = _horizontalBorder; - const unsigned actualHeight = _height - 2 * _horizontalBorder; + const int xOffset = _verticalBorder; + const int actualWidth = _width - 2 * _verticalBorder; + const int yOffset = _horizontalBorder; + const int actualHeight = _height - 2 * _horizontalBorder; + + size_t totalCount = 0; + size_t totalCapacity = 0; + int ledCounter = 0; for (const Led& led : leds) { @@ -38,10 +51,10 @@ ImageToLedsMap::ImageToLedsMap( } // Compute the index boundaries for this led - unsigned minX_idx = xOffset + unsigned(qRound(actualWidth * led.minX_frac)); - unsigned maxX_idx = xOffset + unsigned(qRound(actualWidth * led.maxX_frac)); - unsigned minY_idx = yOffset + unsigned(qRound(actualHeight * led.minY_frac)); - unsigned maxY_idx = yOffset + unsigned(qRound(actualHeight * led.maxY_frac)); + int minX_idx = xOffset + int32_t(qRound(actualWidth * led.minX_frac)); + int maxX_idx = xOffset + int32_t(qRound(actualWidth * led.maxX_frac)); + int minY_idx = yOffset + int32_t(qRound(actualHeight * led.minY_frac)); + int maxY_idx = yOffset + int32_t(qRound(actualHeight * led.maxY_frac)); // make sure that the area is at least a single led large minX_idx = qMin(minX_idx, xOffset + actualWidth - 1); @@ -56,31 +69,70 @@ ImageToLedsMap::ImageToLedsMap( } // Add all the indices in the above defined rectangle to the indices for this led - const auto maxYLedCount = qMin(maxY_idx, yOffset+actualHeight); - const auto maxXLedCount = qMin(maxX_idx, xOffset+actualWidth); + const int maxYLedCount = qMin(maxY_idx, yOffset+actualHeight); + const int maxXLedCount = qMin(maxX_idx, xOffset+actualWidth); - std::vector ledColors; - ledColors.reserve((size_t) maxXLedCount*maxYLedCount); + const int realYLedCount = qAbs(maxYLedCount - minY_idx); + const int realXLedCount = qAbs(maxXLedCount - minX_idx); - for (unsigned y = minY_idx; y < maxYLedCount; ++y) + bool skipPixelProcessing {false}; + if (_nextPixelCount > 1) { - for (unsigned x = minX_idx; x < maxXLedCount; ++x) + skipPixelProcessing = true; + } + + size_t totalSize = static_cast(realYLedCount * realXLedCount); + + if (!skipPixelProcessing && totalSize > 1600) + { + skipPixelProcessing = true; + _nextPixelCount = 2; + Warning(_log, "Mapping LED/light [%d]. The current mapping area contains %d pixels which is huge. Therefore every %d pixels will be skipped. You can enable reduced processing to hide that warning.", ledCounter, totalSize, _nextPixelCount); + } + + std::vector ledColors; + ledColors.reserve(totalSize); + + for (int y = minY_idx; y < maxYLedCount; y += _nextPixelCount) + { + for (int x = minX_idx; x < maxXLedCount; x += _nextPixelCount) { - ledColors.push_back(y*width + x); + ledColors.push_back( y * width + x); } } // Add the constructed vector to the map _colorsMap.push_back(ledColors); + + totalCount += ledColors.size(); + totalCapacity += ledColors.capacity(); + + ledCounter++; } + Debug(_log, "Total index number is: %d (memory: %d). Reduced pixel set factor: %d, Accuracy level: %d, Image size: %d x %d, LED areas: %d", + totalCount, totalCapacity, reducedPixelSetFactor, accuracyLevel, width, height, leds.size()); + } -unsigned ImageToLedsMap::width() const +int ImageToLedsMap::width() const { return _width; } -unsigned ImageToLedsMap::height() const +int ImageToLedsMap::height() const { return _height; } + +void ImageToLedsMap::setAccuracyLevel (int accuracyLevel) +{ + if (accuracyLevel > 4 ) + { + Warning(_log, "Accuracy level %d is too high, it will be set to 4", accuracyLevel); + accuracyLevel = 4; + } + //Set cluster number for dominant color advanced + _clusterCount = accuracyLevel + 1; + +} + diff --git a/libsrc/hyperion/schema/schema-color.json b/libsrc/hyperion/schema/schema-color.json index abe39560..a56d3bed 100644 --- a/libsrc/hyperion/schema/schema-color.json +++ b/libsrc/hyperion/schema/schema-color.json @@ -9,20 +9,44 @@ "type" : "string", "required" : true, "title" : "edt_conf_color_imageToLedMappingType_title", - "enum" : ["multicolor_mean", "unicolor_mean"], + "enum" : ["multicolor_mean", "unicolor_mean", "multicolor_mean_squared", "dominant_color", "dominant_color_advanced"], "default" : "multicolor_mean", "options" : { - "enum_titles" : ["edt_conf_enum_multicolor_mean", "edt_conf_enum_unicolor_mean"] + "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"] }, "propertyOrder" : 1 }, + "accuracyLevel": { + "type": "integer", + "title": "edt_conf_color_accuracyLevel_title", + "minimum": 1, + "maximum": 4, + "default": 2, + "propertyOrder": 2, + "options": { + "dependencies": { + "imageToLedMappingType": "dominant_color_advanced" + } + } + }, + "reducedPixelSetFactorFactor": { + "type": "string", + "title": "edt_conf_color_reducedPixelSetFactorFactor_title", + "default": 0, + "enum" : ["0", "1", "2", "3"], + "default" : "0", + "options" : { + "enum_titles" : ["edt_conf_enum_disabled", "edt_conf_enum_low", "edt_conf_enum_medium", "edt_conf_enum_high"] + }, + "propertyOrder": 3 + }, "channelAdjustment" : { "type" : "array", "title" : "edt_conf_color_channelAdjustment_header_title", "minItems": 1, "required" : true, - "propertyOrder" : 3, + "propertyOrder" : 4, "items" : { "type" : "object", diff --git a/test/TestImage2LedsMap.cpp b/test/TestImage2LedsMap.cpp index 052a21ef..bb9aa618 100644 --- a/test/TestImage2LedsMap.cpp +++ b/test/TestImage2LedsMap.cpp @@ -2,6 +2,7 @@ // Utils includes #include #include +#include // Hyperion includes #include @@ -9,6 +10,9 @@ int main() { + Logger* log = Logger::getInstance("TestImageLedsMap"); + Logger::setLogLevel(Logger::DEBUG); + const QString schemaFile = ":/hyperion-schema"; const QString configFile = ":/hyperion_default.config"; @@ -25,7 +29,7 @@ int main() const ColorRgb testColor = {64, 123, 12}; Image image(64, 64, testColor); - hyperion::ImageToLedsMap map(64, 64, 0, 0, ledString.leds()); + hyperion::ImageToLedsMap map(log, 64, 64, 0, 0, ledString.leds()); std::vector ledColors(ledString.leds().size()); map.getMeanLedColor(image, ledColors); From 21c5c6a1220c3ea5c69de502713ac2d3b7aabc94 Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Fri, 17 Feb 2023 16:12:11 +0100 Subject: [PATCH 17/23] Update de.json (POEditor.com) --- assets/webconfig/i18n/de.json | 39 ++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/assets/webconfig/i18n/de.json b/assets/webconfig/i18n/de.json index b7c5cd5c..c98852c7 100644 --- a/assets/webconfig/i18n/de.json +++ b/assets/webconfig/i18n/de.json @@ -10,6 +10,9 @@ "InfoDialog_nowrite_foottext": "Die Webkonfiguration wird automatisch wieder freigegeben, sobald das Problem behoben wurde!", "InfoDialog_nowrite_text": "Hyperion hat keinen Schreibzugriff auf die aktuell geladene Konfiguration. Bitte korrigiere die Dateizugriffsrechte, um fortzufahren.", "InfoDialog_nowrite_title": "Fehler beim Schreibzugriff!", + "InfoDialog_systemRestart_title": "Neustart", + "InfoDialog_systemResume_title": "Aktivieren", + "InfoDialog_systemSuspend_title": "Ruhezustand", "about_3rd_party_licenses": "Drittanbieter Lizenzen", "about_3rd_party_licenses_error": "Wir hatten Probleme beim Laden der Drittanbieter Lizenzen aus dem Internet.
    Klicke hier, um die Datei auf GitHub aufzurufen.", "about_build": "Build", @@ -55,7 +58,9 @@ "conf_leds_error_get_properties_title": "Geräteeigenschaften", "conf_leds_error_hwled_gt_layout": "Die Zahl der gegebenen Hardware LEDs ($1) ist größer, als die Anzahl der im LED-Layout definierten ($2), $3 {{plural:$3|LED wird|LEDs werden}} schwarz bleiben.", "conf_leds_error_hwled_gt_maxled": "Die Zahl der gegebenen Hardware LEDs ($1) ist größer, als die Anzahl der durch den LED-Steuerungstyp unterstützen ($2).
    Die Anzahl Hardware LEDs wird auf ($3) gesetzt.", + "conf_leds_error_hwled_gt_maxled_protocol": "Die Anzahl der Hardware-LEDs ($1) ist größer als die maximale Anzahl der vom Streaming-Protokoll unterstützten LEDs ($2).
    Das Streaming-Protokoll wird auf ($3) geändert.", "conf_leds_error_hwled_lt_layout": "Die Zahl der gegebenen Hardware LEDs ($1) ist kleiner, als die Anzahl der im LED-Layout definierten ($2).
    Im LED-Layout dürfen nicht mehr LEDs konfiguriert sein, als vorhanden.", + "conf_leds_error_wled_segment_missing": "Das aktuell konfigurierte Segment ($1) ist in WLED-Gerät nicht konfiguriert.
    Überprüfe die WLED-Konfiguration!
    Die Konfigurationsseite zeigt die aktuelle WLED-Einstellung.", "conf_leds_info_ws281x": "Hyperion muss mit 'root' Rechten für diesen LED-Steuerungstyp laufen!", "conf_leds_layout_advanced": "Erweiterte Optionen", "conf_leds_layout_blacklist_num_title": "LED-Anzahl", @@ -244,6 +249,8 @@ "edt_conf_bb_unknownFrameCnt_title": "Unbekannte Bilder", "edt_conf_bge_heading_title": "Hintergrund Effekt/Farbe", "edt_conf_bobls_heading_title": "Boblight Server", + "edt_conf_color_accuracyLevel_expl": "Stufe, wie genau dominante Farben ausgewertet werden. Eine höhere Stufe erzeugt genauere Ergebnisse, erfordert aber auch mehr Rechenleistung. Sollte mit reduzierter Pixelverarbeitung kombiniert werden.", + "edt_conf_color_accuracyLevel_title": "Genauigkeitsstufe", "edt_conf_color_backlightColored_expl": "Die Hintergrundbeleuchtung kann mit oder ohne Farbanteile genutzt werden.", "edt_conf_color_backlightColored_title": "Farbige Hintergrundbeleuchtung", "edt_conf_color_backlightThreshold_expl": "Eine Beleuchtung die dauerhaft aktiv ist. (Automatisch deaktiviert bei Effekten, Farben oder im Zustand \"Aus\")", @@ -274,7 +281,7 @@ "edt_conf_color_heading_title": "Farbkalibrierung", "edt_conf_color_id_expl": "Eine vom Benutzer frei angegebene ID.", "edt_conf_color_id_title": "ID", - "edt_conf_color_imageToLedMappingType_expl": "Sofern nicht \"Mehrfarbig\", wird dein LED-Layout mit einer anderen Bildzuweisung überschrieben", + "edt_conf_color_imageToLedMappingType_expl": "Sofern nicht \"Durchschnittsfarbe einfach \", wird dein LED-Layout mit einer anderen Bildzuweisung überschrieben", "edt_conf_color_imageToLedMappingType_title": "LED-Bereich Zuordnungstyp", "edt_conf_color_leds_expl": "Zugewiesen zu allen (*) LEDs oder nur zu bestimmten LED Nummern (0-17).", "edt_conf_color_leds_title": "LED-Iindex", @@ -282,6 +289,8 @@ "edt_conf_color_magenta_title": "Magenta", "edt_conf_color_red_expl": "Kalibrierter Rotwert.", "edt_conf_color_red_title": "Rot", + "edt_conf_color_reducedPixelSetFactorFactor_expl": "Es wird nur eine reduzierte Menge von Pixeln pro definiertem LED-Bereich ausgewertet, Niedrig ~25%, Mittel ~10%, Hoch ~6%", + "edt_conf_color_reducedPixelSetFactorFactor_title": "Reduzierte Pixelverarbeitung", "edt_conf_color_saturationGain_expl": "Anpassung der Farbsättigung. 1,0 bedeutet keine Änderung, Werte größer 1,0 erhöhen die Sättigung, kleiner 1,0 verringern diese.", "edt_conf_color_saturationGain_title": "Sättigungsverstärkung", "edt_conf_color_white_expl": "Kalibrierter Weißwert.", @@ -313,6 +322,8 @@ "edt_conf_enum_color": "Farbe", "edt_conf_enum_custom": "Benutzerdefiniert", "edt_conf_enum_decay": "Dämpfung", + "edt_conf_enum_delay": "Nur Verzögerung", + "edt_conf_enum_disabled": "Deaktiviert", "edt_conf_enum_dl_error": "nur Fehler", "edt_conf_enum_dl_informational": "informativ", "edt_conf_enum_dl_nodebug": "keine Debugausgabe", @@ -321,9 +332,12 @@ "edt_conf_enum_dl_verbose1": "Stufe 1", "edt_conf_enum_dl_verbose2": "Stufe 2", "edt_conf_enum_dl_verbose3": "Stufe 3", + "edt_conf_enum_dominant_color": "Dominante Farbe - pro LED", + "edt_conf_enum_dominant_color_advanced": "Dominante Farbe Fortgeschrittene - pro LED", "edt_conf_enum_effect": "Effekt", "edt_conf_enum_gbr": "GBR", "edt_conf_enum_grb": "GRB", + "edt_conf_enum_high": "Hoch", "edt_conf_enum_hsv": "HSV", "edt_conf_enum_left_right": "von links nach rechts", "edt_conf_enum_linear": "Linear", @@ -331,7 +345,10 @@ "edt_conf_enum_logsilent": "Stille", "edt_conf_enum_logverbose": "Ausführlich", "edt_conf_enum_logwarn": "Warnung", - "edt_conf_enum_multicolor_mean": "Mehrfarbig", + "edt_conf_enum_low": "Niedrig", + "edt_conf_enum_medium": "Mittel", + "edt_conf_enum_multicolor_mean": "Durchschnittsfarbe einfach - pro LED", + "edt_conf_enum_multicolor_mean_squared": "Durchschnittsfarbe zum Quadrat - pro LED", "edt_conf_enum_please_select": "Bitte auswählen", "edt_conf_enum_rbg": "RBG", "edt_conf_enum_rgb": "RGB", @@ -341,7 +358,7 @@ "edt_conf_enum_transeffect_sudden": "Sofort", "edt_conf_enum_udp_ddp": "DDP", "edt_conf_enum_udp_raw": "RAW", - "edt_conf_enum_unicolor_mean": "Einfarbig", + "edt_conf_enum_unicolor_mean": "Durchschnittsfarbe Gesamtbild - auf alle LED angewandt", "edt_conf_fbs_heading_title": "Flatbuffers Server", "edt_conf_fbs_timeout_expl": "Wenn für die angegebene Zeit keine Daten empfangen werden, wird die Komponente (vorübergehend) deaktiviert", "edt_conf_fbs_timeout_title": "Zeitüberschreitung", @@ -439,8 +456,6 @@ "edt_conf_smooth_heading_title": "Glättung", "edt_conf_smooth_interpolationRate_expl": "Frequenz in der Zwischenschritte zur Glättung berechnet werden.", "edt_conf_smooth_interpolationRate_title": "Interpolationsfrequenz", - "edt_conf_smooth_outputRate_expl": "Die Ausgangfrequenz zum LED-Gerät", - "edt_conf_smooth_outputRate_title": "Ausgabefrequenz", "edt_conf_smooth_time_ms_expl": "Wie lange soll die Glättung Bilder sammeln?", "edt_conf_smooth_time_ms_title": "Zeit", "edt_conf_smooth_type_expl": "Algorithmus der Glättung.", @@ -614,10 +629,17 @@ "edt_dev_spec_rgbw_calibration_green": "Grün/Weiß-Kanal Aspekt", "edt_dev_spec_rgbw_calibration_limit": "Grenzwert für Weißkanal", "edt_dev_spec_rgbw_calibration_red": "Rot/Weiß-Kanal Aspekt", + "edt_dev_spec_segmentId_title": "Segment-ID", + "edt_dev_spec_segmentsOverlapValidation_error": "Korrigiere die WLED-Konfiguration! Das Segment darf sich nicht mit {{Plural:$1|Segment|Segmenten}} überschneiden: \"$2\".", + "edt_dev_spec_segmentsSwitchOffOthers_title": "Abschalten anderer Segmente", + "edt_dev_spec_segments_disabled_title": "Segment-Streaming ist in WLED deaktiviert.", + "edt_dev_spec_segments_title": "Stream zum Segment", "edt_dev_spec_serial_title": "Seriennummer", "edt_dev_spec_spipath_title": "SPI Pfad", "edt_dev_spec_sslHSTimeoutMax_title": "Streamer Handshake maximum Timeout", "edt_dev_spec_sslHSTimeoutMin_title": "Streamer Handshake minimum Timeout", + "edt_dev_spec_stayOnAfterStreaming_title": "LED bleiben nach dem Stream an", + "edt_dev_spec_stayOnAfterStreaming_title_info": "Die LEDs bleiben nach dem Streaming oder der Wiederherstellung des Status eingeschaltet.", "edt_dev_spec_stream_protocol_title": "Streaming-Protokoll", "edt_dev_spec_switchOffOnBlack_title": "Aus bei schwarz", "edt_dev_spec_switchOffOnbelowMinBrightness_title": "Aus bei Minimum", @@ -964,8 +986,11 @@ "remote_losthint": "Notiz: Alle Änderungen gehen nach einem Neustart verloren.", "remote_maptype_intro": "Für gewöhnlich entscheidet dein LED-Layout welcher Bildbereich welche LED zugewiesen bekommt, dies kann hier geändert werden. $1", "remote_maptype_label": "LED-Bereich Zuordnung", - "remote_maptype_label_multicolor_mean": "Mehrfarbig", - "remote_maptype_label_unicolor_mean": "Einfarbig", + "remote_maptype_label_dominant_color": "Dominante Farbe", + "remote_maptype_label_dominant_color_advanced": "Dominante Farbe Fortgeschrittene", + "remote_maptype_label_multicolor_mean": "Durchschnittsfarbe einfach", + "remote_maptype_label_multicolor_mean_squared": "Durchschnittsfarbe zum Quadrat", + "remote_maptype_label_unicolor_mean": "Durchschnittsfarbe Gesamtbild", "remote_optgroup_syseffets": "Mitgelieferte Effekte", "remote_optgroup_templates_custom": "Nutzer Vorlage", "remote_optgroup_templates_system": "System Vorlage", From a1bfa63343a19420295c72a5a8265c34fc7862b9 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Fri, 17 Feb 2023 17:09:12 +0100 Subject: [PATCH 18/23] Update schema-webConfig.json --- libsrc/hyperion/schema/schema-webConfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsrc/hyperion/schema/schema-webConfig.json b/libsrc/hyperion/schema/schema-webConfig.json index 15532f78..17d9e60d 100644 --- a/libsrc/hyperion/schema/schema-webConfig.json +++ b/libsrc/hyperion/schema/schema-webConfig.json @@ -16,7 +16,7 @@ "title" : "edt_conf_general_port_title", "minimum" : 80, "maximum" : 65535, - "default" : 8090.3, + "default" : 8090, "propertyOrder" : 3 }, "sslPort" : From acdf733936d2bb15c83ab03d928b9ae7805ec874 Mon Sep 17 00:00:00 2001 From: Michael Rochelle Date: Sun, 19 Feb 2023 00:36:39 -0800 Subject: [PATCH 19/23] Audio Grabber Feature (#1570) * Creating Audio Grabber Creating Audio Grabber Creating Audio Grabber. Successfully began capturing audio in windows. Starting to implement a hard-coded UV Visualizer. Got Windows DirectSound Implementation working. Hardcoded basic VU Meter. Begin working on linux audio grabber implementation. Finished Linux Draft Implementation. Minor Mods to windows implementation. Windows: - Free memory used by device id. - Prevent starting audio if the grabber is disabled - More debug logging Linux: - Prevent starting audio if the grabber is disabled Added strings to english Removed "custom" from device selection Made hard-coded visualizer values configurable. wrote values to imageData with BGR priority to enable configurable values to be set in RGB format. created logic to support "Automatic" to enable the API to select the default device. Add language key for audio in "Remote Control" section. Removed audio configuration for number of channels. This was causing an error with some devices. Fixed logic to update capture while its active. Optimizing code . UI Tweaks Destructuring. Fixed build error on linux. Custom Effects - Clean-ups and Enhancements (#1163) * Cleanup EffectFileHandler * Support Custom Effect Schemas and align EffectFileHandler * Change back to colon prefix for system effects * WebSockets - Fix error in handling fragmented frames * Correct missing colon updates * Update json with image file location for custom gif effects * Image effect deletion - considere full filename is stored in JSON * Correct selection lists indentions Creating Audio Grabber Creating Audio Grabber Creating Audio Grabber. Successfully began capturing audio in windows. Starting to implement a hard-coded UV Visualizer. Got Windows DirectSound Implementation working. Hardcoded basic VU Meter. Begin working on linux audio grabber implementation. Finished Linux Draft Implementation. Minor Mods to windows implementation. Windows: - Free memory used by device id. - Prevent starting audio if the grabber is disabled - More debug logging Linux: - Prevent starting audio if the grabber is disabled Added strings to english Removed "custom" from device selection Made hard-coded visualizer values configurable. wrote values to imageData with BGR priority to enable configurable values to be set in RGB format. created logic to support "Automatic" to enable the API to select the default device. Add language key for audio in "Remote Control" section. Removed audio configuration for number of channels. This was causing an error with some devices. Fixed logic to update capture while its active. Optimizing code . UI Tweaks Destructuring. Fixed build error on linux. Commented setVideoMode from AudioGrabber. Linux Threading changes. Implementing new API Continuing to implement audio into new APIs Fixed Audio Grabber for DirectSound on Windows Fixed UI for Audio Grabber Configuration Default AUDIO to off unless specified. fixed missing #ifdef for audio grabber. Added logic to calculate a dynamic multiplier from the signal input. Updating linux api for discovering devices. Fixed HTML/JS issues with view. Fixed NPE in Windows. Disabled setting thread priority in linux. updated the schema options check to pass through hidden states and commented the change. Updated grabber start conditions Updated Audio grabber to instantiate similar to video grabber Updated windows grabber to set "started" flag to false when shutting down. Removed "tryStart" to prevent enabling audio capture unnecessarily. Fixing instance audio grabber device configuration Added configurable resolution Reduced tolerance to 5% Fixed issue where grabber failed for additional instances when "start" was called multiple times. Fixed resolution calculation Change averaging algorithm to prevent overflowing the sum. Updated logic to stop audio grabber when disabled. Fix integer casting and rounding. Restart grabber on configuration change. Fix missing include/grabber/AudioGrabber. Disable tolerance. Added configurable tolerance. Fixed tolerance algorithm. reset multiplier on configuration change. Line Endings Proposed change and questions/request to fix implementing more of LordGrey's suggestions. Fix mode for snd_pcm_open. Latest ALSA uses SND_PCM_NONBLOCK instead of SND_PCM_OPEN_NONBLOCK defaulted multiplier to 0 "auto" defaulted tolerance to 20% changed 100 to 100.0 for pixel value percentage calculation to fix value from being 0. missed a 100 as a double so precision isn't lost during math operation. Fix Windows grabber and further cleanups Enable Audio grabbing in standard build Remove empty methods Fix audio capture priority setting Remove unused code Clean-up default config Allow additional json-editor attributes Allow multiple effects and resetting to defaults Correct default values Allow to build for Qt < 5.14 Update CodeQL build dependency Update build dependencies Remove effect1 placeholder * Renamed uvMeter to VU Meter (Volume Unit) - Fixed issues flagged by code scanning bot. * Moved stop call into destructor of implementing class. * Removed commented linux audio channel configuration logic. --------- Co-authored-by: Michael Rochelle --- .github/workflows/apt/amd64.json | 32 +- .github/workflows/codeql.yml | 2 +- .gitignore | 1 + CMakeLists.txt | 13 +- HyperionConfig.h.in | 4 + assets/webconfig/content/dashboard.html | 9 + assets/webconfig/i18n/en.json | 25 ++ assets/webconfig/index.html | 6 +- assets/webconfig/js/content_dashboard.js | 13 +- assets/webconfig/js/content_grabber.js | 177 +++++++- assets/webconfig/js/content_instcapture.js | 43 +- assets/webconfig/js/content_remote.js | 6 +- assets/webconfig/js/ui_utils.js | 5 +- cmake/packages.cmake | 3 + config/hyperion.config.json.default | 382 +++++++++--------- doc/development/CompileHowto.md | 6 +- include/db/SettingsTable.h | 2 +- include/grabber/AudioGrabber.h | 196 +++++++++ include/grabber/AudioGrabberLinux.h | 91 +++++ include/grabber/AudioGrabberWindows.h | 81 ++++ include/grabber/AudioWrapper.h | 69 ++++ include/grabber/GrabberType.h | 2 + include/hyperion/CaptureCont.h | 18 + include/hyperion/GrabberWrapper.h | 4 + include/hyperion/Hyperion.h | 3 + include/utils/Components.h | 4 + include/utils/GlobalSignals.h | 7 + include/utils/settings.h | 3 + .../JSONRPC_schema/schema-componentstate.json | 2 +- libsrc/api/JsonAPI.cpp | 57 +++ libsrc/forwarder/MessageForwarder.cpp | 16 + libsrc/grabber/CMakeLists.txt | 4 + libsrc/grabber/audio/AudioGrabber.cpp | 201 +++++++++ libsrc/grabber/audio/AudioGrabberLinux.cpp | 317 +++++++++++++++ libsrc/grabber/audio/AudioGrabberWindows.cpp | 354 ++++++++++++++++ libsrc/grabber/audio/AudioWrapper.cpp | 63 +++ libsrc/grabber/audio/CMakeLists.txt | 35 ++ libsrc/hyperion/CaptureCont.cpp | 61 +++ libsrc/hyperion/ComponentRegister.cpp | 6 + libsrc/hyperion/GrabberWrapper.cpp | 63 ++- libsrc/hyperion/SettingsManager.cpp | 14 + libsrc/hyperion/hyperion.schema.json | 4 + libsrc/hyperion/resource.qrc | 1 + .../hyperion/schema/schema-grabberAudio.json | 163 ++++++++ .../hyperion/schema/schema-instCapture.json | 23 ++ .../utils/jsonschema/QJsonSchemaChecker.cpp | 3 +- src/hyperion-remote/hyperion-remote.cpp | 4 +- src/hyperiond/CMakeLists.txt | 4 + src/hyperiond/hyperiond.cpp | 23 ++ src/hyperiond/hyperiond.h | 8 + 50 files changed, 2390 insertions(+), 243 deletions(-) create mode 100644 include/grabber/AudioGrabber.h create mode 100644 include/grabber/AudioGrabberLinux.h create mode 100644 include/grabber/AudioGrabberWindows.h create mode 100644 include/grabber/AudioWrapper.h create mode 100644 libsrc/grabber/audio/AudioGrabber.cpp create mode 100644 libsrc/grabber/audio/AudioGrabberLinux.cpp create mode 100644 libsrc/grabber/audio/AudioGrabberWindows.cpp create mode 100644 libsrc/grabber/audio/AudioWrapper.cpp create mode 100644 libsrc/grabber/audio/CMakeLists.txt create mode 100644 libsrc/hyperion/schema/schema-grabberAudio.json diff --git a/.github/workflows/apt/amd64.json b/.github/workflows/apt/amd64.json index 8c8e4e47..9e0cce1b 100644 --- a/.github/workflows/apt/amd64.json +++ b/.github/workflows/apt/amd64.json @@ -2,64 +2,64 @@ { "distribution": "Bionic", "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev", - "package-depends": "libpython3.6, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libturbojpeg, libcec4", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev", + "package-depends": "libpython3.6, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libasound2, libturbojpeg, libcec4", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Ubuntu 18.04 (Bionic Beaver) (amd64)" }, { "distribution": "Focal", "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.8, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg, libcec4", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", + "package-depends": "libpython3.8, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg, libcec4", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Ubuntu 20.04 (Focal Fossa) (amd64)" }, { "distribution": "Jammy", "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libturbojpeg, libcec6", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", + "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Ubuntu 22.04 (Jammy Jellyfish) (amd64)" }, { "distribution": "Kinetic", "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libturbojpeg, libcec6", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", + "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Ubuntu 22.10 (Kinetic Kudu) (amd64)" }, { "distribution": "Stretch", "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev", - "package-depends": "libpython3.5, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libturbojpeg0, libcec4", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev", + "package-depends": "libpython3.5, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libasound2, libturbojpeg0, libcec4", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Debian 9.x (Stretch) (amd64)" }, { "distribution": "Buster", "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.7, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg0, libcec4", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", + "package-depends": "libpython3.7, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec4", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Debian 10.x (Buster) (amd64)" }, { "distribution": "Bullseye", "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg0, libcec6", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", + "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Debian 11.x (Bullseye) (amd64)" }, { "distribution": "Bookworm", "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg0, libcec6", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", + "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Debian 12.x (Bookworm) (amd64)" } diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a2c530b1..ac3b57ba 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -32,7 +32,7 @@ jobs: if: ${{ matrix.language == 'cpp' }} run: | sudo apt-get update - sudo apt-get install --yes git cmake build-essential qtbase5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libcec-dev libxcb-image0-dev libxcb-util0-dev libxcb-shm0-dev libxcb-render0-dev libxcb-randr0-dev libxrandr-dev libxrender-dev libavahi-core-dev libavahi-compat-libdnssd-dev libturbojpeg0-dev libjpeg-dev libssl-dev + sudo apt-get install --yes git cmake build-essential qtbase5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libcec-dev libxcb-image0-dev libxcb-util0-dev libxcb-shm0-dev libxcb-render0-dev libxcb-randr0-dev libxrandr-dev libxrender-dev libavahi-core-dev libavahi-compat-libdnssd-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev - name: Initialize CodeQL uses: github/codeql-action/init@v2 diff --git a/.gitignore b/.gitignore index f86f420b..029eb5ba 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ libsrc/flatbufserver/hyperion_request_generated.h # Ignore .vs/* CMakeSettings.json +/out # Allow !.vs/launch.vs.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 68e791bc..e0c45882 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,7 @@ SET ( DEFAULT_MF OFF ) SET ( DEFAULT_OSX OFF ) SET ( DEFAULT_QT ON ) SET ( DEFAULT_V4L2 OFF ) +SET ( DEFAULT_AUDIO ON ) SET ( DEFAULT_X11 OFF ) SET ( DEFAULT_XCB OFF ) @@ -172,8 +173,10 @@ if ( "${PLATFORM}" MATCHES "osx" ) set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${SUBDIRPY}) include_directories("/opt/X11/include/") - SET ( DEFAULT_OSX ON ) - SET ( DEFAULT_DEV_USB_HID ON ) + SET ( DEFAULT_OSX ON ) + SET ( DEFAULT_AUDIO OFF ) + SET ( DEFAULT_DEV_USB_HID ON ) + elseif ( "${PLATFORM}" MATCHES "rpi" ) SET ( DEFAULT_DISPMANX ON ) SET ( DEFAULT_DEV_WS281XPWM ON ) @@ -222,6 +225,7 @@ if (HYPERION_LIGHT) SET ( DEFAULT_OSX OFF ) SET ( DEFAULT_QT OFF ) SET ( DEFAULT_V4L2 OFF ) + SET ( DEFAULT_AUDIO OFF ) SET ( DEFAULT_X11 OFF ) SET ( DEFAULT_XCB OFF ) @@ -273,6 +277,11 @@ message(STATUS "ENABLE_V4L2 = ${ENABLE_V4L2}") option(ENABLE_X11 "Enable the X11 grabber" ${DEFAULT_X11}) message(STATUS "ENABLE_X11 = ${ENABLE_X11}") +option(ENABLE_AUDIO "Enable the AUDIO grabber" ${DEFAULT_AUDIO}) +message(STATUS "ENABLE_AUDIO = ${ENABLE_AUDIO}") + +option(ENABLE_WS281XPWM "Enable the WS281x-PWM device" ${DEFAULT_WS281XPWM} ) +message(STATUS "ENABLE_WS281XPWM = ${ENABLE_WS281XPWM}") option(ENABLE_XCB "Enable the XCB grabber" ${DEFAULT_XCB}) message(STATUS "ENABLE_XCB = ${ENABLE_XCB}") diff --git a/HyperionConfig.h.in b/HyperionConfig.h.in index 4f49658c..324b703b 100644 --- a/HyperionConfig.h.in +++ b/HyperionConfig.h.in @@ -9,6 +9,10 @@ // Define to enable the DirectX grabber #cmakedefine ENABLE_DX +// Define to enable the framebuffer grabber +// Define to enable the Audio grabber +#cmakedefine ENABLE_AUDIO + // Define to enable the Framebuffer grabber #cmakedefine ENABLE_FB diff --git a/assets/webconfig/content/dashboard.html b/assets/webconfig/content/dashboard.html index 77b3e950..84b08f72 100644 --- a/assets/webconfig/content/dashboard.html +++ b/assets/webconfig/content/dashboard.html @@ -42,6 +42,14 @@ + + + Audio-Grabber + + disabled + + + @@ -135,6 +143,7 @@ + diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 9f56ef26..00af79eb 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -51,6 +51,7 @@ "conf_grabber_fg_intro": "Screen capture is your local system capture as input source, Hyperion is installed on.", "conf_grabber_inst_grabber_config_info": "Configure your capturing hardware devices to be used by the instance in advance", "conf_grabber_v4l_intro": "USB capture is a (capture) device connected via USB which is used to input source pictures for processing.", + "conf_grabber_audio_intro": "Audio capture utilizes an audio input device as the source for visualization.", "conf_helptable_expl": "Explanation", "conf_helptable_option": "Option", "conf_leds_config_error": "Error in LED/LED layout configuration", @@ -427,6 +428,8 @@ "edt_conf_instC_systemEnable_title": "Enable screen capture", "edt_conf_instC_v4lEnable_expl": "Enables the USB capture for this LED hardware instance", "edt_conf_instC_v4lEnable_title": "Enable USB capture", + "edt_conf_instC_audioEnable_expl": "Enables the Audio capture for this led hardware instance", + "edt_conf_instC_audioEnable_title": "Enable Audio capture", "edt_conf_instC_video_grabber_device_expl": "The video capture device used", "edt_conf_instC_video_grabber_device_title": "Video capture device", "edt_conf_instCapture_heading_title": "Capture Devices", @@ -527,6 +530,27 @@ "edt_conf_v4l2_hardware_set_defaults_tip": "Set device's default values for brightness, contrast, hue and saturation", "edt_conf_v4l2_noSignalCounterThreshold_title": "Signal Counter Threshold", "edt_conf_v4l2_noSignalCounterThreshold_expl": "Count of frames (check that with grabber's current FPS mode) after which the no signal is triggered", + "edt_conf_audio_device_expl": "Selected audio input device", + "edt_conf_audio_device_title": "Audio Device", + "edt_conf_audio_effects_expl": "Select an effect on how the audio signal is transformed to", + "edt_conf_audio_effects_title": "Audio Effects", + "edt_conf_audio_effect_enum_vumeter": "VU-Meter", + "edt_conf_audio_effect_hotcolor_expl": "Hot Color", + "edt_conf_audio_effect_hotcolor_title": "Hot Color", + "edt_conf_audio_effect_multiplier_expl": "Audio Signal Value multiplier", + "edt_conf_audio_effect_multiplier_title": "Multiplier", + "edt_conf_audio_effect_safecolor_expl": "Safe Color", + "edt_conf_audio_effect_safecolor_title": "Safe Color", + "edt_conf_audio_effect_safevalue_expl": "Safe Threshold", + "edt_conf_audio_effect_safevalue_title": "Safe Threshold", + "edt_conf_audio_effect_set_defaults": "Reset to default values", + "edt_conf_audio_effect_tolerance_expl": "Tolerance used when auto calculating a signal multipler from 0-100", + "edt_conf_audio_effect_tolerance_title": "Tolerance", + "edt_conf_audio_effect_warncolor_expl": "Warning Color", + "edt_conf_audio_effect_warncolor_title": "Warning Color", + "edt_conf_audio_effect_warnvalue_expl": "Warning Threshold", + "edt_conf_audio_effect_warnvalue_title": "Warning Threshold", + "edt_conf_audio_heading_title": "Audio Capture", "edt_conf_webc_crtPath_expl": "Path to the certification file (format should be PEM)", "edt_conf_webc_crtPath_title": "Certificate path", "edt_conf_webc_docroot_expl": "Local webinterface root path (just for webui developer)", @@ -876,6 +900,7 @@ "general_comp_PROTOSERVER": "Protocol Buffers Server", "general_comp_SMOOTHING": "Smoothing", "general_comp_V4L": "Capture USB-Input", + "general_comp_AUDIO": "Audio Capture", "general_country_cn": "China", "general_country_de": "Germany", "general_country_es": "Spain", diff --git a/assets/webconfig/index.html b/assets/webconfig/index.html index bb1a7d0b..9249eb39 100644 --- a/assets/webconfig/index.html +++ b/assets/webconfig/index.html @@ -24,8 +24,10 @@ - - + diff --git a/assets/webconfig/js/content_dashboard.js b/assets/webconfig/js/content_dashboard.js index 1fca3574..cc4c4ffa 100644 --- a/assets/webconfig/js/content_dashboard.js +++ b/assets/webconfig/js/content_dashboard.js @@ -58,7 +58,8 @@ $(document).ready(function () { if (components[idx].name != "ALL") { if ((components[idx].name === "FORWARDER" && window.currentHyperionInstance != 0) || (components[idx].name === "GRABBER" && !window.serverConfig.framegrabber.enable) || - (components[idx].name === "V4L" && !window.serverConfig.grabberV4L2.enable)) + (components[idx].name === "V4L" && !window.serverConfig.grabberV4L2.enable) || + (components[idx].name === "AUDIO" && !window.serverConfig.grabberAudio.enable)) continue; var comp_enabled = components[idx].enabled ? "checked" : ""; @@ -104,8 +105,9 @@ $(document).ready(function () { var screenGrabberAvailable = (window.serverInfo.grabbers.screen.available.length !== 0); var videoGrabberAvailable = (window.serverInfo.grabbers.video.available.length !== 0); + const audioGrabberAvailable = (window.serverInfo.grabbers.audio.available.length !== 0); - if (screenGrabberAvailable || videoGrabberAvailable) { + if (screenGrabberAvailable || videoGrabberAvailable || audioGrabberAvailable) { if (screenGrabberAvailable) { var screenGrabber = window.serverConfig.framegrabber.enable ? $.i18n('general_enabled') : $.i18n('general_disabled'); @@ -120,6 +122,13 @@ $(document).ready(function () { } else { $("#dash_video_grabber_row").hide(); } + + if (audioGrabberAvailable) { + const audioGrabber = window.serverConfig.grabberAudio.enable ? $.i18n('general_enabled') : $.i18n('general_disabled'); + $('#dash_audio_grabber').html(audioGrabber); + } else { + $("#dash_audio_grabber_row").hide(); + } } else { $("#dash_capture_hw").hide(); } diff --git a/assets/webconfig/js/content_grabber.js b/assets/webconfig/js/content_grabber.js index 80304cb7..20da6030 100755 --- a/assets/webconfig/js/content_grabber.js +++ b/assets/webconfig/js/content_grabber.js @@ -4,9 +4,11 @@ $(document).ready(function () { var screenGrabberAvailable = (window.serverInfo.grabbers.screen.available.length !== 0); var videoGrabberAvailable = (window.serverInfo.grabbers.video.available.length !== 0); + const audioGrabberAvailable = (window.serverInfo.grabbers.audio.available.length !== 0); var CEC_ENABLED = (jQuery.inArray("cec", window.serverInfo.services) !== -1); var conf_editor_video = null; + var conf_editor_audio = null; var conf_editor_screen = null; var configuredDevice = ""; @@ -38,6 +40,22 @@ $(document).ready(function () { } } + // Audio-Grabber + if (audioGrabberAvailable) { + $('#conf_cont').append(createRow('conf_cont_audio')); + $('#conf_cont_audio').append(createOptPanel('fa-volume', $.i18n("edt_conf_audio_heading_title"), 'editor_container_audiograbber', 'btn_submit_audiograbber', 'panel-system', 'audiograbberPanelId')); + + if (storedAccess === 'expert') { + const conf_cont_audio_footer = document.getElementById("editor_container_audiograbber").nextElementSibling; + $(conf_cont_audio_footer).prepend(' '); + } + + if (window.showOptHelp) { + $('#conf_cont_audio').append(createHelpTable(window.schema.grabberAudio.properties, $.i18n("edt_conf_audio_heading_title"), "audiograbberHelpPanelId")); + } + } + JSONEditor.defaults.custom_validators.push(function (schema, value, path) { var errors = []; @@ -694,6 +712,121 @@ $(document).ready(function () { }); } + // External Input Sources (Audio-Grabbers) + if (audioGrabberAvailable) { + + conf_editor_audio = createJsonEditor('editor_container_audiograbber', { + grabberAudio: window.schema.grabberAudio + }, true, true); + + conf_editor_audio.on('ready', () => { + // Trigger conf_editor_audio.watch - 'root.grabberAudio.enable' + const audioEnable = window.serverConfig.grabberAudio.enable; + conf_editor_audio.getEditor("root.grabberAudio.enable").setValue(audioEnable); + }); + + conf_editor_audio.on('change', () => { + + // Validate the current editor's content + if (!conf_editor_audio.validate().length) { + const deviceSelected = conf_editor_audio.getEditor("root.grabberAudio.available_devices").getValue(); + switch (deviceSelected) { + case "SELECT": + showInputOptionsForKey(conf_editor_audio, "grabberAudio", ["enable", "available_devices"], false); + break; + case "NONE": + showInputOptionsForKey(conf_editor_audio, "grabberAudio", ["enable", "available_devices"], false); + break; + default: + window.readOnlyMode ? $('#btn_submit_audiograbber').prop('disabled', true) : $('#btn_submit_audiograbber').prop('disabled', false); + break; + } + } + else { + $('#btn_submit_audiograbber').prop('disabled', true); + } + }); + + // Enable + conf_editor_audio.watch('root.grabberAudio.enable', () => { + + const audioEnable = conf_editor_audio.getEditor("root.grabberAudio.enable").getValue(); + if (audioEnable) + { + showInputOptionsForKey(conf_editor_audio, "grabberAudio", "enable", true); + + $('#btn_audiograbber_set_effect_defaults').show(); + + if (window.showOptHelp) { + $('#audiograbberHelpPanelId').show(); + } + + discoverInputSources("audio"); + } + else + { + $('#btn_submit_audiograbber').prop('disabled', false); + $('#btn_audiograbber_set_effect_defaults').hide(); + showInputOptionsForKey(conf_editor_audio, "grabberAudio", "enable", false); + $('#audiograbberHelpPanelId').hide(); + } + }); + + // Available Devices + conf_editor_audio.watch('root.grabberAudio.available_devices', () => { + const deviceSelected = conf_editor_audio.getEditor("root.grabberAudio.available_devices").getValue(); + + if (deviceSelected === "SELECT" || deviceSelected === "NONE" || deviceSelected === "") { + $('#btn_submit_audiograbber').prop('disabled', true); + showInputOptionsForKey(conf_editor_audio, "grabberAudio", ["enable", "available_devices"], false); + } + else + { + showInputOptionsForKey(conf_editor_audio, "grabberAudio", ["enable", "available_devices"], true); + + const deviceProperties = getPropertiesOfDevice("audio", deviceSelected); + + //Update hidden input element + conf_editor_audio.getEditor("root.grabberAudio.device").setValue(deviceProperties.device); + + //Enfore configured JSON-editor dependencies + conf_editor_audio.notifyWatchers("root.grabberAudio.audioEffect"); + + //Enable set defaults button + $('#btn_audiograbber_set_effect_defaults').prop('disabled', false); + + if (conf_editor_audio.validate().length && !window.readOnlyMode) { + $('#btn_submit_audiograbber').prop('disabled', false); + } + } + }); + + $('#btn_submit_audiograbber').off().on('click', function () { + const saveOptions = conf_editor_audio.getValue(); + + const instCaptOptions = window.serverConfig.instCapture; + instCaptOptions.audioEnable = true; + saveOptions.instCapture = instCaptOptions; + + requestWriteConfig(saveOptions); + }); + + // ------------------------------------------------------------------ + + $('#btn_audiograbber_set_effect_defaults').off().on('click', function () { + const currentEffect = conf_editor_audio.getEditor("root.grabberAudio.audioEffect").getValue(); + var effectEditor = conf_editor_audio.getEditor("root.grabberAudio." + currentEffect); + var defaultProperties = effectEditor.schema.defaultProperties; + + var default_values = {}; + for (const item of defaultProperties) { + + default_values[item] = effectEditor.schema.properties[item].default; + } + effectEditor.setValue(default_values); + }); + } + // ------------------------------------------------------------------ ////////////////////////////////////////////////// @@ -706,6 +839,9 @@ $(document).ready(function () { if (videoGrabberAvailable) { createHint("intro", $.i18n('conf_grabber_v4l_intro'), "editor_container_videograbber"); } + if (audioGrabberAvailable) { + createHint("intro", $.i18n('conf_grabber_audio_intro'), "editor_container_audiograbber"); + } } removeOverlay(); @@ -773,6 +909,38 @@ $(document).ready(function () { } }; + // build dynamic audio input enum + const updateAudioSourcesList = function (type, discoveryInfo) { + const enumVals = []; + const enumTitelVals = []; + let enumDefaultVal = ""; + let addSelect = false; + + if (jQuery.isEmptyObject(discoveryInfo)) { + enumVals.push("NONE"); + enumTitelVals.push($.i18n('edt_conf_grabber_discovered_none')); + } + else { + for (const device of discoveryInfo) { + enumVals.push(device.device_name); + } + conf_editor_audio.getEditor('root.grabberAudio').enable(); + configuredDevice = window.serverConfig.grabberAudio.available_devices; + + if ($.inArray(configuredDevice, enumVals) != -1) { + enumDefaultVal = configuredDevice; + } + else { + addSelect = true; + } + } + + if (enumVals.length > 0) { + updateJsonEditorSelection(conf_editor_audio, 'root.grabberAudio', + 'available_devices', {}, enumVals, enumTitelVals, enumDefaultVal, addSelect, false); + } + }; + async function discoverInputSources(type, params) { const result = await requestInputSourcesDiscovery(type, params); @@ -782,7 +950,8 @@ $(document).ready(function () { } else { discoveryResult = { - "video_sources": [] + "video_sources": [], + "audio_soruces": [] }; } @@ -799,6 +968,12 @@ $(document).ready(function () { updateVideoSourcesList(type, discoveredInputSources.video); } break; + case "audio": + discoveredInputSources.audio = discoveryResult.audio_sources; + if (audioGrabberAvailable) { + updateAudioSourcesList(type, discoveredInputSources.audio); + } + break; } } diff --git a/assets/webconfig/js/content_instcapture.js b/assets/webconfig/js/content_instcapture.js index fd399112..abe00081 100644 --- a/assets/webconfig/js/content_instcapture.js +++ b/assets/webconfig/js/content_instcapture.js @@ -3,6 +3,7 @@ $(document).ready(function () { var screenGrabberAvailable = (window.serverInfo.grabbers.screen.available.length !== 0); var videoGrabberAvailable = (window.serverInfo.grabbers.video.available.length !== 0); + const audioGrabberAvailable = (window.serverInfo.grabbers.audio.available.length !== 0); var BOBLIGHT_ENABLED = (jQuery.inArray("boblight", window.serverInfo.services) !== -1); @@ -15,7 +16,7 @@ $(document).ready(function () { // Instance Capture if (window.showOptHelp) { - if (screenGrabberAvailable || videoGrabberAvailable) { + if (screenGrabberAvailable || videoGrabberAvailable || audioGrabberAvailable) { $('#conf_cont').append(createRow('conf_cont_instCapt')); $('#conf_cont_instCapt').append(createOptPanel('fa-camera', $.i18n("edt_conf_instCapture_heading_title"), 'editor_container_instCapt', 'btn_submit_instCapt', '')); $('#conf_cont_instCapt').append(createHelpTable(window.schema.instCapture.properties, $.i18n("edt_conf_instCapture_heading_title"))); @@ -29,7 +30,7 @@ $(document).ready(function () { } else { $('#conf_cont').addClass('row'); - if (screenGrabberAvailable || videoGrabberAvailable) { + if (screenGrabberAvailable || videoGrabberAvailable || audioGrabberAvailable) { $('#conf_cont').append(createOptPanel('fa-camera', $.i18n("edt_conf_instCapture_heading_title"), 'editor_container_instCapt', 'btn_submit_instCapt', '')); } if (BOBLIGHT_ENABLED) { @@ -37,7 +38,7 @@ $(document).ready(function () { } } - if (screenGrabberAvailable || videoGrabberAvailable) { + if (screenGrabberAvailable || videoGrabberAvailable || audioGrabberAvailable) { // Instance Capture conf_editor_instCapt = createJsonEditor('editor_container_instCapt', { @@ -81,12 +82,29 @@ $(document).ready(function () { showInputOptionForItem(conf_editor_instCapt, "instCapture", "v4lPriority", false); } + if (audioGrabberAvailable) { + if (!window.serverConfig.grabberAudio.enable) { + conf_editor_instCapt.getEditor("root.instCapture.audioEnable").setValue(false); + conf_editor_instCapt.getEditor("root.instCapture.audioEnable").disable(); + } + else { + conf_editor_instCapt.getEditor("root.instCapture.audioEnable").setValue(window.serverConfig.instCapture.audioEnable); + + } + } else { + showInputOptionForItem(conf_editor_instCapt, "instCapture", "audioGrabberDevice", false); + showInputOptionForItem(conf_editor_instCapt, "instCapture", "audioEnable", false); + showInputOptionForItem(conf_editor_instCapt, "instCapture", "audioPriority", false); + } + }); conf_editor_instCapt.on('change', function () { if (!conf_editor_instCapt.validate().length) { - if (!window.serverConfig.framegrabber.enable && !window.serverConfig.grabberV4L2.enable) { + if (!window.serverConfig.framegrabber.enable && + !window.serverConfig.grabberV4L2.enable && + !window.serverConfig.grabberAudio.enable) { $('#btn_submit_instCapt').prop('disabled', true); } else { window.readOnlyMode ? $('#btn_submit_instCapt').prop('disabled', true) : $('#btn_submit_instCapt').prop('disabled', false); @@ -130,6 +148,23 @@ $(document).ready(function () { } }); + conf_editor_instCapt.watch('root.instCapture.audioEnable', () => { + const audioEnable = conf_editor_instCapt.getEditor("root.instCapture.audioEnable").getValue(); + if (audioEnable) { + conf_editor_instCapt.getEditor("root.instCapture.audioGrabberDevice").setValue(window.serverConfig.grabberAudio.available_devices); + conf_editor_instCapt.getEditor("root.instCapture.audioGrabberDevice").disable(); + showInputOptions("instCapture", ["audioGrabberDevice"], true); + showInputOptions("instCapture", ["audioPriority"], true); + } + else { + if (!window.serverConfig.grabberAudio.enable) { + conf_editor_instCapt.getEditor("root.instCapture.audioEnable").disable(); + } + showInputOptions("instCapture", ["audioGrabberDevice"], false); + showInputOptions("instCapture", ["audioPriority"], false); + } + }); + $('#btn_submit_instCapt').off().on('click', function () { requestWriteConfig(conf_editor_instCapt.getValue()); }); diff --git a/assets/webconfig/js/content_remote.js b/assets/webconfig/js/content_remote.js index e689c885..09fc475c 100644 --- a/assets/webconfig/js/content_remote.js +++ b/assets/webconfig/js/content_remote.js @@ -154,6 +154,9 @@ $(document).ready(function () { case "V4L": owner = $.i18n('general_comp_V4L') + ': (' + owner + ')'; break; + case "AUDIO": + owner = $.i18n('general_comp_AUDIO') + ': (' + owner + ')'; + break; case "BOBLIGHTSERVER": owner = $.i18n('general_comp_BOBLIGHTSERVER'); break; @@ -219,7 +222,8 @@ $(document).ready(function () { for (const comp of components) { if (comp.name === "ALL" || (comp.name === "FORWARDER" && window.currentHyperionInstance != 0) || (comp.name === "GRABBER" && !window.serverConfig.framegrabber.enable) || - (comp.name === "V4L" && !window.serverConfig.grabberV4L2.enable)) + (comp.name === "V4L" && !window.serverConfig.grabberV4L2.enable) || + (comp.name === "AUDIO" && !window.serverConfig.grabberAudio.enable)) continue; const enable_style = (comp.enabled ? "checked" : ""); diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index e1a2ae5b..d2a998dc 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -1220,6 +1220,7 @@ function getSystemInfo() { //info += '- Log lvl: ' + window.serverConfig.logger.level + '\n'; info += '- Avail Screen Cap.: ' + window.serverInfo.grabbers.screen.available + '\n'; info += '- Avail Video Cap.: ' + window.serverInfo.grabbers.video.available + '\n'; + info += '- Avail Audio Cap.: ' + window.serverInfo.grabbers.audio.available + '\n'; info += '- Avail Services: ' + window.serverInfo.services + '\n'; info += '- Config path: ' + shy.rootPath + '\n'; info += '- Database: ' + (shy.readOnlyMode ? "ready-only" : "read/write") + '\n'; @@ -1317,9 +1318,9 @@ function showInputOptionsForKey(editor, item, showForKeys, state) { } } - for (var key in editor.schema.properties[item].properties) { + for (let key in editor.schema.properties[item].properties) { if ($.inArray(key, keysToshow) === -1) { - var accessLevel = editor.schema.properties[item].properties[key].access; + const accessLevel = editor.schema.properties[item].properties[key].access; var hidden = false; if (editor.schema.properties[item].properties[key].options) { diff --git a/cmake/packages.cmake b/cmake/packages.cmake index 1dd8f6a6..046cefe7 100644 --- a/cmake/packages.cmake +++ b/cmake/packages.cmake @@ -155,6 +155,9 @@ if(ENABLE_FLATBUF_CONNECT) if(ENABLE_V4L2) SET ( CPACK_COMPONENTS_ALL ${CPACK_COMPONENTS_ALL} "hyperion_v4l2" ) endif() + if(ENABLE_AUDIO) + SET ( CPACK_COMPONENTS_ALL ${CPACK_COMPONENTS_ALL} "hyperion_audio" ) + endif() if(ENABLE_X11) SET ( CPACK_COMPONENTS_ALL ${CPACK_COMPONENTS_ALL} "hyperion_x11" ) endif() diff --git a/config/hyperion.config.json.default b/config/hyperion.config.json.default index a52f64ba..84218810 100644 --- a/config/hyperion.config.json.default +++ b/config/hyperion.config.json.default @@ -1,239 +1,236 @@ { - "general" : - { - "name" : "My Hyperion Config", - "configVersion": "configVersionValue", - "previousVersion": "previousVersionValue", - "watchedVersionBranch" : "Stable", - "showOptHelp" : true + "general": { + "name": "My Hyperion Config", + "configVersion": "configVersionValue", + "previousVersion": "previousVersionValue", + "watchedVersionBranch": "Stable", + "showOptHelp": true }, - "logger" : - { - "level" : "warn" + "logger": { + "level": "warn" }, - "device" : - { - "type" : "file", - "hardwareLedCount" : 1, - "autoStart" : true, - "output" : "/dev/null", - "colorOrder" : "rgb", - "latchTime" : 0, + "device": { + "type": "file", + "hardwareLedCount": 1, + "autoStart": true, + "output": "/dev/null", + "colorOrder": "rgb", + "latchTime": 0, "rewriteTime": 0, "enableAttempts": 6, "enableAttemptsInterval": 15 }, - "color" : - { - "imageToLedMappingType" : "multicolor_mean", - "channelAdjustment" : - [ + "color": { + "imageToLedMappingType": "multicolor_mean", + "channelAdjustment": [ { - "id" : "default", - "leds" : "*", - "white" : [255,255,255], - "red" : [255,0,0], - "green" : [0,255,0], - "blue" : [0,0,255], - "cyan" : [0,255,255], - "magenta" : [255,0,255], - "yellow" : [255,255,0], - "gammaRed" : 2.2, - "gammaGreen" : 2.2, - "gammaBlue" : 2.2, - "backlightThreshold" : 0, - "backlightColored" : false, - "brightness" : 100, - "brightnessCompensation" : 100, - "saturationGain" : 1.0, - "brightnessGain" : 1.0 + "id": "default", + "leds": "*", + "white": [ 255, 255, 255 ], + "red": [ 255, 0, 0 ], + "green": [ 0, 255, 0 ], + "blue": [ 0, 0, 255 ], + "cyan": [ 0, 255, 255 ], + "magenta": [ 255, 0, 255 ], + "yellow": [ 255, 255, 0 ], + "gammaRed": 2.2, + "gammaGreen": 2.2, + "gammaBlue": 2.2, + "backlightThreshold": 0, + "backlightColored": false, + "brightness": 100, + "brightnessCompensation": 100, + "saturationGain": 1.0, + "brightnessGain": 1.0 } ] }, - "smoothing" : - { - "enable" : true, - "type" : "linear", - "time_ms" : 200, - "updateFrequency" : 25.0000, - "interpolationRate" : 25.0000, - "decay" : 1, - "dithering" : false, - "updateDelay" : 0 + "smoothing": { + "enable": true, + "type": "linear", + "time_ms": 200, + "updateFrequency": 25.0000, + "interpolationRate": 25.0000, + "decay": 1, + "dithering": false, + "updateDelay": 0 }, - "grabberV4L2" : - { - "enable" : false, - "device" : "none", - "input" : 0, - "encoding" : "NO_CHANGE", - "width" : 0, - "height" : 0, - "fps" : 15, - "flip" : "NO_CHANGE", - "fpsSoftwareDecimation" : 0, - "sizeDecimation" : 8, - "cropLeft" : 0, - "cropRight" : 0, - "cropTop" : 0, - "cropBottom" : 0, - "redSignalThreshold" : 0, - "greenSignalThreshold" : 100, - "blueSignalThreshold" : 0, - "signalDetection" : false, - "noSignalCounterThreshold" : 200, - "cecDetection" : false, - "sDVOffsetMin" : 0.1, - "sDVOffsetMax" : 0.9, - "sDHOffsetMin" : 0.4, - "sDHOffsetMax" : 0.46, - "hardware_brightness" : 0, - "hardware_contrast" : 0, - "hardware_saturation" : 0, - "hardware_hue" : 0 + "grabberV4L2": { + "enable": false, + "device": "none", + "input": 0, + "encoding": "NO_CHANGE", + "width": 0, + "height": 0, + "fps": 15, + "flip": "NO_CHANGE", + "fpsSoftwareDecimation": 0, + "sizeDecimation": 8, + "cropLeft": 0, + "cropRight": 0, + "cropTop": 0, + "cropBottom": 0, + "redSignalThreshold": 0, + "greenSignalThreshold": 100, + "blueSignalThreshold": 0, + "signalDetection": false, + "noSignalCounterThreshold": 200, + "cecDetection": false, + "sDVOffsetMin": 0.1, + "sDVOffsetMax": 0.9, + "sDHOffsetMin": 0.4, + "sDHOffsetMax": 0.46, + "hardware_brightness": 0, + "hardware_contrast": 0, + "hardware_saturation": 0, + "hardware_hue": 0 }, - "framegrabber" : - { - "enable" : false, - "device" : "auto", - "input" : 0, - "width" : 80, - "height" : 45, - "fps" : 10, - "pixelDecimation" : 8, - "cropLeft" : 0, - "cropRight" : 0, - "cropTop" : 0, - "cropBottom" : 0 + "grabberAudio": { + "enable": false, + "device": "auto", + "audioEffect": "vuMeter", + "vuMeter": { + "flip": "NO_CHANGE", + "hotColor": [ 255, 0, 0 ], + "multiplier": 1, + "safeColor": [ 0, 255, 0 ], + "safeValue": 45, + "tolerance": 5, + "warnColor": [ 255, 255, 0 ], + "warnValue": 80 + } }, - "blackborderdetector" : - { - "enable" : true, - "threshold" : 5, - "unknownFrameCnt" : 600, - "borderFrameCnt" : 50, - "maxInconsistentCnt" : 10, - "blurRemoveCnt" : 1, - "mode" : "default" + "framegrabber": { + "enable": false, + "device": "auto", + "input": 0, + "width": 80, + "height": 45, + "fps": 10, + "pixelDecimation": 8, + "cropLeft": 0, + "cropRight": 0, + "cropTop": 0, + "cropBottom": 0 }, - "foregroundEffect" : - { - "enable" : true, - "type" : "effect", - "color" : [0,0,255], - "effect" : "Rainbow swirl fast", - "duration_ms" : 3000 + "blackborderdetector": { + "enable": true, + "threshold": 5, + "unknownFrameCnt": 600, + "borderFrameCnt": 50, + "maxInconsistentCnt": 10, + "blurRemoveCnt": 1, + "mode": "default" }, - "backgroundEffect" : - { - "enable" : false, - "type" : "effect", - "color" : [255,138,0], - "effect" : "Warm mood blobs" + "foregroundEffect": { + "enable": true, + "type": "effect", + "color": [ 0, 0, 255 ], + "effect": "Rainbow swirl fast", + "duration_ms": 3000 }, - "forwarder" : - { - "enable" : false, - "jsonapi" : [], - "flatbuffer" : [] + "backgroundEffect": { + "enable": false, + "type": "effect", + "color": [ 255, 138, 0 ], + "effect": "Warm mood blobs" }, - "jsonServer" : - { - "port" : 19444 + "forwarder": { + "enable": false, + "jsonapi": [], + "flatbuffer": [] }, - "flatbufServer" : - { - "enable" : true, - "port" : 19400, - "timeout" : 5 + "jsonServer": { + "port": 19444 }, - "protoServer" : - { - "enable" : true, - "port" : 19445, - "timeout" : 5 + "flatbufServer": { + "enable": true, + "port": 19400, + "timeout": 5 }, - "boblightServer" : - { - "enable" : false, - "port" : 19333, - "priority" : 128 + "protoServer": { + "enable": true, + "port": 19445, + "timeout": 5 }, - "webConfig" : - { - "document_root" : "", - "port" : 8090, - "sslPort" : 8092, - "crtPath" : "", - "keyPath" : "", - "keyPassPhrase" : "" + "boblightServer": { + "enable": false, + "port": 19333, + "priority": 128 }, - "effects" : - { - "paths" : ["$ROOT/custom-effects"], - "disable": [""] + "webConfig": { + "document_root": "", + "port": 8090, + "sslPort": 8092, + "crtPath": "", + "keyPath": "", + "keyPassPhrase": "" }, - "instCapture" : - { - "systemEnable" : false, - "systemGrabberDevice" : "NONE", - "systemPriority" : 250, - "v4lEnable" : false, - "v4lGrabberDevice" : "NONE", - "v4lPriority" : 240 + "effects": { + "paths": [ "$ROOT/custom-effects" ], + "disable": [ "" ] }, - "network" : - { - "internetAccessAPI" : false, - "restirctedInternetAccessAPI" : false, - "ipWhitelist" : [], - "apiAuth" : true, - "localApiAuth" : false, + "instCapture": { + "systemEnable": false, + "systemGrabberDevice": "NONE", + "systemPriority": 250, + "v4lEnable": false, + "v4lGrabberDevice": "NONE", + "v4lPriority": 240, + "audioEnable": false, + "audioGrabberDevice": "NONE", + "audioPriority": 230 + }, + + "network": { + "internetAccessAPI": false, + "restirctedInternetAccessAPI": false, + "ipWhitelist": [], + "apiAuth": true, + "localApiAuth": false, "localAdminAuth": true }, - "ledConfig" : - { - "classic": - { - "top" : 1, - "bottom" : 0, - "left" : 0, - "right" : 0, - "glength" : 0, - "gpos" : 0, - "position" : 0, - "reverse" : false, - "hdepth" : 8, - "vdepth" : 5, - "overlap" : 0, - "edgegap" : 0, - "ptlh" : 0, - "ptlv" : 0, - "ptrh" : 100, - "ptrv" : 0, - "pblh" : 0, - "pblv" : 100, - "pbrh" : 100, - "pbrv" : 100 - }, + "ledConfig": { + "classic": { + "top": 1, + "bottom": 0, + "left": 0, + "right": 0, + "glength": 0, + "gpos": 0, + "position": 0, + "reverse": false, + "hdepth": 8, + "vdepth": 5, + "overlap": 0, + "edgegap": 0, + "ptlh": 0, + "ptlv": 0, + "ptrh": 100, + "ptrv": 0, + "pblh": 0, + "pblv": 100, + "pbrh": 100, + "pbrv": 100 + }, "matrix": { "ledshoriz": 1, @@ -244,8 +241,7 @@ } }, - "leds": - [ + "leds": [ { "hmax": 1, "hmin": 0, diff --git a/doc/development/CompileHowto.md b/doc/development/CompileHowto.md index f13f880d..f3b5502c 100644 --- a/doc/development/CompileHowto.md +++ b/doc/development/CompileHowto.md @@ -87,14 +87,14 @@ cd $HYPERION_HOME ```console sudo apt-get update -sudo apt-get install git cmake build-essential qtbase5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libturbojpeg0-dev libjpeg-dev libssl-dev +sudo apt-get install git cmake build-essential qtbase5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev ``` **Ubuntu (22.04+) - Qt6 based** ```console sudo apt-get update -sudo apt-get install git cmake build-essential qt6-base-dev libqt6serialport6-dev libvulkan-dev libgl1-mesa-dev libusb-1.0-0-dev python3-dev libturbojpeg0-dev libjpeg-dev libssl-dev pkg-config +sudo apt-get install git cmake build-essential qt6-base-dev libqt6serialport6-dev libvulkan-dev libgl1-mesa-dev libusb-1.0-0-dev python3-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev pkg-config ``` **For Linux X11/XCB grabber support** @@ -136,7 +136,7 @@ See [AUR](https://aur.archlinux.org/packages/?O=0&SeB=nd&K=hyperion&outdated=&SB The following dependencies are needed to build hyperion.ng on fedora. ```console sudo dnf -y groupinstall "Development Tools" -sudo dnf install python3-devel qt-devel qt5-qtbase-devel qt5-qtserialport-devel xrandr xcb-util-image-devel qt5-qtx11extras-devel turbojpeg-devel libusb-devel xcb-util-devel dbus-devel openssl-devel fedora-packager rpmdevtools gcc libcec-devel +sudo dnf install python3-devel qt-devel qt5-qtbase-devel qt5-qtserialport-devel xrandr xcb-util-image-devel qt5-qtx11extras-devel alsa-lib-devel turbojpeg-devel libusb-devel xcb-util-devel dbus-devel openssl-devel fedora-packager rpmdevtools gcc libcec-devel ``` After installing the dependencies, you can continue with the compile instructions later on this page (the more detailed way..). diff --git a/include/db/SettingsTable.h b/include/db/SettingsTable.h index 19bf1054..d7184869 100644 --- a/include/db/SettingsTable.h +++ b/include/db/SettingsTable.h @@ -111,7 +111,7 @@ public: // server port services list << "jsonServer" << "protoServer" << "flatbufServer" << "forwarder" << "webConfig" << "network" // capture - << "framegrabber" << "grabberV4L2" + << "framegrabber" << "grabberV4L2" << "grabberAudio" // other << "logger" << "general"; diff --git a/include/grabber/AudioGrabber.h b/include/grabber/AudioGrabber.h new file mode 100644 index 00000000..115f3aa8 --- /dev/null +++ b/include/grabber/AudioGrabber.h @@ -0,0 +1,196 @@ +#ifndef AUDIOGRABBER_H +#define AUDIOGRABBER_H + +#include +#include +#include + +// Hyperion-utils includes +#include +#include +#include + +/// +/// Base Audio Grabber Class +/// +/// This class is extended by the windows audio grabber to provied DirectX9 access to the audio devices +/// This class is extended by the linux audio grabber to provide ALSA access to the audio devices +/// +/// @brief The DirectX9 capture implementation +/// +class AudioGrabber : public Grabber +{ + Q_OBJECT + public: + + /// + /// Device properties + /// + /// this structure holds the name, id, and inputs of the enumerated audio devices. + /// + struct DeviceProperties + { + QString name = QString(); + QString id = QString(); + QMultiMap inputs = QMultiMap(); + }; + + AudioGrabber(); + ~AudioGrabber() override; + + /// + /// Start audio capturing session + /// + /// @returns true if successful + virtual bool start(); + + /// + /// Stop audio capturing session + /// + virtual void stop(); + + /// + /// Restart the audio capturing session + /// + void restart(); + + Logger* getLog(); + + /// + /// Set Device + /// + /// configures the audio device used by the grabber + /// + /// @param[in] device identifier of audio device + void setDevice(const QString& device); + + /// + /// Set Configuration + /// + /// sets the audio grabber's configuration parameters + /// + /// @param[in] config object of configuration parameters + void setConfiguration(const QJsonObject& config); + + /// + /// Reset Multiplier + /// + /// resets the calcualted audio multiplier so that it is recalculated + /// currently the multiplier is only reduced based on loudness. + /// + /// TODO: also calculate a low signal and reset the multiplier + /// + void resetMultiplier(); + + /// + /// Discover + /// + /// discovers audio devices in the system + /// + /// @param[in] params discover parameters + /// @return array of audio devices + virtual QJsonArray discover(const QJsonObject& params); + + signals: + void newFrame(const Image& image); + + protected: + + /// + /// Process Audio Frame + /// + /// this functions takes in an audio buffer and emits a visual representation of the audio data + /// + /// @param[in] buffer The audio buffer to process + /// @param[in] length The length of audio data in the buffer + void processAudioFrame(int16_t* buffer, int length); + + /// + /// Audio device id / properties map + /// + /// properties include information such as name, inputs, and etc... + /// + QMap _deviceProperties; + + /// + /// Current device + /// + QString _device; + + /// + /// Hot Color + /// + /// the color of the leds when the signal is high or hot + /// + QColor _hotColor; + + /// + /// Warn value + /// + /// The maximum value of the warning color. above this threshold the signal is considered hot + /// + int _warnValue; + + /// + /// Warn color + /// + /// the color of the leds when the signal is in between the safe and warn value threshold + /// + QColor _warnColor; + + /// + /// Save value + /// + /// The maximum value of the safe color. above this threshold the signal enteres the warn zone. + /// below the signal is in the safe zone. + /// + int _safeValue; + + /// + /// Safe color + /// + /// the color of the leds when the signal is below the safe threshold + /// + QColor _safeColor; + + /// + /// Multiplier + /// + /// this value is used to multiply the input signal value. Some inputs may have a very low signal + /// and the multiplier is used to get the desired visualization. + /// + /// When the multiplier is configured to 0, the multiplier is automatically configured based off of the average + /// signal amplitude and tolernace. + /// + double _multiplier; + + /// + /// Tolerance + /// + /// The tolerance is used to calculate what percentage of the top end part of the signal to ignore when + /// calculating the multiplier. This enables the effect to reach the hot zone with an auto configured multiplier + /// + int _tolerance; + + /// + /// Dynamic Multiplier + /// + /// This is the current value of the automatically configured multiplier. + /// + double _dynamicMultiplier; + + /// + /// Started + /// + /// true if the capturing session has started. + /// + bool _started; + +private: + /// + /// @brief free the _screen pointer + /// + void freeResources(); +}; + +#endif // AUDIOGRABBER_H diff --git a/include/grabber/AudioGrabberLinux.h b/include/grabber/AudioGrabberLinux.h new file mode 100644 index 00000000..0f19ae6c --- /dev/null +++ b/include/grabber/AudioGrabberLinux.h @@ -0,0 +1,91 @@ +#ifndef AUDIOGRABBERLINUX_H +#define AUDIOGRABBERLINUX_H + +#include +#include +#include + +// Hyperion-utils includes +#include + +/// +/// @brief The Linux Audio capture implementation +/// +class AudioGrabberLinux : public AudioGrabber +{ + public: + + AudioGrabberLinux(); + ~AudioGrabberLinux() override; + + /// + /// Process audio buffer + /// + void processAudioBuffer(snd_pcm_sframes_t frames); + + /// + /// Is Running Flag + /// + std::atomic _isRunning; + + /// + /// Current capture device + /// + snd_pcm_t * _captureDevice; + + public slots: + + /// + /// Start audio capturing session + /// + /// @returns true if successful + bool start() override; + + /// + /// Stop audio capturing session + /// + void stop() override; + + /// + /// Discovery audio devices + /// + QJsonArray discover(const QJsonObject& params) override; + + private: + /// + /// Refresh audio devices + /// + void refreshDevices(); + + /// + /// Configure current audio capture interface + /// + bool configureCaptureInterface(); + + /// + /// Get device name from path + /// + QString getDeviceName(const QString& devicePath) const; + + /// + /// Current sample rate + /// + unsigned int _sampleRate; + + /// + /// Audio capture thread + /// + pthread_t _audioThread; + + /// + /// ALSA device configuration parameters + /// + snd_pcm_hw_params_t * _captureDeviceConfig; +}; + +/// +/// Audio processing thread function +/// +static void* AudioThreadRunner(void* params); + +#endif // AUDIOGRABBERLINUX_H diff --git a/include/grabber/AudioGrabberWindows.h b/include/grabber/AudioGrabberWindows.h new file mode 100644 index 00000000..747212c2 --- /dev/null +++ b/include/grabber/AudioGrabberWindows.h @@ -0,0 +1,81 @@ +#ifndef AUDIOGRABBERWINDOWS_H +#define AUDIOGRABBERWINDOWS_H + +// Hyperion-utils includes +#include +#include + +/// +/// @brief The Windows Audio capture implementation +/// +class AudioGrabberWindows : public AudioGrabber +{ + public: + + AudioGrabberWindows(); + ~AudioGrabberWindows() override; + + public slots: + bool start() override; + void stop() override; + QJsonArray discover(const QJsonObject& params) override; + + private: + void refreshDevices(); + bool configureCaptureInterface(); + QString getDeviceName(const QString& devicePath) const; + + void processAudioBuffer(); + + LPDIRECTSOUNDCAPTURE8 recordingDevice; + LPDIRECTSOUNDCAPTUREBUFFER8 recordingBuffer; + + HANDLE audioThread; + DWORD bufferCapturePosition; + DWORD bufferCaptureSize; + DWORD notificationSize; + + static DWORD WINAPI AudioThreadRunner(LPVOID param); + HANDLE notificationEvent; + std::atomic isRunning{ false }; + +static BOOL CALLBACK DirectSoundEnumProcessor(LPGUID deviceIdGuid, LPCTSTR deviceDescStr, + LPCTSTR deviceModelStr, LPVOID context) +{ + // Skip undefined audio devices + if (deviceIdGuid == NULL) + return TRUE; + + QMap* devices = (QMap*)context; + + AudioGrabber::DeviceProperties device; + + // Process Device ID + LPOLESTR deviceIdStr; + HRESULT res = StringFromCLSID(*deviceIdGuid, &deviceIdStr); + if (FAILED(res)) + { + Error(Logger::getInstance("AUDIOGRABBER"), "Failed to get CLSID-string for %s with error: 0x%08x: %s", deviceDescStr, res, std::system_category().message(res).c_str()); + return FALSE; + } + + QString deviceId = QString::fromWCharArray(deviceIdStr); + + CoTaskMemFree(deviceIdStr); + + // Process Device Information + QString deviceName = QString::fromLocal8Bit(deviceDescStr); + + Debug(Logger::getInstance("AUDIOGRABBER"), "Found Audio Device: %s", deviceDescStr); + + device.id = deviceId; + device.name = deviceName; + + devices->insert(deviceId, device); + + return TRUE; +} + +}; + +#endif // AUDIOGRABBERWINDOWS_H diff --git a/include/grabber/AudioWrapper.h b/include/grabber/AudioWrapper.h new file mode 100644 index 00000000..9e13c933 --- /dev/null +++ b/include/grabber/AudioWrapper.h @@ -0,0 +1,69 @@ +#pragma once + +#include + +#ifdef WIN32 + #include +#endif + +#ifdef __linux__ + #include +#endif + +/// +/// Audio Grabber wrapper +/// +class AudioWrapper : public GrabberWrapper +{ + public: + + // The AudioWrapper has no params... + + /// + /// Constructs the Audio grabber with a specified grab size and update rate. + /// + /// @param[in] device Audio Device Identifier + /// @param[in] updateRate_Hz The audio grab rate [Hz] + /// + AudioWrapper(); + + /// + /// Destructor of this Audio grabber. Releases any claimed resources. + /// + ~AudioWrapper() override; + + /// + /// Settings update handler + /// + void handleSettingsUpdate(settings::type type, const QJsonDocument& config) override; + + public slots: + /// + /// Performs a single frame grab and computes the led-colors + /// + void action() override; + + /// + /// Start audio capturing session + /// + /// @returns true if successful + bool start() override; + + /// + /// Stop audio capturing session + /// + void stop() override; + + private: + void newFrame(const Image& image); + + /// The actual grabber +#ifdef WIN32 + AudioGrabberWindows _grabber; +#endif + +#ifdef __linux__ + AudioGrabberLinux _grabber; +#endif + +}; diff --git a/include/grabber/GrabberType.h b/include/grabber/GrabberType.h index c5e1de8a..a2ebd431 100644 --- a/include/grabber/GrabberType.h +++ b/include/grabber/GrabberType.h @@ -4,12 +4,14 @@ enum class GrabberType { SCREEN, VIDEO, + AUDIO, }; enum class GrabberTypeFilter { ALL, SCREEN, VIDEO, + AUDIO, }; #endif // GRABBERTYPE_H diff --git a/include/hyperion/CaptureCont.h b/include/hyperion/CaptureCont.h index 93e63eab..c1eca98e 100644 --- a/include/hyperion/CaptureCont.h +++ b/include/hyperion/CaptureCont.h @@ -20,6 +20,7 @@ public: void setSystemCaptureEnable(bool enable); void setV4LCaptureEnable(bool enable); + void setAudioCaptureEnable(bool enable); private slots: /// @@ -48,11 +49,22 @@ private slots: /// void handleV4lImage(const QString& name, const Image & image); + /// + /// @brief forward audio image + /// @param image The image + /// + void handleAudioImage(const QString& name, const Image& image); + /// /// @brief Is called from _v4lInactiveTimer to set source after specific time to inactive /// void setV4lInactive(); + /// + /// @brief Is called from _audioInactiveTimer to set source after specific time to inactive + /// + void setAudioInactive(); + /// /// @brief Is called from _systemInactiveTimer to set source after specific time to inactive /// @@ -73,4 +85,10 @@ private: quint8 _v4lCaptPrio; QString _v4lCaptName; QTimer* _v4lInactiveTimer; + + /// Reflect state of audio capture and prio + bool _audioCaptEnabled; + quint8 _audioCaptPrio; + QString _audioCaptName; + QTimer* _audioInactiveTimer; }; diff --git a/include/hyperion/GrabberWrapper.h b/include/hyperion/GrabberWrapper.h index fd58449c..8edf6ab0 100644 --- a/include/hyperion/GrabberWrapper.h +++ b/include/hyperion/GrabberWrapper.h @@ -43,8 +43,10 @@ public: static QMap GRABBER_SYS_CLIENTS; static QMap GRABBER_V4L_CLIENTS; + static QMap GRABBER_AUDIO_CLIENTS; static bool GLOBAL_GRABBER_SYS_ENABLE; static bool GLOBAL_GRABBER_V4L_ENABLE; + static bool GLOBAL_GRABBER_AUDIO_ENABLE; /// /// Starts the grabber which produces led values with the specified update rate @@ -78,6 +80,8 @@ public: void setSysGrabberState(bool sysGrabberState){ GLOBAL_GRABBER_SYS_ENABLE = sysGrabberState; } bool getV4lGrabberState() const { return GLOBAL_GRABBER_V4L_ENABLE; } void setV4lGrabberState(bool v4lGrabberState){ GLOBAL_GRABBER_V4L_ENABLE = v4lGrabberState; } + bool getAudioGrabberState() const { return GLOBAL_GRABBER_AUDIO_ENABLE; } + void setAudioGrabberState(bool audioGrabberState) { GLOBAL_GRABBER_AUDIO_ENABLE = audioGrabberState; } static QStringList availableGrabbers(GrabberTypeFilter type = GrabberTypeFilter::ALL); diff --git a/include/hyperion/Hyperion.h b/include/hyperion/Hyperion.h index 34351502..3a442f40 100644 --- a/include/hyperion/Hyperion.h +++ b/include/hyperion/Hyperion.h @@ -454,6 +454,9 @@ signals: /// Signal which is emitted, when a new V4l proto image should be forwarded void forwardV4lProtoMessage(const QString&, const Image&); + /// Signal which is emitted, when a new Audio proto image should be forwarded + void forwardAudioProtoMessage(const QString&, const Image&); + #if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER) /// Signal which is emitted, when a new Flat-/Proto- Buffer image should be forwarded void forwardBufferMessage(const QString&, const Image&); diff --git a/include/utils/Components.h b/include/utils/Components.h index ac5c95ed..841604a9 100644 --- a/include/utils/Components.h +++ b/include/utils/Components.h @@ -23,6 +23,7 @@ enum Components #endif COMP_GRABBER, COMP_V4L, + COMP_AUDIO, COMP_COLOR, COMP_IMAGE, COMP_EFFECT, @@ -50,6 +51,7 @@ inline const char* componentToString(Components c) #endif case COMP_GRABBER: return "Framegrabber"; case COMP_V4L: return "V4L capture device"; + case COMP_AUDIO: return "Audio capture device"; case COMP_COLOR: return "Solid color"; case COMP_EFFECT: return "Effect"; case COMP_IMAGE: return "Image"; @@ -79,6 +81,7 @@ inline const char* componentToIdString(Components c) #endif case COMP_GRABBER: return "GRABBER"; case COMP_V4L: return "V4L"; + case COMP_AUDIO: return "AUDIO"; case COMP_COLOR: return "COLOR"; case COMP_EFFECT: return "EFFECT"; case COMP_IMAGE: return "IMAGE"; @@ -107,6 +110,7 @@ inline Components stringToComponent(const QString& component) #endif if (cmp == "GRABBER") return COMP_GRABBER; if (cmp == "V4L") return COMP_V4L; + if (cmp == "AUDIO") return COMP_AUDIO; if (cmp == "COLOR") return COMP_COLOR; if (cmp == "EFFECT") return COMP_EFFECT; if (cmp == "IMAGE") return COMP_IMAGE; diff --git a/include/utils/GlobalSignals.h b/include/utils/GlobalSignals.h index 8819d8ce..8fbdba29 100644 --- a/include/utils/GlobalSignals.h +++ b/include/utils/GlobalSignals.h @@ -56,6 +56,13 @@ signals: void setBufferImage(const QString& name, const Image& image); #endif + /// + /// @brief PIPE audioCapture images from audioCapture over HyperionDaemon to Hyperion class + /// @param name The name of the audio capture (path) that is currently active + /// @param image The prepared image + /// + void setAudioImage(const QString& name, const Image& image); + /// /// @brief PIPE the register command for a new global input over HyperionDaemon to Hyperion class /// @param[in] priority The priority of the channel diff --git a/include/utils/settings.h b/include/utils/settings.h index 175a2df1..dab90204 100644 --- a/include/utils/settings.h +++ b/include/utils/settings.h @@ -19,6 +19,7 @@ namespace settings { SYSTEMCAPTURE, GENERAL, V4L2, + AUDIO, JSONSERVER, LEDCONFIG, LEDS, @@ -52,6 +53,7 @@ namespace settings { case SYSTEMCAPTURE: return "framegrabber"; case GENERAL: return "general"; case V4L2: return "grabberV4L2"; + case AUDIO: return "grabberAudio"; case JSONSERVER: return "jsonServer"; case LEDCONFIG: return "ledConfig"; case LEDS: return "leds"; @@ -84,6 +86,7 @@ namespace settings { else if (type == "framegrabber") return SYSTEMCAPTURE; else if (type == "general") return GENERAL; else if (type == "grabberV4L2") return V4L2; + else if (type == "grabberAudio") return AUDIO; else if (type == "jsonServer") return JSONSERVER; else if (type == "ledConfig") return LEDCONFIG; else if (type == "leds") return LEDS; diff --git a/libsrc/api/JSONRPC_schema/schema-componentstate.json b/libsrc/api/JSONRPC_schema/schema-componentstate.json index 08cd1912..f46324dc 100644 --- a/libsrc/api/JSONRPC_schema/schema-componentstate.json +++ b/libsrc/api/JSONRPC_schema/schema-componentstate.json @@ -21,7 +21,7 @@ "component": { "type" : "string", - "enum" : ["ALL", "SMOOTHING", "BLACKBORDER", "FORWARDER", "BOBLIGHTSERVER", "GRABBER", "V4L", "LEDDEVICE"], + "enum" : ["ALL", "SMOOTHING", "BLACKBORDER", "FORWARDER", "BOBLIGHTSERVER", "GRABBER", "V4L", "AUDIO", "LEDDEVICE"], "required": true }, "state": diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index e4418d54..212dc14b 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -29,6 +29,18 @@ #include #endif +#if defined(ENABLE_AUDIO) + #include + + #ifdef WIN32 + #include + #endif + + #ifdef __linux__ + #include + #endif +#endif + #if defined(ENABLE_X11) #include #endif @@ -554,6 +566,7 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString info["ledDevices"] = ledDevices; QJsonObject grabbers; + // SCREEN QJsonObject screenGrabbers; if (GrabberWrapper::getInstance() != nullptr) { @@ -573,6 +586,7 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString } screenGrabbers["available"] = availableScreenGrabbers; + // VIDEO QJsonObject videoGrabbers; if (GrabberWrapper::getInstance() != nullptr) { @@ -592,8 +606,31 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString } videoGrabbers["available"] = availableVideoGrabbers; + // AUDIO + QJsonObject audioGrabbers; + if (GrabberWrapper::getInstance() != nullptr) + { + QStringList activeGrabbers = GrabberWrapper::getInstance()->getActive(_hyperion->getInstanceIndex(), GrabberTypeFilter::AUDIO); + + QJsonArray activeGrabberNames; + for (auto grabberName : activeGrabbers) + { + activeGrabberNames.append(grabberName); + } + + audioGrabbers["active"] = activeGrabberNames; + } + QJsonArray availableAudioGrabbers; + for (auto grabber : GrabberWrapper::availableGrabbers(GrabberTypeFilter::AUDIO)) + { + availableAudioGrabbers.append(grabber); + } + audioGrabbers["available"] = availableAudioGrabbers; + grabbers.insert("screen", screenGrabbers); grabbers.insert("video", videoGrabbers); + grabbers.insert("audio", audioGrabbers); + info["grabbers"] = grabbers; info["videomode"] = QString(videoMode2String(_hyperion->getCurrentVideoMode())); @@ -1607,6 +1644,7 @@ void JsonAPI::handleInputSourceCommand(const QJsonObject& message, const QString QJsonObject inputSourcesDiscovered; inputSourcesDiscovered.insert("sourceType", sourceType); QJsonArray videoInputs; + QJsonArray audioInputs; #if defined(ENABLE_V4L2) || defined(ENABLE_MF) @@ -1623,6 +1661,24 @@ void JsonAPI::handleInputSourceCommand(const QJsonObject& message, const QString } else #endif + +#if defined(ENABLE_AUDIO) + if (sourceType == "audio") + { + AudioGrabber* grabber; +#ifdef WIN32 + grabber = new AudioGrabberWindows(); +#endif + +#ifdef __linux__ + grabber = new AudioGrabberLinux(); +#endif + QJsonObject params; + audioInputs = grabber->discover(params); + delete grabber; + } + else +#endif { DebugIf(verbose, _log, "sourceType: [%s]", QSTRING_CSTR(sourceType)); @@ -1719,6 +1775,7 @@ void JsonAPI::handleInputSourceCommand(const QJsonObject& message, const QString } inputSourcesDiscovered["video_sources"] = videoInputs; + inputSourcesDiscovered["audio_sources"] = audioInputs; DebugIf(verbose, _log, "response: [%s]", QString(QJsonDocument(inputSourcesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); diff --git a/libsrc/forwarder/MessageForwarder.cpp b/libsrc/forwarder/MessageForwarder.cpp index e9dc40f9..b795cbe8 100644 --- a/libsrc/forwarder/MessageForwarder.cpp +++ b/libsrc/forwarder/MessageForwarder.cpp @@ -115,6 +115,9 @@ void MessageForwarder::enableTargets(bool enable, const QJsonObject& config) case hyperion::COMP_V4L: connect(_hyperion, &Hyperion::forwardV4lProtoMessage, this, &MessageForwarder::forwardFlatbufferMessage, Qt::UniqueConnection); break; + case hyperion::COMP_AUDIO: + connect(_hyperion, &Hyperion::forwardAudioProtoMessage, this, &MessageForwarder::forwardFlatbufferMessage, Qt::UniqueConnection); + break; #if defined(ENABLE_FLATBUF_SERVER) case hyperion::COMP_FLATBUFSERVER: #endif @@ -153,6 +156,7 @@ void MessageForwarder::handlePriorityChanges(int priority) switch (activeCompId) { case hyperion::COMP_GRABBER: disconnect(_hyperion, &Hyperion::forwardV4lProtoMessage, nullptr, nullptr); + disconnect(_hyperion, &Hyperion::forwardAudioProtoMessage, nullptr, nullptr); #if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER) disconnect(_hyperion, &Hyperion::forwardBufferMessage, nullptr, nullptr); #endif @@ -160,11 +164,20 @@ void MessageForwarder::handlePriorityChanges(int priority) break; case hyperion::COMP_V4L: disconnect(_hyperion, &Hyperion::forwardSystemProtoMessage, nullptr, nullptr); + disconnect(_hyperion, &Hyperion::forwardAudioProtoMessage, nullptr, nullptr); #if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER) disconnect(_hyperion, &Hyperion::forwardBufferMessage, nullptr, nullptr); #endif connect(_hyperion, &Hyperion::forwardV4lProtoMessage, this, &MessageForwarder::forwardFlatbufferMessage, Qt::UniqueConnection); break; + case hyperion::COMP_AUDIO: + disconnect(_hyperion, &Hyperion::forwardSystemProtoMessage, nullptr, nullptr); + disconnect(_hyperion, &Hyperion::forwardV4lProtoMessage, nullptr, nullptr); +#if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER) + disconnect(_hyperion, &Hyperion::forwardBufferMessage, nullptr, nullptr); +#endif + connect(_hyperion, &Hyperion::forwardAudioProtoMessage, this, &MessageForwarder::forwardFlatbufferMessage, Qt::UniqueConnection); + break; #if defined(ENABLE_FLATBUF_SERVER) case hyperion::COMP_FLATBUFSERVER: #endif @@ -172,6 +185,7 @@ void MessageForwarder::handlePriorityChanges(int priority) case hyperion::COMP_PROTOSERVER: #endif #if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER) + disconnect(_hyperion, &Hyperion::forwardAudioProtoMessage, nullptr, nullptr); disconnect(_hyperion, &Hyperion::forwardSystemProtoMessage, nullptr, nullptr); disconnect(_hyperion, &Hyperion::forwardV4lProtoMessage, nullptr, nullptr); connect(_hyperion, &Hyperion::forwardBufferMessage, this, &MessageForwarder::forwardFlatbufferMessage, Qt::UniqueConnection); @@ -180,6 +194,7 @@ void MessageForwarder::handlePriorityChanges(int priority) default: disconnect(_hyperion, &Hyperion::forwardSystemProtoMessage, nullptr, nullptr); disconnect(_hyperion, &Hyperion::forwardV4lProtoMessage, nullptr, nullptr); + disconnect(_hyperion, &Hyperion::forwardAudioProtoMessage, nullptr, nullptr); #if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER) disconnect(_hyperion, &Hyperion::forwardBufferMessage, nullptr, nullptr); #endif @@ -373,6 +388,7 @@ void MessageForwarder::stopFlatbufferTargets() { disconnect(_hyperion, &Hyperion::forwardSystemProtoMessage, nullptr, nullptr); disconnect(_hyperion, &Hyperion::forwardV4lProtoMessage, nullptr, nullptr); + disconnect(_hyperion, &Hyperion::forwardAudioProtoMessage, nullptr, nullptr); #if defined(ENABLE_FLATBUF_SERVER) || defined(ENABLE_PROTOBUF_SERVER) disconnect(_hyperion, &Hyperion::forwardBufferMessage, nullptr, nullptr); #endif diff --git a/libsrc/grabber/CMakeLists.txt b/libsrc/grabber/CMakeLists.txt index 5c0feea1..c729ad0a 100644 --- a/libsrc/grabber/CMakeLists.txt +++ b/libsrc/grabber/CMakeLists.txt @@ -33,3 +33,7 @@ endif(ENABLE_QT) if (ENABLE_DX) add_subdirectory(directx) endif(ENABLE_DX) + +if (ENABLE_AUDIO) + add_subdirectory(audio) +endif() diff --git a/libsrc/grabber/audio/AudioGrabber.cpp b/libsrc/grabber/audio/AudioGrabber.cpp new file mode 100644 index 00000000..1e625a1e --- /dev/null +++ b/libsrc/grabber/audio/AudioGrabber.cpp @@ -0,0 +1,201 @@ +#include +#include +#include +#include +#include +#include +#include + +// Constants +namespace { + const uint16_t RESOLUTION = 255; +} + +#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) +namespace QColorConstants +{ + const QColor Black = QColor(0xFF, 0x00, 0x00); + const QColor Red = QColor(0xFF, 0x00, 0x00); + const QColor Green = QColor(0x00, 0xFF, 0x00); + const QColor Blue = QColor(0x00, 0x00, 0xFF); + const QColor Yellow = QColor(0xFF, 0xFF, 0x00); +} +#endif + //End of constants + +AudioGrabber::AudioGrabber() + : Grabber("AudioGrabber") + , _deviceProperties() + , _device("none") + , _hotColor(QColorConstants::Red) + , _warnValue(80) + , _warnColor(QColorConstants::Yellow) + , _safeValue(45) + , _safeColor(QColorConstants::Green) + , _multiplier(0) + , _tolerance(20) + , _dynamicMultiplier(INT16_MAX) + , _started(false) +{ +} + +AudioGrabber::~AudioGrabber() +{ + freeResources(); +} + +void AudioGrabber::freeResources() +{ +} + +void AudioGrabber::setDevice(const QString& device) +{ + _device = device; + + if (_started) + { + this->stop(); + this->start(); + } +} + +void AudioGrabber::setConfiguration(const QJsonObject& config) +{ + QJsonArray hotColorArray = config["hotColor"].toArray(QJsonArray::fromVariantList(QList({ QVariant(255), QVariant(0), QVariant(0) }))); + QJsonArray warnColorArray = config["warnColor"].toArray(QJsonArray::fromVariantList(QList({ QVariant(255), QVariant(255), QVariant(0) }))); + QJsonArray safeColorArray = config["safeColor"].toArray(QJsonArray::fromVariantList(QList({ QVariant(0), QVariant(255), QVariant(0) }))); + + _hotColor = QColor(hotColorArray.at(0).toInt(), hotColorArray.at(1).toInt(), hotColorArray.at(2).toInt()); + _warnColor = QColor(warnColorArray.at(0).toInt(), warnColorArray.at(1).toInt(), warnColorArray.at(2).toInt()); + _safeColor = QColor(safeColorArray.at(0).toInt(), safeColorArray.at(1).toInt(), safeColorArray.at(2).toInt()); + + _warnValue = config["warnValue"].toInt(80); + _safeValue = config["safeValue"].toInt(45); + _multiplier = config["multiplier"].toDouble(0); + _tolerance = config["tolerance"].toInt(20); +} + +void AudioGrabber::resetMultiplier() +{ + _dynamicMultiplier = INT16_MAX; +} + +void AudioGrabber::processAudioFrame(int16_t* buffer, int length) +{ + // Apply Visualizer and Construct Image + + // TODO: Pass Audio Frame to python and let the script calculate the image. + + // TODO: Support Stereo capture with different meters per side + + // Default VUMeter - Later Make this pluggable for different audio effects + + double averageAmplitude = 0; + // Calculate the the average amplitude value in the buffer + for (int i = 0; i < length; i++) + { + averageAmplitude += fabs(buffer[i]) / length; + } + + double * currentMultiplier; + + if (_multiplier < std::numeric_limits::epsilon()) + { + // Dynamically calculate multiplier. + const double pendingMultiplier = INT16_MAX / fmax(1.0, averageAmplitude + ((_tolerance / 100.0) * averageAmplitude)); + + if (pendingMultiplier < _dynamicMultiplier) + _dynamicMultiplier = pendingMultiplier; + + currentMultiplier = &_dynamicMultiplier; + } + else + { + // User defined multiplier + currentMultiplier = &_multiplier; + } + + // Apply multiplier to average amplitude + const double result = averageAmplitude * (*currentMultiplier); + + // Calculate the average percentage + const double percentage = fmin(result / INT16_MAX, 1); + + // Calculate the value + const int value = static_cast(ceil(percentage * RESOLUTION)); + + // Draw Image + QImage image(1, RESOLUTION, QImage::Format_RGB888); + image.fill(QColorConstants::Black); + + int safePixelValue = static_cast(round(( _safeValue / 100.0) * RESOLUTION)); + int warnPixelValue = static_cast(round(( _warnValue / 100.0) * RESOLUTION)); + + for (int i = 0; i < RESOLUTION; i++) + { + QColor color = QColorConstants::Black; + int position = RESOLUTION - i; + + if (position < safePixelValue) + { + color = _safeColor; + } + else if (position < warnPixelValue) + { + color = _warnColor; + } + else + { + color = _hotColor; + } + + if (position < value) + { + image.setPixelColor(0, i, color); + } + else + { + image.setPixelColor(0, i, QColorConstants::Black); + } + } + + // Convert to Image + Image finalImage (static_cast(image.width()), static_cast(image.height())); + for (int y = 0; y < image.height(); y++) + { + memcpy((unsigned char*)finalImage.memptr() + y * image.width() * 3, static_cast(image.scanLine(y)), image.width() * 3); + } + + emit newFrame(finalImage); +} + +Logger* AudioGrabber::getLog() +{ + return _log; +} + +bool AudioGrabber::start() +{ + resetMultiplier(); + + _started = true; + + return true; +} + +void AudioGrabber::stop() +{ + _started = false; +} + +void AudioGrabber::restart() +{ + stop(); + start(); +} + +QJsonArray AudioGrabber::discover(const QJsonObject& /*params*/) +{ + QJsonArray result; // Return empty result + return result; +} diff --git a/libsrc/grabber/audio/AudioGrabberLinux.cpp b/libsrc/grabber/audio/AudioGrabberLinux.cpp new file mode 100644 index 00000000..8938e043 --- /dev/null +++ b/libsrc/grabber/audio/AudioGrabberLinux.cpp @@ -0,0 +1,317 @@ +#include + +#include + +#include +#include + +typedef void* (*THREADFUNCPTR)(void*); + +AudioGrabberLinux::AudioGrabberLinux() + : AudioGrabber() + , _isRunning{ false } + , _captureDevice {nullptr} + , _sampleRate(44100) +{ +} + +AudioGrabberLinux::~AudioGrabberLinux() +{ + this->stop(); +} + +void AudioGrabberLinux::refreshDevices() +{ + Debug(_log, "Enumerating Audio Input Devices"); + + _deviceProperties.clear(); + + snd_ctl_t* deviceHandle; + int soundCard {-1}; + int error {-1}; + int cardInput {-1}; + + snd_ctl_card_info_t* cardInfo; + snd_pcm_info_t* deviceInfo; + + snd_ctl_card_info_alloca(&cardInfo); + snd_pcm_info_alloca(&deviceInfo); + + while (snd_card_next(&soundCard) > -1) + { + if (soundCard < 0) + { + break; + } + + char cardId[32]; + sprintf(cardId, "hw:%d", soundCard); + + if ((error = snd_ctl_open(&deviceHandle, cardId, SND_CTL_NONBLOCK)) < 0) + { + Error(_log, "Erorr opening device: (%i): %s", soundCard, snd_strerror(error)); + continue; + } + + if ((error = snd_ctl_card_info(deviceHandle, cardInfo)) < 0) + { + Error(_log, "Erorr getting hardware info: (%i): %s", soundCard, snd_strerror(error)); + snd_ctl_close(deviceHandle); + continue; + } + + cardInput = -1; + + while (true) + { + if (snd_ctl_pcm_next_device(deviceHandle, &cardInput) < 0) + Error(_log, "Error selecting device input"); + + if (cardInput < 0) + break; + + snd_pcm_info_set_device(deviceInfo, static_cast(cardInput)); + snd_pcm_info_set_subdevice(deviceInfo, 0); + snd_pcm_info_set_stream(deviceInfo, SND_PCM_STREAM_CAPTURE); + + if ((error = snd_ctl_pcm_info(deviceHandle, deviceInfo)) < 0) + { + if (error != -ENOENT) + Error(_log, "Digital Audio Info: (%i): %s", soundCard, snd_strerror(error)); + + continue; + } + + AudioGrabber::DeviceProperties device; + + device.id = QString("hw:%1,%2").arg(snd_pcm_info_get_card(deviceInfo)).arg(snd_pcm_info_get_device(deviceInfo)); + device.name = QString("%1: %2").arg(snd_ctl_card_info_get_name(cardInfo),snd_pcm_info_get_name(deviceInfo)); + + Debug(_log, "Found sound card (%s): %s", QSTRING_CSTR(device.id), QSTRING_CSTR(device.name)); + + _deviceProperties.insert(device.id, device); + } + + snd_ctl_close(deviceHandle); + } +} + +bool AudioGrabberLinux::configureCaptureInterface() +{ + int error = -1; + QString name = (_device.isEmpty() || _device == "auto") ? "default" : (_device); + + if ((error = snd_pcm_open(&_captureDevice, QSTRING_CSTR(name) , SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK)) < 0) + { + Error(_log, "Failed to open audio device: %s, - %s", QSTRING_CSTR(_device), snd_strerror(error)); + return false; + } + + if ((error = snd_pcm_hw_params_malloc(&_captureDeviceConfig)) < 0) + { + Error(_log, "Failed to create hardware parameters: %s", snd_strerror(error)); + snd_pcm_close(_captureDevice); + return false; + } + + if ((error = snd_pcm_hw_params_any(_captureDevice, _captureDeviceConfig)) < 0) + { + Error(_log, "Failed to initialize hardware parameters: %s", snd_strerror(error)); + snd_pcm_hw_params_free(_captureDeviceConfig); + snd_pcm_close(_captureDevice); + return false; + } + + if ((error = snd_pcm_hw_params_set_access(_captureDevice, _captureDeviceConfig, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) + { + Error(_log, "Failed to configure interleaved mode: %s", snd_strerror(error)); + snd_pcm_hw_params_free(_captureDeviceConfig); + snd_pcm_close(_captureDevice); + return false; + } + + if ((error = snd_pcm_hw_params_set_format(_captureDevice, _captureDeviceConfig, SND_PCM_FORMAT_S16_LE)) < 0) + { + Error(_log, "Failed to configure capture format: %s", snd_strerror(error)); + snd_pcm_hw_params_free(_captureDeviceConfig); + snd_pcm_close(_captureDevice); + return false; + } + + if ((error = snd_pcm_hw_params_set_rate_near(_captureDevice, _captureDeviceConfig, &_sampleRate, nullptr)) < 0) + { + Error(_log, "Failed to configure sample rate: %s", snd_strerror(error)); + snd_pcm_hw_params_free(_captureDeviceConfig); + snd_pcm_close(_captureDevice); + return false; + } + + if ((error = snd_pcm_hw_params(_captureDevice, _captureDeviceConfig)) < 0) + { + Error(_log, "Failed to configure hardware parameters: %s", snd_strerror(error)); + snd_pcm_hw_params_free(_captureDeviceConfig); + snd_pcm_close(_captureDevice); + return false; + } + + snd_pcm_hw_params_free(_captureDeviceConfig); + + if ((error = snd_pcm_prepare(_captureDevice)) < 0) + { + Error(_log, "Failed to prepare audio interface: %s", snd_strerror(error)); + snd_pcm_close(_captureDevice); + return false; + } + + if ((error = snd_pcm_start(_captureDevice)) < 0) + { + Error(_log, "Failed to start audio interface: %s", snd_strerror(error)); + snd_pcm_close(_captureDevice); + return false; + } + + return true; +} + +bool AudioGrabberLinux::start() +{ + if (!_isEnabled) + return false; + + if (_isRunning.load(std::memory_order_acquire)) + return true; + + Debug(_log, "Start Audio With %s", QSTRING_CSTR(getDeviceName(_device))); + + if (!configureCaptureInterface()) + return false; + + _isRunning.store(true, std::memory_order_release); + + pthread_attr_t threadAttributes; + int threadPriority = 1; + + sched_param schedulerParameter; + schedulerParameter.sched_priority = threadPriority; + + if (pthread_attr_init(&threadAttributes) != 0) + { + Debug(_log, "Failed to create thread attributes"); + stop(); + return false; + } + + if (pthread_create(&_audioThread, &threadAttributes, static_cast(&AudioThreadRunner), static_cast(this)) != 0) + { + Debug(_log, "Failed to create audio capture thread"); + stop(); + return false; + } + + AudioGrabber::start(); + + return true; +} + +void AudioGrabberLinux::stop() +{ + if (!_isRunning.load(std::memory_order_acquire)) + return; + + Debug(_log, "Stopping Audio Interface"); + + _isRunning.store(false, std::memory_order_release); + + if (_audioThread != 0) { + pthread_join(_audioThread, NULL); + } + + snd_pcm_close(_captureDevice); + + AudioGrabber::stop(); +} + +void AudioGrabberLinux::processAudioBuffer(snd_pcm_sframes_t frames) +{ + if (!_isRunning.load(std::memory_order_acquire)) + return; + + ssize_t bytes = snd_pcm_frames_to_bytes(_captureDevice, frames); + + int16_t * buffer = static_cast(calloc(static_cast(bytes / 2), sizeof(int16_t))); + + if (frames == 0) + { + buffer[0] = 0; + processAudioFrame(buffer, 1); + } + else + { + snd_pcm_sframes_t framesRead = snd_pcm_readi(_captureDevice, buffer, static_cast(frames)); + + if (framesRead < frames) + { + Error(_log, "Error reading audio. Got %d frames instead of %d", framesRead, frames); + } + else + { + processAudioFrame(buffer, static_cast(snd_pcm_frames_to_bytes(_captureDevice, framesRead)) / 2); + } + } + + free(buffer); +} + +QJsonArray AudioGrabberLinux::discover(const QJsonObject& /*params*/) +{ + refreshDevices(); + + QJsonArray devices; + + for (auto deviceIterator = _deviceProperties.begin(); deviceIterator != _deviceProperties.end(); ++deviceIterator) + { + // Device + QJsonObject device; + QJsonArray deviceInputs; + + device["device"] = deviceIterator.key(); + device["device_name"] = deviceIterator.value().name; + device["type"] = "audio"; + + devices.append(device); + } + + return devices; +} + +QString AudioGrabberLinux::getDeviceName(const QString& devicePath) const +{ + if (devicePath.isEmpty() || devicePath == "auto") + { + return "Default Audio Device"; + } + + return _deviceProperties.value(devicePath).name; +} + +static void * AudioThreadRunner(void* params) +{ + AudioGrabberLinux* This = static_cast(params); + + Debug(This->getLog(), "Audio Thread Started"); + + snd_pcm_sframes_t framesAvailable = 0; + + while (This->_isRunning.load(std::memory_order_acquire)) + { + snd_pcm_wait(This->_captureDevice, 1000); + + if ((framesAvailable = snd_pcm_avail(This->_captureDevice)) > 0) + This->processAudioBuffer(framesAvailable); + + sched_yield(); + } + + Debug(This->getLog(), "Audio Thread Shutting Down"); + return nullptr; +} diff --git a/libsrc/grabber/audio/AudioGrabberWindows.cpp b/libsrc/grabber/audio/AudioGrabberWindows.cpp new file mode 100644 index 00000000..07837bd1 --- /dev/null +++ b/libsrc/grabber/audio/AudioGrabberWindows.cpp @@ -0,0 +1,354 @@ +#include +#include +#include +#include + +#pragma comment(lib,"dsound.lib") +#pragma comment(lib, "dxguid.lib") + +// Constants +namespace { + const int AUDIO_NOTIFICATION_COUNT{ 4 }; +} //End of constants + +AudioGrabberWindows::AudioGrabberWindows() : AudioGrabber() +{ +} + +AudioGrabberWindows::~AudioGrabberWindows() +{ + this->stop(); +} + +void AudioGrabberWindows::refreshDevices() +{ + Debug(_log, "Refreshing Audio Devices"); + + _deviceProperties.clear(); + + // Enumerate Devices + if (FAILED(DirectSoundCaptureEnumerate(DirectSoundEnumProcessor, (VOID*)&_deviceProperties))) + { + Error(_log, "Failed to enumerate audio devices."); + } +} + +bool AudioGrabberWindows::configureCaptureInterface() +{ + CLSID deviceId {}; + + if (!this->_device.isEmpty() && this->_device != "auto") + { + LPCOLESTR clsid = reinterpret_cast(_device.utf16()); + HRESULT res = CLSIDFromString(clsid, &deviceId); + if (FAILED(res)) + { + Error(_log, "Failed to get CLSID for '%s' with error: 0x%08x: %s", QSTRING_CSTR(_device), res, std::system_category().message(res).c_str()); + return false; + } + } + + // Create Capture Device + HRESULT res = DirectSoundCaptureCreate8(&deviceId, &recordingDevice, NULL); + if (FAILED(res)) + { + Error(_log, "Failed to create capture device: '%s' with error: 0x%08x: %s", QSTRING_CSTR(_device), res, std::system_category().message(res).c_str()); + return false; + } + + // Define Audio Format & Create Buffer + WAVEFORMATEX audioFormat { WAVE_FORMAT_PCM, 1, 44100, 88200, 2, 16, 0 }; + // wFormatTag, nChannels, nSamplesPerSec, mAvgBytesPerSec, + // nBlockAlign, wBitsPerSample, cbSize + + notificationSize = max(1024, audioFormat.nAvgBytesPerSec / 8); + notificationSize -= notificationSize % audioFormat.nBlockAlign; + + bufferCaptureSize = notificationSize * AUDIO_NOTIFICATION_COUNT; + + DSCBUFFERDESC bufferDesc; + bufferDesc.dwSize = sizeof(DSCBUFFERDESC); + bufferDesc.dwFlags = 0; + bufferDesc.dwBufferBytes = bufferCaptureSize; + bufferDesc.dwReserved = 0; + bufferDesc.lpwfxFormat = &audioFormat; + bufferDesc.dwFXCount = 0; + bufferDesc.lpDSCFXDesc = NULL; + + // Create Capture Device's Buffer + LPDIRECTSOUNDCAPTUREBUFFER preBuffer; + if (FAILED(recordingDevice->CreateCaptureBuffer(&bufferDesc, &preBuffer, NULL))) + { + Error(_log, "Failed to create capture buffer: '%s'", QSTRING_CSTR(getDeviceName(_device))); + recordingDevice->Release(); + return false; + } + + bufferCapturePosition = 0; + + // Query Capture8 Buffer + if (FAILED(preBuffer->QueryInterface(IID_IDirectSoundCaptureBuffer8, (LPVOID*)&recordingBuffer))) + { + Error(_log, "Failed to retrieve recording buffer"); + preBuffer->Release(); + return false; + } + + preBuffer->Release(); + + // Create Notifications + LPDIRECTSOUNDNOTIFY8 notify; + + if (FAILED(recordingBuffer->QueryInterface(IID_IDirectSoundNotify8, (LPVOID *) ¬ify))) + { + Error(_log, "Failed to configure buffer notifications: '%s'", QSTRING_CSTR(getDeviceName(_device))); + recordingDevice->Release(); + recordingBuffer->Release(); + return false; + } + + // Create Events + notificationEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + + if (notificationEvent == NULL) + { + Error(_log, "Failed to configure buffer notifications events: '%s'", QSTRING_CSTR(getDeviceName(_device))); + notify->Release(); + recordingDevice->Release(); + recordingBuffer->Release(); + return false; + } + + // Configure Notifications + DSBPOSITIONNOTIFY positionNotify[AUDIO_NOTIFICATION_COUNT]; + + for (int i = 0; i < AUDIO_NOTIFICATION_COUNT; i++) + { + positionNotify[i].dwOffset = (notificationSize * i) + notificationSize - 1; + positionNotify[i].hEventNotify = notificationEvent; + } + + // Set Notifications + notify->SetNotificationPositions(AUDIO_NOTIFICATION_COUNT, positionNotify); + notify->Release(); + + return true; +} + +bool AudioGrabberWindows::start() +{ + if (!_isEnabled) + { + return false; + } + + if (this->isRunning.load(std::memory_order_acquire)) + { + return true; + } + + //Test, if configured device currently exists + refreshDevices(); + if (!_deviceProperties.contains(_device)) + { + _device = "auto"; + Warning(_log, "Configured audio device is not available. Using '%s'", QSTRING_CSTR(getDeviceName(_device))); + } + + Info(_log, "Capture audio from %s", QSTRING_CSTR(getDeviceName(_device))); + + if (!this->configureCaptureInterface()) + { + return false; + } + + if (FAILED(recordingBuffer->Start(DSCBSTART_LOOPING))) + { + Error(_log, "Failed starting audio capture from '%s'", QSTRING_CSTR(getDeviceName(_device))); + return false; + } + + this->isRunning.store(true, std::memory_order_release); + DWORD threadId; + + this->audioThread = CreateThread( + NULL, + 16, + AudioThreadRunner, + (void *) this, + 0, + &threadId + ); + + if (this->audioThread == NULL) + { + Error(_log, "Failed to create audio capture thread"); + + this->stop(); + return false; + } + + AudioGrabber::start(); + + return true; +} + +void AudioGrabberWindows::stop() +{ + if (!this->isRunning.load(std::memory_order_acquire)) + { + return; + } + + Info(_log, "Shutting down audio capture from: '%s'", QSTRING_CSTR(getDeviceName(_device))); + + this->isRunning.store(false, std::memory_order_release); + + if (FAILED(recordingBuffer->Stop())) + { + Error(_log, "Audio capture failed to stop: '%s'", QSTRING_CSTR(getDeviceName(_device))); + } + + if (FAILED(recordingBuffer->Release())) + { + Error(_log, "Failed to release recording buffer: '%s'", QSTRING_CSTR(getDeviceName(_device))); + } + + if (FAILED(recordingDevice->Release())) + { + Error(_log, "Failed to release recording device: '%s'", QSTRING_CSTR(getDeviceName(_device))); + } + + CloseHandle(notificationEvent); + CloseHandle(this->audioThread); + + AudioGrabber::stop(); +} + +DWORD WINAPI AudioGrabberWindows::AudioThreadRunner(LPVOID param) +{ + AudioGrabberWindows* This = (AudioGrabberWindows*) param; + + while (This->isRunning.load(std::memory_order_acquire)) + { + DWORD result = WaitForMultipleObjects(1, &This->notificationEvent, true, 500); + + switch (result) + { + case WAIT_OBJECT_0: + This->processAudioBuffer(); + break; + } + } + + Debug(This->_log, "Audio capture thread stopped."); + + return 0; +} + +void AudioGrabberWindows::processAudioBuffer() +{ + DWORD readPosition; + DWORD capturePosition; + + // Primary segment + VOID* capturedAudio; + DWORD capturedAudioLength; + + // Wrap around segment + VOID* capturedAudio2; + DWORD capturedAudio2Length; + + LONG lockSize; + + if (FAILED(recordingBuffer->GetCurrentPosition(&capturePosition, &readPosition))) + { + // Failed to get current position + Error(_log, "Failed to get buffer position."); + return; + } + + lockSize = readPosition - bufferCapturePosition; + + if (lockSize < 0) + { + lockSize += bufferCaptureSize; + } + + // Block Align + lockSize -= (lockSize % notificationSize); + + if (lockSize == 0) + { + return; + } + + // Lock Capture Buffer + if (FAILED(recordingBuffer->Lock(bufferCapturePosition, lockSize, &capturedAudio, &capturedAudioLength, + &capturedAudio2, &capturedAudio2Length, 0))) + { + // Handle Lock Error + return; + } + + bufferCapturePosition += capturedAudioLength; + bufferCapturePosition %= bufferCaptureSize; // Circular Buffer + + int frameSize = capturedAudioLength + capturedAudio2Length; + + int16_t * readBuffer = new int16_t[frameSize]; + + // Buffer wrapped around, read second position + if (capturedAudio2 != NULL) + { + bufferCapturePosition += capturedAudio2Length; + bufferCapturePosition %= bufferCaptureSize; // Circular Buffer + } + + // Copy Buffer into memory + CopyMemory(readBuffer, capturedAudio, capturedAudioLength); + + if (capturedAudio2 != NULL) + { + CopyMemory(readBuffer + capturedAudioLength, capturedAudio2, capturedAudio2Length); + } + + // Release Buffer Lock + recordingBuffer->Unlock(capturedAudio, capturedAudioLength, capturedAudio2, capturedAudio2Length); + + // Process Audio Frame + this->processAudioFrame(readBuffer, frameSize); + + delete[] readBuffer; +} + +QJsonArray AudioGrabberWindows::discover(const QJsonObject& params) +{ + refreshDevices(); + + QJsonArray devices; + + for (auto deviceIterator = _deviceProperties.begin(); deviceIterator != _deviceProperties.end(); ++deviceIterator) + { + // Device + QJsonObject device; + QJsonArray deviceInputs; + + device["device"] = deviceIterator.value().id; + device["device_name"] = deviceIterator.value().name; + device["type"] = "audio"; + + devices.append(device); + } + + return devices; +} + +QString AudioGrabberWindows::getDeviceName(const QString& devicePath) const +{ + if (devicePath.isEmpty() || devicePath == "auto") + { + return "Default Device"; + } + return _deviceProperties.value(devicePath).name; +} diff --git a/libsrc/grabber/audio/AudioWrapper.cpp b/libsrc/grabber/audio/AudioWrapper.cpp new file mode 100644 index 00000000..0dd624de --- /dev/null +++ b/libsrc/grabber/audio/AudioWrapper.cpp @@ -0,0 +1,63 @@ +#include +#include +#include +#include + +AudioWrapper::AudioWrapper() + : GrabberWrapper("AudioGrabber", &_grabber) + , _grabber() +{ + // register the image type + qRegisterMetaType>("Image"); + + connect(&_grabber, &AudioGrabber::newFrame, this, &AudioWrapper::newFrame, Qt::DirectConnection); +} + +AudioWrapper::~AudioWrapper() +{ + stop(); +} + +bool AudioWrapper::start() +{ + return (_grabber.start() && GrabberWrapper::start()); +} + +void AudioWrapper::stop() +{ + _grabber.stop(); + GrabberWrapper::stop(); +} + +void AudioWrapper::action() +{ + // Dummy we will push the audio images +} + +void AudioWrapper::newFrame(const Image& image) +{ + emit systemImage(_grabberName, image); +} + +void AudioWrapper::handleSettingsUpdate(settings::type type, const QJsonDocument& config) +{ + if (type == settings::AUDIO) + { + const QJsonObject& obj = config.object(); + + // set global grabber state + setAudioGrabberState(obj["enable"].toBool(false)); + + if (getAudioGrabberState()) + { + _grabber.setDevice(obj["device"].toString()); + _grabber.setConfiguration(obj); + + _grabber.restart(); + } + else + { + stop(); + } + } +} diff --git a/libsrc/grabber/audio/CMakeLists.txt b/libsrc/grabber/audio/CMakeLists.txt new file mode 100644 index 00000000..187fa9d0 --- /dev/null +++ b/libsrc/grabber/audio/CMakeLists.txt @@ -0,0 +1,35 @@ +# Define the current source locations +SET( CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber ) +SET( CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/audio ) + + +if (WIN32) + FILE ( GLOB AUDIO_GRABBER_SOURCES "${CURRENT_HEADER_DIR}/Audio*Windows.h" "${CURRENT_HEADER_DIR}/AudioGrabber.h" "${CURRENT_HEADER_DIR}/AudioWrapper.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*Windows.cpp" "${CURRENT_SOURCE_DIR}/AudioGrabber.cpp" "${CURRENT_SOURCE_DIR}/AudioWrapper.cpp") +elseif(${CMAKE_SYSTEM} MATCHES "Linux") + FILE ( GLOB AUDIO_GRABBER_SOURCES "${CURRENT_HEADER_DIR}/Audio*Linux.h" "${CURRENT_HEADER_DIR}/AudioGrabber.h" "${CURRENT_HEADER_DIR}/AudioWrapper.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*Linux.cpp" "${CURRENT_SOURCE_DIR}/AudioGrabber.cpp" "${CURRENT_SOURCE_DIR}/AudioWrapper.cpp") +elseif (APPLE) + #TODO + #FILE ( GLOB AUDIO_GRABBER_SOURCES "${CURRENT_HEADER_DIR}/Audio*Apple.h" "${CURRENT_HEADER_DIR}/AudioGrabber.h" "${CURRENT_HEADER_DIR}/AudioWrapper.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*Apple.cpp" "${CURRENT_SOURCE_DIR}/AudioGrabber.cpp" "${CURRENT_SOURCE_DIR}/AudioWrapper.cpp") +endif() + +add_library( audio-grabber ${AUDIO_GRABBER_SOURCES} ) + +set(AUDIO_LIBS hyperion) + + +if (WIN32) + set(AUDIO_LIBS ${AUDIO_LIBS} DSound) +elseif(${CMAKE_SYSTEM} MATCHES "Linux") + find_package(ALSA REQUIRED) + if (ALSA_FOUND) + include_directories(${ALSA_INCLUDE_DIRS}) + set(AUDIO_LIBS ${AUDIO_LIBS} ${ALSA_LIBRARIES}) + endif(ALSA_FOUND) + + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) + set(AUDIO_LIBS ${AUDIO_LIBS} Threads::Threads) # PRIVATE +endif() + + +target_link_libraries(audio-grabber ${AUDIO_LIBS} ${QT_LIBRARIES}) diff --git a/libsrc/hyperion/CaptureCont.cpp b/libsrc/hyperion/CaptureCont.cpp index 3cb566ed..4f3a7aae 100644 --- a/libsrc/hyperion/CaptureCont.cpp +++ b/libsrc/hyperion/CaptureCont.cpp @@ -20,6 +20,10 @@ CaptureCont::CaptureCont(Hyperion* hyperion) , _v4lCaptPrio(0) , _v4lCaptName() , _v4lInactiveTimer(new QTimer(this)) + , _audioCaptEnabled(false) + , _audioCaptPrio(0) + , _audioCaptName() + , _audioInactiveTimer(new QTimer(this)) { // settings changes connect(_hyperion, &Hyperion::settingsChanged, this, &CaptureCont::handleSettingsUpdate); @@ -37,6 +41,11 @@ CaptureCont::CaptureCont(Hyperion* hyperion) _v4lInactiveTimer->setSingleShot(true); _v4lInactiveTimer->setInterval(1000); + // inactive timer audio + connect(_audioInactiveTimer, &QTimer::timeout, this, &CaptureCont::setAudioInactive); + _audioInactiveTimer->setSingleShot(true); + _audioInactiveTimer->setInterval(1000); + // init handleSettingsUpdate(settings::INSTCAPTURE, _hyperion->getSetting(settings::INSTCAPTURE)); } @@ -65,6 +74,17 @@ void CaptureCont::handleSystemImage(const QString& name, const Image& _hyperion->setInputImage(_systemCaptPrio, image); } +void CaptureCont::handleAudioImage(const QString& name, const Image& image) +{ + if (_audioCaptName != name) + { + _hyperion->registerInput(_audioCaptPrio, hyperion::COMP_AUDIO, "System", name); + _audioCaptName = name; + } + _audioInactiveTimer->start(); + _hyperion->setInputImage(_audioCaptPrio, image); +} + void CaptureCont::setSystemCaptureEnable(bool enable) { if(_systemCaptEnabled != enable) @@ -111,24 +131,56 @@ void CaptureCont::setV4LCaptureEnable(bool enable) } } +void CaptureCont::setAudioCaptureEnable(bool enable) +{ + if (_audioCaptEnabled != enable) + { + if (enable) + { + _hyperion->registerInput(_audioCaptPrio, hyperion::COMP_AUDIO); + connect(GlobalSignals::getInstance(), &GlobalSignals::setAudioImage, this, &CaptureCont::handleAudioImage); + connect(GlobalSignals::getInstance(), &GlobalSignals::setAudioImage, _hyperion, &Hyperion::forwardAudioProtoMessage); + } + else + { + disconnect(GlobalSignals::getInstance(), &GlobalSignals::setAudioImage, this, 0); + _hyperion->clear(_audioCaptPrio); + _audioInactiveTimer->stop(); + _audioCaptName = ""; + } + _audioCaptEnabled = enable; + _hyperion->setNewComponentState(hyperion::COMP_AUDIO, enable); + emit GlobalSignals::getInstance()->requestSource(hyperion::COMP_AUDIO, int(_hyperion->getInstanceIndex()), enable); + } +} + void CaptureCont::handleSettingsUpdate(settings::type type, const QJsonDocument& config) { if(type == settings::INSTCAPTURE) { const QJsonObject& obj = config.object(); + if(_v4lCaptPrio != obj["v4lPriority"].toInt(240)) { setV4LCaptureEnable(false); // clear prio _v4lCaptPrio = obj["v4lPriority"].toInt(240); } + if(_systemCaptPrio != obj["systemPriority"].toInt(250)) { setSystemCaptureEnable(false); // clear prio _systemCaptPrio = obj["systemPriority"].toInt(250); } + if (_audioCaptPrio != obj["audioPriority"].toInt(230)) + { + setAudioCaptureEnable(false); // clear prio + _audioCaptPrio = obj["audioPriority"].toInt(230); + } + setV4LCaptureEnable(obj["v4lEnable"].toBool(false)); setSystemCaptureEnable(obj["systemEnable"].toBool(false)); + setAudioCaptureEnable(obj["audioEnable"].toBool(true)); } } @@ -142,6 +194,10 @@ void CaptureCont::handleCompStateChangeRequest(hyperion::Components component, b { setV4LCaptureEnable(enable); } + else if (component == hyperion::COMP_AUDIO) + { + setAudioCaptureEnable(enable); + } } void CaptureCont::setV4lInactive() @@ -153,3 +209,8 @@ void CaptureCont::setSystemInactive() { _hyperion->setInputInactive(_systemCaptPrio); } + +void CaptureCont::setAudioInactive() +{ + _hyperion->setInputInactive(_audioCaptPrio); +} diff --git a/libsrc/hyperion/ComponentRegister.cpp b/libsrc/hyperion/ComponentRegister.cpp index 65d0ac83..fd2f261d 100644 --- a/libsrc/hyperion/ComponentRegister.cpp +++ b/libsrc/hyperion/ComponentRegister.cpp @@ -20,6 +20,7 @@ ComponentRegister::ComponentRegister(Hyperion* hyperion) bool areScreenGrabberAvailable = !GrabberWrapper::availableGrabbers(GrabberTypeFilter::VIDEO).isEmpty(); bool areVideoGrabberAvailable = !GrabberWrapper::availableGrabbers(GrabberTypeFilter::VIDEO).isEmpty(); + bool areAudioGrabberAvailable = !GrabberWrapper::availableGrabbers(GrabberTypeFilter::AUDIO).isEmpty(); bool flatBufServerAvailable { false }; bool protoBufServerAvailable{ false }; @@ -36,6 +37,11 @@ ComponentRegister::ComponentRegister(Hyperion* hyperion) vect << COMP_GRABBER; } + if (areAudioGrabberAvailable) + { + vect << COMP_AUDIO; + } + if (areVideoGrabberAvailable) { vect << COMP_V4L; diff --git a/libsrc/hyperion/GrabberWrapper.cpp b/libsrc/hyperion/GrabberWrapper.cpp index 1c846aa4..4d88a6f2 100644 --- a/libsrc/hyperion/GrabberWrapper.cpp +++ b/libsrc/hyperion/GrabberWrapper.cpp @@ -18,8 +18,10 @@ const int GrabberWrapper::DEFAULT_PIXELDECIMATION = 8; /// Map of Hyperion instances with grabber name that requested screen capture QMap GrabberWrapper::GRABBER_SYS_CLIENTS = QMap(); QMap GrabberWrapper::GRABBER_V4L_CLIENTS = QMap(); +QMap GrabberWrapper::GRABBER_AUDIO_CLIENTS = QMap(); bool GrabberWrapper::GLOBAL_GRABBER_SYS_ENABLE = false; bool GrabberWrapper::GLOBAL_GRABBER_V4L_ENABLE = false; +bool GrabberWrapper::GLOBAL_GRABBER_AUDIO_ENABLE = false; GrabberWrapper::GrabberWrapper(const QString& grabberName, Grabber * ggrabber, int updateRate_Hz) : _grabberName(grabberName) @@ -38,9 +40,12 @@ GrabberWrapper::GrabberWrapper(const QString& grabberName, Grabber * ggrabber, i connect(_timer, &QTimer::timeout, this, &GrabberWrapper::action); // connect the image forwarding - (_grabberName.startsWith("V4L")) - ? connect(this, &GrabberWrapper::systemImage, GlobalSignals::getInstance(), &GlobalSignals::setV4lImage) - : connect(this, &GrabberWrapper::systemImage, GlobalSignals::getInstance(), &GlobalSignals::setSystemImage); + if (_grabberName.startsWith("V4L")) + connect(this, &GrabberWrapper::systemImage, GlobalSignals::getInstance(), &GlobalSignals::setV4lImage); + else if (_grabberName.startsWith("Audio")) + connect(this, &GrabberWrapper::systemImage, GlobalSignals::getInstance(), &GlobalSignals::setAudioImage); + else + connect(this, &GrabberWrapper::systemImage, GlobalSignals::getInstance(), &GlobalSignals::setSystemImage); // listen for source requests connect(GlobalSignals::getInstance(), &GlobalSignals::requestSource, this, &GrabberWrapper::handleSourceRequest); @@ -99,6 +104,12 @@ QStringList GrabberWrapper::getActive(int inst, GrabberTypeFilter type) const result << GRABBER_V4L_CLIENTS.value(inst); } + if (type == GrabberTypeFilter::AUDIO || type == GrabberTypeFilter::ALL) + { + if (GRABBER_AUDIO_CLIENTS.contains(inst)) + result << GRABBER_AUDIO_CLIENTS.value(inst); + } + return result; } @@ -148,6 +159,13 @@ QStringList GrabberWrapper::availableGrabbers(GrabberTypeFilter type) #endif } + if (type == GrabberTypeFilter::AUDIO || type == GrabberTypeFilter::ALL) + { + #ifdef ENABLE_AUDIO + grabbers << "audio"; + #endif + } + return grabbers; } @@ -187,7 +205,9 @@ void GrabberWrapper::updateTimer(int interval) void GrabberWrapper::handleSettingsUpdate(settings::type type, const QJsonDocument& config) { - if(type == settings::SYSTEMCAPTURE && !_grabberName.startsWith("V4L")) + if (type == settings::SYSTEMCAPTURE && + !_grabberName.startsWith("V4L") && + !_grabberName.startsWith("Audio")) { // extract settings const QJsonObject& obj = config.object(); @@ -235,26 +255,42 @@ void GrabberWrapper::handleSettingsUpdate(settings::type type, const QJsonDocume void GrabberWrapper::handleSourceRequest(hyperion::Components component, int hyperionInd, bool listen) { - if(component == hyperion::Components::COMP_GRABBER && !_grabberName.startsWith("V4L")) + if (component == hyperion::Components::COMP_GRABBER && + !_grabberName.startsWith("V4L") && + !_grabberName.startsWith("Audio")) { - if(listen) + if (listen) GRABBER_SYS_CLIENTS.insert(hyperionInd, _grabberName); else GRABBER_SYS_CLIENTS.remove(hyperionInd); - if(GRABBER_SYS_CLIENTS.empty() || !getSysGrabberState()) + if (GRABBER_SYS_CLIENTS.empty() || !getSysGrabberState()) stop(); else start(); } - else if(component == hyperion::Components::COMP_V4L && _grabberName.startsWith("V4L")) + else if (component == hyperion::Components::COMP_V4L && + _grabberName.startsWith("V4L")) { - if(listen) + if (listen) GRABBER_V4L_CLIENTS.insert(hyperionInd, _grabberName); else GRABBER_V4L_CLIENTS.remove(hyperionInd); - if(GRABBER_V4L_CLIENTS.empty() || !getV4lGrabberState()) + if (GRABBER_V4L_CLIENTS.empty() || !getV4lGrabberState()) + stop(); + else + start(); + } + else if (component == hyperion::Components::COMP_AUDIO && + _grabberName.startsWith("Audio")) + { + if (listen) + GRABBER_AUDIO_CLIENTS.insert(hyperionInd, _grabberName); + else + GRABBER_AUDIO_CLIENTS.remove(hyperionInd); + + if (GRABBER_AUDIO_CLIENTS.empty()) stop(); else start(); @@ -264,6 +300,11 @@ void GrabberWrapper::handleSourceRequest(hyperion::Components component, int hyp void GrabberWrapper::tryStart() { // verify start condition - if(!_grabberName.startsWith("V4L") && !GRABBER_SYS_CLIENTS.empty() && getSysGrabberState()) + if (!_grabberName.startsWith("V4L") && + !_grabberName.startsWith("Audio") && + !GRABBER_SYS_CLIENTS.empty() && + getSysGrabberState()) + { start(); + } } diff --git a/libsrc/hyperion/SettingsManager.cpp b/libsrc/hyperion/SettingsManager.cpp index e8e444a9..b9d78edc 100644 --- a/libsrc/hyperion/SettingsManager.cpp +++ b/libsrc/hyperion/SettingsManager.cpp @@ -501,6 +501,20 @@ bool SettingsManager::handleConfigUpgrade(QJsonObject& config) Debug(_log, "GrabberV4L2 records migrated"); } + if (config.contains("grabberAudio")) + { + QJsonObject newGrabberAudioConfig = config["grabberAudio"].toObject(); + + //Add new element enable + if (!newGrabberAudioConfig.contains("enable")) + { + newGrabberAudioConfig["enable"] = false; + migrated = true; + } + config["grabberAudio"] = newGrabberAudioConfig; + Debug(_log, "GrabberAudio records migrated"); + } + if (config.contains("framegrabber")) { QJsonObject newFramegrabberConfig = config["framegrabber"].toObject(); diff --git a/libsrc/hyperion/hyperion.schema.json b/libsrc/hyperion/hyperion.schema.json index 36541d6b..12e1ba73 100644 --- a/libsrc/hyperion/hyperion.schema.json +++ b/libsrc/hyperion/hyperion.schema.json @@ -27,6 +27,10 @@ { "$ref": "schema-grabberV4L2.json" }, + "grabberAudio" : + { + "$ref": "schema-grabberAudio.json" + }, "framegrabber" : { "$ref": "schema-framegrabber.json" diff --git a/libsrc/hyperion/resource.qrc b/libsrc/hyperion/resource.qrc index fdc66d5f..f1c327cf 100644 --- a/libsrc/hyperion/resource.qrc +++ b/libsrc/hyperion/resource.qrc @@ -7,6 +7,7 @@ schema/schema-color.json schema/schema-smoothing.json schema/schema-grabberV4L2.json + schema/schema-grabberAudio.json schema/schema-framegrabber.json schema/schema-blackborderdetector.json schema/schema-foregroundEffect.json diff --git a/libsrc/hyperion/schema/schema-grabberAudio.json b/libsrc/hyperion/schema/schema-grabberAudio.json new file mode 100644 index 00000000..b4225872 --- /dev/null +++ b/libsrc/hyperion/schema/schema-grabberAudio.json @@ -0,0 +1,163 @@ +{ + "type": "object", + "required": true, + "title": "edt_conf_audio_heading_title", + "properties": { + "enable": { + "type": "boolean", + "title": "edt_conf_general_enable_title", + "required": true, + "default": false, + "propertyOrder": 1 + }, + "available_devices": { + "type": "string", + "title": "edt_conf_grabber_discovered_title", + "default": "edt_conf_grabber_discovery_inprogress", + "options": { + "infoText": "edt_conf_grabber_discovered_title_info" + }, + "propertyOrder": 2, + "required": false + }, + "device": { + "type": "string", + "title": "edt_conf_enum_custom", + "default": "auto", + "options": { + "hidden": true + }, + "required": true, + "propertyOrder": 3, + "comment": "The 'available_audio_devices' settings are dynamically inserted into the WebUI under PropertyOrder '1'." + }, + "audioEffect": { + "type": "string", + "title": "edt_conf_audio_effects_title", + "required": true, + "enum": [ "vuMeter" ], + "default": "vuMeter", + "options": { + "enum_titles": [ "edt_conf_audio_effect_enum_vumeter" ] + }, + "propertyOrder": 4 + }, + "vuMeter": { + "type": "object", + "title": "", + "required": true, + "propertyOrder": 5, + "options": { + "dependencies": { + "audioEffect": "vuMeter" + } + }, + "properties": { + "multiplier": { + "type": "number", + "title": "edt_conf_audio_effect_multiplier_title", + "default": 1, + "minimum": 0, + "step": 0.01, + "required": true, + "propertyOrder": 1, + "comment": "The multiplier is used to scale the audio input signal. Increase or decrease to achieve the desired effect. Set to 0 for auto" + }, + "tolerance": { + "type": "number", + "title": "edt_conf_audio_effect_tolerance_title", + "default": 5, + "minimum": 0, + "step": 1, + "append": "edt_append_percent", + "required": true, + "propertyOrder": 2, + "comment": "The tolerance is a percentage value from 0 - 100 used during auto multiplier calculation." + }, + "hotColor": { + "type": "array", + "title": "edt_conf_audio_effect_hotcolor_title", + "default": [ 255, 0, 0 ], + "format": "colorpicker", + "items": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "minItems": 3, + "maxItems": 3, + "required": true, + "propertyOrder": 3, + "comment": "Hot Color is the color the led's will reach when audio level exceeds the warn percentage" + }, + "warnColor": { + "type": "array", + "title": "edt_conf_audio_effect_warncolor_title", + "default": [ 255, 255, 0 ], + "format": "colorpicker", + "items": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "minItems": 3, + "maxItems": 3, + "required": true, + "propertyOrder": 4, + "comment": "Warn Color is the color the led's will reach when audio level exceeds the safe percentage" + }, + "warnValue": { + "type": "number", + "title": "edt_conf_audio_effect_warnvalue_title", + "default": 80, + "minimum": 0, + "step": 1, + "append": "edt_append_percent", + "required": true, + "propertyOrder": 5, + "comment": "Warn percentage is the percentage used to determine the maximum percentage of the audio warning level" + }, + "safeColor": { + "type": "array", + "title": "edt_conf_audio_effect_safecolor_title", + "default": [ 0, 255, 0 ], + "format": "colorpicker", + "items": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "minItems": 3, + "maxItems": 3, + "required": true, + "propertyOrder": 6, + "comment": "Safe Color is the color the led's will reach when audio level is below the warning percentage" + }, + "safeValue": { + "type": "number", + "title": "edt_conf_audio_effect_safevalue_title", + "default": 45, + "minimum": 0, + "step": 1, + "append": "edt_append_percent", + "required": true, + "propertyOrder": 7, + "comment": "Safe percentage is the percentage used to determine the maximum percentage of the audio safe level" + }, + "flip": { + "type": "string", + "title": "edt_conf_v4l2_flip_title", + "enum": [ "NO_CHANGE", "HORIZONTAL", "VERTICAL", "BOTH" ], + "default": "NO_CHANGE", + "options": { + "enum_titles": [ "edt_conf_enum_NO_CHANGE", "edt_conf_enum_HORIZONTAL", "edt_conf_enum_VERTICAL", "edt_conf_enum_BOTH" ] + }, + "required": true, + "access": "advanced", + "propertyOrder": 8 + } + } + } + }, + "additionalProperties": true +} diff --git a/libsrc/hyperion/schema/schema-instCapture.json b/libsrc/hyperion/schema/schema-instCapture.json index 28fe9f4c..a4076a6a 100644 --- a/libsrc/hyperion/schema/schema-instCapture.json +++ b/libsrc/hyperion/schema/schema-instCapture.json @@ -48,6 +48,29 @@ "maximum": 253, "default": 240, "propertyOrder": 6 + }, + "audioEnable": { + "type": "boolean", + "required": true, + "title": "edt_conf_instC_audioEnable_title", + "default": false, + "propertyOrder": 7 + }, + "audioGrabberDevice": { + "type": "string", + "required": true, + "title": "edt_conf_instC_video_grabber_device_title", + "default": "NONE", + "propertyOrder": 7 + }, + "audioPriority": { + "type": "integer", + "required": true, + "title": "edt_conf_general_priority_title", + "minimum": 100, + "maximum": 253, + "default": 230, + "propertyOrder": 9 } }, "additionalProperties" : false diff --git a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp index edb49a5b..5cec2ec3 100644 --- a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp +++ b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp @@ -162,7 +162,8 @@ void QJsonSchemaChecker::validate(const QJsonValue& value, const QJsonObject& sc ; // references have already been collected else if (attribute == "title" || attribute == "description" || attribute == "default" || attribute == "format" || attribute == "defaultProperties" || attribute == "propertyOrder" || attribute == "append" || attribute == "step" - || attribute == "access" || attribute == "options" || attribute == "script" || attribute == "allowEmptyArray" || attribute == "comment") + || attribute == "access" || attribute == "options" || attribute == "script" || attribute == "allowEmptyArray" || attribute == "comment" + || attribute == "watch" || attribute == "template") ; // nothing to do. else { diff --git a/src/hyperion-remote/hyperion-remote.cpp b/src/hyperion-remote/hyperion-remote.cpp index 37d2ec03..f47fbb14 100644 --- a/src/hyperion-remote/hyperion-remote.cpp +++ b/src/hyperion-remote/hyperion-remote.cpp @@ -113,8 +113,8 @@ int main(int argc, char * argv[]) #endif BooleanOption & argClear = parser.add('x', "clear" , "Clear data for the priority channel provided by the -p option"); BooleanOption & argClearAll = parser.add(0x0, "clearall" , "Clear data for all active priority channels"); - Option & argEnableComponent = parser.add