diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b557cbf7..e31f065e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -36,7 +36,12 @@ 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 libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev + sudo apt-get install --yes git 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 libftdi1-dev + + - name: Temporarily downgrade CMake to 3.28.3 # Please remove if GitHub has updated Cmake (greater than 3.30.0) + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: '3.28.3' - name: 🔁 Initialize CodeQL uses: github/codeql-action/init@v3 @@ -44,7 +49,7 @@ jobs: languages: ${{ matrix.language }} queries: +security-and-quality config-file: ./.github/config/codeql.yml - + - name: 👷 Autobuild uses: github/codeql-action/autobuild@v3 diff --git a/.github/workflows/qt5_6.yml b/.github/workflows/qt5_6.yml index fe5fa41d..7c3cc396 100644 --- a/.github/workflows/qt5_6.yml +++ b/.github/workflows/qt5_6.yml @@ -117,9 +117,14 @@ jobs: echo '::group::Update/Install dependencies' brew untap --force homebrew/core homebrew/cask brew update || true - brew install qt@${{ inputs.qt_version }} vulkan-headers ninja || true + brew install qt@${{ inputs.qt_version }} vulkan-headers ninja libftdi || true echo '::endgroup::' + - name: Temporarily downgrade CMake to 3.28.3 # Please remove if GitHub has updated Cmake (greater than 3.30.0) + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: '3.28.3' + - name: 👷 Build shell: bash run: ./.github/scripts/build.sh @@ -163,26 +168,32 @@ jobs: uses: actions/cache@v4 with: path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey - key: ${{ runner.os }}${{ inputs.qt_version == '6' && '-chocolatey-qt6' || '-chocolatey' }} + key: ${{ runner.os }}${{ '-chocolatey' }} - - name: 📥 Install DirectX SDK, OpenSSL, libjpeg-turbo ${{ inputs.qt_version == '6' && 'and Vulkan-SDK' || '' }} + - name: 📥 Install DirectX SDK, OpenSSL, libjpeg-turbo shell: powershell run: | - choco install --no-progress directx-sdk ${{env.VULKAN_SDK}} -y + choco install --no-progress directx-sdk -y choco install --no-progress ${{env.OPENSSL}} -y Invoke-WebRequest https://netcologne.dl.sourceforge.net/project/libjpeg-turbo/3.0.1/libjpeg-turbo-3.0.1-vc64.exe -OutFile libjpeg-turbo.exe -UserAgent NativeHost .\libjpeg-turbo /S env: - VULKAN_SDK: ${{ inputs.qt_version == '6' && 'vulkan-sdk' || '' }} OPENSSL: ${{ inputs.qt_version == '6' && 'openssl' || 'openssl --version=1.1.1.2100' }} - - name: 📥 Install Qt - uses: jurplel/install-qt-action@v3 + - name: Install Vulkan SDK + if: ${{ inputs.qt_version == '6' }} + uses: jakoch/install-vulkan-sdk-action@v1.0.4 with: - version: ${{ inputs.qt_version == '6' && '6.5.2' || '5.15.2' }} + install_runtime: false + cache: true + stripdown: true + + - name: 📥 Install Qt + uses: jurplel/install-qt-action@v4 + with: + version: ${{ inputs.qt_version == '6' && '6.7' || '5.15.*' }} target: 'desktop' modules: ${{ inputs.qt_version == '6' && 'qtserialport' || '' }} - arch: 'win64_msvc2019_64' cache: 'true' cache-key-prefix: 'cache-qt-windows' @@ -190,6 +201,11 @@ jobs: shell: cmd run: call "${{env.VCINSTALLDIR}}\Auxiliary\Build\vcvars64.bat" + - name: Temporarily downgrade CMake to 3.28.3 # Please remove if GitHub has updated Cmake (greater than 3.30.0) + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: '3.28.3' + - name: 👷 Build shell: bash run: ./.github/scripts/build.sh @@ -226,7 +242,7 @@ jobs: echo '::endgroup::' - name: 💾 Artifact download - uses: actions/download-artifact@v4.1.7 + uses: actions/download-artifact@v4.1.8 with: pattern: artifact-* path: all-artifacts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e770924..c892f259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Support for ftdi chip based LED-devices with ws2812, sk6812 apa102 LED types (Many thanks to @nurikk) (#1746) +- Support for Skydimo devices (being an Adalight variant) - Support gaps on Matrix Layout (#1696) - Windows: Added a new grabber that uses the DXGI DDA (Desktop Duplication API). This has much better performance than the DX grabber as it does more of its work on the GPU. diff --git a/CMakeLists.txt b/CMakeLists.txt index 30e97148..d27cbc4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,6 +94,7 @@ set(DEFAULT_DEV_SPI OFF) set(DEFAULT_DEV_TINKERFORGE OFF) set(DEFAULT_DEV_USB_HID OFF) set(DEFAULT_DEV_WS281XPWM OFF) +set(DEFAULT_DEV_FTDI ON ) # Services set(DEFAULT_EFFECTENGINE ON ) @@ -121,9 +122,10 @@ if(${CMAKE_SYSTEM} MATCHES "Linux") set(DEFAULT_DEV_USB_HID ON) set(DEFAULT_CEC ON) elseif (WIN32) - set(DEFAULT_DX ON) - set(DEFAULT_DDA ON) - set(DEFAULT_MF ON) + set(DEFAULT_DX ON ) + set(DEFAULT_DDA ON ) + set(DEFAULT_MF ON ) + set(DEFAULT_DEV_FTDI OFF) else() set(DEFAULT_FB OFF) set(DEFAULT_V4L2 OFF) @@ -364,6 +366,9 @@ message(STATUS "ENABLE_DEV_USB_HID = ${ENABLE_DEV_USB_HID}") option(ENABLE_DEV_WS281XPWM "Enable the WS281x-PWM device" ${DEFAULT_DEV_WS281XPWM}) message(STATUS "ENABLE_DEV_WS281XPWM = ${ENABLE_DEV_WS281XPWM}") +option(ENABLE_DEV_FTDI "Enable the FTDI devices" ${DEFAULT_DEV_FTDI} ) +message(STATUS "ENABLE_DEV_FTDI = ${ENABLE_DEV_FTDI}") + removeIndent() message(STATUS "Services options:") diff --git a/README.md b/README.md index 8e8c3fb0..5d86739f 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,14 @@ * Low CPU load makes it perfect for SoCs like Raspberry Pi * Json interface which allows easy integration into scripts * A command line utility for testing and integration in automated environment -* Priority channels are not coupled to a specific led data provider which means that a provider can post led data and leave without the need to maintain a connection to Hyperion. This is ideal for a remote application (like our [Android app](https://play.google.com/store/apps/details?id=nl.hyperion.hyperionpro)). +* Priority channels are not coupled to a specific led data provider which means that a provider can post led data and leave without the need to maintain a connection to Hyperion. This is ideal for a remote application (like our former [Android app](https://play.google.com/store/apps/details?id=nl.hyperion.hyperionpro), which is no longer available). * Black border detector and processor * A scriptable (Python) effect engine with 39 build-in effects for your inspiration * A multi language web interface to configure and remote control hyperion ### Supported Hardware -You can find a list of supported hardware [here](https://docs.hyperion-project.org/en/user/leddevices/). +You can find a list of supported hardware [here](https://docs.hyperion-project.org/user/leddevices/Overview.html). If you need further support please open a topic at the forum!
[![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) @@ -50,10 +50,10 @@ Find here more details on [supported platforms and configuration sets](doc/devel ## Documentation Covers these topics: -- [Installation](https://docs.hyperion-project.org/en/user/Installation.html) -- [Configuration](https://docs.hyperion-project.org/en/user/Configuration.html) -- [Effect development](https://docs.hyperion-project.org/en/effects/#effect-files) -- [JSON API](https://docs.hyperion-project.org/en/json/) +- [Getting Started and Installation](https://docs.hyperion-project.org/user/GettingStarted.html) +- [Configuration](https://docs.hyperion-project.org/user/Configuration.html) +- [Effect development](https://docs.hyperion-project.org/effects/Effects.html) +- [JSON API](https://docs.hyperion-project.org/json/JSON.html) [![Visit 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) @@ -64,7 +64,7 @@ Released and unreleased changes at [CHANGELOG.md](CHANGELOG.md). See [CompileHowto.md](doc/development/CompileHowto.md). ## Installation -See [Documentation](https://docs.hyperion-project.org/en/user/Installation.html) or on the [Release Repository](https://releases.hyperion-project.org). +See [Getting Started](https://docs.hyperion-project.org/user/GettingStarted.html) or on the [Release Repository](https://releases.hyperion-project.org). ## Download GitHub Releases are available on the [Hyperion release page](https://github.com/hyperion-project/hyperion.ng/releases). diff --git a/assets/webconfig/i18n/de.json b/assets/webconfig/i18n/de.json index a41167a6..44214b9a 100644 --- a/assets/webconfig/i18n/de.json +++ b/assets/webconfig/i18n/de.json @@ -159,8 +159,9 @@ "conf_leds_note_layout_overwrite": "Achtung: Überschreiben erzeugt ein Standardlayout für {{plural:$1| eine LED| alle $1 LEDs}} gemäß der gegebenen Hardware LED-Anzahl", "conf_leds_optgroup_RPiGPIO": "RPi GPIO", "conf_leds_optgroup_RPiPWM": "RPi PWM", - "conf_leds_optgroup_RPiSPI": "RPi SPI", + "conf_leds_optgroup_SPI": "SPI", "conf_leds_optgroup_debug": "Debug", + "conf_leds_optgroup_ftdi": "USB/Ftdi", "conf_leds_optgroup_network": "Netzwerk", "conf_leds_optgroup_other": "Andere", "conf_leds_optgroup_usb": "USB/Seriell", @@ -191,6 +192,7 @@ "conf_network_tok_diaTitle": "Neues Token erstellt!", "conf_network_tok_grantMsg": "Eine App fordert Zugriff auf die Hyperion API durch ein Token. Möchtest du dies zulassen? Bitte überprüfe die angegebenen Informationen!", "conf_network_tok_grantT": "App Token angefordert", + "conf_network_tok_idhead": "ID", "conf_network_tok_intro": "Hier kannst du Token zur API-Authentifizierung erstellen oder löschen. Neu erstellte Token werden einmalig angezeigt.", "conf_network_tok_lastuse": "Zuletzt genutzt", "conf_network_tok_title": "Token Management", @@ -611,6 +613,11 @@ "edt_conf_webc_sslport_title": "HTTPS Port", "edt_dev_auth_key_title": "Autorisierungs-Token", "edt_dev_auth_key_title_info": "Autorisierungs-Token für den Zugriff auf das Gerät erforderlich", + "edt_dev_enum_auto": "Auto", + "edt_dev_enum_auto_accurate": "Auto genau", + "edt_dev_enum_auto_max": "Auto maximal", + "edt_dev_enum_cold_white": "Kaltweiß", + "edt_dev_enum_neutral_white": "Neutralweiß", "edt_dev_enum_sub_min_cool_adjust": "Minimale Anpassung: cool", "edt_dev_enum_sub_min_warm_adjust": "Minimale Anpassung: warm", "edt_dev_enum_subtract_minimum": "Subtrahiere Minimum", @@ -735,7 +742,7 @@ "edt_dev_spec_username_title": "Benutzername", "edt_dev_spec_verbose_title": "Protokollierung der HUE-Kommandos", "edt_dev_spec_vid_title": "VID", - "edt_dev_spec_whiteLedAlgor_title": "Weiß Algorithmus", + "edt_dev_spec_whiteLedAlgor_title": "Weißabgleich Algorithmus", "edt_dev_spec_whitepoint_title": "Weißpunkt", "edt_eff_alarmcolor": "Alarm Farbe", "edt_eff_backgroundColor": "Hintergrundfarbe", @@ -962,6 +969,7 @@ "general_country_us": "Amerika", "general_disabled": "deaktiviert", "general_enabled": "aktiviert", + "general_speech_bg": "Bulgarisch", "general_speech_ca": "Katalanisch", "general_speech_cs": "Tschechisch", "general_speech_da": "Dänisch", diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 071ba69a..852fe202 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -85,6 +85,7 @@ "conf_leds_layout_cl_bottomleft": "Bottom Left (Corner)", "conf_leds_layout_cl_bottomright": "Bottom Right (Corner)", "conf_leds_layout_cl_cornergap": "Corner Gap", + "conf_leds_layout_cl_disabled": "Deactivated", "conf_leds_layout_cl_edgegap": "Edge Gap", "conf_leds_layout_cl_entertainment": "Entertainment Area", "conf_leds_layout_cl_entertainment_center": "Entertainment Area Center", @@ -103,6 +104,7 @@ "conf_leds_layout_cl_lightPosBottomLeft112": "Bottom: 0 - 50% from Left", "conf_leds_layout_cl_lightPosBottomLeft121": "Bottom: 50 - 100% from Left", "conf_leds_layout_cl_lightPosBottomLeftNewMid": "Bottom: 25 - 75% from Left", + "conf_leds_layout_cl_lightPosEntire": "Whole picture", "conf_leds_layout_cl_lightPosTopLeft112": "Top: 0 - 50% from Left", "conf_leds_layout_cl_lightPosTopLeft121": "Top: 50 - 100% from Left", "conf_leds_layout_cl_lightPosTopLeftNewMid": "Top: 25 - 75% from Left", @@ -161,11 +163,12 @@ "conf_leds_note_layout_overwrite": "Note: Overwrite creates a default layout for {{plural:$1| one LED| all $1 LEDs}} given by the hardware LED count", "conf_leds_optgroup_RPiGPIO": "RPi GPIO", "conf_leds_optgroup_RPiPWM": "RPi PWM", - "conf_leds_optgroup_RPiSPI": "RPi SPI", + "conf_leds_optgroup_SPI": "SPI", "conf_leds_optgroup_debug": "Debug", "conf_leds_optgroup_network": "Network", "conf_leds_optgroup_other": "Other", "conf_leds_optgroup_usb": "USB/Serial", + "conf_leds_optgroup_ftdi": "USB/Ftdi", "conf_logging_btn_autoscroll": "Auto scrolling", "conf_logging_btn_clipboard": "Copy Log to Clipboard", "conf_logging_btn_pbupload": "Upload a report for support requests", @@ -624,6 +627,11 @@ "edt_dev_enum_sub_min_cool_adjust": "Subtract cool white", "edt_dev_enum_sub_min_warm_adjust": "Subtract warm white", "edt_dev_enum_subtract_minimum": "Subtract minimum", + "edt_dev_enum_cold_white": "Cold white", + "edt_dev_enum_neutral_white": "Neutral white", + "edt_dev_enum_auto": "Auto", + "edt_dev_enum_auto_max": "Auto max", + "edt_dev_enum_auto_accurate": "Auto accurate", "edt_dev_enum_white_off": "White off", "edt_dev_general_autostart_title": "Autostart", "edt_dev_general_autostart_title_info": "The LED device is switched-on during startup or not", @@ -660,13 +668,14 @@ "edt_dev_spec_colorComponent_title": "Colour component", "edt_dev_spec_debugLevel_title": "Debug Level", "edt_dev_spec_delayAfterConnect_title": "Delay after connect", - "edt_dev_spec_devices_discovered_none": "No Devices Discovered", - "edt_dev_spec_devices_discovered_title": "Devices Discovered", + "edt_dev_spec_devices_discovered_none": "No Devices discovered", + "edt_dev_spec_devices_discovered_title": "Devices discovered", "edt_dev_spec_devices_discovered_title_info": "Select your LED-Device discovered", "edt_dev_spec_devices_discovered_title_info_custom": "Select your LED-Device discovered or configure a custome one", "edt_dev_spec_devices_discovery_inprogress": "Discovery in progress", "edt_dev_spec_dithering_title": "Dithering", "edt_dev_spec_dmaNumber_title": "DMA channel", + "edt_dev_spec_fullBrightnessAtStart_title": "Full brightness at start", "edt_dev_spec_gamma_title": "Gamma", "edt_dev_spec_globalBrightnessControlMaxLevel_title": "Max Current Level", "edt_dev_spec_globalBrightnessControlThreshold_title": "Adaptive Current Threshold", @@ -684,6 +693,7 @@ "edt_dev_spec_ledType_title": "LED Type", "edt_dev_spec_lightid_itemtitle": "ID", "edt_dev_spec_lightid_title": "Light ID(s)", + "edt_dev_spec_lights_discovered_none": "No Lights discovered", "edt_dev_spec_lights_itemtitle": "Light", "edt_dev_spec_lights_name": "Name", "edt_dev_spec_lights_title": "Light(s)", @@ -705,6 +715,7 @@ "edt_dev_spec_port_expl": "Service Port [1-65535]", "edt_dev_spec_port_title": "Port", "edt_dev_spec_printTimeStamp_title": "Add timestamp", + "edt_dev_spec_skydimo_mode_title": "Skydimo Mode", "edt_dev_spec_stream_protocol_title": "Streaming protocol", "edt_dev_spec_pwmChannel_title": "PWM channel", "edt_dev_spec_razer_device_title": "Razer Chroma Device", @@ -1182,9 +1193,10 @@ "wiz_identify_tip": "Identify configured device by lighting it up", "wiz_identify_light": "Identify $1", "wiz_layout": "Generate Layout", + "wiz_layout_led_position_title": "LED position", + "wiz_layout_led_positions_title": "LED position layout wizard", + "wiz_layout_led_positions_expl": "Select the LED position for the $1 controller lights.", "wiz_layout_tip": "Generate a layout for the configured device", - "wiz_ids_disabled": "Deactivated", - "wiz_ids_entire": "Whole picture", "wiz_nanoleaf_failure_auth_token": "Please press the Nanoleaf Power On/Off button within 30 seconds", "wiz_nanoleaf_failure_auth_token_t": "User authorization token generating timeout", "wiz_nanoleaf_press_onoff_button": "Please press the Power On/Off button on your Nanoleaf device for 5-7 seconds", diff --git a/assets/webconfig/i18n/sv.json b/assets/webconfig/i18n/sv.json index b5ad8f86..1727fb04 100644 --- a/assets/webconfig/i18n/sv.json +++ b/assets/webconfig/i18n/sv.json @@ -159,8 +159,9 @@ "conf_leds_note_layout_overwrite": "Varning: Åsidosättande skapar en standardlayout för {{plural:$1| en LED| varje $1 lysdioder}} enligt det givna antalet lysdioder för hårdvara", "conf_leds_optgroup_RPiGPIO": "RPi GPIO", "conf_leds_optgroup_RPiPWM": "RPi PWM", - "conf_leds_optgroup_RPiSPI": "RPi SPI", + "conf_leds_optgroup_SPI": "SPI", "conf_leds_optgroup_debug": "Felsöka", + "conf_leds_optgroup_ftdi": "USB/Ftdi", "conf_leds_optgroup_network": "Nätverk", "conf_leds_optgroup_other": "Annat", "conf_leds_optgroup_usb": "USB/Seriell", @@ -612,6 +613,11 @@ "edt_conf_webc_sslport_title": "HTTPS-Port", "edt_dev_auth_key_title": "Auktorisationsnyckel", "edt_dev_auth_key_title_info": "Auktorisationsnyckel krävs för att få åtkomst till enheten", + "edt_dev_enum_auto": "Auto", + "edt_dev_enum_auto_accurate": "Auto noggrann", + "edt_dev_enum_auto_max": "Auto max", + "edt_dev_enum_cold_white": "Kallvitt", + "edt_dev_enum_neutral_white": "Neutralvitt", "edt_dev_enum_sub_min_cool_adjust": "Minsta justering: kall", "edt_dev_enum_sub_min_warm_adjust": "Minsta justering: varm", "edt_dev_enum_subtract_minimum": "Subtrahera minimum", @@ -963,6 +969,7 @@ "general_country_us": "USA", "general_disabled": "Inaktiverad", "general_enabled": "Aktiverad", + "general_speech_bg": "Bulgariska", "general_speech_ca": "Katalanska", "general_speech_cs": "Tjeckiska", "general_speech_da": "Danska", diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index 6609b3bd..70d9ce18 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -197,7 +197,7 @@ $(document).ready(function () { removeStorage("loginToken"); requestRequiresDefaultPasswortChange(); } - else if (event.reason == "Selected Hyperion instance isn't running") { + else if (event.reason == "Selected Hyperion instance is not running") { //Switch to default instance instanceSwitch(0); } else { diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index c9ec2eac..f8b6c080 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -18,10 +18,11 @@ var bottomRight2bottomLeft = null; var bottomLeft2topLeft = null; var toggleKeystoneCorrectionArea = false; -var devRPiSPI = ['apa102', 'apa104', 'ws2801', 'lpd6803', 'lpd8806', 'p9813', 'sk6812spi', 'sk6822spi', 'sk9822', 'ws2812spi']; +var devSPI = ['apa102', 'apa104', 'ws2801', 'lpd6803', 'lpd8806', 'p9813', 'sk6812spi', 'sk6822spi', 'sk9822', 'ws2812spi']; +var devFTDI = ['apa102_ftdi', 'sk6812_ftdi', 'ws2812_ftdi']; var devRPiPWM = ['ws281x']; var devRPiGPIO = ['piblaster']; -var devNET = ['atmoorb', 'cololight', 'fadecandy', 'philipshue', 'nanoleaf', 'razer', 'tinkerforge', 'tpm2net', 'udpe131', 'udpartnet', 'udpddp', 'udph801', 'udpraw', 'wled', 'yeelight']; +var devNET = ['atmoorb', 'cololight', 'fadecandy', 'homeassistant', 'philipshue', 'nanoleaf', 'razer', 'tinkerforge', 'tpm2net', 'udpe131', 'udpartnet', 'udpddp', 'udph801', 'udpraw', 'wled', 'yeelight']; var devSerial = ['adalight', 'dmx', 'atmo', 'sedu', 'tpm2', 'karate']; var devHID = ['hyperionusbasp', 'lightpack', 'paintpack', 'rawhid']; @@ -1099,6 +1100,7 @@ $(document).ready(function () { switch (ledType) { case "wled": case "cololight": + case "homeassistant": case "nanoleaf": showAllDeviceInputOptions("hostList", false); case "apa102": @@ -1121,6 +1123,12 @@ $(document).ready(function () { case "karate": case "sedu": case "tpm2": + + //FTDI devices + case "apa102_ftdi": + case "sk6812_ftdi": + case "ws2812_ftdi": + if (storedAccess === 'expert') { filter.discoverAll = true; } @@ -1139,6 +1147,7 @@ $(document).ready(function () { .catch(error => { showNotification('danger', "Device discovery for " + ledType + " failed with error:" + error); }); + break; case "philipshue": { @@ -1271,7 +1280,21 @@ $(document).ready(function () { if (hostList !== "SELECT") { const host = conf_editor.getEditor("root.specificOptions.host").getValue(); const token = conf_editor.getEditor("root.specificOptions.token").getValue(); - if (host !== "" && token !== "") { + if (host !== "" && token !== "" && entityIds) { + canIdentify = true; + canSave = true; + } + } + } + break; + + case "homeassistant": { + const hostList = conf_editor.getEditor("root.specificOptions.hostList").getValue(); + if (hostList !== "SELECT") { + const host = conf_editor.getEditor("root.specificOptions.host").getValue(); + const token = conf_editor.getEditor("root.specificOptions.token").getValue(); + const entityIds = conf_editor.getEditor("root.specificOptions.entityIds").getValue(); + if (host !== "" && token !== "" && entityIds) { canIdentify = true; canSave = true; } @@ -1379,6 +1402,16 @@ $(document).ready(function () { getProperties_device(ledType, host, params); break; + case "homeassistant": + var token = conf_editor.getEditor("root.specificOptions.token").getValue(); + if (token === "") { + return; + } + + params = { host: host, token: token, filter: "states" }; + getProperties_device(ledType, host, params); + break; + case "nanoleaf": $('#btn_wiz_holder').show(); @@ -1441,6 +1474,9 @@ $(document).ready(function () { case "sk9822": case "ws2812spi": case "piblaster": + case "apa102_ftdi": + case "sk6812_ftdi": + case "ws2812_ftdi": default: } @@ -1541,6 +1577,14 @@ $(document).ready(function () { var host = ""; switch (ledType) { + case "homeassistant": + host = conf_editor.getEditor("root.specificOptions.host").getValue(); + if (host === "") { + return + } + params = { host: host, token: token, filter: "states" }; + break; + case "nanoleaf": host = conf_editor.getEditor("root.specificOptions.host").getValue(); if (host === "") { @@ -1643,6 +1687,16 @@ $(document).ready(function () { default: } }); + + conf_editor.watch('root.specificOptions.entityIds', () => { + var entityIds = conf_editor.getEditor("root.specificOptions.entityIds").getValue(); + if (entityIds.length > 0) { + $('#btn_test_controller').prop('disabled', false); + } else { + $('#btn_test_controller').prop('disabled', true); + } + }); + }); //philipshueentertainment backward fix @@ -1657,9 +1711,10 @@ $(document).ready(function () { optArr[3] = []; optArr[4] = []; optArr[5] = []; + optArr[6] = []; for (var idx = 0; idx < ledDevices.length; idx++) { - if ($.inArray(ledDevices[idx], devRPiSPI) != -1) + if ($.inArray(ledDevices[idx], devSPI) != -1) optArr[0].push(ledDevices[idx]); else if ($.inArray(ledDevices[idx], devRPiPWM) != -1) optArr[1].push(ledDevices[idx]); @@ -1671,8 +1726,12 @@ $(document).ready(function () { optArr[4].push(ledDevices[idx]); else if ($.inArray(ledDevices[idx], devHID) != -1) optArr[4].push(ledDevices[idx]); + else if (ledDevices[idx].endsWith("_ftdi")) { + var title = ledDevices[idx].replace('_ftdi', ''); + optArr[5].push(ledDevices[idx] + ":" + title); + } else - optArr[5].push(ledDevices[idx]); + optArr[6].push(ledDevices[idx]); } $("#leddevices").append(createSel(optArr[0], $.i18n('conf_leds_optgroup_RPiSPI'))); @@ -1680,9 +1739,10 @@ $(document).ready(function () { $("#leddevices").append(createSel(optArr[2], $.i18n('conf_leds_optgroup_RPiGPIO'))); $("#leddevices").append(createSel(optArr[3], $.i18n('conf_leds_optgroup_network'))); $("#leddevices").append(createSel(optArr[4], $.i18n('conf_leds_optgroup_usb'))); + $("#leddevices").append(createSel(optArr[5], $.i18n('conf_leds_optgroup_ftdi'), true)); if (storedAccess === 'expert' || window.serverConfig.device.type === "file") { - $("#leddevices").append(createSel(optArr[5], $.i18n('conf_leds_optgroup_other'))); + $("#leddevices").append(createSel(optArr[6], $.i18n('conf_leds_optgroup_other'))); } $("#leddevices").val(window.serverConfig.device.type); @@ -1727,6 +1787,13 @@ $(document).ready(function () { params = { host: host }; break; + case "homeassistant": + var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + var token = conf_editor.getEditor("root.specificOptions.token").getValue(); + const entityIds = conf_editor.getEditor("root.specificOptions.entityIds").getValue(); + params = { host: host, token: token, entity_id: entityIds }; + break; + case "nanoleaf": var host = conf_editor.getEditor("root.specificOptions.host").getValue(); var token = conf_editor.getEditor("root.specificOptions.token").getValue(); @@ -1861,6 +1928,7 @@ function saveLedConfig(genDefLayout = false) { } break; + case "homeassistant": case "nanoleaf": case "wled": case "yeelight": @@ -1886,6 +1954,9 @@ function saveLedConfig(genDefLayout = false) { case "sk9822": case "ws2812spi": case "piblaster": + case "apa102_ftdi": + case "sk6812_ftdi": + case "ws2812_ftdi": default: if (genDefLayout === true) { ledConfig = { @@ -1938,8 +2009,10 @@ var updateOutputSelectList = function (ledType, discoveryInfo) { ledTypeGroup = "devNET"; } else if ($.inArray(ledType, devSerial) != -1) { ledTypeGroup = "devSerial"; - } else if ($.inArray(ledType, devRPiSPI) != -1) { - ledTypeGroup = "devRPiSPI"; + } else if ($.inArray(ledType, devSPI) != -1) { + ledTypeGroup = "devSPI"; + } else if ($.inArray(ledType, devFTDI) != -1) { + ledTypeGroup = "devFTDI"; } else if ($.inArray(ledType, devRPiGPIO) != -1) { ledTypeGroup = "devRPiGPIO"; } else if ($.inArray(ledType, devRPiPWM) != -1) { @@ -2062,7 +2135,63 @@ var updateOutputSelectList = function (ledType, discoveryInfo) { } } break; - case "devRPiSPI": + + case "devFTDI": + key = "output"; + + if (discoveryInfo.devices.length == 0) { + enumVals.push("NONE"); + enumTitleVals.push($.i18n('edt_dev_spec_devices_discovered_none')); + $('#btn_submit_controller').prop('disabled', true); + showAllDeviceInputOptions(key, false); + } + else { + switch (ledType) { + case "ws2812_ftdi": + case "sk6812_ftdi": + case "apa102_ftdi": + for (const device of discoveryInfo.devices) { + enumVals.push(device.ftdiOpenString); + + var title = "FTDI"; + if (device.manufacturer) { + title = device.manufacturer; + } + + if (device.serialNumber) { + title += " - " + device.serialNumber; + } + title += " (" + device.vendorIdentifier + "|" + device.productIdentifier + ")"; + + if (device.description) { + title += " " + device.description; + } + + enumTitleVals.push(title); + } + + // Select configured device + var configuredDeviceType = window.serverConfig.device.type; + var configuredOutput = window.serverConfig.device.output; + if (ledType === configuredDeviceType) { + if ($.inArray(configuredOutput, enumVals) != -1) { + enumDefaultVal = configuredOutput; + } else { + enumVals.push(window.serverConfig.device.output); + enumDefaultVal = configuredOutput; + } + } + else { + addSelect = true; + } + + break; + default: + } + } + break; + + case "devSPI": case "devRPiGPIO": key = "output"; @@ -2128,7 +2257,6 @@ var updateOutputSelectList = function (ledType, discoveryInfo) { async function discover_device(ledType, params) { const result = await requestLedDeviceDiscovery(ledType, params); - var discoveryResult = {}; if (result) { if (result.error) { @@ -2234,6 +2362,12 @@ function updateElements(ledType, key) { } break; + case "homeassistant": + updateElementsHomeAssistant(ledType, key); + hardwareLedCount = 1; + conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); + break; + case "atmo": case "karate": var ledProperties = devicesProperties[ledType][key]; @@ -2361,6 +2495,63 @@ function validateWledLedCount(hardwareLedCount) { } } +function updateElementsHomeAssistant(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 light selection list values + var enumVals = []; + var enumTitleVals = []; + var enumDefaultVal = []; + + if (devicesProperties[ledType] && devicesProperties[ledType][key]) { + var ledDeviceProperties = devicesProperties[ledType][key]; + + if (!jQuery.isEmptyObject(ledDeviceProperties)) { + if (ledDeviceProperties && ledDeviceProperties.lightEntities) { + + + for (const light of ledDeviceProperties.lightEntities) { + enumVals.push(light.entity_id); + enumTitleVals.push(light.attributes.friendly_name); + } + + } + } + } + + // Select configured device + if (configuredDeviceType == ledType && configuredHost == host) { + let configuredEntityIds = window.serverConfig.device.entityIds; + for (const light of configuredEntityIds) { + if ($.inArray(enumVals, light) != -1) { + enumVals.push(light); + } + enumDefaultVal.push(light); + } + } + + if (enumVals.length < 1) { + enumVals.push("NONE"); + enumTitleVals.push($.i18n('edt_dev_spec_lights_discovered_none')); + } + else { + $('#btn_wiz_holder').show(); + } + + + let addSchemaElements = { + "uniqueItems": true, + "minItems": 1, + "required": true + }; + + updateJsonEditorMultiSelection(conf_editor, 'root.specificOptions', 'entityIds', addSchemaElements, enumVals, enumTitleVals, enumDefaultVal); +} + function updateElementsWled(ledType, key) { // Get configured device's details @@ -2456,6 +2647,7 @@ function updateElementsWled(ledType, key) { } showInputOptionForItem(conf_editor, "root.specificOptions.segments", "switchOffOtherSegments", showAdditionalOptions); } + function sortByPanelCoordinates(arr, topToBottom, leftToRight) { arr.sort((a, b) => { //Nanoleaf corodinates start at bottom left, therefore reverse topToBottom @@ -2514,7 +2706,7 @@ function nanoleafGeneratelayout(panelLayout, panelOrderTopDown, panelOrderLeftRi 29: { name: "4DLightstrip", led: true, sideLengthX: 50, sideLengthY: 50 }, 30: { name: "Skylight Panel", led: true, sideLengthX: 180, sideLengthY: 180 }, 31: { name: "SkylightControllerPrimary", led: true, sideLengthX: 180, sideLengthY: 180 }, - 32: { name: "SkylightControllerPassive", led: true, sideLengthX: 180, sideLengthY: 180 }, + 32: { name: "SkylightControllerPassive", led: true, sideLengthX: 180, sideLengthY: 180 }, 999: { name: "Unknown", led: true, sideLengthX: 100, sideLengthY: 100 } }; diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index 4bfeb85e..578e3128 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -321,7 +321,7 @@ function showInfoDialog(type, header, message) { $(document).on('click', '[data-dismiss-modal]', function () { var target = $(this).data('dismiss-modal'); $($.find(target)).modal('hide'); -}); + }); } function createHintH(type, text, container) { @@ -478,7 +478,7 @@ function createJsonEditor(container, schema, setconfig, usePanel, arrayre) { return editor; } -function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal, addSelect, addCustom, addCustomAsFirst, customText) { +function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVals, newTitleVals, newDefaultVal, addSelect, addCustom, addCustomAsFirst, customText) { var editor = rootEditor.getEditor(path); var orginalProperties = editor.schema.properties[key]; @@ -516,8 +516,8 @@ function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVa if (addCustom) { - if (newTitelVals.length === 0) { - newTitelVals = [...newEnumVals]; + if (newTitleVals.length === 0) { + newTitleVals = [...newEnumVals]; } if (!!!customText) { @@ -526,10 +526,10 @@ function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVa if (addCustomAsFirst) { newEnumVals.unshift("CUSTOM"); - newTitelVals.unshift(customText); + newTitleVals.unshift(customText); } else { newEnumVals.push("CUSTOM"); - newTitelVals.push(customText); + newTitleVals.push(customText); } if (newSchema[key].options.infoText) { @@ -540,7 +540,7 @@ function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVa if (addSelect) { newEnumVals.unshift("SELECT"); - newTitelVals.unshift("edt_conf_enum_please_select"); + newTitleVals.unshift("edt_conf_enum_please_select"); newDefaultVal = "SELECT"; } @@ -548,8 +548,8 @@ function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVa newSchema[key]["enum"] = newEnumVals; } - if (newTitelVals) { - newSchema[key]["options"]["enum_titles"] = newTitelVals; + if (newTitleVals) { + newSchema[key]["options"]["enum_titles"] = newTitleVals; } if (newDefaultVal) { newSchema[key]["default"] = newDefaultVal; @@ -572,7 +572,7 @@ function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVa rootEditor.notifyWatchers(path + "." + key); } -function updateJsonEditorMultiSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal) { +function updateJsonEditorMultiSelection(rootEditor, path, key, addElements, newEnumVals, newTitleVals, newDefaultVal) { var editor = rootEditor.getEditor(path); var orginalProperties = editor.schema.properties[key]; @@ -617,8 +617,8 @@ function updateJsonEditorMultiSelection(rootEditor, path, key, addElements, newE newSchema[key]["items"]["enum"] = newEnumVals; } - if (newTitelVals) { - newSchema[key]["items"]["options"]["enum_titles"] = newTitelVals; + if (newTitleVals) { + newSchema[key]["items"]["options"]["enum_titles"] = newTitleVals; } if (newDefaultVal) { @@ -923,8 +923,8 @@ function createTableRow(list, head, align) { el.style.verticalAlign = "middle"; var purifyConfig = { - ADD_TAGS: ['button'], - ADD_ATTR: ['onclick'] + ADD_TAGS: ['button'], + ADD_ATTR: ['onclick'] }; el.innerHTML = DOMPurify.sanitize(list[i], purifyConfig); row.appendChild(el); @@ -1403,7 +1403,7 @@ function loadScript(src, callback, ...params) { if (isScriptLoaded(src)) { debugMessage('Script ' + src + ' already loaded'); if (callback && typeof callback === 'function') { - callback( ...params); + callback(...params); } return; } diff --git a/assets/webconfig/js/wizard.js b/assets/webconfig/js/wizard.js index 2524924f..220888ef 100755 --- a/assets/webconfig/js/wizard.js +++ b/assets/webconfig/js/wizard.js @@ -37,27 +37,37 @@ function createLedDeviceWizards(ledType) { $('#btn_led_device_wiz').off(); if (ledType == "philipshue") { $('#btn_wiz_holder').show(); - data = { ledType }; + wizardName = ledType; + data = { wizardName }; title = 'wiz_hue_title'; } else if (ledType == "nanoleaf") { $('#btn_wiz_holder').hide(); - data = { ledType }; + wizardName = ledType; + data = { wizardName }; title = 'wiz_nanoleaf_user_auth_title'; } + else if (ledType == "homeassistant") { + $('#btn_wiz_holder').hide(); + wizardName = "layoutLedPositions"; + data = { wizardName, ledType }; + title = 'wiz_layout_led_positions_title'; + } else if (ledType == "atmoorb") { $('#btn_wiz_holder').show(); - data = { ledType }; + wizardName = ledType; + data = { wizardName }; title = 'wiz_atmoorb_title'; } else if (ledType == "yeelight") { $('#btn_wiz_holder').show(); - data = { ledType }; + wizardName = ledType; + data = { wizardName }; title = 'wiz_yeelight_title'; } if (Object.keys(data).length !== 0) { - startLedDeviceWizard(data, title, ledType + "Wizard"); + startLedDeviceWizard(data, title, wizardName + "Wizard"); } } @@ -66,8 +76,7 @@ function startLedDeviceWizard(data, hint, wizardName) { createHint("wizard", $.i18n(hint), "btn_wiz_holder", "btn_led_device_wiz"); $('#btn_led_device_wiz').off(); $('#btn_led_device_wiz').on('click', async (e) => { - const { [wizardName]: winzardObject } = await import('./wizards/LedDevice_' + data.ledType + '.js'); - winzardObject.start(e); + const { [wizardName]: winzardObject } = await import('./wizards/LedDevice_' + data.wizardName + '.js'); + winzardObject.start(e, data); }); } - diff --git a/assets/webconfig/js/wizards/LedDevice_atmoorb.js b/assets/webconfig/js/wizards/LedDevice_atmoorb.js index 67d9bd5a..768bdda5 100644 --- a/assets/webconfig/js/wizards/LedDevice_atmoorb.js +++ b/assets/webconfig/js/wizards/LedDevice_atmoorb.js @@ -151,17 +151,7 @@ const atmoorbWizard = (() => { $('#wh_topcontainer').toggle(false); $('#orb_ids_t, #btn_wiz_save').toggle(true); - const lightOptions = [ - "top", "topleft", "topright", - "bottom", "bottomleft", "bottomright", - "left", "lefttop", "leftmiddle", "leftbottom", - "right", "righttop", "rightmiddle", "rightbottom", - "entire", - "lightPosTopLeft112", "lightPosTopLeftNewMid", "lightPosTopLeft121", - "lightPosBottomLeft14", "lightPosBottomLeft12", "lightPosBottomLeft34", "lightPosBottomLeft11", - "lightPosBottomLeft112", "lightPosBottomLeftNewMid", "lightPosBottomLeft121" - ]; - + const lightOptions = utils.getLayoutPositions(); lightOptions.unshift("disabled"); $('.lidsb').html(""); @@ -178,10 +168,9 @@ const atmoorbWizard = (() => { let options = ""; for (const opt in lightOptions) { const val = lightOptions[opt]; - const txt = (val !== 'entire' && val !== 'disabled') ? 'conf_leds_layout_cl_' : 'wiz_ids_'; options += ''; } let enabled = 'enabled'; diff --git a/assets/webconfig/js/wizards/LedDevice_layoutLedPositions.js b/assets/webconfig/js/wizards/LedDevice_layoutLedPositions.js new file mode 100644 index 00000000..d316713f --- /dev/null +++ b/assets/webconfig/js/wizards/LedDevice_layoutLedPositions.js @@ -0,0 +1,74 @@ +//**************************** +// Wizard LED Layout +//**************************** + +import { ledDeviceWizardUtils as utils } from './LedDevice_utils.js'; + +const layoutLedPositionsWizard = (() => { + + let wiz_editor; + + function createEditor() { + wiz_editor = createJsonEditor('editor_container_wiz', { + layoutPosition: { + "type": "string", + "title": "wiz_layout_led_position_title", + "enum": utils.getLayoutPositions(), + "options": { + "enum_titles": utils.getLayoutPositionsTitles() + } + } + }, true, true); + } + + function stopWizardLedLayout(reload) { + resetWizard(reload); + } + + function beginWizardLayoutLedPositions() { + createEditor(); + setStorage("wizardactive", true); + + $('#btn_wiz_abort').off().on('click', function () { + stopWizardLedLayout(true); + }); + + $('#btn_wiz_ok').off().on('click', function () { + const layoutPosition = wiz_editor.getEditor("root.layoutPosition").getValue(); + const layoutObject = utils.assignLightPos(layoutPosition); + + var layoutObjects = []; + layoutObjects.push(JSON.parse(JSON.stringify(layoutObject))); + aceEdt.set(layoutObjects); + + stopWizardLedLayout(true); + }); + } + + return { + start: function (e, data) { + $('#wiz_header').html('' + $.i18n('wiz_layout_led_positions_title')); + $('#wizp1_body').html('
' + $.i18n('wiz_layout_led_positions_expl', data.ledType) + '

' + + '
' + ); + $('#wizp1_footer').html('' + ); + + if (getStorage("darkMode") == "on") + $('#wizard_logo').attr("src", 'img/hyperion/logo_negativ.png'); + + //open modal + $("#wizard_modal").modal({ + backdrop: "static", + keyboard: false, + show: true + }); + + beginWizardLayoutLedPositions(); + } + }; +})(); + +export { layoutLedPositionsWizard }; + diff --git a/assets/webconfig/js/wizards/LedDevice_philipshue.js b/assets/webconfig/js/wizards/LedDevice_philipshue.js index bfc33bd8..8c1d4c14 100644 --- a/assets/webconfig/js/wizards/LedDevice_philipshue.js +++ b/assets/webconfig/js/wizards/LedDevice_philipshue.js @@ -794,17 +794,7 @@ const philipshueWizard = (() => { } $('#hue_ids_t, #btn_wiz_save').toggle(true); - const lightOptions = [ - "top", "topleft", "topright", - "bottom", "bottomleft", "bottomright", - "left", "lefttop", "leftmiddle", "leftbottom", - "right", "righttop", "rightmiddle", "rightbottom", - "entire", - "lightPosTopLeft112", "lightPosTopLeftNewMid", "lightPosTopLeft121", - "lightPosBottomLeft14", "lightPosBottomLeft12", "lightPosBottomLeft34", "lightPosBottomLeft11", - "lightPosBottomLeft112", "lightPosBottomLeftNewMid", "lightPosBottomLeft121" - ]; - + const lightOptions = utils.getLayoutPositions(); if (isEntertainmentReady && hueEntertainmentConfigs.length > 0) { lightOptions.unshift("entertainment_center"); lightOptions.unshift("entertainment"); @@ -866,10 +856,9 @@ const philipshueWizard = (() => { let options = ""; for (const opt in lightOptions) { const val = lightOptions[opt]; - const txt = (val != 'entire' && val != 'disabled') ? 'conf_leds_layout_cl_' : 'wiz_ids_'; options += ''; } $('.lidsb').append(createTableRow([id + ' (' + lightName + ')', diff --git a/assets/webconfig/js/wizards/LedDevice_utils.js b/assets/webconfig/js/wizards/LedDevice_utils.js index 1f3eab3e..a1f05471 100644 --- a/assets/webconfig/js/wizards/LedDevice_utils.js +++ b/assets/webconfig/js/wizards/LedDevice_utils.js @@ -52,6 +52,17 @@ const ledDeviceWizardUtils = (() => { const i = positionMap[pos] || positionMap["lightPosEntire"]; i.name = name; return i; + }, + getLayoutPositions: function () { + return Object.keys(positionMap); + }, + getLayoutPositionsTitles: function () { + + let layoutPositionTitles = []; + for (const layoutPosition of Object.keys(positionMap)) { + layoutPositionTitles.push($.i18n('conf_leds_layout_cl_' + layoutPosition)); + } + return layoutPositionTitles; } }; diff --git a/assets/webconfig/js/wizards/LedDevice_yeelight.js b/assets/webconfig/js/wizards/LedDevice_yeelight.js index 4f53eb07..2be0f91c 100644 --- a/assets/webconfig/js/wizards/LedDevice_yeelight.js +++ b/assets/webconfig/js/wizards/LedDevice_yeelight.js @@ -173,17 +173,7 @@ const yeelightWizard = (() => { $('#wh_topcontainer').toggle(false); $('#yee_ids_t, #btn_wiz_save').toggle(true); - const lightOptions = [ - "top", "topleft", "topright", - "bottom", "bottomleft", "bottomright", - "left", "lefttop", "leftmiddle", "leftbottom", - "right", "righttop", "rightmiddle", "rightbottom", - "entire", - "lightPosTopLeft112", "lightPosTopLeftNewMid", "lightPosTopLeft121", - "lightPosBottomLeft14", "lightPosBottomLeft12", "lightPosBottomLeft34", "lightPosBottomLeft11", - "lightPosBottomLeft112", "lightPosBottomLeftNewMid", "lightPosBottomLeft121" - ]; - + const lightOptions = utils.getLayoutPositions(); lightOptions.unshift("disabled"); $('.lidsb').html(""); @@ -200,10 +190,9 @@ const yeelightWizard = (() => { let options = ""; for (const opt in lightOptions) { const val = lightOptions[opt]; - const txt = (val !== 'entire' && val !== 'disabled') ? 'conf_leds_layout_cl_' : 'wiz_ids_'; options += ''; } let enabled = 'enabled'; diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index c12edca9..eea412f1 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -24,36 +24,19 @@ if (ENABLE_MDNS) if(USE_SYSTEM_QMDNS_LIBS) find_package(qmdnsengine REQUIRED) else() - include(ExternalProject) - ExternalProject_Add(qmdns - PREFIX ${CMAKE_CURRENT_BINARY_DIR}/external/qmdnsengine - BUILD_ALWAYS OFF - DOWNLOAD_COMMAND "" - SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/qmdnsengine - BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/external/qmdnsengine/bin - CMAKE_ARGS -DBUILD_SHARED_LIBS:BOOL=OFF - -DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_BINARY_DIR} - -DBIN_INSTALL_DIR:STRING=lib - -DLIB_INSTALL_DIR:STRING=lib - -DINCLUDE_INSTALL_DIR:STRING=include - -DCMAKE_PREFIX_PATH:PATH=${CMAKE_PREFIX_PATH} - -DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER} - -DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER} - -DCMAKE_C_FLAGS:STRING=${CMAKE_C_FLAGS} - -DCMAKE_CXX_FLAGS:STRING=${CMAKE_CXX_FLAGS} - -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} - -Wno-dev # We don't want to be warned over unused variables - INSTALL_DIR ${CMAKE_BINARY_DIR} - BUILD_BYPRODUCTS /lib/${CMAKE_STATIC_LIBRARY_PREFIX}qmdnsengine${CMAKE_STATIC_LIBRARY_SUFFIX} - ) + # Build QMdnsEngine as static library + set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build statically version of QMdnsEngine") - add_library(qmdnsengine STATIC IMPORTED GLOBAL) - add_dependencies(qmdnsengine qmdns) - ExternalProject_Get_Property(qmdns INSTALL_DIR) - set_target_properties(qmdnsengine PROPERTIES - IMPORTED_LOCATION "${INSTALL_DIR}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}qmdnsengine${CMAKE_STATIC_LIBRARY_SUFFIX}" - INTERFACE_INCLUDE_DIRECTORIES "${INSTALL_DIR}/include" - ) + # Suppress warnings about "Compatibility with CMake < 3.5 will be removed from a future version of CMake" + set(CMAKE_WARN_DEPRECATED OFF CACHE BOOL "" FORCE) + + # Add QMdnsEngine directory to the build + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/external/qmdnsengine") + endif() + + if(TARGET qmdnsengine AND NOT TARGET qmdns) + add_library(qmdns INTERFACE IMPORTED GLOBAL) + target_link_libraries(qmdns INTERFACE qmdnsengine) endif() endif() @@ -149,6 +132,7 @@ if(ENABLE_PROTOBUF_SERVER) set(protobuf_BUILD_TESTS OFF CACHE BOOL "Build protobuf with tests") set(protobuf_BUILD_SHARED_LIBS OFF CACHE BOOL "Build protobuf shared") set(protobuf_WITH_ZLIB OFF CACHE BOOL "Build protobuf with zlib support") + set(protobuf_BUILD_LIBUPB OFF CACHE BOOL "Build libupb") if (WIN32) set(protobuf_MSVC_STATIC_RUNTIME OFF CACHE BOOL "Build protobuf static") diff --git a/dependencies/external/flatbuffers b/dependencies/external/flatbuffers index 0100f6a5..595bf000 160000 --- a/dependencies/external/flatbuffers +++ b/dependencies/external/flatbuffers @@ -1 +1 @@ -Subproject commit 0100f6a5779831fa7a651e4b67ef389a8752bd9b +Subproject commit 595bf0007ab1929570c7671f091313c8fc20644e diff --git a/dependencies/external/mbedtls b/dependencies/external/mbedtls index edb8fec9..2ca6c285 160000 --- a/dependencies/external/mbedtls +++ b/dependencies/external/mbedtls @@ -1 +1 @@ -Subproject commit edb8fec9882084344a314368ac7fd957a187519c +Subproject commit 2ca6c285a0dd3f33982dd57299012dacab1ff206 diff --git a/dependencies/external/protobuf b/dependencies/external/protobuf index 7f94235e..3d9f7c43 160000 --- a/dependencies/external/protobuf +++ b/dependencies/external/protobuf @@ -1 +1 @@ -Subproject commit 7f94235e552599141950d7a4a3eaf93bc87d1b22 +Subproject commit 3d9f7c430a5ae1385512908801492d4421c3cdb7 diff --git a/doc/development/CompileHowto.md b/doc/development/CompileHowto.md index 2de4a770..8338bf31 100644 --- a/doc/development/CompileHowto.md +++ b/doc/development/CompileHowto.md @@ -61,14 +61,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 libasound2-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 libftdi1-dev ``` **Ubuntu (22.04+) - Qt6 based** ```console sudo apt-get update -sudo apt-get install git cmake build-essential qt6-base-dev libqt6serialport6-dev libxkbcommon-dev libvulkan-dev libgl1-mesa-dev libusb-1.0-0-dev python3-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev pkg-config +sudo apt-get install git cmake build-essential qt6-base-dev libqt6serialport6-dev libxkbcommon-dev libvulkan-dev libgl1-mesa-dev libusb-1.0-0-dev python3-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev pkg-config libftdi1-dev ``` **For Linux X11/XCB grabber support** @@ -110,16 +110,21 @@ 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 alsa-lib-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 libftdi1-dev ``` After installing the dependencies, you can continue with the compile instructions later on this page (the more detailed way..). -## OSX +## macOS To install on OS X you either need [Homebrew](https://brew.sh/) or [Macport](https://www.macports.org/) but Homebrew is the recommended way to install the packages. To use Homebrew, XCode is required as well, use `brew doctor` to check your install. -First you need to install the dependencies: +First you need to install the dependencies for either the QT5 or QT6 build: +####QT5 ```console -brew install git qt@5 python3 cmake libusb openssl@1.1 +brew install git qt@5 python3 cmake libusb openssl@1.1 libftdi pkg-config +``` +####QT6 +```console +brew install git qt python3 cmake libusb openssl@1.1 libftdi pkg-config ``` ## Windows @@ -147,7 +152,7 @@ We assume a 64bit Windows 10. Install the following; ## The general quick way (without big comments) -**complete automated process for Mac/Linux:** +**complete automated process (Linux only):** ```console wget -qO- https://raw.githubusercontent.com/hyperion-project/hyperion.ng/master/bin/compile.sh | sh ``` diff --git a/include/grabber/video/mediafoundation/MFGrabber.h b/include/grabber/video/mediafoundation/MFGrabber.h index 47c8cc62..da3b90c9 100644 --- a/include/grabber/video/mediafoundation/MFGrabber.h +++ b/include/grabber/video/mediafoundation/MFGrabber.h @@ -46,6 +46,7 @@ public: int numerator = 0; int denominator = 0; PixelFormat pf = PixelFormat::NO_CHANGE; + long defstride = 0; GUID guid = GUID_NULL; }; diff --git a/include/mdns/MdnsServiceRegister.h b/include/mdns/MdnsServiceRegister.h index 33bf7057..32980cc9 100644 --- a/include/mdns/MdnsServiceRegister.h +++ b/include/mdns/MdnsServiceRegister.h @@ -22,6 +22,7 @@ const MdnsServiceMap mDnsServiceMap = { //LED Devices {"cololight" , {"_hap._tcp.local.", "ColoLight.*"}}, + {"homeassistant", {"_home-assistant._tcp.local.", ".*"}}, {"nanoleaf" , {"_nanoleafapi._tcp.local.", ".*"}}, {"philipshue" , {"_hue._tcp.local.", ".*"}}, {"wled" , {"_wled._tcp.local.", ".*"}}, diff --git a/include/utils/PixelFormat.h b/include/utils/PixelFormat.h index 584ffc20..10eb9a1e 100644 --- a/include/utils/PixelFormat.h +++ b/include/utils/PixelFormat.h @@ -10,6 +10,7 @@ enum class PixelFormat { YUYV, UYVY, BGR16, + RGB24, BGR24, RGB32, BGR32, @@ -36,6 +37,10 @@ inline PixelFormat parsePixelFormat(const QString& pixelFormat) { return PixelFormat::BGR16; } + else if (format.compare("rgb24") == 0) + { + return PixelFormat::RGB24; + } else if (format.compare("bgr24") == 0) { return PixelFormat::BGR24; @@ -80,6 +85,10 @@ inline QString pixelFormatToString(const PixelFormat& pixelFormat) { return "BGR16"; } + else if (pixelFormat == PixelFormat::RGB24) + { + return "RGB24"; + } else if (pixelFormat == PixelFormat::BGR24) { return "BGR24"; @@ -115,10 +124,10 @@ inline QString pixelFormatToString(const PixelFormat& pixelFormat) enum class FlipMode { + NO_CHANGE, HORIZONTAL, VERTICAL, - BOTH, - NO_CHANGE + BOTH }; inline FlipMode parseFlipMode(const QString& flipMode) diff --git a/include/utils/RgbToRgbw.h b/include/utils/RgbToRgbw.h index 0bcd6f46..4fb4e34b 100644 --- a/include/utils/RgbToRgbw.h +++ b/include/utils/RgbToRgbw.h @@ -11,7 +11,12 @@ namespace RGBW { SUBTRACT_MINIMUM, SUB_MIN_WARM_ADJUST, SUB_MIN_COOL_ADJUST, - WHITE_OFF + WHITE_OFF, + COLD_WHITE, + NEUTRAL_WHITE, + AUTO, + AUTO_MAX, + AUTO_ACCURATE }; WhiteAlgorithm stringToWhiteAlgorithm(const QString& str); diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 67cfc49e..fd93cc55 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -745,7 +745,7 @@ void JsonAPI::handleConfigSetCommand(const QJsonObject &message, const JsonApiCo } else { - sendErrorReply("Saving configuration while Hyperion is disabled isn't possible", cmd); + sendErrorReply("It is not possible saving a configuration while Hyperion is disabled", cmd); } } } diff --git a/libsrc/grabber/dda/DDAGrabber.cpp b/libsrc/grabber/dda/DDAGrabber.cpp index aea46046..6be4d40a 100644 --- a/libsrc/grabber/dda/DDAGrabber.cpp +++ b/libsrc/grabber/dda/DDAGrabber.cpp @@ -174,7 +174,7 @@ int DDAGrabber::grabFrame(Image &image) // Acquire the next frame. CComPtr desktopResource; DXGI_OUTDUPL_FRAME_INFO frameInfo; - hr = d->desktopDuplication->AcquireNextFrame(INFINITE, &frameInfo, &desktopResource); + hr = d->desktopDuplication->AcquireNextFrame(500, &frameInfo, &desktopResource); if (hr == DXGI_ERROR_ACCESS_LOST || hr == DXGI_ERROR_INVALID_CALL) { if (!restartCapture()) @@ -185,7 +185,7 @@ int DDAGrabber::grabFrame(Image &image) } if (hr == DXGI_ERROR_WAIT_TIMEOUT) { - // This shouldn't happen since we specified an INFINITE timeout. + // Nothing changed on the screen in the 500ms we waited. return 0; } RETURN_IF_ERROR(hr, "Failed to acquire next frame", 0); diff --git a/libsrc/grabber/video/EncoderThread.cpp b/libsrc/grabber/video/EncoderThread.cpp index e891c821..79aa1e38 100644 --- a/libsrc/grabber/video/EncoderThread.cpp +++ b/libsrc/grabber/video/EncoderThread.cpp @@ -131,7 +131,7 @@ void EncoderThread::process() #if defined(ENABLE_V4L2) _pixelFormat, #else - PixelFormat::BGR24, + PixelFormat::BGR24, // MF-Grabber always sends RGB24, but memory layout is RGBTRIPLE (b,g,r) -> process as BGR24 #endif image ); diff --git a/libsrc/grabber/video/mediafoundation/MFGrabber.cpp b/libsrc/grabber/video/mediafoundation/MFGrabber.cpp index 1cacf4aa..178e248d 100644 --- a/libsrc/grabber/video/mediafoundation/MFGrabber.cpp +++ b/libsrc/grabber/video/mediafoundation/MFGrabber.cpp @@ -363,6 +363,18 @@ done: _height = props.height; _frameByteSize = _width * _height * 3; _lineLength = _width * 3; + // adjust flipMode for bottom-up images + if (props.defstride < 0) + { + if (_flipMode == FlipMode::NO_CHANGE) + _flipMode = FlipMode::HORIZONTAL; + else if (_flipMode == FlipMode::HORIZONTAL) + _flipMode = FlipMode::NO_CHANGE; + else if (_flipMode == FlipMode::VERTICAL) + _flipMode = FlipMode::BOTH; + else if (_flipMode == FlipMode::BOTH) + _flipMode = FlipMode::VERTICAL; + } } // Cleanup @@ -436,6 +448,14 @@ void MFGrabber::enumVideoCaptureDevices() properties.denominator = denominator; properties.pf = pixelformat; properties.guid = format; + + HRESULT hr = pType->GetUINT32(MF_MT_DEFAULT_STRIDE, (UINT32*)&properties.defstride); + if (FAILED(hr)) + { + hr = MFGetStrideForBitmapInfoHeader(format.Data1, width, &properties.defstride); + if (FAILED(hr)) + DebugIf (verbose, _log, "failed to get default stride"); + } devicePropertyList.append(properties); DebugIf (verbose, _log, "%s %d x %d @ %d fps (%s)", QSTRING_CSTR(dev), properties.width, properties.height, properties.fps, QSTRING_CSTR(pixelFormatToString(properties.pf))); @@ -797,7 +817,7 @@ QJsonArray MFGrabber::discover(const QJsonObject& params) resolution_default["width"] = 640; resolution_default["height"] = 480; resolution_default["fps"] = 25; - format_default["format"] = "bgr24"; + format_default["format"] = "rgb24"; format_default["resolution"] = resolution_default; video_inputs_default["inputIdx"] = 0; video_inputs_default["standards"] = "PAL"; diff --git a/libsrc/grabber/video/mediafoundation/MFSourceReaderCB.h b/libsrc/grabber/video/mediafoundation/MFSourceReaderCB.h index 2bcef437..f172a8f3 100644 --- a/libsrc/grabber/video/mediafoundation/MFSourceReaderCB.h +++ b/libsrc/grabber/video/mediafoundation/MFSourceReaderCB.h @@ -27,7 +27,7 @@ static PixelFormat GetPixelFormatForGuid(const GUID guid) { if (IsEqualGUID(guid, MFVideoFormat_RGB32)) return PixelFormat::RGB32; - if (IsEqualGUID(guid, MFVideoFormat_RGB24)) return PixelFormat::BGR24; + if (IsEqualGUID(guid, MFVideoFormat_RGB24)) return PixelFormat::RGB24; if (IsEqualGUID(guid, MFVideoFormat_YUY2)) return PixelFormat::YUYV; if (IsEqualGUID(guid, MFVideoFormat_UYVY)) return PixelFormat::UYVY; #ifdef HAVE_TURBO_JPEG @@ -145,11 +145,11 @@ public: } #ifdef HAVE_TURBO_JPEG - if (_pixelformat != PixelFormat::MJPEG && _pixelformat != PixelFormat::BGR24 && _pixelformat != PixelFormat::NO_CHANGE) + if (_pixelformat != PixelFormat::MJPEG && _pixelformat != PixelFormat::RGB24 && _pixelformat != PixelFormat::NO_CHANGE) #else - if (_pixelformat != PixelFormat::BGR24 && _pixelformat != PixelFormat::NO_CHANGE) + if (_pixelformat != PixelFormat::RGB24 && _pixelformat != PixelFormat::NO_CHANGE) #endif - pSample = TransformSample(_transform, pSample); + pSample = TransformSample(_transform, pSample); // forced conversion to RGB24, but memory layout is RGBTRIPLE (b,g,r) -> process as BGR24 _hrStatus = pSample->ConvertToContiguousBuffer(&buffer); if (FAILED(_hrStatus)) @@ -181,9 +181,9 @@ public: _bEOS = TRUE; // Reached the end of the stream. #ifdef HAVE_TURBO_JPEG - if (_pixelformat != PixelFormat::MJPEG && _pixelformat != PixelFormat::BGR24 && _pixelformat != PixelFormat::NO_CHANGE) + if (_pixelformat != PixelFormat::MJPEG && _pixelformat != PixelFormat::RGB24 && _pixelformat != PixelFormat::NO_CHANGE) #else - if (_pixelformat != PixelFormat::BGR24 && _pixelformat != PixelFormat::NO_CHANGE) + if (_pixelformat != PixelFormat::RGB24 && _pixelformat != PixelFormat::NO_CHANGE) #endif SAFE_RELEASE(pSample); @@ -196,9 +196,9 @@ public: { _pixelformat = format; #ifdef HAVE_TURBO_JPEG - if (format == PixelFormat::MJPEG || format == PixelFormat::BGR24 || format == PixelFormat::NO_CHANGE) + if (format == PixelFormat::MJPEG || format == PixelFormat::RGB24 || format == PixelFormat::NO_CHANGE) #else - if (format == PixelFormat::BGR24 || format == PixelFormat::NO_CHANGE) + if (format == PixelFormat::RGB24 || format == PixelFormat::NO_CHANGE) #endif return S_OK; @@ -392,10 +392,10 @@ private: private: long _nRefCount; CRITICAL_SECTION _critsec; - MFGrabber* _grabber; + MFGrabber* _grabber; BOOL _bEOS; HRESULT _hrStatus; - IMFTransform* _transform; + IMFTransform* _transform; PixelFormat _pixelformat; std::atomic _isBusy; }; diff --git a/libsrc/grabber/video/v4l2/V4L2Grabber.cpp b/libsrc/grabber/video/v4l2/V4L2Grabber.cpp index 081b978b..e6256d7e 100644 --- a/libsrc/grabber/video/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/video/v4l2/V4L2Grabber.cpp @@ -54,7 +54,9 @@ Q_GLOBAL_STATIC_WITH_ARGS(ControlIDPropertyMap, _controlIDPropertyMap, (initCont static PixelFormat GetPixelFormat(const unsigned int format) { if (format == V4L2_PIX_FMT_RGB32) return PixelFormat::RGB32; - if (format == V4L2_PIX_FMT_RGB24) return PixelFormat::BGR24; + if (format == V4L2_PIX_FMT_BGR32) return PixelFormat::BGR32; + if (format == V4L2_PIX_FMT_RGB24) return PixelFormat::RGB24; + if (format == V4L2_PIX_FMT_BGR24) return PixelFormat::BGR24; if (format == V4L2_PIX_FMT_YUYV) return PixelFormat::YUYV; if (format == V4L2_PIX_FMT_UYVY) return PixelFormat::UYVY; if (format == V4L2_PIX_FMT_NV12) return PixelFormat::NV12; @@ -557,10 +559,18 @@ void V4L2Grabber::init_device(VideoStandard videoStandard) fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB32; break; - case PixelFormat::BGR24: + case PixelFormat::BGR32: + fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_BGR32; + break; + + case PixelFormat::RGB24: fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB24; break; + case PixelFormat::BGR24: + fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_BGR24; + break; + case PixelFormat::YUYV: fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; break; @@ -691,7 +701,23 @@ void V4L2Grabber::init_device(VideoStandard videoStandard) } break; + case V4L2_PIX_FMT_BGR32: + { + _pixelFormat = PixelFormat::BGR32; + _frameByteSize = _width * _height * 4; + Debug(_log, "Pixel format=BGR32"); + } + break; + case V4L2_PIX_FMT_RGB24: + { + _pixelFormat = PixelFormat::RGB24; + _frameByteSize = _width * _height * 3; + Debug(_log, "Pixel format=RGB24"); + } + break; + + case V4L2_PIX_FMT_BGR24: { _pixelFormat = PixelFormat::BGR24; _frameByteSize = _width * _height * 3; @@ -699,7 +725,6 @@ void V4L2Grabber::init_device(VideoStandard videoStandard) } break; - case V4L2_PIX_FMT_YUYV: { _pixelFormat = PixelFormat::YUYV; @@ -743,9 +768,9 @@ void V4L2Grabber::init_device(VideoStandard videoStandard) default: #ifdef HAVE_TURBO_JPEG - throw_exception("Only pixel formats RGB32, BGR24, YUYV, UYVY, NV12, I420 and MJPEG are supported"); + throw_exception("Only pixel formats RGB32, BGR32, RGB24, BGR24, YUYV, UYVY, NV12, I420 and MJPEG are supported"); #else - throw_exception("Only pixel formats RGB32, BGR24, YUYV, UYVY, NV12 and I420 are supported"); + throw_exception("Only pixel formats RGB32, BGR32, RGB24, BGR24, YUYV, UYVY, NV12 and I420 are supported"); #endif return; } diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 259ebf14..6a03e3b8 100644 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -19,6 +19,7 @@ include_directories( dev_spi dev_rpi_pwm dev_tinker + dev_ftdi ) file (GLOB Leddevice_SOURCES @@ -63,7 +64,11 @@ if(ENABLE_DEV_WS281XPWM) file (GLOB Leddevice_PWM_SOURCES "${CURRENT_SOURCE_DIR}/dev_rpi_pwm/*.h" "${CURRENT_SOURCE_DIR}/dev_rpi_pwm/*.cpp") endif() -set(LedDevice_RESOURCES ${CURRENT_SOURCE_DIR}/LedDeviceSchemas.qrc) +if (ENABLE_DEV_FTDI) + FILE ( GLOB Leddevice_FTDI_SOURCES "${CURRENT_SOURCE_DIR}/dev_ftdi/*.h" "${CURRENT_SOURCE_DIR}/dev_ftdi/*.cpp") +endif() + +set(LedDevice_RESOURCES ${CURRENT_SOURCE_DIR}/LedDeviceSchemas.qrc ) set(Leddevice_SOURCES ${Leddevice_SOURCES} @@ -74,6 +79,7 @@ set(Leddevice_SOURCES ${Leddevice_SPI_SOURCES} ${Leddevice_TINKER_SOURCES} ${Leddevice_USB_HID_SOURCES} + ${Leddevice_FTDI_SOURCES} ) # auto generate header file that include all available leddevice headers @@ -165,3 +171,10 @@ if(ENABLE_MDNS) target_link_libraries(leddevice mdns) endif() +if( ENABLE_DEV_FTDI ) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIB_FTDI REQUIRED IMPORTED_TARGET libftdi1 ) + target_include_directories(leddevice PRIVATE PkgConfig::LIB_FTDI) + target_link_libraries(leddevice PkgConfig::LIB_FTDI) +endif() + diff --git a/libsrc/leddevice/LedDeviceSchemas.qrc b/libsrc/leddevice/LedDeviceSchemas.qrc index be976000..7c179650 100644 --- a/libsrc/leddevice/LedDeviceSchemas.qrc +++ b/libsrc/leddevice/LedDeviceSchemas.qrc @@ -7,6 +7,7 @@ schemas/schema-dmx.json schemas/schema-fadecandy.json schemas/schema-file.json + schemas/schema-homeassistant.json schemas/schema-hyperionusbasp.json schemas/schema-lightpack.json schemas/schema-lpd6803.json @@ -38,5 +39,8 @@ schemas/schema-yeelight.json schemas/schema-razer.json schemas/schema-cololight.json + schemas/schema-ws2812_ftdi.json + schemas/schema-apa102_ftdi.json + schemas/schema-sk6812_ftdi.json diff --git a/libsrc/leddevice/dev_ftdi/LedDeviceAPA102_ftdi.cpp b/libsrc/leddevice/dev_ftdi/LedDeviceAPA102_ftdi.cpp new file mode 100644 index 00000000..32ae7570 --- /dev/null +++ b/libsrc/leddevice/dev_ftdi/LedDeviceAPA102_ftdi.cpp @@ -0,0 +1,52 @@ +#include "LedDeviceAPA102_ftdi.h" + +#define LED_HEADER 0b11100000 +#define LED_BRIGHTNESS_FULL 31 + +LedDeviceAPA102_ftdi::LedDeviceAPA102_ftdi(const QJsonObject &deviceConfig) : ProviderFtdi(deviceConfig) +{ +} + +LedDevice *LedDeviceAPA102_ftdi::construct(const QJsonObject &deviceConfig) +{ + return new LedDeviceAPA102_ftdi(deviceConfig); +} + +bool LedDeviceAPA102_ftdi::init(const QJsonObject &deviceConfig) +{ + bool isInitOK = false; + // Initialise sub-class + if (ProviderFtdi::init(deviceConfig)) + { + _brightnessControlMaxLevel = deviceConfig["brightnessControlMaxLevel"].toInt(LED_BRIGHTNESS_FULL); + Info(_log, "[%s] Setting maximum brightness to [%d] = %d%%", QSTRING_CSTR(_activeDeviceType), _brightnessControlMaxLevel, _brightnessControlMaxLevel * 100 / LED_BRIGHTNESS_FULL); + + CreateHeader(); + isInitOK = true; + } + return isInitOK; +} + +void LedDeviceAPA102_ftdi::CreateHeader() +{ + const unsigned int startFrameSize = 4; + // Endframe, add additional 4 bytes to cover SK9922 Reset frame (in case SK9922 were sold as AP102) - has no effect on APA102 + const unsigned int endFrameSize = (_ledCount / 32) * 4 + 4; + const unsigned int APAbufferSize = (_ledCount * 4) + startFrameSize + endFrameSize; + _ledBuffer.resize(APAbufferSize, 0); + Debug(_log, "APA102 buffer created for %d LEDs", _ledCount); +} + +int LedDeviceAPA102_ftdi::write(const std::vector &ledValues) +{ + for (signed iLed = 0; iLed < static_cast(_ledCount); ++iLed) + { + const ColorRgb &rgb = ledValues[iLed]; + _ledBuffer[4 + iLed * 4 + 0] = LED_HEADER | _brightnessControlMaxLevel; + _ledBuffer[4 + iLed * 4 + 1] = rgb.red; + _ledBuffer[4 + iLed * 4 + 2] = rgb.green; + _ledBuffer[4 + iLed * 4 + 3] = rgb.blue; + } + + return writeBytes(_ledBuffer.size(), _ledBuffer.data()); +} diff --git a/libsrc/leddevice/dev_ftdi/LedDeviceAPA102_ftdi.h b/libsrc/leddevice/dev_ftdi/LedDeviceAPA102_ftdi.h new file mode 100644 index 00000000..699332d7 --- /dev/null +++ b/libsrc/leddevice/dev_ftdi/LedDeviceAPA102_ftdi.h @@ -0,0 +1,50 @@ +#ifndef LEDEVICET_APA102_H +#define LEDEVICET_APA102_H +#include "ProviderFtdi.h" + +class LedDeviceAPA102_ftdi : public ProviderFtdi +{ + Q_OBJECT + +public: + + /// + /// @brief Constructs an APA102 LED-device + /// + /// @param deviceConfig Device's configuration as JSON-Object + /// + explicit LedDeviceAPA102_ftdi(const QJsonObject& deviceConfig); + + /// + /// @brief Constructs the LED-device + /// + /// @param[in] deviceConfig Device's configuration as JSON-Object + /// @return LedDevice constructed + static LedDevice* construct(const QJsonObject& deviceConfig); + +private: + + /// + /// @brief Initialise the device's configuration + /// + /// @param[in] deviceConfig the JSON device configuration + /// @return True, if success + /// + bool init(const QJsonObject& deviceConfig) override; + + void CreateHeader(); + + /// + /// @brief Writes the RGB-Color values to the LEDs. + /// + /// @param[in] ledValues The RGB-color per LED + /// @return Zero on success, else negative + /// + int write(const std::vector& ledValues) override; + + /// The brighness level. Possibile values 1 .. 31. + int _brightnessControlMaxLevel; + +}; + +#endif // LEDEVICET_APA102_H diff --git a/libsrc/leddevice/dev_ftdi/LedDeviceSk6812_ftdi.cpp b/libsrc/leddevice/dev_ftdi/LedDeviceSk6812_ftdi.cpp new file mode 100644 index 00000000..03dcd039 --- /dev/null +++ b/libsrc/leddevice/dev_ftdi/LedDeviceSk6812_ftdi.cpp @@ -0,0 +1,96 @@ +#include "LedDeviceSk6812_ftdi.h" + +LedDeviceSk6812_ftdi::LedDeviceSk6812_ftdi(const QJsonObject &deviceConfig) + : ProviderFtdi(deviceConfig), + _whiteAlgorithm(RGBW::WhiteAlgorithm::INVALID), + SPI_BYTES_PER_COLOUR(4), + bitpair_to_byte{ + 0b10001000, + 0b10001100, + 0b11001000, + 0b11001100} +{ +} + +LedDevice *LedDeviceSk6812_ftdi::construct(const QJsonObject &deviceConfig) +{ + return new LedDeviceSk6812_ftdi(deviceConfig); +} + +bool LedDeviceSk6812_ftdi::init(const QJsonObject &deviceConfig) +{ + + bool isInitOK = false; + + // Initialise sub-class + if (ProviderFtdi::init(deviceConfig)) + { + _brightnessControlMaxLevel = deviceConfig["brightnessControlMaxLevel"].toInt(255); + Info(_log, "[%s] Setting maximum brightness to [%d]", QSTRING_CSTR(_activeDeviceType), _brightnessControlMaxLevel); + + + QString whiteAlgorithm = deviceConfig["whiteAlgorithm"].toString("white_off"); + + _whiteAlgorithm = RGBW::stringToWhiteAlgorithm(whiteAlgorithm); + if (_whiteAlgorithm == RGBW::WhiteAlgorithm::INVALID) + { + QString errortext = QString ("unknown whiteAlgorithm: %1").arg(whiteAlgorithm); + this->setInError(errortext); + isInitOK = false; + } + else + { + + Debug(_log, "whiteAlgorithm : %s", QSTRING_CSTR(whiteAlgorithm)); + + WarningIf((_baudRate_Hz < 2050000 || _baudRate_Hz > 3750000), _log, "Baud rate %d outside recommended range (2050000 -> 3750000)", _baudRate_Hz); + + const int SPI_FRAME_END_LATCH_BYTES = 3; + _ledBuffer.resize(_ledRGBWCount * SPI_BYTES_PER_COLOUR + SPI_FRAME_END_LATCH_BYTES, 0x00); + + isInitOK = true; + } + } + return isInitOK; +} + + +inline __attribute__((always_inline)) uint8_t LedDeviceSk6812_ftdi::scale(uint8_t i, uint8_t scale) { + return (((uint16_t)i) * (1+(uint16_t)(scale))) >> 8; +} + +int LedDeviceSk6812_ftdi::write(const std::vector &ledValues) +{ + unsigned spi_ptr = 0; + const int SPI_BYTES_PER_LED = sizeof(ColorRgbw) * SPI_BYTES_PER_COLOUR; + + ColorRgbw temp_rgbw; + ColorRgb scaled_color; + for (const ColorRgb &color : ledValues) + { + scaled_color.red = scale(color.red, _brightnessControlMaxLevel); + scaled_color.green = scale(color.green, _brightnessControlMaxLevel); + scaled_color.blue = scale(color.blue, _brightnessControlMaxLevel); + + RGBW::Rgb_to_Rgbw(scaled_color, &temp_rgbw, _whiteAlgorithm); + + uint32_t colorBits = + ((uint32_t)temp_rgbw.red << 24) + + ((uint32_t)temp_rgbw.green << 16) + + ((uint32_t)temp_rgbw.blue << 8) + + temp_rgbw.white; + + for (int j = SPI_BYTES_PER_LED - 1; j >= 0; j--) + { + _ledBuffer[spi_ptr + j] = bitpair_to_byte[colorBits & 0x3]; + colorBits >>= 2; + } + spi_ptr += SPI_BYTES_PER_LED; + } + + _ledBuffer[spi_ptr++] = 0; + _ledBuffer[spi_ptr++] = 0; + _ledBuffer[spi_ptr++] = 0; + + return writeBytes(_ledBuffer.size(), _ledBuffer.data()); +} diff --git a/libsrc/leddevice/dev_ftdi/LedDeviceSk6812_ftdi.h b/libsrc/leddevice/dev_ftdi/LedDeviceSk6812_ftdi.h new file mode 100644 index 00000000..017b0f30 --- /dev/null +++ b/libsrc/leddevice/dev_ftdi/LedDeviceSk6812_ftdi.h @@ -0,0 +1,52 @@ +#ifndef LEDEVICESK6812ftdi_H +#define LEDEVICESK6812ftdi_H + +#include "ProviderFtdi.h" + +class LedDeviceSk6812_ftdi : public ProviderFtdi +{ +public: + + /// + /// @brief Constructs a Sk6801 LED-device + /// + /// @param deviceConfig Device's configuration as JSON-Object + /// + explicit LedDeviceSk6812_ftdi(const QJsonObject& deviceConfig); + + /// + /// @brief Constructs the LED-device + /// + /// @param[in] deviceConfig Device's configuration as JSON-Object + /// @return LedDevice constructed + static LedDevice* construct(const QJsonObject& deviceConfig); + +private: + + /// + /// @brief Initialise the device's configuration + /// + /// @param[in] deviceConfig the JSON device configuration + /// @return True, if success + /// + bool init(const QJsonObject& deviceConfig) override; + + /// + /// @brief Writes the RGB-Color values to the LEDs. + /// + /// @param[in] ledValues The RGB-color per LED + /// @return Zero on success, else negative + /// + int write(const std::vector& ledValues) override; + + inline __attribute__((always_inline)) uint8_t scale(uint8_t i, uint8_t scale); + + RGBW::WhiteAlgorithm _whiteAlgorithm; + + const int SPI_BYTES_PER_COLOUR; + uint8_t bitpair_to_byte[4]; + + int _brightnessControlMaxLevel; +}; + +#endif // LEDEVICESK6812ftdi_H diff --git a/libsrc/leddevice/dev_ftdi/LedDeviceWs2812_ftdi.cpp b/libsrc/leddevice/dev_ftdi/LedDeviceWs2812_ftdi.cpp new file mode 100644 index 00000000..57c0ac03 --- /dev/null +++ b/libsrc/leddevice/dev_ftdi/LedDeviceWs2812_ftdi.cpp @@ -0,0 +1,93 @@ +#include "LedDeviceWs2812_ftdi.h" + +/* +From the data sheet: + +(TH+TL=1.25μs±600ns) + +T0H, 0 code, high level time, 0.40µs ±0.150ns +T0L, 0 code, low level time, 0.85µs ±0.150ns +T1H, 1 code, high level time, 0.80µs ±0.150ns +T1L, 1 code, low level time, 0.45µs ±0.150ns +WT, Wait for the processing time, NA +Trst, Reset code,low level time, 50µs (not anymore... need 300uS for latest revision) + +To normalise the pulse times so they fit in 4 SPI bits: + +On the assumption that the "low" time doesnt matter much + +A SPI bit time of 0.40uS = 2.5 Mbit/sec +T0 is sent as 1000 +T1 is sent as 1100 + +With a bit of excel testing, we can work out the maximum and minimum speeds: +2106000 MIN +2590500 AVG +3075000 MAX + +Wait time: +Not Applicable for WS2812 + +Reset time: +using the max of 3075000, the bit time is 0.325 +Reset time is 300uS = 923 bits = 116 bytes + +*/ + +LedDeviceWs2812_ftdi::LedDeviceWs2812_ftdi(const QJsonObject &deviceConfig) + : ProviderFtdi(deviceConfig), + SPI_BYTES_PER_COLOUR(4), + SPI_FRAME_END_LATCH_BYTES(116), + bitpair_to_byte{ + 0b10001000, + 0b10001100, + 0b11001000, + 0b11001100, + } +{ +} + +LedDevice *LedDeviceWs2812_ftdi::construct(const QJsonObject &deviceConfig) +{ + return new LedDeviceWs2812_ftdi(deviceConfig); +} + +bool LedDeviceWs2812_ftdi::init(const QJsonObject &deviceConfig) +{ + bool isInitOK = false; + + // Initialise sub-class + if (ProviderFtdi::init(deviceConfig)) + { + WarningIf((_baudRate_Hz < 2106000 || _baudRate_Hz > 3075000), _log, "Baud rate %d outside recommended range (2106000 -> 3075000)", _baudRate_Hz); + _ledBuffer.resize(_ledRGBCount * SPI_BYTES_PER_COLOUR + SPI_FRAME_END_LATCH_BYTES, 0x00); + isInitOK = true; + } + + return isInitOK; +} + +int LedDeviceWs2812_ftdi::write(const std::vector &ledValues) +{ + unsigned spi_ptr = 0; + const int SPI_BYTES_PER_LED = sizeof(ColorRgb) * SPI_BYTES_PER_COLOUR; + + for (const ColorRgb &color : ledValues) + { + uint32_t colorBits = ((unsigned int)color.red << 16) | ((unsigned int)color.green << 8) | color.blue; + + for (int j = SPI_BYTES_PER_LED - 1; j >= 0; j--) + { + _ledBuffer[spi_ptr + j] = bitpair_to_byte[colorBits & 0x3]; + colorBits >>= 2; + } + spi_ptr += SPI_BYTES_PER_LED; + } + + for (int j = 0; j < SPI_FRAME_END_LATCH_BYTES; j++) + { + _ledBuffer[spi_ptr++] = 0; + } + + return writeBytes(_ledBuffer.size(), _ledBuffer.data()); +} diff --git a/libsrc/leddevice/dev_ftdi/LedDeviceWs2812_ftdi.h b/libsrc/leddevice/dev_ftdi/LedDeviceWs2812_ftdi.h new file mode 100644 index 00000000..972b935b --- /dev/null +++ b/libsrc/leddevice/dev_ftdi/LedDeviceWs2812_ftdi.h @@ -0,0 +1,49 @@ +#ifndef LEDEVICEWS2812_ftdi_H +#define LEDEVICEWS2812_ftdi_H + +#include "ProviderFtdi.h" + + +class LedDeviceWs2812_ftdi : public ProviderFtdi +{ +public: + + /// + /// @brief Constructs a Ws2812 LED-device + /// + /// @param deviceConfig Device's configuration as JSON-Object + /// + explicit LedDeviceWs2812_ftdi(const QJsonObject& deviceConfig); + + /// + /// @brief Constructs the LED-device + /// + /// @param[in] deviceConfig Device's configuration as JSON-Object + /// @return LedDevice constructed + static LedDevice* construct(const QJsonObject& deviceConfig); + +private: + + /// + /// @brief Initialise the device's configuration + /// + /// @param[in] deviceConfig the JSON device configuration + /// @return True, if success + /// + bool init(const QJsonObject& deviceConfig) override; + + /// + /// @brief Writes the RGB-Color values to the LEDs. + /// + /// @param[in] ledValues The RGB-color per LED + /// @return Zero on success, else negative + /// + int write(const std::vector& ledValues) override; + + const int SPI_BYTES_PER_COLOUR; + const int SPI_FRAME_END_LATCH_BYTES; + + uint8_t bitpair_to_byte[4]; +}; + +#endif // LEDEVICEWS2812_ftdi_H diff --git a/libsrc/leddevice/dev_ftdi/ProviderFtdi.cpp b/libsrc/leddevice/dev_ftdi/ProviderFtdi.cpp new file mode 100644 index 00000000..ce168821 --- /dev/null +++ b/libsrc/leddevice/dev_ftdi/ProviderFtdi.cpp @@ -0,0 +1,208 @@ +// LedDevice includes +#include +#include "ProviderFtdi.h" +#include + +#include +#include + +#define ANY_FTDI_VENDOR 0x0 +#define ANY_FTDI_PRODUCT 0x0 + +#define FTDI_CHECK_RESULT(statement) if (statement) {setInError(ftdi_get_error_string(_ftdic)); return rc;} + +namespace Pin +{ +// enumerate the AD bus for convenience. +enum bus_t +{ + SK = 0x01, // ADBUS0, SPI data clock + DO = 0x02, // ADBUS1, SPI data out + CS = 0x08, // ADBUS3, SPI chip select, active low +}; +} + +const uint8_t pinInitialState = Pin::CS; +// Use these pins as outputs +const uint8_t pinDirection = Pin::SK | Pin::DO | Pin::CS; + +const QString ProviderFtdi::AUTO_SETTING = QString("auto"); + +ProviderFtdi::ProviderFtdi(const QJsonObject &deviceConfig) + : LedDevice(deviceConfig), + _ftdic(nullptr), + _baudRate_Hz(1000000) +{ +} + +bool ProviderFtdi::init(const QJsonObject &deviceConfig) +{ + bool isInitOK = false; + + if (LedDevice::init(deviceConfig)) + { + _baudRate_Hz = deviceConfig["rate"].toInt(_baudRate_Hz); + _deviceName = deviceConfig["output"].toString(AUTO_SETTING); + + Debug(_log, "_baudRate_Hz [%d]", _baudRate_Hz); + Debug(_log, "_deviceName [%s]", QSTRING_CSTR(_deviceName)); + + isInitOK = true; + } + return isInitOK; +} + +int ProviderFtdi::open() +{ + int rc = 0; + + _ftdic = ftdi_new(); + + if (ftdi_init(_ftdic) < 0) + { + _ftdic = nullptr; + setInError("Could not initialize the ftdi library"); + return -1; + } + + Debug(_log, "Opening FTDI device=%s", QSTRING_CSTR(_deviceName)); + + FTDI_CHECK_RESULT((rc = ftdi_usb_open_string(_ftdic, QSTRING_CSTR(_deviceName))) < 0); + /* doing this disable resets things if they were in a bad state */ + FTDI_CHECK_RESULT((rc = ftdi_disable_bitbang(_ftdic)) < 0); + FTDI_CHECK_RESULT((rc = ftdi_setflowctrl(_ftdic, SIO_DISABLE_FLOW_CTRL)) < 0); + FTDI_CHECK_RESULT((rc = ftdi_set_bitmode(_ftdic, 0x00, BITMODE_RESET)) < 0); + FTDI_CHECK_RESULT((rc = ftdi_set_bitmode(_ftdic, 0xff, BITMODE_MPSSE)) < 0); + + double reference_clock = 60e6; + int divisor = (reference_clock / 2 / _baudRate_Hz) - 1; + std::vector buf = { + DIS_DIV_5, + TCK_DIVISOR, + static_cast(divisor), + static_cast(divisor >> 8), + SET_BITS_LOW, // opcode: set low bits (ADBUS[0-7] + pinInitialState, // argument: inital pin state + pinDirection + }; + + FTDI_CHECK_RESULT((rc = ftdi_write_data(_ftdic, buf.data(), buf.size())) != buf.size()); + + _isDeviceReady = true; + return rc; +} + +int ProviderFtdi::close() +{ + LedDevice::close(); + if (_ftdic != nullptr) { + Debug(_log, "Closing FTDI device"); + // Delay to give time to push color black from writeBlack() into the led, + // otherwise frame transmission will be terminated half way through + wait(30); + ftdi_set_bitmode(_ftdic, 0x00, BITMODE_RESET); + ftdi_usb_close(_ftdic); + ftdi_free(_ftdic); + _ftdic = nullptr; + } + return 0; +} + +void ProviderFtdi::setInError(const QString &errorMsg, bool isRecoverable) +{ + close(); + + LedDevice::setInError(errorMsg, isRecoverable); +} + +int ProviderFtdi::writeBytes(const qint64 size, const uint8_t *data) +{ + int rc; + int count_arg = size - 1; + std::vector buf = { + SET_BITS_LOW, + pinInitialState & ~Pin::CS, + pinDirection, + MPSSE_DO_WRITE | MPSSE_WRITE_NEG, + static_cast(count_arg), + static_cast(count_arg >> 8), + SET_BITS_LOW, + pinInitialState | Pin::CS, + pinDirection + }; + // insert before last SET_BITS_LOW command + // SET_BITS_LOW takes 2 arguments, so we're inserting data in -3 position from the end + buf.insert(buf.end() - 3, &data[0], &data[size]); + + FTDI_CHECK_RESULT((rc = ftdi_write_data(_ftdic, buf.data(), buf.size())) != buf.size()); + return rc; +} + +QJsonObject ProviderFtdi::discover(const QJsonObject & /*params*/) +{ + QJsonObject devicesDiscovered; + QJsonArray deviceList; + struct ftdi_device_list *devlist; + struct ftdi_context *ftdic; + + ftdic = ftdi_new(); + + if (ftdi_usb_find_all(ftdic, &devlist, ANY_FTDI_VENDOR, ANY_FTDI_PRODUCT) > 0) + { + struct ftdi_device_list *curdev = devlist; + QMap deviceIndexes; + + while (curdev) + { + libusb_device_descriptor desc; + int rc = libusb_get_device_descriptor(curdev->dev, &desc); + if (rc == 0) + { + QString vendorIdentifier = QString("0x%1").arg(desc.idVendor, 4, 16, QChar{'0'}); + QString productIdentifier = QString("0x%1").arg(desc.idProduct, 4, 16, QChar{'0'}); + QString vendorAndProduct = QString("%1:%2") + .arg(vendorIdentifier) + .arg(productIdentifier); + uint8_t deviceIndex = deviceIndexes.value(vendorAndProduct, 0); + + char serial_string[128] = {0}; + char manufacturer_string[128] = {0}; + char description_string[128] = {0}; + ftdi_usb_get_strings2(ftdic, curdev->dev, manufacturer_string, 128, description_string, 128, serial_string, 128); + + QString serialNumber {serial_string}; + QString ftdiOpenString; + if(!serialNumber.isEmpty()) + { + ftdiOpenString = QString("s:%1:%2").arg(vendorAndProduct).arg(serialNumber); + } + else + { + ftdiOpenString = QString("i:%1:%2").arg(vendorAndProduct).arg(deviceIndex); + } + + deviceList.push_back(QJsonObject{ + {"ftdiOpenString", ftdiOpenString}, + {"vendorIdentifier", vendorIdentifier}, + {"productIdentifier", productIdentifier}, + {"deviceIndex", deviceIndex}, + {"serialNumber", serialNumber}, + {"manufacturer", manufacturer_string}, + {"description", description_string} + }); + deviceIndexes.insert(vendorAndProduct, deviceIndex + 1); + } + curdev = curdev->next; + } + } + + ftdi_list_free(&devlist); + ftdi_free(ftdic); + + devicesDiscovered.insert("ledDeviceType", _activeDeviceType); + devicesDiscovered.insert("devices", deviceList); + + Debug(_log, "FTDI devices discovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); + + return devicesDiscovered; +} diff --git a/libsrc/leddevice/dev_ftdi/ProviderFtdi.h b/libsrc/leddevice/dev_ftdi/ProviderFtdi.h new file mode 100644 index 00000000..955b2672 --- /dev/null +++ b/libsrc/leddevice/dev_ftdi/ProviderFtdi.h @@ -0,0 +1,76 @@ +#ifndef PROVIDERFtdi_H +#define PROVIDERFtdi_H + +// LedDevice includes +#include + +#include + +/// +/// The ProviderFtdi implements an abstract base-class for LedDevices using a Ftdi-device. +/// +class ProviderFtdi : public LedDevice +{ + Q_OBJECT + +public: + + /// + /// @brief Constructs a Ftdi LED-device + /// + ProviderFtdi(const QJsonObject& deviceConfig); + + static const QString AUTO_SETTING; + +protected: + /// + /// @brief Opens the output device. + /// + /// @return Zero on success (i.e. device is ready), else negative + /// + int open() override; + + /// + /// Sets configuration + /// + /// @param deviceConfig the json device config + /// @return true if success + bool init(const QJsonObject& deviceConfig) override; + + /// + /// @brief Closes the UDP device. + /// + /// @return Zero on success (i.e. device is closed), else negative + /// + int close() override; + + + /// @brief Write the given bytes to the Ftdi-device + /// + /// @param[in[ size The length of the data + /// @param[in] data The data + /// @return Zero on success, else negative + /// + int writeBytes(const qint64 size, const uint8_t* data); + + + QJsonObject discover(const QJsonObject& params) override; + + /// The Ftdi serial-device + struct ftdi_context *_ftdic; + + /// The used baud-rate of the output device + qint32 _baudRate_Hz; + QString _deviceName; + +protected slots: + + /// + /// @brief Set device in error state + /// + /// @param errorMsg The error message to be logged + /// + void setInError(const QString& errorMsg, bool isRecoverable=true) override; +}; + +#endif // PROVIDERFtdi_H diff --git a/libsrc/leddevice/dev_net/LedDeviceHomeAssistant.cpp b/libsrc/leddevice/dev_net/LedDeviceHomeAssistant.cpp new file mode 100644 index 00000000..df4c1de1 --- /dev/null +++ b/libsrc/leddevice/dev_net/LedDeviceHomeAssistant.cpp @@ -0,0 +1,446 @@ +// Local-Hyperion includes +#include "LedDeviceHomeAssistant.h" + +#include +// mDNS discover +#ifdef ENABLE_MDNS +#include +#include +#endif +#include +#include + +#include + +// Constants +namespace { +const bool verbose = false; + +// Configuration settings +const char CONFIG_HOST[] = "host"; +const char CONFIG_PORT[] = "port"; +const char CONFIG_AUTH_TOKEN[] = "token"; +const char CONFIG_ENITYIDS[] = "entityIds"; +const char CONFIG_BRIGHTNESS[] = "brightness"; +const char CONFIG_BRIGHTNESS_OVERWRITE[] = "overwriteBrightness"; +const char CONFIG_FULL_BRIGHTNESS_AT_START[] = "fullBrightnessAtStart"; +const char CONFIG_ON_OFF_BLACK[] = "switchOffOnBlack"; +const char CONFIG_TRANSITIONTIME[] = "transitionTime"; + +const bool DEFAULT_IS_BRIGHTNESS_OVERWRITE = true; +const bool DEFAULT_IS_FULL_BRIGHTNESS_AT_START = true; +const int BRI_MAX = 255; +const bool DEFAULT_IS_SWITCH_OFF_ON_BLACK = false; + +// Home Assistant API +const int API_DEFAULT_PORT = 8123; +const char API_BASE_PATH[] = "/api/"; +const char API_STATES[] = "states"; +const char API_LIGHT_TURN_ON[] = "services/light/turn_on"; +const char API_LIGHT_TURN_OFF[] = "services/light/turn_off"; + +const char ENTITY_ID[] = "entity_id"; +const char RGB_COLOR[] = "rgb_color"; +const char BRIGHTNESS[] = "brightness"; +const char TRANSITION[] = "transition"; +const char FLASH[] = "flash"; + +// // Home Assistant ssdp services +const char SSDP_ID[] = "ssdp:all"; +const char SSDP_FILTER_HEADER[] = "ST"; +const char SSDP_FILTER[] = "(.*)home-assistant.io(.*)"; + +} //End of constants + +LedDeviceHomeAssistant::LedDeviceHomeAssistant(const QJsonObject& deviceConfig) + : LedDevice(deviceConfig) + , _restApi(nullptr) + , _apiPort(API_DEFAULT_PORT) + , _isBrightnessOverwrite(DEFAULT_IS_BRIGHTNESS_OVERWRITE) + , _isFullBrightnessAtStart(DEFAULT_IS_FULL_BRIGHTNESS_AT_START) + , _brightness (BRI_MAX) +{ +#ifdef ENABLE_MDNS + QMetaObject::invokeMethod(MdnsBrowser::getInstance().data(), "browseForServiceType", + Qt::QueuedConnection, Q_ARG(QByteArray, MdnsServiceRegister::getServiceType(_activeDeviceType))); +#endif +} + +LedDevice* LedDeviceHomeAssistant::construct(const QJsonObject& deviceConfig) +{ + return new LedDeviceHomeAssistant(deviceConfig); +} + +LedDeviceHomeAssistant::~LedDeviceHomeAssistant() +{ + delete _restApi; + _restApi = nullptr; +} + +bool LedDeviceHomeAssistant::init(const QJsonObject& deviceConfig) +{ + bool isInitOK{ false }; + + if ( LedDevice::init(deviceConfig) ) + { + // Overwrite non supported/required features + if (deviceConfig["rewriteTime"].toInt(0) > 0) + { + Info(_log, "Home Assistant lights do not require rewrites. Refresh time is ignored."); + setRewriteTime(0); + } + DebugIf(verbose, _log, "deviceConfig: [%s]", QString(QJsonDocument(_devConfig).toJson(QJsonDocument::Compact)).toUtf8().constData()); + + //Set hostname as per configuration and default port + _hostName = deviceConfig[CONFIG_HOST].toString(); + _apiPort = deviceConfig[CONFIG_PORT].toInt(API_DEFAULT_PORT); + _bearerToken = deviceConfig[CONFIG_AUTH_TOKEN].toString(); + + _isBrightnessOverwrite = _devConfig[CONFIG_BRIGHTNESS_OVERWRITE].toBool(DEFAULT_IS_BRIGHTNESS_OVERWRITE); + _isFullBrightnessAtStart = _devConfig[CONFIG_FULL_BRIGHTNESS_AT_START].toBool(DEFAULT_IS_FULL_BRIGHTNESS_AT_START); + _brightness = _devConfig[CONFIG_BRIGHTNESS].toInt(BRI_MAX); + _switchOffOnBlack = _devConfig[CONFIG_ON_OFF_BLACK].toBool(DEFAULT_IS_SWITCH_OFF_ON_BLACK); + int transitionTimeMs = _devConfig[CONFIG_TRANSITIONTIME].toInt(0); + _transitionTime = transitionTimeMs / 1000.0; + + Debug(_log, "Hostname/IP : %s", QSTRING_CSTR(_hostName)); + Debug(_log, "Port : %d", _apiPort ); + + Debug(_log, "Overwrite Brightn.: %s", _isBrightnessOverwrite ? "Yes" : "No" ); + Debug(_log, "Set Brightness to : %d", _brightness); + Debug(_log, "Full Bri. at start: %s", _isFullBrightnessAtStart ? "Yes" : "No" ); + Debug(_log, "Off on Black : %s", _switchOffOnBlack ? "Yes" : "No" ); + Debug(_log, "Transition Time : %d ms", transitionTimeMs ); + + _lightEntityIds = _devConfig[ CONFIG_ENITYIDS ].toVariant().toStringList(); + int configuredLightsCount = _lightEntityIds.size(); + + if ( configuredLightsCount == 0 ) + { + this->setInError( "No light entity-ids configured" ); + isInitOK = false; + } + else + { + Debug(_log, "Lights configured : %d", configuredLightsCount ); + isInitOK = true; + } + } + + return isInitOK; +} + +bool LedDeviceHomeAssistant::initLedsConfiguration() +{ + bool isInitOK = false; + + //Currently on one light is supported + QString lightEntityId = _lightEntityIds[0]; + + //Get properties for configured light entitiy to check availability + _restApi->setPath({ API_STATES, lightEntityId}); + httpResponse response = _restApi->get(); + if (response.error()) + { + QString errorReason = QString("%1 get properties failed with error: '%2'").arg(_activeDeviceType,response.getErrorReason()); + this->setInError(errorReason); + } + else + { + QJsonObject propertiesDetails = response.getBody().object(); + if (propertiesDetails.isEmpty()) + { + QString errorReason = QString("Light [%1] does not exist").arg(lightEntityId); + this->setInError(errorReason); + } + else + { + if (propertiesDetails.value("state").toString().compare("unavailable") == 0) + { + Warning(_log, "Light [%s] is currently unavailable", QSTRING_CSTR(lightEntityId)); + } + isInitOK = true; + } + } + return isInitOK; +} + +bool LedDeviceHomeAssistant::openRestAPI() +{ + bool isInitOK{ true }; + + if (_restApi == nullptr) + { + if (_apiPort == 0) + { + _apiPort = API_DEFAULT_PORT; + } + + _restApi = new ProviderRestApi(_address.toString(), _apiPort); + _restApi->setLogger(_log); + + _restApi->setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + _restApi->setHeader("Authorization", QByteArrayLiteral("Bearer ") + _bearerToken.toUtf8()); + + //Base-path is api-path + _restApi->setBasePath(API_BASE_PATH); + } + return isInitOK; +} + +int LedDeviceHomeAssistant::open() +{ + int retval = -1; + _isDeviceReady = false; + + if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort)) + { + if (openRestAPI()) + { + // Read LedDevice configuration and validate against device configuration + if (initLedsConfiguration()) + { + // Everything is OK, device is ready + _isDeviceReady = true; + retval = 0; + } + } + else + { + _restApi->setHost(_address.toString()); + _restApi->setPort(_apiPort); + } + } + return retval; +} + +QJsonArray LedDeviceHomeAssistant::discoverSsdp() const +{ + QJsonArray deviceList; + SSDPDiscover ssdpDiscover; + ssdpDiscover.skipDuplicateKeys(true); + ssdpDiscover.setSearchFilter(SSDP_FILTER, SSDP_FILTER_HEADER); + QString searchTarget = SSDP_ID; + + if (ssdpDiscover.discoverServices(searchTarget) > 0) + { + deviceList = ssdpDiscover.getServicesDiscoveredJson(); + } + return deviceList; +} + +QJsonObject LedDeviceHomeAssistant::discover(const QJsonObject& /*params*/) +{ + QJsonObject devicesDiscovered; + devicesDiscovered.insert("ledDeviceType", _activeDeviceType); + + QJsonArray deviceList; + +#ifdef ENABLE_MDNS + QString discoveryMethod("mDNS"); + deviceList = MdnsBrowser::getInstance().data()->getServicesDiscoveredJson( + MdnsServiceRegister::getServiceType(_activeDeviceType), + MdnsServiceRegister::getServiceNameFilter(_activeDeviceType), + DEFAULT_DISCOVER_TIMEOUT + ); +#else + QString discoveryMethod("ssdp"); + deviceList = discoverSsdp(); +#endif + + devicesDiscovered.insert("discoveryMethod", discoveryMethod); + devicesDiscovered.insert("devices", deviceList); + + DebugIf(verbose, _log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); + + return devicesDiscovered; +} + +QJsonObject LedDeviceHomeAssistant::getProperties(const QJsonObject& params) +{ + DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + QJsonObject properties; + + _hostName = params[CONFIG_HOST].toString(""); + _apiPort = API_DEFAULT_PORT; + _bearerToken = params[CONFIG_AUTH_TOKEN].toString(""); + + Info(_log, "Get properties for %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName)); + + if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort)) + { + if (openRestAPI()) + { + QString filter = params["filter"].toString(""); + _restApi->setPath(filter); + + // Perform request + httpResponse response = _restApi->get(); + if (response.error()) + { + Warning(_log, "%s get properties failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); + } + + QJsonObject propertiesDetails; + const QJsonDocument jsonDoc = response.getBody(); + if (jsonDoc.isArray()) { + const QJsonArray jsonArray = jsonDoc.array(); + QVector filteredVector; + + // Iterate over the array and filter objects with entity_id starting with "light." + for (const QJsonValue &value : jsonArray) + { + QJsonObject obj = value.toObject(); + QString entityId = obj[ENTITY_ID].toString(); + + if (entityId.startsWith("light.")) + { + filteredVector.append(obj); + } + } + + // Sort the filtered vector by "friendly_name" in ascending order + std::sort(filteredVector.begin(), filteredVector.end(), [](const QJsonValue &a, const QJsonValue &b) { + QString nameA = a.toObject()["attributes"].toObject()["friendly_name"].toString(); + QString nameB = b.toObject()["attributes"].toObject()["friendly_name"].toString(); + return nameA < nameB; // Ascending order + }); + // Convert the sorted vector back to a QJsonArray + QJsonArray sortedArray; + for (const QJsonValue &value : filteredVector) { + sortedArray.append(value); + } + + propertiesDetails.insert("lightEntities", sortedArray); + + } + + if (!propertiesDetails.isEmpty()) + { + propertiesDetails.insert("ledCount", 1); + } + properties.insert("properties", propertiesDetails); + } + + DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData()); + } + return properties; +} + +void LedDeviceHomeAssistant::identify(const QJsonObject& params) +{ + DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + + _hostName = params[CONFIG_HOST].toString(""); + _apiPort = API_DEFAULT_PORT; + _bearerToken = params[CONFIG_AUTH_TOKEN].toString(""); + + Info(_log, "Identify %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName)); + + if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort)) + { + if (openRestAPI()) + { + QJsonArray lightEntityIds = params[ ENTITY_ID ].toArray(); + + _restApi->setPath(API_LIGHT_TURN_ON); + QJsonObject serviceAttributes{{ENTITY_ID, lightEntityIds}}; + serviceAttributes.insert(FLASH, "short"); + + httpResponse response = _restApi->post(serviceAttributes); + if (response.error()) + { + Warning(_log, "%s identification failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); + } + } + } +} + +bool LedDeviceHomeAssistant::powerOn() +{ + bool isOn = false; + if (_isDeviceReady) + { + _restApi->setPath(API_LIGHT_TURN_ON); + QJsonObject serviceAttributes {{ENTITY_ID, QJsonArray::fromStringList(_lightEntityIds)}}; + + if (_isFullBrightnessAtStart) + { + serviceAttributes.insert(BRIGHTNESS, BRI_MAX); + } + + httpResponse response = _restApi->post(serviceAttributes); + if (response.error()) + { + QString errorReason = QString("Power-on request failed with error: '%1'").arg(response.getErrorReason()); + this->setInError(errorReason); + isOn = false; + } + else { + isOn = true; + } + } + return isOn; +} + +bool LedDeviceHomeAssistant::powerOff() +{ + bool isOff = true; + if (_isDeviceReady) + { + _restApi->setPath(API_LIGHT_TURN_OFF); + QJsonObject serviceAttributes {{ENTITY_ID, QJsonArray::fromStringList(_lightEntityIds)}}; + httpResponse response = _restApi->post(serviceAttributes); + if (response.error()) + { + QString errorReason = QString("Power-off request failed with error: '%1'").arg(response.getErrorReason()); + this->setInError(errorReason); + isOff = false; + } + } + return isOff; +} + +int LedDeviceHomeAssistant::write(const std::vector& ledValues) +{ + int retVal = 0; + + QJsonObject serviceAttributes {{ENTITY_ID, QJsonArray::fromStringList(_lightEntityIds)}}; + ColorRgb ledValue = ledValues.at(0); + + if (_switchOffOnBlack && ledValue == ColorRgb::BLACK) + { + _restApi->setPath(API_LIGHT_TURN_OFF); + } + else + { + // http://hostname:port/api/services/light/turn_on + // { + // "entity_id": [ entity-IDs ], + // "rgb_color": [R,G,B] + // } + + _restApi->setPath(API_LIGHT_TURN_ON); + QJsonArray rgbColor {ledValue.red, ledValue.green, ledValue.blue}; + serviceAttributes.insert(RGB_COLOR, rgbColor); + + if (_isBrightnessOverwrite) + { + serviceAttributes.insert(BRIGHTNESS, _brightness); + } + if (_transitionTime > 0) + { + // Transition time in seconds + serviceAttributes.insert(TRANSITION, _transitionTime); + } + } + + httpResponse response = _restApi->post(serviceAttributes); + if (response.error()) + { + Warning(_log,"Updating lights failed with error: '%s'", QSTRING_CSTR(response.getErrorReason()) ); + retVal = -1; + } + + return retVal; +} diff --git a/libsrc/leddevice/dev_net/LedDeviceHomeAssistant.h b/libsrc/leddevice/dev_net/LedDeviceHomeAssistant.h new file mode 100644 index 00000000..ef4a841d --- /dev/null +++ b/libsrc/leddevice/dev_net/LedDeviceHomeAssistant.h @@ -0,0 +1,181 @@ +#ifndef LEDEVICEHOMEASSISTANT_H +#define LEDEVICEHOMEASSISTANT_H + +// LedDevice includes +#include +#include "ProviderRestApi.h" + +// Qt includes +#include +#include +#include + +/// +/// Implementation of the LedDevice interface for sending to +/// lights made available via the Home Assistant platform. +/// +class LedDeviceHomeAssistant : LedDevice +{ +public: + /// + /// @brief Constructs LED-device for Home Assistant Lights + /// + /// following code shows all configuration options + /// @code + /// "device" : + /// { + /// "type" : "homeassistant" + /// "host" : "hostname or IP", + /// "port" : port + /// "token": "bearer token", + /// }, + ///@endcode + /// + /// @param deviceConfig Device's configuration as JSON-Object + /// + explicit LedDeviceHomeAssistant(const QJsonObject& deviceConfig); + + /// + /// @brief Destructor of the LED-device + /// + ~LedDeviceHomeAssistant() override; + + /// + /// @brief Constructs the LED-device + /// + /// @param[in] deviceConfig Device's configuration as JSON-Object + /// @return LedDevice constructed + static LedDevice* construct(const QJsonObject& deviceConfig); + + /// + /// @brief Discover Home Assistant lights available (for configuration). + /// + /// @param[in] params Parameters used to overwrite discovery default behaviour + /// + /// @return A JSON structure holding a list of devices found + /// + QJsonObject discover(const QJsonObject& params) override; + + /// + /// @brief Get the Home Assistant light's resource properties + /// + /// Following parameters are required + /// @code + /// { + /// "host" : "hostname or IP", + /// "port" : port + /// "token" : "bearer token", + /// "filter": "resource to query", root "/" is used, if empty + /// } + ///@endcode + /// + /// @param[in] params Parameters to query device + /// @return A JSON structure holding the device's properties + /// + QJsonObject getProperties(const QJsonObject& params) override; + + /// + /// @brief Send an update to the Nanoleaf device to identify it. + /// + /// Following parameters are required + /// @code + /// { + /// "host" : "hostname or IP", + /// "port" : port + /// "token" : "bearer token", + /// "entity_id": array of lightIds + /// } + ///@endcode + /// + /// @param[in] params Parameters to address device + /// + void identify(const QJsonObject& params) override; + +protected: + + /// + /// @brief Initialise the Home Assistant light's configuration and network address details + /// + /// @param[in] deviceConfig the JSON device configuration + /// @return True, if success + /// + bool init(const QJsonObject& deviceConfig) override; + + /// + /// @brief Opens the output device. + /// + /// @return Zero on success (i.e. device is ready), else negative + /// + int open() override; + + /// + /// @brief Writes the RGB-Color values to the Home Assistant light. + /// + /// @param[in] ledValues The RGB-color + /// @return Zero on success, else negative + ////// + int write(const std::vector& ledValues) override; + + /// + /// @brief Power-/turn on the Home Assistant light. + /// + /// @brief Store the device's original state. + /// + bool powerOn() override; + + /// + /// @brief Power-/turn off the Home Assistant light. + /// + /// @return True if success + /// + bool powerOff() override; + +private: + + /// + /// @brief Initialise the access to the REST-API wrapper + /// + /// @return True, if success + /// + bool openRestAPI(); + + /// + /// @brief Get Nanoleaf device details and configuration + /// + /// @return True, if Nanoleaf device capabilities fit configuration + /// + bool initLedsConfiguration(); + + /// + /// @brief Discover Home Assistant lights available (for configuration). + /// + /// @return A JSON structure holding a list of devices found + /// + QJsonArray discoverSsdp() const; + + // /// + // /// @brief Get number of panels that can be used as LEds. + // /// + // /// @return Number of usable LED panels + // /// + // int getHwLedCount(const QJsonObject& jsonLayout) const; + + QString _hostName; + QHostAddress _address; + ProviderRestApi* _restApi; + int _apiPort; + QString _bearerToken; + + /// List of the HA light entity_ids. + QStringList _lightEntityIds; + + bool _isBrightnessOverwrite; + bool _isFullBrightnessAtStart; + int _brightness; + bool _switchOffOnBlack; + /// Transition time in seconds + double _transitionTime; + +}; + +#endif // LEDEVICEHOMEASSISTANT_H diff --git a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp index e3df5c7d..54d7bd61 100644 --- a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp @@ -31,7 +31,7 @@ const char CONFIG_TRANSITIONTIME[] = "transitiontime"; const char CONFIG_BLACK_LIGHTS_TIMEOUT[] = "blackLightsTimeout"; const char CONFIG_ON_OFF_BLACK[] = "switchOffOnBlack"; const char CONFIG_RESTORE_STATE[] = "restoreOriginalState"; -const char CONFIG_lightIdS[] = "lightIds"; +const char CONFIG_LIGHTIDS[] = "lightIds"; const char CONFIG_USE_HUE_API_V2[] = "useAPIv2"; const char CONFIG_USE_HUE_ENTERTAINMENT_API[] = "useEntertainmentAPI"; const char CONFIG_groupId[] = "groupId"; @@ -1849,7 +1849,7 @@ bool LedDevicePhilipsHue::setLights() _useEntertainmentAPI = false; Error(_log, "Group-ID [%s] is not usable - Entertainment API usage was disabled!", QSTRING_CSTR(_groupId) ); } - lights = _devConfig[ CONFIG_lightIdS ].toVariant().toStringList(); + lights = _devConfig[ CONFIG_LIGHTIDS ].toVariant().toStringList(); } _lightIds = lights; diff --git a/libsrc/leddevice/dev_net/LedDeviceRazer.cpp b/libsrc/leddevice/dev_net/LedDeviceRazer.cpp index 6f01098b..26a116a8 100644 --- a/libsrc/leddevice/dev_net/LedDeviceRazer.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceRazer.cpp @@ -23,7 +23,7 @@ namespace { const char CONFIG_RAZER_DEVICE_TYPE[] = "subType"; const char CONFIG_SINGLE_COLOR[] = "singleColor"; - // WLED JSON-API elements + // API elements const char API_DEFAULT_HOST[] = "localhost"; const int API_DEFAULT_PORT = 54235; diff --git a/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp b/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp index 8c45e233..f13b8677 100644 --- a/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp +++ b/libsrc/leddevice/dev_serial/LedDeviceAdalight.cpp @@ -58,6 +58,11 @@ bool LedDeviceAdalight::init(const QJsonObject &deviceConfig) case Adalight::ADA: Debug( _log, "Adalight driver uses standard Adalight protocol"); break; + + case Adalight::SKYDIMO: + Debug( _log, "Adalight driver uses Skydimo protocol"); + break; + default: Error( _log, "Adalight driver - unsupported protocol"); return false; @@ -71,10 +76,6 @@ bool LedDeviceAdalight::init(const QJsonObject &deviceConfig) void LedDeviceAdalight::prepareHeader() { - // create ledBuffer - uint totalLedCount = _ledCount; - _bufferLength = static_cast(HEADER_SIZE + _ledRGBCount); - switch (_streamProtocol) { case Adalight::LBAPA: { @@ -82,7 +83,6 @@ void LedDeviceAdalight::prepareHeader() const unsigned int bytesPerRGBLed = 4; const unsigned int endFrameSize = qMax(((_ledCount + 15) / 16), bytesPerRGBLed); _bufferLength = HEADER_SIZE + (_ledCount * bytesPerRGBLed) + startFrameSize + endFrameSize; - _ledBuffer.resize(static_cast(_bufferLength), 0x00); // init constant data values @@ -91,39 +91,47 @@ void LedDeviceAdalight::prepareHeader() _ledBuffer[iLed*4+HEADER_SIZE] = 0xFF; } } - break; - + break; + case Adalight::SKYDIMO: + { + _bufferLength = static_cast(HEADER_SIZE + _ledRGBCount); + _ledBuffer.resize(static_cast(_bufferLength), 0x00); + _ledBuffer[0] = 'A'; + _ledBuffer[1] = 'd'; + _ledBuffer[2] = 'a'; + _ledBuffer[3] = 0; + _ledBuffer[4] = 0; + _ledBuffer[5] = static_cast(_ledCount); + } + break; case Adalight::AWA: - _bufferLength += 8; - [[fallthrough]]; + { + _bufferLength = static_cast(HEADER_SIZE + _ledRGBCount + 8); + _ledBuffer.resize(static_cast(_bufferLength), 0x00); + _ledBuffer[0] = 'A'; + _ledBuffer[1] = 'w'; + _ledBuffer[2] = _white_channel_calibration ? 'A' : 'a'; + qToBigEndian(static_cast(_ledCount-1), &_ledBuffer[3]); + _ledBuffer[5] = _ledBuffer[3] ^ _ledBuffer[4] ^ 0x55; // Checksum + } + break; case Adalight::ADA: [[fallthrough]]; default: - totalLedCount -= 1; + _bufferLength = static_cast(HEADER_SIZE + _ledRGBCount); _ledBuffer.resize(static_cast(_bufferLength), 0x00); - break; - } - - _ledBuffer[0] = 'A'; - if (_streamProtocol == Adalight::AWA ) - { - _ledBuffer[1] = 'w'; - _ledBuffer[2] = _white_channel_calibration ? 'A' : 'a'; - } - else - { + _ledBuffer[0] = 'A'; _ledBuffer[1] = 'd'; _ledBuffer[2] = 'a'; + qToBigEndian(static_cast(_ledCount-1), &_ledBuffer[3]); + _ledBuffer[5] = _ledBuffer[3] ^ _ledBuffer[4] ^ 0x55; // Checksum + break; } - qToBigEndian(static_cast(totalLedCount), &_ledBuffer[3]); - _ledBuffer[5] = _ledBuffer[3] ^ _ledBuffer[4] ^ 0x55; // Checksum - Debug( _log, "Adalight header for %d leds (size: %d): %c%c%c 0x%02x 0x%02x 0x%02x", _ledCount, _ledBuffer.size(), _ledBuffer[0], _ledBuffer[1], _ledBuffer[2], _ledBuffer[3], _ledBuffer[4], _ledBuffer[5] ); } - int LedDeviceAdalight::write(const std::vector & ledValues) { if (_ledCount != ledValues.size()) diff --git a/libsrc/leddevice/dev_serial/LedDeviceAdalight.h b/libsrc/leddevice/dev_serial/LedDeviceAdalight.h index 56065127..d0752dff 100644 --- a/libsrc/leddevice/dev_serial/LedDeviceAdalight.h +++ b/libsrc/leddevice/dev_serial/LedDeviceAdalight.h @@ -10,7 +10,8 @@ typedef enum ProtocolType { ADA = 0, LBAPA, - AWA + AWA, + SKYDIMO } PROTOCOLTYPE; } diff --git a/libsrc/leddevice/schemas/schema-adalight.json b/libsrc/leddevice/schemas/schema-adalight.json index ac9575d4..699852a3 100644 --- a/libsrc/leddevice/schemas/schema-adalight.json +++ b/libsrc/leddevice/schemas/schema-adalight.json @@ -11,10 +11,10 @@ "streamProtocol": { "type": "string", "title": "edt_dev_spec_stream_protocol_title", - "enum": [ "0", "1", "2" ], + "enum": [ "0", "1", "2", "3" ], "default": "0", "options": { - "enum_titles": [ "edt_dev_spec_ada_mode_title", "edt_dev_spec_LBap102Mode_title","edt_dev_spec_awa_mode_title" ] + "enum_titles": [ "edt_dev_spec_ada_mode_title", "edt_dev_spec_LBap102Mode_title","edt_dev_spec_awa_mode_title", "edt_dev_spec_skydimo_mode_title" ] }, "propertyOrder": 2 }, diff --git a/libsrc/leddevice/schemas/schema-apa102_ftdi.json b/libsrc/leddevice/schemas/schema-apa102_ftdi.json new file mode 100644 index 00000000..35ace3d0 --- /dev/null +++ b/libsrc/leddevice/schemas/schema-apa102_ftdi.json @@ -0,0 +1,27 @@ +{ + "type": "object", + "required": true, + "properties": { + "output": { + "type": "string", + "title":"edt_dev_spec_outputPath_title", + "propertyOrder": 1 + }, + "rate": { + "type": "integer", + "title": "edt_dev_spec_baudrate_title", + "default": 5000000, + "propertyOrder": 2 + }, + "brightnessControlMaxLevel": { + "type": "integer", + "title": "edt_conf_color_brightness_title", + "default": 31, + "minimum": 1, + "maximum": 31, + "propertyOrder": 3 + + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/libsrc/leddevice/schemas/schema-homeassistant.json b/libsrc/leddevice/schemas/schema-homeassistant.json new file mode 100644 index 00000000..87ad345a --- /dev/null +++ b/libsrc/leddevice/schemas/schema-homeassistant.json @@ -0,0 +1,135 @@ +{ + "type": "object", + "required": true, + "properties": { + "hostList": { + "type": "string", + "title": "edt_dev_spec_devices_discovered_title", + "enum": [ "NONE" ], + "options": { + "enum_titles": [ "edt_dev_spec_devices_discovery_inprogress" ], + "infoText": "edt_dev_spec_devices_discovered_title_info" + }, + "required": true, + "propertyOrder": 1 + }, + "host": { + "type": "string", + "format": "hostname_or_ip", + "title": "edt_dev_spec_targetIpHost_title", + "options": { + "infoText": "edt_dev_spec_targetIpHost_title_info" + }, + "required": true, + "propertyOrder": 2 + }, + "port": { + "type": "integer", + "title": "edt_dev_spec_port_title", + "default": 8123, + "minimum": 0, + "maximum": 65535, + "access": "expert", + "propertyOrder": 3 + }, + "token": { + "type": "string", + "title": "edt_dev_auth_key_title", + "options": { + "infoText": "edt_dev_auth_key_title_info" + }, + "propertyOrder": 4 + }, + "restoreOriginalState": { + "type": "boolean", + "format": "checkbox", + "title": "edt_dev_spec_restoreOriginalState_title", + "default": true, + "required": true, + "options": { + "hidden": true, + "infoText": "edt_dev_spec_restoreOriginalState_title_info" + }, + "propertyOrder": 5 + }, + "overwriteBrightness": { + "type": "boolean", + "format": "checkbox", + "title": "edt_dev_spec_brightnessOverwrite_title", + "default": true, + "required": true, + "access": "advanced", + "propertyOrder": 5 + }, + "brightness": { + "type": "integer", + "title": "edt_dev_spec_brightness_title", + "default": 255, + "minimum": 1, + "maximum": 255, + "options": { + "dependencies": { + "overwriteBrightness": true + } + }, + "access": "advanced", + "propertyOrder": 6 + }, + "fullBrightnessAtStart": { + "type": "boolean", + "format": "checkbox", + "title": "edt_dev_spec_fullBrightnessAtStart_title", + "default": true, + "required": true, + "access": "advanced", + "propertyOrder": 7 + }, + "switchOffOnBlack": { + "type": "boolean", + "format": "checkbox", + "title": "edt_dev_spec_switchOffOnBlack_title", + "default": false, + "access": "advanced", + "propertyOrder": 8 + }, + "transitionTime": { + "type": "integer", + "title": "edt_dev_spec_transistionTime_title", + "default": 0, + "append": "ms", + "minimum": 0, + "maximum": 2000, + "required": false, + "access": "advanced", + "propertyOrder": 9 + }, + "entityIds": { + "title": "edt_dev_spec_lightid_title", + "type": "array", + "required": true, + "format": "select", + "options": { + "hidden": true + }, + "items": { + "type": "string", + "title": "edt_dev_spec_lights_itemtitle" + }, + "propertyOrder": 10 + }, + "latchTime": { + "type": "integer", + "title": "edt_dev_spec_latchtime_title", + "default": 250, + "append": "edt_append_ms", + "minimum": 100, + "maximum": 2000, + "access": "expert", + "options": { + "infoText": "edt_dev_spec_latchtime_title_info" + }, + "propertyOrder": 11 + } + }, + "additionalProperties": true +} diff --git a/libsrc/leddevice/schemas/schema-sk6812_ftdi.json b/libsrc/leddevice/schemas/schema-sk6812_ftdi.json new file mode 100644 index 00000000..5667909b --- /dev/null +++ b/libsrc/leddevice/schemas/schema-sk6812_ftdi.json @@ -0,0 +1,60 @@ +{ + "type": "object", + "required": true, + "properties": { + "output": { + "type": "string", + "title": "edt_dev_spec_outputPath_title", + "required": true, + "propertyOrder": 1 + }, + "rate": { + "type": "integer", + "step": 100000, + "title": "edt_dev_spec_baudrate_title", + "default": 3200000, + "minimum": 2050000, + "maximum": 3750000, + "propertyOrder": 2 + }, + "brightnessControlMaxLevel": { + "type": "integer", + "title": "edt_conf_color_brightness_title", + "default": 255, + "minimum": 1, + "maximum": 255, + "propertyOrder": 3 + }, + "whiteAlgorithm": { + "type": "string", + "title": "edt_dev_spec_whiteLedAlgor_title", + "enum": [ + "subtract_minimum", + "sub_min_cool_adjust", + "sub_min_warm_adjust", + "cold_white", + "neutral_white", + "auto", + "auto_max", + "auto_accurate", + "white_off" + ], + "default": "white_off", + "options": { + "enum_titles": [ + "edt_dev_enum_subtract_minimum", + "edt_dev_enum_sub_min_cool_adjust", + "edt_dev_enum_sub_min_warm_adjust", + "edt_dev_enum_cold_white", + "edt_dev_enum_neutral_white", + "edt_dev_enum_auto", + "edt_dev_enum_auto_max", + "edt_dev_enum_auto_accurate", + "edt_dev_enum_white_off" + ] + }, + "propertyOrder": 4 + } + }, + "additionalProperties": true +} diff --git a/libsrc/leddevice/schemas/schema-sk6812spi.json b/libsrc/leddevice/schemas/schema-sk6812spi.json index 49b7fef7..41cd5bf1 100644 --- a/libsrc/leddevice/schemas/schema-sk6812spi.json +++ b/libsrc/leddevice/schemas/schema-sk6812spi.json @@ -22,10 +22,30 @@ "whiteAlgorithm": { "type": "string", "title":"edt_dev_spec_whiteLedAlgor_title", - "enum" : ["subtract_minimum","sub_min_cool_adjust","sub_min_warm_adjust","white_off"], + "enum" : [ + "subtract_minimum", + "sub_min_cool_adjust", + "sub_min_warm_adjust", + "cold_white", + "neutral_white", + "auto", + "auto_max", + "auto_accurate", + "white_off" + ], "default": "subtract_minimum", "options" : { - "enum_titles" : ["edt_dev_enum_subtract_minimum", "edt_dev_enum_sub_min_cool_adjust","edt_dev_enum_sub_min_warm_adjust", "edt_dev_enum_white_off"] + "enum_titles" : [ + "edt_dev_enum_subtract_minimum", + "edt_dev_enum_sub_min_cool_adjust", + "edt_dev_enum_sub_min_warm_adjust", + "edt_dev_enum_cold_white", + "edt_dev_enum_neutral_white", + "edt_dev_enum_auto", + "edt_dev_enum_auto_max", + "edt_dev_enum_auto_accurate", + "edt_dev_enum_white_off" + ] }, "propertyOrder" : 4 }, diff --git a/libsrc/leddevice/schemas/schema-ws2812_ftdi.json b/libsrc/leddevice/schemas/schema-ws2812_ftdi.json new file mode 100644 index 00000000..c6e7a575 --- /dev/null +++ b/libsrc/leddevice/schemas/schema-ws2812_ftdi.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "required": true, + "properties": { + "output": { + "type": "string", + "title": "edt_dev_spec_outputPath_title", + "propertyOrder": 1 + }, + "rate": { + "type": "integer", + "title": "edt_dev_spec_baudrate_title", + "default": 3075000, + "minimum": 2106000, + "maximum": 3075000, + "propertyOrder": 2 + } + }, + "additionalProperties": true +} \ No newline at end of file diff --git a/libsrc/leddevice/schemas/schema-ws281x.json b/libsrc/leddevice/schemas/schema-ws281x.json index 2ccfb16d..1af09eee 100644 --- a/libsrc/leddevice/schemas/schema-ws281x.json +++ b/libsrc/leddevice/schemas/schema-ws281x.json @@ -43,10 +43,30 @@ "whiteAlgorithm": { "type": "string", "title":"edt_dev_spec_whiteLedAlgor_title", - "enum" : ["subtract_minimum","sub_min_cool_adjust","sub_min_warm_adjust","white_off"], + "enum" : [ + "subtract_minimum", + "sub_min_cool_adjust", + "sub_min_warm_adjust", + "cold_white", + "neutral_white", + "auto", + "auto_max", + "auto_accurate", + "white_off" + ], "default": "subtract_minimum", "options" : { - "enum_titles" : ["edt_dev_enum_subtract_minimum", "edt_dev_enum_sub_min_cool_adjust","edt_dev_enum_sub_min_warm_adjust", "edt_dev_enum_white_off"] + "enum_titles" : [ + "edt_dev_enum_subtract_minimum", + "edt_dev_enum_sub_min_cool_adjust", + "edt_dev_enum_sub_min_warm_adjust", + "edt_dev_enum_cold_white", + "edt_dev_enum_neutral_white", + "edt_dev_enum_auto", + "edt_dev_enum_auto_max", + "edt_dev_enum_auto_accurate", + "edt_dev_enum_white_off" + ] }, "propertyOrder" : 7 }, diff --git a/libsrc/utils/ImageResampler.cpp b/libsrc/utils/ImageResampler.cpp index 483e35a9..b205e0d3 100644 --- a/libsrc/utils/ImageResampler.cpp +++ b/libsrc/utils/ImageResampler.cpp @@ -133,6 +133,22 @@ void ImageResampler::processImage(const uint8_t * data, int width, int height, i break; } + case PixelFormat::RGB24: + { + for (int yDest = yDestStart, ySource = cropTop + (_verticalDecimation >> 1); yDest <= yDestEnd; ySource += _verticalDecimation, ++yDest) + { + for (int xDest = xDestStart, xSource = cropLeft + (_horizontalDecimation >> 1); xDest <= xDestEnd; xSource += _horizontalDecimation, ++xDest) + { + ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); + int index = lineLength * ySource + (xSource << 1) + xSource; + rgb.red = data[index ]; + rgb.green = data[index+1]; + rgb.blue = data[index+2]; + } + } + break; + } + case PixelFormat::BGR24: { for (int yDest = yDestStart, ySource = cropTop + (_verticalDecimation >> 1); yDest <= yDestEnd; ySource += _verticalDecimation, ++yDest) diff --git a/libsrc/utils/RgbToRgbw.cpp b/libsrc/utils/RgbToRgbw.cpp index f82fadf5..7e96a1b6 100644 --- a/libsrc/utils/RgbToRgbw.cpp +++ b/libsrc/utils/RgbToRgbw.cpp @@ -3,6 +3,8 @@ #include #include +#define ROUND_DIVIDE(number, denom) (((number) + (denom) / 2) / (denom)) + namespace RGBW { WhiteAlgorithm stringToWhiteAlgorithm(const QString& str) @@ -19,7 +21,27 @@ WhiteAlgorithm stringToWhiteAlgorithm(const QString& str) { return WhiteAlgorithm::SUB_MIN_COOL_ADJUST; } - if (str.isEmpty() || str == "white_off") + if (str == "cold_white") + { + return WhiteAlgorithm::COLD_WHITE; + } + if (str == "neutral_white") + { + return WhiteAlgorithm::NEUTRAL_WHITE; + } + if (str == "auto") + { + return WhiteAlgorithm::AUTO; + } + if (str == "auto_max") + { + return WhiteAlgorithm::AUTO_MAX; + } + if (str == "auto_accurate") + { + return WhiteAlgorithm::AUTO_ACCURATE; + } + if (str.isEmpty() || str == "white_off") { return WhiteAlgorithm::WHITE_OFF; } @@ -77,6 +99,63 @@ void Rgb_to_Rgbw(ColorRgb input, ColorRgbw * output, WhiteAlgorithm algorithm) output->white = 0; break; } + + case WhiteAlgorithm::AUTO_MAX: + { + output->red = input.red; + output->green = input.green; + output->blue = input.blue; + output->white = input.red > input.green ? (input.red > input.blue ? input.red : input.blue) : (input.green > input.blue ? input.green : input.blue); + break; + } + + case WhiteAlgorithm::AUTO_ACCURATE: + { + output->white = input.red < input.green ? (input.red < input.blue ? input.red : input.blue) : (input.green < input.blue ? input.green : input.blue); + output->red = input.red - output->white; + output->green = input.green - output->white; + output->blue = input.blue - output->white; + break; + } + + case WhiteAlgorithm::AUTO: + { + + output->red = input.red; + output->green = input.green; + output->blue = input.blue; + output->white = input.red < input.green ? (input.red < input.blue ? input.red : input.blue) : (input.green < input.blue ? input.green : input.blue); + break; + } + case WhiteAlgorithm::NEUTRAL_WHITE: + case WhiteAlgorithm::COLD_WHITE: + { + //cold white config + uint8_t gain = 0xFF; + uint8_t red = 0xA0; + uint8_t green = 0xA0; + uint8_t blue = 0xA0; + + if (algorithm == WhiteAlgorithm::NEUTRAL_WHITE) { + gain = 0xFF; + red = 0xB0; + green = 0xB0; + blue = 0x70; + } + + uint8_t _r = qMin((uint32_t)(ROUND_DIVIDE(red * input.red, 0xFF)), (uint32_t)0xFF); + uint8_t _g = qMin((uint32_t)(ROUND_DIVIDE(green * input.green, 0xFF)), (uint32_t)0xFF); + uint8_t _b = qMin((uint32_t)(ROUND_DIVIDE(blue * input.blue, 0xFF)), (uint32_t)0xFF); + + output->white = qMin(_r, qMin(_g, _b)); + output->red = input.red - _r; + output->green = input.green - _g; + output->blue = input.blue - _b; + + uint8_t _w = qMin((uint32_t)(ROUND_DIVIDE(gain * output->white, 0xFF)), (uint32_t)0xFF); + output->white = _w; + break; + } default: break; }