From e0060eb406dd6fdd78bf77456f4aa80324f035e2 Mon Sep 17 00:00:00 2001 From: Paulchen-Panther <16664240+Paulchen-Panther@users.noreply.github.com> Date: Sun, 26 Mar 2023 15:22:06 +0200 Subject: [PATCH 001/117] cleanup --- .codedocs | 76 --------------------------------- .lgtm.yml | 27 ------------ bin/scripts/docker-compile.sh | 2 +- doc/development/CompileHowto.md | 20 +-------- 4 files changed, 3 insertions(+), 122 deletions(-) delete mode 100644 .codedocs delete mode 100644 .lgtm.yml diff --git a/.codedocs b/.codedocs deleted file mode 100644 index ead816de..00000000 --- a/.codedocs +++ /dev/null @@ -1,76 +0,0 @@ -# Hyperion.NG .codedocs Configuration File - -#--------------------------------------------------------------------------- -# CodeDocs Configuration -#--------------------------------------------------------------------------- - -# Include the Doxygen configuration from another file. -# The file must be a relative path with respect to the root of the repository. - -DOXYFILE = - -# Specify external repository to link documentation with. -# This is similar to Doxygen's TAGFILES option, but will automatically link to -# tags of other repositories already using CodeDocs. List each repository to -# link with by giving its location in the form of owner/repository. -# For example: -# TAGLINKS = doxygen/doxygen CodeDocs/osg -# Note: these repositories must already be built on CodeDocs. - -TAGLINKS = - -#--------------------------------------------------------------------------- -# Doxygen Configuration -#--------------------------------------------------------------------------- - -# Doxygen configuration may also be placed in this file. -# Currently, the following Doxygen configuration options are available. Refer -# to http://doxygen.org/manual/config.html for detailed explanation of the -# options. To request support for more options, contact support@codedocs.xyz. -# -# ABBREVIATE_BRIEF = -# ALIASES = -# ALPHABETICAL_INDEX = -# ALWAYS_DETAILED_SEC = -# CASE_SENSE_NAMES = -# CLASS_DIAGRAMS = -# DISABLE_INDEX = -# DISTRIBUTE_GROUP_DOC = -# EXAMPLE_PATH = - EXCLUDE = .ci/ \ - assets/ \ - bin/ - config/ \ - effects/ \ - test/ \ -# EXCLUDE_PATTERNS = -# EXCLUDE_SYMBOLS = -# EXTENSION_MAPPING = -# EXTRACT_LOCAL_CLASSES = -# FILE_PATTERNS = -# GENERATE_TAGFILE = -# GENERATE_TREEVIEW = -# HIDE_COMPOUND_REFERENCE = -# HIDE_SCOPE_NAMES = -# HIDE_UNDOC_CLASSES = -# HIDE_UNDOC_MEMBERS = -# HTML_TIMESTAMP = -# INLINE_GROUPED_CLASSES = -# INPUT_ENCODING = -# INTERNAL_DOCS = -# OPTIMIZE_OUTPUT_FOR_C = - PROJECT_BRIEF = "The successor to Hyperion aka Hyperion Next Generation" - PROJECT_NAME = "Hyperion.NG" -# PROJECT_NUMBER = -# SHORT_NAMES = -# SHOW_FILES = -# SHOW_INCLUDE_FILES = -# SHOW_NAMESPACES = -# SORT_BRIEF_DOCS = -# SORT_BY_SCOPE_NAME = -# SORT_MEMBER_DOCS = -# STRICT_PROTO_MATCHING = -# TYPEDEF_HIDES_STRUCT = - USE_MDFILE_AS_MAINPAGE = README.md -# VERBATIM_HEADERS = -# diff --git a/.lgtm.yml b/.lgtm.yml deleted file mode 100644 index 6a799607..00000000 --- a/.lgtm.yml +++ /dev/null @@ -1,27 +0,0 @@ -extraction: - cpp: - prepare: - packages: - - "git" - - "cmake" - - "build-essential" - - "qtbase5-dev" - - "libqt5serialport5-dev" - - "libqt5sql5-sqlite" - - "libqt5svg5-dev" - - "libqt5x11extras5-dev" - - "libusb-1.0-0-dev" - - "python3-dev" - - "libcec-dev" - - "libxcb-image0-dev" - - "libxcb-util0-dev" - - "libxcb-shm0-dev" - - "libxcb-render0-dev" - - "libxcb-randr0-dev" - - "libxrandr-dev" - - "libxrender-dev" - - "libavahi-core-dev" - - "libavahi-compat-libdnssd-dev" - - "libturbojpeg0-dev" - - "libjpeg-dev" - - "libssl-dev" diff --git a/bin/scripts/docker-compile.sh b/bin/scripts/docker-compile.sh index 5381529a..a6abd11d 100755 --- a/bin/scripts/docker-compile.sh +++ b/bin/scripts/docker-compile.sh @@ -72,7 +72,7 @@ echo "######################################################## # These are possible arguments to modify the script behaviour with their default values # # docker-compile.sh -h, --help # Show this help message -# docker-compile.sh -i, --image # The docker image, e.g., x86_64, armv6l, armv7l, aarch64, rpi-raspbian +# docker-compile.sh -i, --image # The docker image, e.g., x86_64, armv6l, armv7l, aarch64 # docker-compile.sh -t, --tag # The docker tag, e.g., stretch, buster, bullseye, bookworm # docker-compile.sh -b, --type # Release or Debug build # docker-compile.sh -p, --packages # If true, build packages with CPack diff --git a/doc/development/CompileHowto.md b/doc/development/CompileHowto.md index f3b5502c..8a2cc7df 100644 --- a/doc/development/CompileHowto.md +++ b/doc/development/CompileHowto.md @@ -5,24 +5,6 @@ If you are using [Docker](https://www.docker.com/), you can compile Hyperion ins The compiled binaries and packages will be available at the deploy folder next to the script.
Note: call the script with `./docker-compile.sh -h` for more options. -## Native compilation on Raspberry Pi for: - -**Raspbian Stretch** -```console -wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -i rpi-raspbian -t stretch -``` -**Raspbian Buster/Raspberry Pi OS** -```console -wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -i rpi-raspbian -t buster -``` -**Raspberry Pi OS Bullseye** -```console -wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -i rpi-raspbian -t bullseye -``` -**Raspberry Pi OS Bookworm** -```console -wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -i rpi-raspbian -t bookworm -``` ## Cross compilation on x86_64 for: **x86_64 (Debian Stretch):** @@ -69,6 +51,7 @@ wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/ ```console wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -i armv7l -t bullseye ``` + ## Cross compilation on x86_64 for developers Using additional options you can cross compile locally -l: use a local hyperion source code directory rather than cloning from GitHub @@ -79,6 +62,7 @@ Using additional options you can cross compile locally cd $HYPERION_HOME ./bin/scripts/docker-compile.sh -l -c -i armv7l -t bullseye ``` + # The usual way ## Debian/Ubuntu/Win10LinuxSubsystem From 08e7c7d8c2a40fbb1fa284461925cc4809a313ab Mon Sep 17 00:00:00 2001 From: Paulchen-Panther <16664240+Paulchen-Panther@users.noreply.github.com> Date: Mon, 27 Mar 2023 20:54:34 +0200 Subject: [PATCH 002/117] [HotFix] Supplement to the PR #1570 - Fix Issue #1596 --- .github/workflows/apt/arm64.json | 28 ++++++++++++++-------------- .github/workflows/apt/armhf.json | 32 ++++++++++++++++---------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/apt/arm64.json b/.github/workflows/apt/arm64.json index 8cc8b2e5..5f64a3ab 100644 --- a/.github/workflows/apt/arm64.json +++ b/.github/workflows/apt/arm64.json @@ -2,56 +2,56 @@ { "distribution": "Bionic", "architecture": "arm64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev", - "package-depends": "libpython3.6, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libturbojpeg, libcec4", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev", + "package-depends": "libpython3.6, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libasound2, libturbojpeg, libcec4", "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Ubuntu 18.04 (Bionic Beaver) (arm64)" }, { "distribution": "Focal", "architecture": "arm64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.8, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg, libcec4", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", + "package-depends": "libpython3.8, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg, libcec4", "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Ubuntu 20.04 (Focal Fossa) (arm64)" }, { "distribution": "Jammy", "architecture": "arm64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libturbojpeg, libcec6", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", + "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Ubuntu 22.04 (Jammy Jellyfish) (arm64)" }, { "distribution": "Kinetic", "architecture": "arm64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libturbojpeg, libcec6", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", + "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Ubuntu 22.10 (Kinetic Kudu) (arm64)" }, { "distribution": "Buster", "architecture": "arm64", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.7, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg0, libcec4", + "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", + "package-depends": "libpython3.7, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec4", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Debian 10.x (Buster) (arm64)" }, { "distribution": "Bullseye", "architecture": "arm64", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg0, libcec6", + "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", + "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Debian 11.x (Bullseye) (arm64)" }, { "distribution": "Bookworm", "architecture": "arm64", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg0, libcec6", + "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", + "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Debian 12.x (Bookworm) (arm64)", "exclude" : true diff --git a/.github/workflows/apt/armhf.json b/.github/workflows/apt/armhf.json index f7f5aa3c..33847459 100644 --- a/.github/workflows/apt/armhf.json +++ b/.github/workflows/apt/armhf.json @@ -2,64 +2,64 @@ { "distribution": "Bionic", "architecture": "armhf", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev", - "package-depends": "libpython3.6, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libturbojpeg, libcec4", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev", + "package-depends": "libpython3.6, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libasound2, libturbojpeg, libcec4", "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Ubuntu 18.04 (Bionic Beaver) (armhf)" }, { "distribution": "Focal", "architecture": "armhf", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.8, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg, libcec4", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", + "package-depends": "libpython3.8, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg, libcec4", "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Ubuntu 20.04 (Focal Fossa) (armhf)" }, { "distribution": "Jammy", "architecture": "armhf", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libturbojpeg, libcec6", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", + "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Ubuntu 22.04 (Jammy Jellyfish) (armhf)" }, { "distribution": "Kinetic", "architecture": "armhf", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libturbojpeg, libcec6", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", + "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Ubuntu 22.10 (Kinetic Kudu) (armhf)" }, { "distribution": "Stretch", "architecture": "armhf", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl1.0-dev, libraspberrypi-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.5, libusb-1.0-0, libqt5widgets5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libturbojpeg0, libcec4", + "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl1.0-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", + "package-depends": "libpython3.5, libusb-1.0-0, libqt5widgets5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libasound2, libturbojpeg0, libcec4", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description":"Debian 9.x (Stretch) (armhf)" }, { "distribution": "Buster", "architecture": "armhf", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl1.0-dev, libraspberrypi-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.7, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg0, libcec4", + "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl1.0-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", + "package-depends": "libpython3.7, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec4", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Debian 10.x (Buster) (armhf)" }, { "distribution": "Bullseye", "architecture": "armhf", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg0, libcec6", + "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", + "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Debian 11.x (Bullseye) (armhf)" }, { "distribution": "Bookworm", "architecture": "armhf", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libturbojpeg0, libcec6", + "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", + "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Debian 12.x (Bookworm) (armhf)", "exclude" : true From e3496eb4dc1d6d9efeb8a013ca741ab53c0ec564 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 17:25:49 +0200 Subject: [PATCH 003/117] Bump SamKirkland/FTP-Deploy-Action from 4.3.3 to 4.3.4 (#1598) Bumps [SamKirkland/FTP-Deploy-Action](https://github.com/SamKirkland/FTP-Deploy-Action) from 4.3.3 to 4.3.4. - [Release notes](https://github.com/SamKirkland/FTP-Deploy-Action/releases) - [Commits](https://github.com/SamKirkland/FTP-Deploy-Action/compare/4.3.3...v4.3.4) --- updated-dependencies: - dependency-name: SamKirkland/FTP-Deploy-Action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/apt.yml | 2 +- .github/workflows/nightly.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apt.yml b/.github/workflows/apt.yml index fc5eca13..41cc80f9 100644 --- a/.github/workflows/apt.yml +++ b/.github/workflows/apt.yml @@ -121,7 +121,7 @@ jobs: done - name: Upload packages to APT server (DRAFT) - uses: SamKirkland/FTP-Deploy-Action@4.3.3 + uses: SamKirkland/FTP-Deploy-Action@v4.3.4 with: server: apt.hyperion-project.org username: ${{ secrets.APT_USER }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 75365c4c..00772c41 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -171,7 +171,7 @@ jobs: done - name: Upload packages to nightly server - uses: SamKirkland/FTP-Deploy-Action@4.3.3 + uses: SamKirkland/FTP-Deploy-Action@v4.3.4 with: server: nightly.apt.hyperion-project.org username: ${{ secrets.NIGHTLY_USER }} From 79b31e16e019a2ec4bdf607ed8bb38ac7af39504 Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Wed, 26 Apr 2023 00:22:19 +0000 Subject: [PATCH 004/117] Update submodule rpi_ws281x --- dependencies/external/rpi_ws281x | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/external/rpi_ws281x b/dependencies/external/rpi_ws281x index 15330cb3..1f47b59e 160000 --- a/dependencies/external/rpi_ws281x +++ b/dependencies/external/rpi_ws281x @@ -1 +1 @@ -Subproject commit 15330cb384aed2411ec7e42712ad4ed7af940877 +Subproject commit 1f47b59ed603223d1376d36c788c89af67ae2fdc From 5535b884bfea368e92fc5bd24a2826e0bd9d62b3 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sun, 7 May 2023 14:04:45 +0200 Subject: [PATCH 005/117] Fixes (#1605) * Correct misleading Error messages * Fix that Audio Capture is not shown when there is no screen nor video grabber * Fix - Audio Capture enabled after reboot automatically (#1581) --- assets/webconfig/js/content_index.js | 4 +++- libsrc/hyperion/CaptureCont.cpp | 6 +++--- libsrc/hyperion/GrabberWrapper.cpp | 2 +- src/hyperiond/SuspendHandler.cpp | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index 5a5203ca..e8ec28a8 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -218,7 +218,9 @@ $(document).ready(function () { loadContent(undefined,true); //Hide capture menu entries, if no grabbers are available - if ((window.serverInfo.grabbers.screen.available.length === 0) && (window.serverInfo.grabbers.video.available.length === 0)) { + if ((window.serverInfo.grabbers.screen.available.length === 0) && + (window.serverInfo.grabbers.video.available.length === 0) && + (window.serverInfo.grabbers.audio.available.length === 0)) { $("#MenuItemGrabber").attr('style', 'display:none') if ((jQuery.inArray("boblight", window.serverInfo.services) === -1)) { $("#MenuItemInstCapture").attr('style', 'display:none') diff --git a/libsrc/hyperion/CaptureCont.cpp b/libsrc/hyperion/CaptureCont.cpp index 4f3a7aae..5ae2a9e5 100644 --- a/libsrc/hyperion/CaptureCont.cpp +++ b/libsrc/hyperion/CaptureCont.cpp @@ -97,7 +97,7 @@ void CaptureCont::setSystemCaptureEnable(bool enable) } else { - disconnect(GlobalSignals::getInstance(), &GlobalSignals::setSystemImage, this, 0); + disconnect(GlobalSignals::getInstance(), &GlobalSignals::setSystemImage, this, nullptr); _hyperion->clear(_systemCaptPrio); _systemInactiveTimer->stop(); _systemCaptName = ""; @@ -120,7 +120,7 @@ void CaptureCont::setV4LCaptureEnable(bool enable) } else { - disconnect(GlobalSignals::getInstance(), &GlobalSignals::setV4lImage, this, 0); + disconnect(GlobalSignals::getInstance(), &GlobalSignals::setV4lImage, this, nullptr); _hyperion->clear(_v4lCaptPrio); _v4lInactiveTimer->stop(); _v4lCaptName = ""; @@ -143,7 +143,7 @@ void CaptureCont::setAudioCaptureEnable(bool enable) } else { - disconnect(GlobalSignals::getInstance(), &GlobalSignals::setAudioImage, this, 0); + disconnect(GlobalSignals::getInstance(), &GlobalSignals::setAudioImage, this, nullptr); _hyperion->clear(_audioCaptPrio); _audioInactiveTimer->stop(); _audioCaptName = ""; diff --git a/libsrc/hyperion/GrabberWrapper.cpp b/libsrc/hyperion/GrabberWrapper.cpp index 4d88a6f2..e0e4a27c 100644 --- a/libsrc/hyperion/GrabberWrapper.cpp +++ b/libsrc/hyperion/GrabberWrapper.cpp @@ -290,7 +290,7 @@ void GrabberWrapper::handleSourceRequest(hyperion::Components component, int hyp else GRABBER_AUDIO_CLIENTS.remove(hyperionInd); - if (GRABBER_AUDIO_CLIENTS.empty()) + if (GRABBER_AUDIO_CLIENTS.empty() || !getAudioGrabberState()) stop(); else start(); diff --git a/src/hyperiond/SuspendHandler.cpp b/src/hyperiond/SuspendHandler.cpp index a2014ef6..b680b3ea 100644 --- a/src/hyperiond/SuspendHandler.cpp +++ b/src/hyperiond/SuspendHandler.cpp @@ -268,7 +268,7 @@ SuspendHandlerLinux::SuspendHandlerLinux() QDBusConnection systemBus = QDBusConnection::systemBus(); if (!systemBus.isConnected()) { - Error(Logger::getInstance("DAEMON"), "Suspend/resume handler - System bus is not connected"); + Info(Logger::getInstance("DAEMON"), "The suspend/resume feature is not supported by your system configuration"); } else { @@ -288,7 +288,7 @@ SuspendHandlerLinux::SuspendHandlerLinux() QDBusConnection sessionBus = QDBusConnection::sessionBus(); if (!sessionBus.isConnected()) { - Error(Logger::getInstance("DAEMON"), "Lock/unlock handler- Session bus is not connected"); + Info(Logger::getInstance("DAEMON"), "The lock/unlock feature is not supported by your system configuration"); } else { From 2a17de37f16e0f23582645a5145352bb6aa76a59 Mon Sep 17 00:00:00 2001 From: lsellens Date: Thu, 11 May 2023 14:11:33 -0500 Subject: [PATCH 006/117] fix for matrix effect (#1602) --- effects/matrix.gif | Bin 0 -> 239117 bytes effects/matrix.json | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 effects/matrix.gif diff --git a/effects/matrix.gif b/effects/matrix.gif new file mode 100644 index 0000000000000000000000000000000000000000..f84fae90580130851a2d88fde387c4aa4cce7459 GIT binary patch literal 239117 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

k=Uw=n$zjOXW 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

$F;QP_%U;oW5Cobq6^6I>#jPb|OY1WF~sQBz(Hl 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 Ccdd;8 literal 0 HcmV?d00001 diff --git a/effects/matrix.json b/effects/matrix.json index c0ae4ea5..2a2f295c 100644 --- a/effects/matrix.json +++ b/effects/matrix.json @@ -9,8 +9,8 @@ "cropTop": 0, "fps": 30, "grayscale": false, - "imageSource": "url", + "imageSource": "file", "reverse": false, - "url": "https://i.gifer.com/embedded/download/1j6F.gif" + "file": "matrix.gif" } } From 54d03b80659a40e34f4ae5bf6011807fcf30bd68 Mon Sep 17 00:00:00 2001 From: Paulchen-Panther <16664240+Paulchen-Panther@users.noreply.github.com> Date: Sun, 14 May 2023 16:29:57 +0200 Subject: [PATCH 007/117] added workflow_dispatch to allow the bot to create the APT repository itself --- .github/workflows/apt.yml | 27 +++++++++++++++++++++++++-- .github/workflows/push-master.yml | 8 +++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/apt.yml b/.github/workflows/apt.yml index 41cc80f9..06212416 100644 --- a/.github/workflows/apt.yml +++ b/.github/workflows/apt.yml @@ -1,6 +1,26 @@ name: Hyperion APT Build on: workflow_call: + inputs: + head_sha: + type: string + description: The branch, tag or SHA to checkout + required: true + secrets: + APT_GPG: + required: true + APT_USER: + required: true + APT_PASSWORD: + required: true + APT_DRAFT: + required: true + workflow_dispatch: + inputs: + head_sha: + type: string + description: The branch, tag or SHA to checkout + required: true secrets: APT_GPG: required: true @@ -36,6 +56,7 @@ jobs: steps: - uses: actions/checkout@v3 with: + ref: ${{ github.event.inputs.head_sha || github.event.client_payload.head_sha }} submodules: true - name: Generate environment variables @@ -73,7 +94,7 @@ jobs: cp ../hyperion_*.deb /deploy" - name: Upload package artifact - if: startsWith(github.event.ref, 'refs/tags') + if: ${{ startsWith(github.event.ref, 'refs/tags') || github.event_name == 'workflow_dispatch' }} uses: actions/upload-artifact@v3 with: path: deploy @@ -81,12 +102,14 @@ jobs: publish: name: Publish APT packages - if: startsWith(github.event.ref, 'refs/tags') + if: ${{ startsWith(github.event.ref, 'refs/tags') || github.event_name == 'workflow_dispatch' }} needs: [setup, build] runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.head_sha || github.event.client_payload.head_sha }} - name: Import GPG key uses: crazy-max/ghaction-import-gpg@v5.2.0 diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml index c820a447..dc290a5b 100644 --- a/.github/workflows/push-master.yml +++ b/.github/workflows/push-master.yml @@ -195,8 +195,6 @@ jobs: if: startsWith(github.event.ref, 'refs/tags') needs: [Linux, macOS, windows] uses: ./.github/workflows/apt.yml - secrets: - APT_GPG: ${{ secrets.APT_GPG }} - APT_USER: ${{ secrets.APT_USER }} - APT_PASSWORD: ${{ secrets.APT_PASSWORD }} - APT_DRAFT: ${{ secrets.APT_DRAFT }} + secrets: inherit + with: + head_sha: master From 00ce3ff089a06718beddaf14dfca171149e55725 Mon Sep 17 00:00:00 2001 From: Paulchen-Panther <16664240+Paulchen-Panther@users.noreply.github.com> Date: Mon, 29 May 2023 21:43:28 +0200 Subject: [PATCH 008/117] Update Discord Invite --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf3f7d8b..028922b1 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![CodeQL Analysis](https://github.com/hyperion-project/hyperion.ng/actions/workflows/codeql.yml/badge.svg)](https://github.com/hyperion-project/hyperion.ng/actions/workflows/codeql.yml) [![Forum](https://img.shields.io/website/https/hyperion-project.org.svg?label=Forum&down_color=red&down_message=offline&up_color=4bc51d&up_message=online&logo=homeadvisor&logoColor=white)](https://www.hyperion-project.org) [![Documentation](https://img.shields.io/website/https/docs.hyperion-project.org.svg?label=Documentation&down_color=red&down_message=offline&up_color=4bc51d&up_message=online&logo=read-the-docs)](https://docs.hyperion-project.org) -[![Discord](https://img.shields.io/discord/785578322167463937?label=Discord&logo=discord&logoColor=white&color=4bc51d)](https://discord.gg/khkR8Vx3ff) +[![Discord](https://img.shields.io/discord/785578322167463937?label=Discord&logo=discord&logoColor=white&color=4bc51d)](https://discord.gg/XtVTb3HEKS) ![made-with-love](https://img.shields.io/badge/Made%20With-♥-ff0000.svg) ## About Hyperion From a7ce0f8a4c61fa7bf132e2f96807ffba49cf7e37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 11 Jun 2023 20:42:35 +0200 Subject: [PATCH 009/117] Bump crazy-max/ghaction-import-gpg from 5.2.0 to 5.3.0 (#1608) Bumps [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) from 5.2.0 to 5.3.0. - [Release notes](https://github.com/crazy-max/ghaction-import-gpg/releases) - [Commits](https://github.com/crazy-max/ghaction-import-gpg/compare/v5.2.0...v5.3.0) --- updated-dependencies: - dependency-name: crazy-max/ghaction-import-gpg dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/apt.yml | 2 +- .github/workflows/nightly.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apt.yml b/.github/workflows/apt.yml index 06212416..66b90cb0 100644 --- a/.github/workflows/apt.yml +++ b/.github/workflows/apt.yml @@ -112,7 +112,7 @@ jobs: ref: ${{ github.event.inputs.head_sha || github.event.client_payload.head_sha }} - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v5.2.0 + uses: crazy-max/ghaction-import-gpg@v5.3.0 with: gpg_private_key: ${{ secrets.APT_GPG }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 00772c41..8a4da1e2 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -138,7 +138,7 @@ jobs: uses: actions/checkout@v3 - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v5.2.0 + uses: crazy-max/ghaction-import-gpg@v5.3.0 with: gpg_private_key: ${{ secrets.APT_GPG }} From af1a31b842a8aa7385e0e8ecde16be14c7c1260d Mon Sep 17 00:00:00 2001 From: Christoph Pohl Date: Sun, 25 Jun 2023 10:34:47 +0200 Subject: [PATCH 010/117] Update dependencies for Debian Bookworm (#1613) --- .github/workflows/apt/amd64.json | 2 +- .github/workflows/apt/arm64.json | 2 +- .github/workflows/apt/armhf.json | 2 +- CHANGELOG.md | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/apt/amd64.json b/.github/workflows/apt/amd64.json index 90e04ff7..7c794117 100644 --- a/.github/workflows/apt/amd64.json +++ b/.github/workflows/apt/amd64.json @@ -59,7 +59,7 @@ "distribution": "Bookworm", "architecture": "amd64", "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6", + "package-depends": "libpython3.11, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg0, libcec6", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Debian 12.x (Bookworm) (amd64)" } diff --git a/.github/workflows/apt/arm64.json b/.github/workflows/apt/arm64.json index 5f64a3ab..5f0f233e 100644 --- a/.github/workflows/apt/arm64.json +++ b/.github/workflows/apt/arm64.json @@ -51,7 +51,7 @@ "distribution": "Bookworm", "architecture": "arm64", "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6", + "package-depends": "libpython3.11, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg0, libcec6", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Debian 12.x (Bookworm) (arm64)", "exclude" : true diff --git a/.github/workflows/apt/armhf.json b/.github/workflows/apt/armhf.json index 33847459..66842969 100644 --- a/.github/workflows/apt/armhf.json +++ b/.github/workflows/apt/armhf.json @@ -59,7 +59,7 @@ "distribution": "Bookworm", "architecture": "armhf", "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6", + "package-depends": "libpython3.11, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg0, libcec6", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Debian 12.x (Bookworm) (armhf)", "exclude" : true diff --git a/CHANGELOG.md b/CHANGELOG.md index ce4dead4..996ca847 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed missing Include limits in QJsonSchemaChecker +- Fixed dependencies for deb packages in Debian Bookworm ## Removed From 64642a44578fe48dce8144c23180d29c160c7906 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Mon, 10 Jul 2023 20:39:35 +0200 Subject: [PATCH 011/117] Fix WLED UI handling non supported segment streaming (#1610) --- assets/webconfig/js/content_leds.js | 7 +++---- libsrc/leddevice/dev_net/LedDeviceWled.cpp | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 5cff7fd3..2a95af81 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -2331,6 +2331,7 @@ function updateElementsWled(ledType, key) { var enumSegSelectVals = []; var enumSegSelectTitleVals = []; var enumSegSelectDefaultVal = ""; + var defaultSegmentId = "-1"; if (devicesProperties[ledType] && devicesProperties[ledType][key]) { var ledDeviceProperties = devicesProperties[ledType][key]; @@ -2338,9 +2339,8 @@ function updateElementsWled(ledType, key) { if (!jQuery.isEmptyObject(ledDeviceProperties)) { if (ledDeviceProperties.info) { - if (ledDeviceProperties.info.liveseg && ledDeviceProperties.info.liveseg < 0) { + if (!ledDeviceProperties.info.hasOwnProperty("liveseg") || ledDeviceProperties.info.liveseg < 0) { // "Use main segment only" is disabled - var defaultSegmentId = "-1"; enumSegSelectVals.push(defaultSegmentId); enumSegSelectTitleVals.push($.i18n('edt_dev_spec_segments_disabled_title')); enumSegSelectDefaultVal = defaultSegmentId; @@ -2392,13 +2392,12 @@ function updateElementsWled(ledType, key) { hardwareLedCount = 1; } - if (segmentConfig) { + if (segmentConfig && segmentConfig.streamSegmentId > defaultSegmentId) { var configuredstreamSegmentId = window.serverConfig.device.segments.streamSegmentId.toString(); enumSegSelectVals = [configuredstreamSegmentId]; enumSegSelectTitleVals = ["Segment " + configuredstreamSegmentId]; enumSegSelectDefaultVal = configuredstreamSegmentId; } else { - defaultSegmentId = "-1"; enumSegSelectVals.push(defaultSegmentId); enumSegSelectTitleVals.push($.i18n('edt_dev_spec_segments_disabled_title')); enumSegSelectDefaultVal = defaultSegmentId; diff --git a/libsrc/leddevice/dev_net/LedDeviceWled.cpp b/libsrc/leddevice/dev_net/LedDeviceWled.cpp index 3aa4662f..3ada5091 100644 --- a/libsrc/leddevice/dev_net/LedDeviceWled.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceWled.cpp @@ -301,7 +301,7 @@ bool LedDeviceWled::isReadyForSegmentStreaming(semver::version& version) const } else { - Error(_log, "Version provided to test for streaming readiness is not valid "); + Error(_log, "Version provided to test for segment streaming readiness is not valid "); } return isReady; } @@ -325,7 +325,7 @@ bool LedDeviceWled::isReadyForDDPStreaming(semver::version& version) const } else { - Error(_log, "Version provided to test for streaming readiness is not valid "); + Error(_log, "Version provided to test for DDP streaming readiness is not valid "); } return isReady; } From 42c98da47046de9f0ff44f7990c6a4103d12385e Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Fri, 21 Jul 2023 16:45:37 +0200 Subject: [PATCH 012/117] Update platforms (#1617) * Remove stretch, bionic, add lunar * Fix CEC CMakeList for Ubuntu 23.04 * Fix git version identification when run in docker and local code * Update SYSTEM_LIBS_SKIP list * Updates after Ubuntu Server 20.04, latest PI OS Light and CoreElec 20 * Update year * Skip List working with Fedora38 Server (x86) and libreElec 11 (x86) * Update platform tag handling * Show error, if ssl lib cannot be found * Update supported platforms * Script to install selected Pull Requests * Fix misspelled explanation + improve description * Correct run-id evaluation * Support python3 and python2 * Support to copy existing config for PR testing --- .github/workflows/apt/amd64.json | 16 +- .github/workflows/apt/arm64.json | 8 - .github/workflows/apt/armhf.json | 16 -- .github/workflows/pull-request.yml | 10 +- .github/workflows/push-master.yml | 10 +- CMakeLists.txt | 21 +- Installation.md | 6 +- assets/webconfig/i18n/en.json | 4 +- bin/scripts/docker-compile.sh | 5 +- bin/scripts/install_pr.sh | 239 +++++++++++++++++++ cmake/Dependencies.cmake | 23 +- cmake/FindGitVersion.cmake | 2 +- debian/distributions | 13 +- doc/development/CompileHowto.md | 16 +- doc/development/SupportedPlatforms.md | 25 +- libsrc/cec/CMakeLists.txt | 1 + libsrc/hyperion/schema/schema-webConfig.json | 2 +- src/hyperiond/CMakeLists.txt | 2 +- 18 files changed, 310 insertions(+), 109 deletions(-) create mode 100755 bin/scripts/install_pr.sh diff --git a/.github/workflows/apt/amd64.json b/.github/workflows/apt/amd64.json index 7c794117..ecfa56c3 100644 --- a/.github/workflows/apt/amd64.json +++ b/.github/workflows/apt/amd64.json @@ -1,12 +1,4 @@ [ - { - "distribution": "Bionic", - "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev", - "package-depends": "libpython3.6, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libasound2, libturbojpeg, libcec4", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Ubuntu 18.04 (Bionic Beaver) (amd64)" - }, { "distribution": "Focal", "architecture": "amd64", @@ -32,12 +24,12 @@ "description": "Ubuntu 22.10 (Kinetic Kudu) (amd64)" }, { - "distribution": "Stretch", + "distribution": "Lunar", "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev", - "package-depends": "libpython3.5, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libasound2, libturbojpeg0, libcec4", + "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", + "package-depends": "libpython3.11, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Debian 9.x (Stretch) (amd64)" + "description": "Ubuntu 23.04 (Lunar Lobster) (amd64)" }, { "distribution": "Buster", diff --git a/.github/workflows/apt/arm64.json b/.github/workflows/apt/arm64.json index 5f0f233e..8cf6325e 100644 --- a/.github/workflows/apt/arm64.json +++ b/.github/workflows/apt/arm64.json @@ -1,12 +1,4 @@ [ - { - "distribution": "Bionic", - "architecture": "arm64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev", - "package-depends": "libpython3.6, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libasound2, libturbojpeg, libcec4", - "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Ubuntu 18.04 (Bionic Beaver) (arm64)" - }, { "distribution": "Focal", "architecture": "arm64", diff --git a/.github/workflows/apt/armhf.json b/.github/workflows/apt/armhf.json index 66842969..ed4b9b4d 100644 --- a/.github/workflows/apt/armhf.json +++ b/.github/workflows/apt/armhf.json @@ -1,12 +1,4 @@ [ - { - "distribution": "Bionic", - "architecture": "armhf", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl1.0-dev, libmbedtls-dev", - "package-depends": "libpython3.6, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libasound2, libturbojpeg, libcec4", - "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Ubuntu 18.04 (Bionic Beaver) (armhf)" - }, { "distribution": "Focal", "architecture": "armhf", @@ -31,14 +23,6 @@ "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", "description": "Ubuntu 22.10 (Kinetic Kudu) (armhf)" }, - { - "distribution": "Stretch", - "architecture": "armhf", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl1.0-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.5, libusb-1.0-0, libqt5widgets5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls10, libasound2, libturbojpeg0, libcec4", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description":"Debian 9.x (Stretch) (armhf)" - }, { "distribution": "Buster", "architecture": "armhf", diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 4aab0779..b7cd0bd3 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -18,16 +18,16 @@ jobs: dockerImage: [ x86_64, armv6l, armv7l, aarch64 ] include: - dockerImage: x86_64 - dockerName: Debian Stretch (x86_64) + dockerName: Debian Buster (x86_64) platform: x11 - dockerImage: armv6l - dockerName: Debian Stretch (Raspberry Pi v1 & ZERO) + dockerName: Debian Buster (Raspberry Pi v1 & ZERO) platform: rpi - dockerImage: armv7l - dockerName: Debian Stretch (Raspberry Pi 2 & 3) + dockerName: Debian Buster (Raspberry Pi 2 & 3) platform: rpi - dockerImage: aarch64 - dockerName: Debian Stretch (Generic AARCH64) + dockerName: Debian Buster (Generic AARCH64) platform: amlogic steps: @@ -47,7 +47,7 @@ jobs: - name: Build packages env: DOCKER_IMAGE: ${{ matrix.dockerImage }} - DOCKER_TAG: stretch + DOCKER_TAG: buster DOCKER_NAME: ${{ matrix.dockerName }} PLATFORM: ${{ matrix.platform }} shell: bash diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml index dc290a5b..b5c575f3 100644 --- a/.github/workflows/push-master.yml +++ b/.github/workflows/push-master.yml @@ -20,16 +20,16 @@ jobs: dockerImage: [ x86_64, armv6l, armv7l, aarch64 ] include: - dockerImage: x86_64 - dockerName: Debian Stretch (x86_64) + dockerName: Debian Buster (x86_64) platform: x11 - dockerImage: armv6l - dockerName: Debian Stretch (Raspberry Pi v1 & ZERO) + dockerName: Debian Buster (Raspberry Pi v1 & ZERO) platform: rpi - dockerImage: armv7l - dockerName: Debian Stretch (Raspberry Pi 2 & 3) + dockerName: Debian Buster (Raspberry Pi 2 & 3) platform: rpi - dockerImage: aarch64 - dockerName: Debian Stretch (Generic AARCH64) + dockerName: Debian Buster (Generic AARCH64) platform: amlogic steps: @@ -41,7 +41,7 @@ jobs: - name: Build packages env: DOCKER_IMAGE: ${{ matrix.dockerImage }} - DOCKER_TAG: stretch + DOCKER_TAG: buster DOCKER_NAME: ${{ matrix.dockerName }} PLATFORM: ${{ matrix.platform }} shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index e0c45882..cbb1ef3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,19 +180,18 @@ if ( "${PLATFORM}" MATCHES "osx" ) elseif ( "${PLATFORM}" MATCHES "rpi" ) SET ( DEFAULT_DISPMANX ON ) SET ( DEFAULT_DEV_WS281XPWM ON ) -elseif ( "${PLATFORM}" STREQUAL "amlogic" ) +elseif ( "${PLATFORM}" MATCHES "^amlogic" ) SET ( DEFAULT_AMLOGIC ON ) -elseif ( "${PLATFORM}" STREQUAL "amlogic-dev" ) - SET ( DEFAULT_AMLOGIC ON ) - SET ( DEFAULT_DISPMANX OFF ) - SET ( DEFAULT_QT OFF ) - SET ( DEFAULT_CEC OFF ) -elseif ( "${PLATFORM}" STREQUAL "amlogic64" ) - SET ( DEFAULT_AMLOGIC ON ) -elseif ( "${PLATFORM}" MATCHES "x11" ) + if ( "${PLATFORM}" MATCHES "-dev$" ) + SET ( DEFAULT_AMLOGIC ON ) + SET ( DEFAULT_DISPMANX OFF ) + SET ( DEFAULT_QT OFF ) + SET ( DEFAULT_CEC OFF ) + endif() +elseif ( "${PLATFORM}" MATCHES "^x11" ) SET ( DEFAULT_X11 ON ) SET ( DEFAULT_XCB ON ) - if ( "${PLATFORM}" STREQUAL "x11-dev" ) + if ( "${PLATFORM}" MATCHES "-dev$" ) SET ( DEFAULT_AMLOGIC ON) SET ( DEFAULT_DEV_WS281XPWM ON ) endif() @@ -201,7 +200,7 @@ elseif ( "${PLATFORM}" STREQUAL "imx6" ) endif() # enable tests for -dev builds -if ( "${PLATFORM}" MATCHES "-dev" ) +if ( "${PLATFORM}" MATCHES "-dev$" ) SET ( DEFAULT_TESTS ON ) endif() diff --git a/Installation.md b/Installation.md index 9f2fa84a..f55c1cdf 100644 --- a/Installation.md +++ b/Installation.md @@ -7,9 +7,9 @@ For Windows and macOS is an installation file available on our [Release page](ht ## Linux: On the following operating systems, Hyperion can currently be installed/updated using the method listed below: -- Raspbian Stretch/Raspberry Pi OS and later (armhf/arm64) -- Debian Stretch (9) and later (armhf/arm64/x86_64) -- Ubuntu 18.04 and later (armhf/arm64/x86_64) +- Raspbian Buster/Raspberry Pi OS and later (armhf/arm64) +- Debian Buster(10) and later (armhf/arm64/x86_64) +- Ubuntu 20.04 and later (armhf/arm64/x86_64) *** diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 6845685d..263c8bd6 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -560,7 +560,9 @@ "edt_conf_webc_keyPassPhrase_title": "Key password", "edt_conf_webc_keyPath_expl": "Path to the key file (format PEM, encrypted with RSA)", "edt_conf_webc_keyPath_title": "Private key path", - "edt_conf_webc_sslport_expl": "Port oft the HTTPS-Webserver", + "edt_conf_webc_port_expl": "Port for the WebServer, RPC and WebSocket HTTP connections", + "edt_conf_webc_port_title": "HTTP Port", + "edt_conf_webc_sslport_expl": "Port for the WebServer, RPC and WebSocket HTTPS connections", "edt_conf_webc_sslport_title": "HTTPS Port", "edt_dev_auth_key_title": "Authentication Token", "edt_dev_auth_key_title_info": "Authentication Token required to acccess the device", diff --git a/bin/scripts/docker-compile.sh b/bin/scripts/docker-compile.sh index a6abd11d..ce298b96 100755 --- a/bin/scripts/docker-compile.sh +++ b/bin/scripts/docker-compile.sh @@ -62,8 +62,7 @@ function printHelp { echo "######################################################## ## A script to compile Hyperion inside a docker container ## Requires installed Docker: https://www.docker.com/ -## Without arguments it will compile Hyperion for Debian Buster (x86_64) and uses Hyperion code from GitHub repository. -## Supports Raspberry Pi (armv6l, armv7l) cross compilation (Debian Stretch/Buster) and native compilation (Raspbian Stretch/Buster) +## Without arguments it will compile Hyperion for Debian Bullseye (x86_64) and uses Hyperion code from GitHub repository. ## For all images and tags currently available, see https://github.com/orgs/hyperion-project/packages ## ## Homepage: https://www.hyperion-project.org @@ -73,7 +72,7 @@ echo "######################################################## # # docker-compile.sh -h, --help # Show this help message # docker-compile.sh -i, --image # The docker image, e.g., x86_64, armv6l, armv7l, aarch64 -# docker-compile.sh -t, --tag # The docker tag, e.g., stretch, buster, bullseye, bookworm +# docker-compile.sh -t, --tag # The docker tag, e.g., buster, bullseye, bookworm # docker-compile.sh -b, --type # Release or Debug build # docker-compile.sh -p, --packages # If true, build packages with CPack # docker-compile.sh -l, --local # Run build using local code files diff --git a/bin/scripts/install_pr.sh b/bin/scripts/install_pr.sh new file mode 100755 index 00000000..ce029a08 --- /dev/null +++ b/bin/scripts/install_pr.sh @@ -0,0 +1,239 @@ +#!/bin/bash +# Script for downloading a specific open Pull Request Artifact from Hyperion.NG + +# Fixed variables +api_url="https://api.github.com/repos/hyperion-project/hyperion.ng" +type wget > /dev/null 2> /dev/null +hasWget=$? +type curl > /dev/null 2> /dev/null +hasCurl=$? +type python3 > /dev/null 2> /dev/null +hasPython3=$? +type python > /dev/null 2> /dev/null +hasPython2=$? + +if [[ "${hasWget}" -ne 0 ]] && [[ "${hasCurl}" -ne 0 ]]; then + echo '---> Critical Error: wget or curl required to download pull request artifacts' + exit 1 +fi + +if [[ "${hasPython3}" -eq 0 ]]; then + pythonCmd="python3" +else + if [[ "${hasPython2}" -eq 0 ]]; then + pythonCmd="python" + else + echo '---> Critical Error: python3 or python2 required to download pull request artifacts' + fi + exit 1 +fi + +function request_call() { + if [ $hasWget -eq 0 ]; then + echo $(wget --quiet --header="Authorization: token ${PR_TOKEN}" -O - $1) + elif [ $hasCurl -eq 0 ]; then + echo $(curl -skH "Authorization: token ${PR_TOKEN}" $1) + fi +} + +while getopts ":c:t:m:r:" opt; do + case "$opt" in + t) PR_TOKEN=$OPTARG ;; + r) run_id=$OPTARG ;; + m) ARCHITECTURE=$OPTARG ;; + c) CONFIGDIR=$OPTARG ;; + esac +done +shift $(( OPTIND - 1 )) + +# Check for a command line argument (PR number) +if [ "$1" == "" ] || [ $# -gt 1 ] || [ -z ${PR_TOKEN} ]; then + echo "Usage: $0 -t -m -r -c " >&2 + exit 1 +else + pr_number="$1" +fi + +# Set welcome message +echo '*******************************************************************************' +echo 'This script will download a specific open Pull Request Artifact from Hyperion.NG' +echo 'Created by hyperion-project.org - the official Hyperion source.' +echo '*******************************************************************************' + +# Determine the architecture, if not given +if [[ -z ${ARCHITECTURE} ]]; then + ARCHITECTURE=`uname -m` +fi + +#Test if multiarchitecture setup, i.e. user-space is 32bit +if [ ${ARCHITECTURE} == "aarch64" ]; then + USER_ARCHITECTURE=$ARCHITECTURE + IS_V7L=`cat /proc/$$/maps |grep -m1 -c v7l` + if [ $IS_V7L -ne 0 ]; then + USER_ARCHITECTURE="armv7l" + else + IS_V6L=`cat /proc/$$/maps |grep -m1 -c v6l` + if [ $IS_V6L -ne 0 ]; then + USER_ARCHITECTURE="armv6l" + fi + fi + if [ $ARCHITECTURE != $USER_ARCHITECTURE ]; then + echo "---> Identified kernel target architecture: $ARCHITECTURE" + echo "---> Identified user space target architecture: $USER_ARCHITECTURE" + ARCHITECTURE=$USER_ARCHITECTURE + fi +fi + +echo 'armv6l armv7l aarch64 x86_64' | grep -qw ${ARCHITECTURE} +if [ $? -ne 0 ]; then + echo "---> Critical Error: Target architecture $ARCHITECTURE is unknown -> abort" + exit 1 +else + echo "---> Download Pull Request for identified runtime architecture: $ARCHITECTURE" +fi + +# Determine if PR number exists +pulls=$(request_call "$api_url/pulls") + +pr_exists=$(echo "$pulls" | tr '\r\n' ' ' | ${pythonCmd} -c """ +import json,sys +data = json.load(sys.stdin) + +for i in data: + if i['number'] == "$pr_number": + print('exists') + break +""" 2>/dev/null) + +if [ "$pr_exists" != "exists" ]; then + echo "---> Pull Request $pr_number not found -> abort" + exit 1 +fi + +# Get head_sha value from 'pr_number' +head_sha=$(echo "$pulls" | tr '\r\n' ' ' | ${pythonCmd} -c """ +import json,sys +data = json.load(sys.stdin) + +for i in data: + if i['number'] == "$pr_number": + print(i['head']['sha']) + break +""" 2>/dev/null) + +if [ -z "$head_sha" ]; then + echo "---> The specified PR #$pr_number has no longer any artifacts." + echo "---> It may be older than 14 days. Ask the PR creator to recreate the artifacts at the following URL:" + echo "---> https://github.com/hyperion-project/hyperion.ng/pull/$pr_number" + exit 1 +fi + +if [ -z "$run_id" ]; then +# Determine run_id from head_sha +runs=$(request_call "$api_url/actions/runs") +run_id=$(echo "$runs" | tr '\r\n' ' ' | ${pythonCmd} -c """ +import json,sys +data = json.load(sys.stdin) + +for i in data['workflow_runs']: + if i['head_sha'] == '"$head_sha"': + print(i['id']) + break +""" 2>/dev/null) +fi + +if [ -z "$run_id" ]; then + echo "---> The specified PR #$pr_number has no longer any artifacts." + echo "---> It may be older than 14 days. Ask the PR creator to recreate the artifacts at the following URL:" + echo "---> https://github.com/hyperion-project/hyperion.ng/pull/$pr_number" + exit 1 +fi + +# Get archive_download_url from workflow +artifacts=$(request_call "$api_url/actions/runs/$run_id/artifacts") +archive_download_url=$(echo "$artifacts" | tr '\r\n' ' ' | ${pythonCmd} -c """ +import json,sys +data = json.load(sys.stdin) + +for i in data['artifacts']: + if i['name'] == '"$ARCHITECTURE"': + print(i['archive_download_url']) + break +""" 2>/dev/null) + +if [ -z "$archive_download_url" ]; then + echo "---> The specified PR #$pr_number has no longer any artifacts." + echo "---> It may be older than 14 days. Ask the PR creator to recreate the artifacts at the following URL:" + echo "---> https://github.com/hyperion-project/hyperion.ng/pull/$pr_number" + exit 1 +fi + +# Download packed PR artifact +echo "---> Downloading the Pull Request #$pr_number" +if [ $hasCurl -eq 0 ]; then + curl -skH "Authorization: token ${PR_TOKEN}" -o $HOME/temp.zip -L --get $archive_download_url +elif [ $hasWget -eq 0 ]; then + echo "wget" + wget --quiet --header="Authorization: token ${PR_TOKEN}" -O $HOME/temp.zip $archive_download_url +fi + +# Create new folder & extract PR artifact +echo "---> Extracting packed Artifact" +mkdir -p $HOME/hyperion_pr$pr_number +unzip -p $HOME/temp.zip | tar --strip-components=2 -C $HOME/hyperion_pr$pr_number share/hyperion/ -xz + +# Delete PR artifact +echo '---> Remove temporary files' +rm $HOME/temp.zip 2>/dev/null + +# Create the startup script +echo '---> Create startup script' +STARTUP_SCRIPT="#!/bin/bash -e + +# Stop hyperion service, if it is running +"'CURRENT_SERVICE=$(systemctl --type service | { grep -o "hyperion.*\.service" || true; }) +if [[ ! -z ${CURRENT_SERVICE} ]]; then + echo "---> Stop current service: ${CURRENT_SERVICE}" + + STOPCMD="systemctl stop --quiet ${CURRENT_SERVICE} --now" + USERNAME=${SUDO_USER:-$(whoami)} + if [ ${USERNAME} != "root" ]; then + STOPCMD="sudo ${STOPCMD}" + fi + + ${STOPCMD} >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "---> Critical Error: Failed to stop service: ${CURRENT_SERVICE}, Hyperion may not be started. Stop Hyperion manually." + else + echo "---> Service ${CURRENT_SERVICE} successfully stopped, Hyperion will be started" + fi +fi'"" + +TARGET_CONFIGDIR="$HOME/hyperion_pr$pr_number/config" + +if [[ ! -z ${CONFIGDIR} ]]; then +STARTUP_SCRIPT+=" +# Copy existing configuration file +"'echo "Copy existing configuration from "'${CONFIGDIR}" +mkdir -p "$TARGET_CONFIGDIR" +cp -ri "${CONFIGDIR}/*" "$TARGET_CONFIGDIR"" +fi + +STARTUP_SCRIPT+=" +# Start PR artifact +cd $HOME/hyperion_pr$pr_number +./bin/hyperiond -d -u $TARGET_CONFIGDIR" + +# Place startup script +echo "$STARTUP_SCRIPT" > $HOME/hyperion_pr$pr_number/$pr_number.sh + +# Set the executen bit +chmod +x -R $HOME/hyperion_pr$pr_number/$pr_number.sh + +echo "*******************************************************************************" +echo "Download finished!" +$REBOOTMESSAGE +echo "You can test the pull request with this command: ~/hyperion_pr$pr_number/$pr_number.sh" +echo "Remove the test installation with: rm -R ~/hyperion_pr$pr_number" +echo "Feedback is welcome at https://github.com/hyperion-project/hyperion.ng/pull/$pr_number" +echo "*******************************************************************************" diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index a727b0ad..08120831 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -132,28 +132,33 @@ macro(DeployLinux TARGET) include(GetPrerequisites) set(SYSTEM_LIBS_SKIP + "libatomic" "libc" + "libdbus" "libdl" "libexpat" "libfontconfig" - "libfreetype" "libgcc_s" "libgcrypt" - "libGL" - "libGLdispatch" + "libglib" "libglib-2" - "libGLX" "libgpg-error" + "liblz4" + "liblzma" "libm" + "libpcre" + "libpcre2" "libpthread" "librt" "libstdc++" + "libsystemd" "libudev" + "libusb" "libusb-1" "libutil" - "libX11" + "libuuid" "libz" - ) + ) if (ENABLE_DISPMANX) list(APPEND SYSTEM_LIBS_SKIP "libcec") @@ -161,7 +166,9 @@ macro(DeployLinux TARGET) # Extract dependencies ignoring the system ones get_prerequisites(${TARGET_FILE} DEPENDENCIES 0 1 "" "") - + + message(STATUS "Dependencies for target file: ${DEPENDENCIES}") + # Append symlink and non-symlink dependencies to the list set(PREREQUISITE_LIBS "") foreach(DEPENDENCY ${DEPENDENCIES}) @@ -203,6 +210,8 @@ macro(DeployLinux TARGET) get_filename_component(file_canonical ${openssl_lib} REALPATH) gp_append_unique(PREREQUISITE_LIBS ${file_canonical}) endforeach() + else() + message( WARNING "OpenSSL NOT found (https webserver will not work)") endif(OPENSSL_FOUND) # Detect the Qt plugin directory, source: https://github.com/lxde/lxqt-qtplugin/blob/master/src/CMakeLists.txt diff --git a/cmake/FindGitVersion.cmake b/cmake/FindGitVersion.cmake index d0bcf494..4e8db794 100644 --- a/cmake/FindGitVersion.cmake +++ b/cmake/FindGitVersion.cmake @@ -1,4 +1,4 @@ - +execute_process( COMMAND git config --global --add safe.directory ${CMAKE_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} ERROR_QUIET ) execute_process( COMMAND git log -1 --format=%cn-%t/%h-%ct WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE BUILD_ID ERROR_QUIET ) execute_process( COMMAND sh -c "git branch | grep '^*' | sed 's;^*;;g' " WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE VERSION_ID ERROR_QUIET ) execute_process( COMMAND sh -c "git remote --verbose | grep origin | grep fetch | cut -f2 | cut -d' ' -f1" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_REMOTE_PATH ERROR_QUIET ) diff --git a/debian/distributions b/debian/distributions index 214d10e2..12da7848 100644 --- a/debian/distributions +++ b/debian/distributions @@ -1,11 +1,3 @@ -Origin: Hyperion-Project -Label: apt.hyperion-project.org -Codename: bionic -Architectures: amd64 armhf arm64 -Components: main -Description: Official APT Repository by Hyperion Project -SignWith: yes - Origin: Hyperion-Project Label: apt.hyperion-project.org Codename: focal @@ -32,9 +24,8 @@ SignWith: yes Origin: Hyperion-Project Label: apt.hyperion-project.org -Suite: oldoldstable -Codename: stretch -Architectures: armhf amd64 +Codename: lunar +Architectures: amd64 Components: main Description: Official APT Repository by Hyperion Project SignWith: yes diff --git a/doc/development/CompileHowto.md b/doc/development/CompileHowto.md index 8a2cc7df..f50c1878 100644 --- a/doc/development/CompileHowto.md +++ b/doc/development/CompileHowto.md @@ -1,16 +1,12 @@ # With Docker -If you are using [Docker](https://www.docker.com/), you can compile Hyperion inside a docker container. This keeps your system clean and with a simple script it's easy to use. Supported is also cross compiling for Raspberry Pi (Debian Stretch or higher). To compile Hyperion just execute one of the following commands. +If you are using [Docker](https://www.docker.com/), you can compile Hyperion inside a docker container. This keeps your system clean and with a simple script it's easy to use. Supported is also cross compiling for Raspberry Pi (Debian Buster or higher). To compile Hyperion just execute one of the following commands. The compiled binaries and packages will be available at the deploy folder next to the script.
Note: call the script with `./docker-compile.sh -h` for more options. ## Cross compilation on x86_64 for: -**x86_64 (Debian Stretch):** -```console -wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -i x86_64 -t stretch -``` **x86_64 (Debian Buster):** ```console wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -i x86_64 -t buster @@ -23,10 +19,6 @@ wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/ ```console wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -i x86_64 -t bookworm ``` -**Raspberry Pi v1 & ZERO (Debian Stretch)** -```console -wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -i armv6l -t stretch -``` **Raspberry Pi v1 & ZERO (Debian Buster)** ```console wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -i armv6l -t buster @@ -39,10 +31,6 @@ wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/ ```console wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -i armv6l -t bookworm ``` -**Raspberry Pi 2/3/4 (Debian Stretch)** -```console -wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -i armv7l -t stretch -``` **Raspberry Pi 2/3/4 (Debian Buster)** ```console wget -qN https://raw.github.com/hyperion-project/hyperion.ng/master/bin/scripts/docker-compile.sh && chmod +x *.sh && ./docker-compile.sh -i armv7l -t buster @@ -78,7 +66,7 @@ sudo apt-get install git cmake build-essential qtbase5-dev libqt5serialport5-dev ```console sudo apt-get update -sudo apt-get install git cmake build-essential qt6-base-dev libqt6serialport6-dev libvulkan-dev libgl1-mesa-dev libusb-1.0-0-dev python3-dev 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 ``` **For Linux X11/XCB grabber support** diff --git a/doc/development/SupportedPlatforms.md b/doc/development/SupportedPlatforms.md index 2e170c57..39e812c4 100644 --- a/doc/development/SupportedPlatforms.md +++ b/doc/development/SupportedPlatforms.md @@ -7,12 +7,12 @@ In case of problems, it is recommended checking with the wider Hyperion communit | Hardware | OS | Version | Screen-Grabber | Package | Comments | |-----------|-----------------|--------------------|-----------------------------------------|-------------------------------------------------------------------------------|------------------------------------| | X64 | Windows | 10 | QT¹ | [Windows-AMD64.exe](https://github.com/hyperion-project/hyperion.ng/releases) | Direct X9 Grabber via self-compile | -| X64 | Ubuntu | 18.04, 20.04, 22.04² | QT¹
XCB/X11¹ | [Linux-x86_64.deb](https://github.com/hyperion-project/hyperion.ng/releases) | | -| X64 | Debian | 9, 10, 11, 12³| QT¹
XCB/X11¹ | [Linux-x86_64.deb](https://github.com/hyperion-project/hyperion.ng/releases) | | -| RPi 4 | HyperBian | 9, 10, 11, 12³| QT¹
XCB/X11¹
DispmanX | [HyperBian.zip](https://github.com/Hyperion-Project/HyperBian/releases) | | -| RPi 4 | Raspberry Pi OS | 9, 10, 11, 12³| QT¹
XCB/X11¹
DispmanX | [Linux-armv7l.deb](https://github.com/hyperion-project/hyperion.ng/releases) | | -| RPi 3 /3+ | HyperBian | 9, 10, 11, 12³| QT¹
XCB/X11¹
DispmanX | [HyperBian.zip](https://github.com/hyperion-project/hyperion.ng/releases) | | -| RPi 3 /3+ | Raspberry Pi OS | 9, 10, 11, 12³| QT¹
XCB/X11¹
DispmanX | [Linux-armv7l.deb](https://github.com/hyperion-project/hyperion.ng/releases) | | +| X64 | Ubuntu | 20.04, 22.04, 23.04² | QT¹
XCB/X11¹ | [Linux-x86_64.deb](https://github.com/hyperion-project/hyperion.ng/releases) | | +| X64 | Debian | 10, 11, 12³ | QT¹
XCB/X11¹ | [Linux-x86_64.deb](https://github.com/hyperion-project/hyperion.ng/releases) | | +| RPi 4 | HyperBian | 10, 11, 12³ | QT¹
XCB/X11¹
DispmanX | [HyperBian.zip](https://github.com/Hyperion-Project/HyperBian/releases) | | +| RPi 4 | Raspberry Pi OS | 10, 11, 12³ | QT¹
XCB/X11¹
DispmanX | [Linux-armv7l.deb](https://github.com/hyperion-project/hyperion.ng/releases) | | +| RPi 3 /3+ | HyperBian | 10, 11, 12³ | QT¹
XCB/X11¹
DispmanX | [HyperBian.zip](https://github.com/hyperion-project/hyperion.ng/releases) | | +| RPi 3 /3+ | Raspberry Pi OS | 10, 11, 12³ | QT¹
XCB/X11¹
DispmanX | [Linux-armv7l.deb](https://github.com/hyperion-project/hyperion.ng/releases) | | ## Unofficial In case you have an additional working setups you would like to share with the community, please get in touch or issue a PR to have the table updated. @@ -20,13 +20,18 @@ In case you have an additional working setups you would like to share with the c | Hardware | OS | Version | Screen-Grabber | Package | Comments | |---------------|-----------------|----------------|-----------------------------------------|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| | X64 | macOS | 11, 12 | QT
OSX | [macOS-x86_64.tar.gz](https://github.com/hyperion-project/hyperion.ng/releases) | M1 not tested | -| X64 | Fedora | 35 | QT¹
XCB/X11¹ | [Linux-x86_64.rpm](https://github.com/hyperion-project/hyperion.ng/releases) | | +| X64 | Fedora | 38 | QT¹
XCB/X11¹ | [Linux-x86_64.rpm](https://github.com/hyperion-project/hyperion.ng/releases) | | | X64 | Arch | | QT¹
XCB/X11¹ | [Linux-x86_64.rpm](https://github.com/hyperion-project/hyperion.ng/releases) | | -| RPi 0/ 1 / 2 | Raspberry Pi OS | 9, 10, 11, 12³| QT¹
XCB/X11¹
DispmanX | [Linux-armv6l.tar.gz](https://github.com/hyperion-project/hyperion.ng/releases) | No recommended | +| RPi 0/ 1 / 2 | Raspberry Pi OS | 10, 11, 12³| QT¹
XCB/X11¹
DispmanX | [Linux-armv6l.tar.gz](https://github.com/hyperion-project/hyperion.ng/releases) | No recommended | +| X64 | LibreElec | 11.x (Nexus) | [Kodi add-on](https://github.com/hyperion-project/hyperion.kodi/releases) | [Linux-x86_64.tar.gz](https://github.com/hyperion-project/hyperion.ng/releases) | [Install on LibreELEC](https://hyperion-project.org/forum/index.php?thread/10463-install-hyperion-ng-on-libreelec-x86-64-rpi-inoffiziell-unofficially/) | +| RPi 4 | LibreElec | 11.x (Nexus) | - | [Linux-armv7l.tar.gz](https://github.com/hyperion-project/hyperion.ng/releases) | [Install on LibreELEC](https://hyperion-project.org/forum/index.php?thread/10463-install-hyperion-ng-on-libreelec-x86-64-rpi-inoffiziell-unofficially/) | | RPi 4 | LibreElec | 10.x (Matrix) | - | [Linux-armv7l.tar.gz](https://github.com/hyperion-project/hyperion.ng/releases) | [Install on LibreELEC](https://hyperion-project.org/forum/index.php?thread/10463-install-hyperion-ng-on-libreelec-x86-64-rpi-inoffiziell-unofficially/) | | RPi 4 | LibreElec | 9.2.x (Leia) | QT¹
DispmanX | [Linux-armv7l.tar.gz](https://github.com/hyperion-project/hyperion.ng/releases) | [Install on LibreELEC](https://hyperion-project.org/forum/index.php?thread/10463-install-hyperion-ng-on-libreelec-x86-64-rpi-inoffiziell-unofficially/) | +| RPi 3 /3+ | LibreElec | 11.x (Nexus) | - | [Linux-armv7l.tar.gz](https://github.com/hyperion-project/hyperion.ng/releases) | [Install on LibreELEC](https://hyperion-project.org/forum/index.php?thread/10463-install-hyperion-ng-on-libreelec-x86-64-rpi-inoffiziell-unofficially/) | | RPi 3 /3+ | LibreElec | 10.x (Matrix) | - | [Linux-armv7l.tar.gz](https://github.com/hyperion-project/hyperion.ng/releases) | [Install on LibreELEC](https://hyperion-project.org/forum/index.php?thread/10463-install-hyperion-ng-on-libreelec-x86-64-rpi-inoffiziell-unofficially/) | | RPi 3 /3+ | LibreElec | 9.2.x (Leia) | QT¹
DispmanX | [Linux-armv7l.tar.gz](https://github.com/hyperion-project/hyperion.ng/releases) | [Install on LibreELEC](https://hyperion-project.org/forum/index.php?thread/10463-install-hyperion-ng-on-libreelec-x86-64-rpi-inoffiziell-unofficially/) | +| Amlogic | CoreElec | 21.x (Omega) | Amlogic | CoreElec Plugin | Supported via CoreElec project | +| Amlogic | CoreElec | 20.x (Nexus) | Amlogic | CoreElec Plugin | Supported via CoreElec project | | Amlogic | CoreElec | 19.x (Matrix) | Amlogic | CoreElec Plugin | Supported via CoreElec project | | Amlogic | CoreElec | 9.2.x (Leia) | Amlogic | CoreElec Plugin | Supported via CoreElec project | | Vero4K | OSMC | | | | [hyperion-vero4k](https://github.com/hissingshark/hyperion-vero4k) | @@ -35,5 +40,5 @@ In case you have an additional working setups you would like to share with the c Legend --- ¹ Requires an environment with `DISPLAY` defined\ -² 18=Bionic Beaver, 20=Focal Fossa, 22=Jammy Jellyfish\ -³ 9=Stretch, 10=Buster, 11=Bullseye, 12=Bookworm +² 20=Focal Fossa, 22=Jammy Jellyfish, 23=Lunar Lobster\ +³ 10=Buster, 11=Bullseye, 12=Bookworm diff --git a/libsrc/cec/CMakeLists.txt b/libsrc/cec/CMakeLists.txt index 00a1497b..dfd80e7d 100644 --- a/libsrc/cec/CMakeLists.txt +++ b/libsrc/cec/CMakeLists.txt @@ -7,6 +7,7 @@ SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/cec) FILE (GLOB CEC_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp") add_library(cechandler ${CEC_SOURCES}) +list(GET CEC_LIBRARIES 0 CEC_LIBRARIES) add_definitions(-DCEC_LIBRARY="${CEC_LIBRARIES}") include_directories(${CEC_INCLUDE_DIRS}) diff --git a/libsrc/hyperion/schema/schema-webConfig.json b/libsrc/hyperion/schema/schema-webConfig.json index 17d9e60d..6101dc76 100644 --- a/libsrc/hyperion/schema/schema-webConfig.json +++ b/libsrc/hyperion/schema/schema-webConfig.json @@ -13,7 +13,7 @@ "port" : { "type" : "integer", - "title" : "edt_conf_general_port_title", + "title" : "edt_conf_webc_port_title", "minimum" : 80, "maximum" : 65535, "default" : 8090, diff --git a/src/hyperiond/CMakeLists.txt b/src/hyperiond/CMakeLists.txt index bc38505b..35326c22 100644 --- a/src/hyperiond/CMakeLists.txt +++ b/src/hyperiond/CMakeLists.txt @@ -160,7 +160,7 @@ if (APPLE) MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/cmake/osxbundle/Info.plist.in MACOSX_BUNDLE_BUNDLE_NAME "Hyperion" MACOSX_BUNDLE_BUNDLE_VERSION ${HYPERION_VERSION} - MACOSX_BUNDLE_COPYRIGHT "Copyright(c) 2014-2022 Hyperion Project" + MACOSX_BUNDLE_COPYRIGHT "Copyright(c) 2014-2023 Hyperion Project" MACOSX_BUNDLE_GUI_IDENTIFIER "com.hyperion-project.${PROJECT_NAME}" MACOSX_BUNDLE_ICON_FILE "Hyperion.icns" MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME} ${HYPERION_VERSION}" From 1257cfff70c50aadb8b26c45619a0f9e4a8c1652 Mon Sep 17 00:00:00 2001 From: Paulchen-Panther <16664240+Paulchen-Panther@users.noreply.github.com> Date: Fri, 21 Jul 2023 18:59:23 +0000 Subject: [PATCH 013/117] Set OpenSSL to 1.1.1 (Windows) --- .github/workflows/pull-request.yml | 5 +++-- .github/workflows/push-master.yml | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index b7cd0bd3..ef37014c 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -150,7 +150,7 @@ jobs: with: path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey key: ${{ runner.os }}-chocolatey - + # - name: Install Python # shell: powershell # run: | @@ -159,7 +159,8 @@ jobs: - name: Install OpenSSL, DirectX SDK shell: powershell run: | - choco install --no-progress openssl directx-sdk -y + choco install --no-progress openssl --version=1.1.1.2100 -y + choco install --no-progress directx-sdk -y - name: Install libjpeg-turbo run: | diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml index b5c575f3..1edd8594 100644 --- a/.github/workflows/push-master.yml +++ b/.github/workflows/push-master.yml @@ -124,7 +124,8 @@ jobs: - name: Install OpenSSL, DirectX SDK shell: powershell run: | - choco install --no-progress openssl directx-sdk -y + choco install --no-progress openssl --version=1.1.1.2100 -y + choco install --no-progress directx-sdk -y - name: Install libjpeg-turbo run: | From da275dd44861e1c2ce3501ffb6eba7300e1446ee Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sat, 29 Jul 2023 19:28:28 +0200 Subject: [PATCH 014/117] Update flatbuffers to v23.5.26 (#1624) --- dependencies/external/flatbuffers | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/external/flatbuffers b/dependencies/external/flatbuffers index 8468eab8..0100f6a5 160000 --- a/dependencies/external/flatbuffers +++ b/dependencies/external/flatbuffers @@ -1 +1 @@ -Subproject commit 8468eab83bacc8bbd6cb5ae22197af06a9437b2d +Subproject commit 0100f6a5779831fa7a651e4b67ef389a8752bd9b From 5bf25c98ada6cb41a5c0ab4c9cc2c7b8ff73bbbe Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sat, 29 Jul 2023 19:28:39 +0200 Subject: [PATCH 015/117] Update mbedTLS to v3.4.0 (#1625) --- dependencies/external/mbedtls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/external/mbedtls b/dependencies/external/mbedtls index 8c892249..1873d3bf 160000 --- a/dependencies/external/mbedtls +++ b/dependencies/external/mbedtls @@ -1 +1 @@ -Subproject commit 8c89224991adff88d53cd380f42a2baa36f91454 +Subproject commit 1873d3bfc2da771672bd8e7e8f41f57e0af77f33 From c0dc08b0c07e22f76ea2c88f5e3e18078ebcdc97 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sat, 29 Jul 2023 19:28:51 +0200 Subject: [PATCH 016/117] Update to Protobuf 23.4.0 (#1626) * Update to protobuf v23.4.0 * Add defines for 3rd party sub-modules used by protobuf * Check out sub-modules recursively --- .github/workflows/nightly.yml | 2 +- .github/workflows/pull-request.yml | 6 +++--- .github/workflows/push-master.yml | 6 +++--- dependencies/CMakeLists.txt | 4 ++++ dependencies/external/protobuf | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 8a4da1e2..7151ce2f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -86,7 +86,7 @@ jobs: steps: - uses: actions/checkout@v3 with: - submodules: true + submodules: recursive - name: Generate environment variables run: | diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index ef37014c..47769bfb 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -34,7 +34,7 @@ jobs: - name: Checkout uses: actions/checkout@v3 with: - submodules: true + submodules: recursive # Append PR number to .version - name: Append PR number to version @@ -78,7 +78,7 @@ jobs: - name: Checkout uses: actions/checkout@v3 with: - submodules: true + submodules: recursive # Append PR number to .version - name: Append PR number to version @@ -127,7 +127,7 @@ jobs: - name: Checkout uses: actions/checkout@v3 with: - submodules: true + submodules: recursive # Append PR number to .version - name: Append PR number to version diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml index 1edd8594..8f1b44c3 100644 --- a/.github/workflows/push-master.yml +++ b/.github/workflows/push-master.yml @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@v3 with: - submodules: true + submodules: recursive # Build process - name: Build packages @@ -64,7 +64,7 @@ jobs: steps: - uses: actions/checkout@v3 with: - submodules: true + submodules: recursive # Install dependencies - name: Install dependencies @@ -99,7 +99,7 @@ jobs: - name: Checkout uses: actions/checkout@v3 with: - submodules: true + submodules: recursive - name: Install Qt uses: jurplel/install-qt-action@v3 diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index 858454ee..c13e96cc 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -160,6 +160,10 @@ if(ENABLE_PROTOBUF_SERVER) # define the protobuf library set(PROTOBUF_LIBRARIES protobuf::libprotobuf) + + # defines for 3rd party sub-modules + set(ABSL_PROPAGATE_CXX_STD ON CACHE BOOL "Build abseil-cpp with C++ version requirements propagated") + endif() # redefine at parent scope diff --git a/dependencies/external/protobuf b/dependencies/external/protobuf index f0dc78d7..2c5fa078 160000 --- a/dependencies/external/protobuf +++ b/dependencies/external/protobuf @@ -1 +1 @@ -Subproject commit f0dc78d7e6e331b8c6bb2d5283e06aa26883ca7c +Subproject commit 2c5fa078d8e86e5f4bd34e6f4c9ea9e8d7d4d44a From d1879c2e393c1e6882de9dd7f04349e4551a6579 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Thu, 3 Aug 2023 22:40:25 +0200 Subject: [PATCH 017/117] ws281x - Update logic to identify is user is admin (#1623) --- assets/webconfig/js/content_leds.js | 2 +- libsrc/leddevice/dev_rpi_pwm/LedDeviceWS281x.cpp | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 2a95af81..1aec94ab 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -2069,7 +2069,7 @@ var updateOutputSelectList = function (ledType, discoveryInfo) { case "devRPiPWM": key = ledType; - if (discoveryInfo.devices.length == 0) { + if (!discoveryInfo.isUserAdmin) { enumVals.push("NONE"); enumTitleVals.push($.i18n('edt_dev_spec_devices_discovered_none')); $('#btn_submit_controller').prop('disabled', true); diff --git a/libsrc/leddevice/dev_rpi_pwm/LedDeviceWS281x.cpp b/libsrc/leddevice/dev_rpi_pwm/LedDeviceWS281x.cpp index 2b67173c..af72304a 100644 --- a/libsrc/leddevice/dev_rpi_pwm/LedDeviceWS281x.cpp +++ b/libsrc/leddevice/dev_rpi_pwm/LedDeviceWS281x.cpp @@ -159,12 +159,10 @@ QJsonObject LedDeviceWS281x::discover(const QJsonObject& /*params*/) QJsonArray deviceList; - if (SysInfo::isUserAdmin()) - { - //Indicate the general availability of the device, if hyperion is run under root - deviceList << QJsonObject ({{"found",true}}); - devicesDiscovered.insert("devices", deviceList); - } + //Indicate the general availability of the device, if hyperion is run under root + devicesDiscovered.insert("isUserAdmin", SysInfo::isUserAdmin()); + + devicesDiscovered.insert("devices", deviceList); DebugIf(verbose,_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); From a5bb7e905ba153140c6563e88937e121ae3a88a9 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sat, 26 Aug 2023 11:12:43 +0200 Subject: [PATCH 018/117] Fix non image updates ignore blacklisted leds (#1635) * Fix #1634 * Refactor to process blacklisted LEDs with less resources --- include/hyperion/LedString.h | 48 ++++++++++++++++----- include/utils/hyperion.h | 38 ----------------- libsrc/hyperion/Hyperion.cpp | 18 ++++++-- libsrc/hyperion/LedString.cpp | 80 ++++++++++++++++++++++++++++++++++- test/TestImage2LedsMap.cpp | 2 +- 5 files changed, 132 insertions(+), 54 deletions(-) diff --git a/include/hyperion/LedString.h b/include/hyperion/LedString.h index 282b4429..6a5bfc52 100644 --- a/include/hyperion/LedString.h +++ b/include/hyperion/LedString.h @@ -10,6 +10,7 @@ // QT includes #include +#include // Forward class declarations namespace Json { class Value; } @@ -73,7 +74,7 @@ inline ColorOrder stringToColorOrder(const QString & order) } /// -/// The Led structure contains the definition of the image portion used to determine a single led's +/// The Led structure contains the definition of the image portion used to determine a single LED's /// color. /// @verbatim /// |--------------------image--| @@ -89,39 +90,66 @@ inline ColorOrder stringToColorOrder(const QString & order) /// struct Led { - /// The minimum vertical scan line included for this leds color + /// The minimum vertical scan line included for this LEDs color double minX_frac; - /// The maximum vertical scan line included for this leds color + /// The maximum vertical scan line included for this LEDs color double maxX_frac; - /// The minimum horizontal scan line included for this leds color + /// The minimum horizontal scan line included for this LEDs color double minY_frac; - /// The maximum horizontal scan line included for this leds color + /// The maximum horizontal scan line included for this LEDs color double maxY_frac; + /// A LEDs at {0,0,0,0} is not visible and therefore treated as blacklisted + bool isBlacklisted {false}; /// the color order ColorOrder colorOrder; }; /// -/// The LedString contains the image integration information of the leds +/// The LedString contains the image integration information of the LEDs /// class LedString { public: /// - /// Returns the led specifications + /// Returns the LED specifications /// /// @return The list with led specifications /// std::vector& leds(); /// - /// Returns the led specifications + /// Returns the LED specifications /// /// @return The list with led specifications /// const std::vector& leds() const; + /// + /// Returns the IDs of blacklisted LEDs + /// + /// @return ID List of blacklisted LEDs + /// + std::vector& blacklistedLedIds(); + + /// + /// Returns the IDs of blacklisted LEDs + /// + /// @return ID List of blacklisted LEDs + /// + const std::vector& blacklistedLedIds() const; + + /// + /// Check, if teh layout has blacklisted LEDs configured + /// + /// @return True, if blacklisted LEDs are configured + /// + bool hasBlackListedLeds (); + + static LedString createLedString(const QJsonArray& ledConfigArray, const ColorOrder deviceOrder); + private: - /// The list with led specifications - std::vector mLeds; + /// The list with LED specifications + std::vector _leds; + /// The list containing IDs of blacklisted LED + std::vector _blacklistedLedIds; }; diff --git a/include/utils/hyperion.h b/include/utils/hyperion.h index 774267a8..88ebc44e 100644 --- a/include/utils/hyperion.h +++ b/include/utils/hyperion.h @@ -177,44 +177,6 @@ namespace hyperion { return adjustment; } - /** - * Construct the 'led-string' with the integration area definition per led and the color - * ordering of the RGB channels - * @param ledsConfig The configuration of the led areas - * @param deviceOrder The default RGB channel ordering - * @return The constructed ledstring - */ - static LedString createLedString(const QJsonArray& ledConfigArray, const ColorOrder deviceOrder) - { - LedString ledString; - const QString deviceOrderStr = colorOrderToString(deviceOrder); - - for (signed i = 0; i < ledConfigArray.size(); ++i) - { - const QJsonObject& ledConfig = ledConfigArray[i].toObject(); - Led led; - - led.minX_frac = qMax(0.0, qMin(1.0, ledConfig["hmin"].toDouble())); - led.maxX_frac = qMax(0.0, qMin(1.0, ledConfig["hmax"].toDouble())); - led.minY_frac = qMax(0.0, qMin(1.0, ledConfig["vmin"].toDouble())); - led.maxY_frac = qMax(0.0, qMin(1.0, ledConfig["vmax"].toDouble())); - // Fix if the user swapped min and max - if (led.minX_frac > led.maxX_frac) - { - std::swap(led.minX_frac, led.maxX_frac); - } - if (led.minY_frac > led.maxY_frac) - { - std::swap(led.minY_frac, led.maxY_frac); - } - - // Get the order of the rgb channels for this led (default is device order) - led.colorOrder = stringToColorOrder(ledConfig["colorOrder"].toString(deviceOrderStr)); - ledString.leds().push_back(led); - } - return ledString; - } - static QSize getLedLayoutGridSize(const QJsonArray& ledConfigArray) { std::vector midPointsX; diff --git a/libsrc/hyperion/Hyperion.cpp b/libsrc/hyperion/Hyperion.cpp index 7107611e..c0441294 100644 --- a/libsrc/hyperion/Hyperion.cpp +++ b/libsrc/hyperion/Hyperion.cpp @@ -52,7 +52,7 @@ Hyperion::Hyperion(quint8 instance, bool readonlyMode) , _instIndex(instance) , _settingsManager(new SettingsManager(instance, this, readonlyMode)) , _componentRegister(nullptr) - , _ledString(hyperion::createLedString(getSetting(settings::LEDS).array(), hyperion::createColorOrder(getSetting(settings::DEVICE).object()))) + , _ledString(LedString::createLedString(getSetting(settings::LEDS).array(), hyperion::createColorOrder(getSetting(settings::DEVICE).object()))) , _imageProcessor(nullptr) , _muxer(nullptr) , _raw2ledAdjustment(hyperion::createLedColorsAdjustment(static_cast(_ledString.leds().size()), getSetting(settings::COLOR).object())) @@ -255,7 +255,7 @@ void Hyperion::handleSettingsUpdate(settings::type type, const QJsonDocument& co #endif // ledstring, img processor, muxer, ledGridSize (effect-engine image based effects), _ledBuffer and ByteOrder of ledstring - _ledString = hyperion::createLedString(leds, hyperion::createColorOrder(getSetting(settings::DEVICE).object())); + _ledString = LedString::createLedString(leds, hyperion::createColorOrder(getSetting(settings::DEVICE).object())); _imageProcessor->setLedString(_ledString); _muxer->updateLedColorsLength(static_cast(_ledString.leds().size())); _ledGridSize = hyperion::getLedLayoutGridSize(leds); @@ -291,7 +291,7 @@ void Hyperion::handleSettingsUpdate(settings::type type, const QJsonDocument& co // force ledString update, if device ByteOrder changed if(_ledDeviceWrapper->getColorOrder() != dev["colorOrder"].toString("rgb")) { - _ledString = hyperion::createLedString(getSetting(settings::LEDS).array(), hyperion::createColorOrder(dev)); + _ledString = LedString::createLedString(getSetting(settings::LEDS).array(), hyperion::createColorOrder(dev)); _imageProcessor->setLedString(_ledString); _ledStringColorOrder.clear(); @@ -671,6 +671,18 @@ void Hyperion::update() else { _ledBuffer = priorityInfo.ledColors; + + if (_ledString.hasBlackListedLeds()) + { + for (int id : _ledString.blacklistedLedIds()) + { + if (id > _ledBuffer.size()-1) + { + break; + } + _ledBuffer.at(id) = ColorRgb::BLACK; + } + } } // emit rawLedColors before transform diff --git a/libsrc/hyperion/LedString.cpp b/libsrc/hyperion/LedString.cpp index bf9f7528..172504c4 100644 --- a/libsrc/hyperion/LedString.cpp +++ b/libsrc/hyperion/LedString.cpp @@ -1,16 +1,92 @@ // STL includes + #include #include // hyperion includes #include +// QT includes +#include + std::vector& LedString::leds() { - return mLeds; + return _leds; } const std::vector& LedString::leds() const { - return mLeds; + return _leds; +} + +std::vector& LedString::blacklistedLedIds() +{ + return _blacklistedLedIds; +} + +const std::vector& LedString::blacklistedLedIds() const +{ + return _blacklistedLedIds; +} + +bool LedString::hasBlackListedLeds() +{ + + if (_blacklistedLedIds.size() > 0) + { + return true; + } + else + { + return false; + } +} + +/** + * Construct the 'led-string' with the integration area definition per led and the color + * ordering of the RGB channels + * @param ledsConfig The configuration of the led areas + * @param deviceOrder The default RGB channel ordering + * @return The constructed ledstring + */ +LedString LedString::createLedString(const QJsonArray& ledConfigArray, const ColorOrder deviceOrder) +{ + LedString ledString; + const QString deviceOrderStr = colorOrderToString(deviceOrder); + + for (signed i = 0; i < ledConfigArray.size(); ++i) + { + const QJsonObject& ledConfig = ledConfigArray[i].toObject(); + Led led; + + led.minX_frac = qMax(0.0, qMin(1.0, ledConfig["hmin"].toDouble())); + led.maxX_frac = qMax(0.0, qMin(1.0, ledConfig["hmax"].toDouble())); + led.minY_frac = qMax(0.0, qMin(1.0, ledConfig["vmin"].toDouble())); + led.maxY_frac = qMax(0.0, qMin(1.0, ledConfig["vmax"].toDouble())); + // Fix if the user swapped min and max + if (led.minX_frac > led.maxX_frac) + { + std::swap(led.minX_frac, led.maxX_frac); + } + if (led.minY_frac > led.maxY_frac) + { + std::swap(led.minY_frac, led.maxY_frac); + } + + // Get the order of the rgb channels for this led (default is device order) + led.colorOrder = stringToColorOrder(ledConfig["colorOrder"].toString(deviceOrderStr)); + + led.isBlacklisted = false; + if (led.minX_frac < std::numeric_limits::epsilon() && + led.maxX_frac < std::numeric_limits::epsilon() && + led.minY_frac < std::numeric_limits::epsilon() && + led.maxY_frac < std::numeric_limits::epsilon() + ) + { + led.isBlacklisted = true; + ledString.blacklistedLedIds().push_back(i); + } + ledString.leds().push_back(led); + } + return ledString; } diff --git a/test/TestImage2LedsMap.cpp b/test/TestImage2LedsMap.cpp index bb9aa618..c0f08430 100644 --- a/test/TestImage2LedsMap.cpp +++ b/test/TestImage2LedsMap.cpp @@ -24,7 +24,7 @@ int main() return -1; } - const LedString ledString = hyperion::createLedString(config["leds"].toArray(), hyperion::createColorOrder(config["device"].toObject())); + const LedString ledString = LedString::createLedString(config["leds"].toArray(), hyperion::createColorOrder(config["device"].toObject())); const ColorRgb testColor = {64, 123, 12}; From 7909997398ff4177f660e26db0c96677a199b69d Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Wed, 6 Sep 2023 20:08:20 +0200 Subject: [PATCH 019/117] Update install_pr.sh Only run named 'Hyperion PR Build' have artifacts --- bin/scripts/install_pr.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/scripts/install_pr.sh b/bin/scripts/install_pr.sh index ce029a08..8549f797 100755 --- a/bin/scripts/install_pr.sh +++ b/bin/scripts/install_pr.sh @@ -136,7 +136,7 @@ import json,sys data = json.load(sys.stdin) for i in data['workflow_runs']: - if i['head_sha'] == '"$head_sha"': + if i['head_sha'] == '"$head_sha"' and i['name'] == 'Hyperion PR Build': print(i['id']) break """ 2>/dev/null) From 48cea4ad9b1bf3ea40e2ac5c8465639f9f08565c Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sat, 30 Sep 2023 18:00:38 +0200 Subject: [PATCH 020/117] Focus on relevant PRs --- bin/scripts/install_pr.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/scripts/install_pr.sh b/bin/scripts/install_pr.sh index 8549f797..bab790a0 100755 --- a/bin/scripts/install_pr.sh +++ b/bin/scripts/install_pr.sh @@ -93,7 +93,7 @@ else fi # Determine if PR number exists -pulls=$(request_call "$api_url/pulls") +pulls=$(request_call "$api_url/pulls?state=open") pr_exists=$(echo "$pulls" | tr '\r\n' ' ' | ${pythonCmd} -c """ import json,sys @@ -106,7 +106,7 @@ for i in data: """ 2>/dev/null) if [ "$pr_exists" != "exists" ]; then - echo "---> Pull Request $pr_number not found -> abort" + echo "---> Pull Request $pr_number not found as open PR -> abort" exit 1 fi @@ -122,7 +122,7 @@ for i in data: """ 2>/dev/null) if [ -z "$head_sha" ]; then - echo "---> The specified PR #$pr_number has no longer any artifacts." + echo "---> The specified PR #$pr_number has no longer any artifacts or has been closed." echo "---> It may be older than 14 days. Ask the PR creator to recreate the artifacts at the following URL:" echo "---> https://github.com/hyperion-project/hyperion.ng/pull/$pr_number" exit 1 @@ -130,13 +130,13 @@ fi if [ -z "$run_id" ]; then # Determine run_id from head_sha -runs=$(request_call "$api_url/actions/runs") +runs=$(request_call "$api_url/actions/runs?head_sha=$head_sha") run_id=$(echo "$runs" | tr '\r\n' ' ' | ${pythonCmd} -c """ import json,sys data = json.load(sys.stdin) for i in data['workflow_runs']: - if i['head_sha'] == '"$head_sha"' and i['name'] == 'Hyperion PR Build': + if i['name'] == 'Hyperion PR Build': print(i['id']) break """ 2>/dev/null) From 08dc59c88561baa7dae422187060489c9b6b6f02 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sun, 1 Oct 2023 21:56:53 +0200 Subject: [PATCH 021/117] Fix #1630 - Audio Capture settings are ignored (#1640) * Fix macOS build * Update minimum cmake version * Correct compile errorswith Qt6.7 * Update minimum cmake version (2) * Use C++17 * Correct compile errors with Qt6.7 * Replace unsupported Lambda UniqueConnection * Support UTF-8 Output on console * Fix #1630 --- CMakeLists.txt | 34 +++++++------- assets/webconfig/js/content_grabber.js | 6 +-- cmake/Dependencies.cmake | 1 + dependencies/CMakeLists-qmdnsengine.txt.in | 2 +- dependencies/CMakeLists.txt | 8 ++-- include/api/JsonAPI.h | 5 ++ include/grabber/AudioGrabberWindows.h | 14 +++--- include/hyperion/ImageToLedsMap.h | 10 ++-- include/utils/global_defines.h | 2 +- libsrc/api/JsonAPI.cpp | 37 +++++++++------ libsrc/api/JsonCB.cpp | 2 +- libsrc/db/DBManager.cpp | 11 +++-- libsrc/effectengine/EffectFileHandler.cpp | 12 ++--- libsrc/forwarder/MessageForwarder.cpp | 10 ++-- libsrc/grabber/audio/AudioGrabber.cpp | 46 +++++++++++++------ libsrc/grabber/audio/AudioGrabberWindows.cpp | 8 +++- libsrc/grabber/audio/CMakeLists.txt | 2 +- libsrc/grabber/qt/QtGrabber.cpp | 2 +- libsrc/hyperion/ComponentRegister.cpp | 2 +- libsrc/hyperion/SettingsManager.cpp | 32 ++++++++----- .../leddevice/dev_net/LedDevicePhilipsHue.cpp | 2 +- libsrc/leddevice/dev_net/LedDeviceWled.cpp | 8 ++-- libsrc/utils/Logger.cpp | 2 +- .../utils/jsonschema/QJsonSchemaChecker.cpp | 4 +- src/hyperion-aml/CMakeLists.txt | 2 +- src/hyperion-dispmanx/CMakeLists.txt | 2 +- src/hyperion-framebuffer/CMakeLists.txt | 2 +- src/hyperion-osx/CMakeLists.txt | 2 +- src/hyperion-qt/CMakeLists.txt | 2 +- src/hyperion-remote/CMakeLists.txt | 2 +- src/hyperion-remote/hyperion-remote.cpp | 4 +- src/hyperion-v4l2/CMakeLists.txt | 2 +- src/hyperion-x11/CMakeLists.txt | 2 +- src/hyperion-xcb/CMakeLists.txt | 2 +- src/hyperiond/console.h | 1 + src/hyperiond/main.cpp | 4 +- 36 files changed, 169 insertions(+), 120 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cbb1ef3f..813cb750 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1.0) +cmake_minimum_required(VERSION 3.5.0) message( STATUS "CMake Version: ${CMAKE_VERSION}" ) @@ -38,26 +38,24 @@ if ( CCACHE_FOUND ) set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache) endif(CCACHE_FOUND) -# enable C++14; MSVC doesn't have c++14 feature switch -if(NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC") - if(APPLE) - include(CheckCXXCompilerFlag) - CHECK_CXX_COMPILER_FLAG("Werror=unguarded-availability" REQUIRED_UNGUARDED_AVAILABILITY) - if(REQUIRED_UNGUARDED_AVAILABILITY) - list(APPEND CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS} "Werror=unguarded-availability") - endif() +# enable C++17 +if(APPLE) + include(CheckCXXCompilerFlag) + CHECK_CXX_COMPILER_FLAG("Werror=unguarded-availability" REQUIRED_UNGUARDED_AVAILABILITY) + if(REQUIRED_UNGUARDED_AVAILABILITY) + list(APPEND CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS} "Werror=unguarded-availability") endif() - - if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR CMAKE_C_COMPILER_ID MATCHES "GNU") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-psabi") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-psabi") - endif() - - set(CMAKE_CXX_STANDARD 14) - set(CXX_STANDARD_REQUIRED ON) - set(CXX_EXTENSIONS OFF) endif() +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" OR CMAKE_C_COMPILER_ID MATCHES "GNU") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-psabi") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-psabi") +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + # Set build variables # Grabber SET ( DEFAULT_AMLOGIC OFF ) diff --git a/assets/webconfig/js/content_grabber.js b/assets/webconfig/js/content_grabber.js index 20da6030..6136673a 100755 --- a/assets/webconfig/js/content_grabber.js +++ b/assets/webconfig/js/content_grabber.js @@ -327,7 +327,7 @@ $(document).ready(function () { var saveOptions = conf_editor_screen.getValue(); var instCaptOptions = window.serverConfig.instCapture; - instCaptOptions.systemEnable = true; + instCaptOptions.systemEnable = saveOptions.framegrabber.enable; saveOptions.instCapture = instCaptOptions; requestWriteConfig(saveOptions); @@ -679,7 +679,7 @@ $(document).ready(function () { var saveOptions = conf_editor_video.getValue(); var instCaptOptions = window.serverConfig.instCapture; - instCaptOptions.v4lEnable = true; + instCaptOptions.v4lEnable = saveOptions.grabberV4L2.enable; saveOptions.instCapture = instCaptOptions; requestWriteConfig(saveOptions); @@ -805,7 +805,7 @@ $(document).ready(function () { const saveOptions = conf_editor_audio.getValue(); const instCaptOptions = window.serverConfig.instCapture; - instCaptOptions.audioEnable = true; + instCaptOptions.audioEnable = saveOptions.grabberAudio.enable; saveOptions.instCapture = instCaptOptions; requestWriteConfig(saveOptions); diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index 08120831..40ada3e1 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -48,6 +48,7 @@ macro(DeployMacOS TARGET) foreach(PLUGIN "platforms" "sqldrivers" "imageformats") if(EXISTS ${PLUGIN_DIR}/${PLUGIN}) file(GLOB files "${PLUGIN_DIR}/${PLUGIN}/*") + list(FILTER files EXCLUDE REGEX ".*libqwebp\\.dylib$") foreach(file ${files}) file(GET_RUNTIME_DEPENDENCIES EXECUTABLES ${file} diff --git a/dependencies/CMakeLists-qmdnsengine.txt.in b/dependencies/CMakeLists-qmdnsengine.txt.in index 12e45328..b3f0812a 100644 --- a/dependencies/CMakeLists-qmdnsengine.txt.in +++ b/dependencies/CMakeLists-qmdnsengine.txt.in @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.2) +cmake_minimum_required(VERSION 3.5) project(qmdnsengine) diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index c13e96cc..bc5044be 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -133,6 +133,9 @@ endif() if(ENABLE_PROTOBUF_SERVER) set(USE_SYSTEM_PROTO_LIBS ${DEFAULT_USE_SYSTEM_PROTO_LIBS} CACHE BOOL "use protobuf library from system") + + # defines for 3rd party sub-modules + set(ABSL_PROPAGATE_CXX_STD ON CACHE BOOL "Build abseil-cpp with C++ version requirements propagated") if (USE_SYSTEM_PROTO_LIBS) find_package(Protobuf REQUIRED) @@ -161,9 +164,6 @@ if(ENABLE_PROTOBUF_SERVER) # define the protobuf library set(PROTOBUF_LIBRARIES protobuf::libprotobuf) - # defines for 3rd party sub-modules - set(ABSL_PROPAGATE_CXX_STD ON CACHE BOOL "Build abseil-cpp with C++ version requirements propagated") - endif() # redefine at parent scope @@ -270,7 +270,7 @@ if(ENABLE_DEV_NETWORK) set(USE_SYSTEM_MBEDTLS_LIBS OFF) endif (NOT MBEDTLS_FOUND) else() - cmake_minimum_required(VERSION 3.2) + cmake_minimum_required(VERSION 3.5.1) set(CMAKE_POLICY_DEFAULT_CMP0071 NEW) diff --git a/include/api/JsonAPI.h b/include/api/JsonAPI.h index 6346ce72..3880fc42 100644 --- a/include/api/JsonAPI.h +++ b/include/api/JsonAPI.h @@ -88,6 +88,11 @@ private slots: /// void handleInstanceStateChange(InstanceState state, quint8 instance, const QString &name = QString()); + /// + /// @brief Stream a new LED Colors update + /// + void streamLedColorsUpdate(); + signals: /// /// Signal emits with the reply message provided with handleMessage() diff --git a/include/grabber/AudioGrabberWindows.h b/include/grabber/AudioGrabberWindows.h index 747212c2..9c3945b6 100644 --- a/include/grabber/AudioGrabberWindows.h +++ b/include/grabber/AudioGrabberWindows.h @@ -39,8 +39,8 @@ class AudioGrabberWindows : public AudioGrabber HANDLE notificationEvent; std::atomic isRunning{ false }; -static BOOL CALLBACK DirectSoundEnumProcessor(LPGUID deviceIdGuid, LPCTSTR deviceDescStr, - LPCTSTR deviceModelStr, LPVOID context) +static BOOL CALLBACK DirectSoundEnumProcessor(LPGUID deviceIdGuid, LPCWSTR deviceDescStr, + LPCWSTR deviceModelStr, LPVOID context) { // Skip undefined audio devices if (deviceIdGuid == NULL) @@ -50,12 +50,15 @@ static BOOL CALLBACK DirectSoundEnumProcessor(LPGUID deviceIdGuid, LPCTSTR devic AudioGrabber::DeviceProperties device; + // Process Device Information + QString deviceName = QString::fromWCharArray(deviceDescStr); + // Process Device ID LPOLESTR deviceIdStr; HRESULT res = StringFromCLSID(*deviceIdGuid, &deviceIdStr); if (FAILED(res)) { - Error(Logger::getInstance("AUDIOGRABBER"), "Failed to get CLSID-string for %s with error: 0x%08x: %s", deviceDescStr, res, std::system_category().message(res).c_str()); + Error(Logger::getInstance("AUDIOGRABBER"), "Failed to get CLSID-string for %s with error: 0x%08x: %s", QSTRING_CSTR(deviceName), res, std::system_category().message(res).c_str()); return FALSE; } @@ -63,10 +66,7 @@ static BOOL CALLBACK DirectSoundEnumProcessor(LPGUID deviceIdGuid, LPCTSTR devic CoTaskMemFree(deviceIdStr); - // Process Device Information - QString deviceName = QString::fromLocal8Bit(deviceDescStr); - - Debug(Logger::getInstance("AUDIOGRABBER"), "Found Audio Device: %s", deviceDescStr); + Debug(Logger::getInstance("AUDIOGRABBER"), "Found Audio Device: %s", QSTRING_CSTR(deviceName)); device.id = deviceId; device.name = deviceName; diff --git a/include/hyperion/ImageToLedsMap.h b/include/hyperion/ImageToLedsMap.h index 17662f28..45e7bb5a 100644 --- a/include/hyperion/ImageToLedsMap.h +++ b/include/hyperion/ImageToLedsMap.h @@ -413,9 +413,13 @@ namespace hyperion } // Compute the average of each color channel - const uint8_t avgRed = uint8_t(std::min(std::lround(sqrt(static_cast(cummRed/pixelNum))), 255L)); - const uint8_t avgGreen = uint8_t(std::min(std::lround(sqrt(static_cast(cummGreen/pixelNum))), 255L)); - const uint8_t avgBlue = uint8_t(std::min(std::lround(sqrt(static_cast(cummBlue/pixelNum))), 255L)); + + #ifdef WIN32 + #undef min + #endif + const uint8_t avgRed = static_cast(std::min(std::lround(std::sqrt(static_cast(cummRed / pixelNum))), 255L)); + const uint8_t avgGreen = static_cast(std::min(std::lround(sqrt(static_cast(cummGreen / pixelNum))), 255L)); + const uint8_t avgBlue = static_cast(std::min(std::lround(sqrt(static_cast(cummBlue / pixelNum))), 255L)); // Return the computed color return {avgRed, avgGreen, avgBlue}; diff --git a/include/utils/global_defines.h b/include/utils/global_defines.h index e5a6808e..e948fb97 100644 --- a/include/utils/global_defines.h +++ b/include/utils/global_defines.h @@ -1,6 +1,6 @@ #pragma once -#define QSTRING_CSTR(str) str.toLocal8Bit().constData() +#define QSTRING_CSTR(str) str.toUtf8().constData() typedef QList< int > QIntList; diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 212dc14b..75863ca2 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -140,6 +140,8 @@ void JsonAPI::initialize() connect(this, &JsonAPI::toggleSuspendAll, _instanceManager, &HyperionIManager::triggerToggleSuspend); connect(this, &JsonAPI::idleAll, _instanceManager, &HyperionIManager::triggerIdle); connect(this, &JsonAPI::toggleIdleAll, _instanceManager, &HyperionIManager::triggerToggleIdle); + + connect(_ledStreamTimer, &QTimer::timeout, this, &JsonAPI::streamLedColorsUpdate, Qt::UniqueConnection); } bool JsonAPI::handleInstanceSwitch(quint8 inst, bool forced) @@ -404,7 +406,7 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString activePriorities.removeAll(PriorityMuxer::LOWEST_PRIORITY); int currentPriority = _hyperion->getCurrentPriority(); - for(int priority : qAsConst(activePriorities)) + for(int priority : std::as_const(activePriorities)) { const Hyperion::InputInfo &priorityInfo = _hyperion->getPriorityInfo(priority); @@ -1139,6 +1141,11 @@ void JsonAPI::handleComponentStateCommand(const QJsonObject &message, const QStr sendSuccessReply(command, tan); } +void JsonAPI::streamLedColorsUpdate() +{ + emit streamLedcolorsUpdate(_currentLedValues); +} + void JsonAPI::handleLedColorsCommand(const QJsonObject &message, const QString &command, int tan) { // create result @@ -1154,21 +1161,21 @@ void JsonAPI::handleLedColorsCommand(const QJsonObject &message, const QString & _streaming_leds_reply["tan"] = tan; connect(_hyperion, &Hyperion::rawLedColors, this, [=](const std::vector &ledValues) { - _currentLedValues = ledValues; - // necessary because Qt::UniqueConnection for lambdas does not work until 5.9 - // see: https://bugreports.qt.io/browse/QTBUG-52438 - if (!_ledStreamConnection) - _ledStreamConnection = connect(_ledStreamTimer, &QTimer::timeout, this, [=]() { - emit streamLedcolorsUpdate(_currentLedValues); - }, - Qt::UniqueConnection); + if (ledValues != _currentLedValues) + { + _currentLedValues = ledValues; + if (!_ledStreamTimer->isActive() || _ledStreamTimer->interval() != streaming_interval) + { + _ledStreamTimer->start(streaming_interval); + } + } + else + { + _ledStreamTimer->stop(); + } + }); - // start the timer - if (!_ledStreamTimer->isActive() || _ledStreamTimer->interval() != streaming_interval) - _ledStreamTimer->start(streaming_interval); - }, - Qt::UniqueConnection); // push once _hyperion->update(); } @@ -1387,7 +1394,7 @@ void JsonAPI::handleAuthorizeCommand(const QJsonObject &message, const QString & if (API::getPendingTokenRequests(vec)) { QJsonArray arr; - for (const auto &entry : qAsConst(vec)) + for (const auto &entry : std::as_const(vec)) { QJsonObject obj; obj["comment"] = entry.comment; diff --git a/libsrc/api/JsonCB.cpp b/libsrc/api/JsonCB.cpp index 965abf37..e3c4b32a 100644 --- a/libsrc/api/JsonCB.cpp +++ b/libsrc/api/JsonCB.cpp @@ -199,7 +199,7 @@ void JsonCB::handlePriorityUpdate(int currentPriority, const PriorityMuxer::Inpu activePriorities.removeAll(PriorityMuxer::LOWEST_PRIORITY); - for (int priority : qAsConst(activePriorities)) { + for (int priority : std::as_const(activePriorities)) { const Hyperion::InputInfo& priorityInfo = activeInputs[priority]; diff --git a/libsrc/db/DBManager.cpp b/libsrc/db/DBManager.cpp index a8711c15..f4494967 100644 --- a/libsrc/db/DBManager.cpp +++ b/libsrc/db/DBManager.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -425,15 +426,15 @@ void DBManager::doAddBindValue(QSqlQuery& query, const QVariantList& variants) c auto t = variant.userType(); switch(t) { - case QVariant::UInt: - case QVariant::Int: - case QVariant::Bool: + case QMetaType::UInt: + case QMetaType::Int: + case QMetaType::Bool: query.addBindValue(variant.toInt()); break; - case QVariant::Double: + case QMetaType::Double: query.addBindValue(variant.toFloat()); break; - case QVariant::ByteArray: + case QMetaType::QByteArray: query.addBindValue(variant.toByteArray()); break; default: diff --git a/libsrc/effectengine/EffectFileHandler.cpp b/libsrc/effectengine/EffectFileHandler.cpp index e1389e83..3d1c77eb 100644 --- a/libsrc/effectengine/EffectFileHandler.cpp +++ b/libsrc/effectengine/EffectFileHandler.cpp @@ -224,7 +224,7 @@ void EffectFileHandler::updateEffects() } QMap availableEffects; - for (const QString& path : qAsConst(efxPathList)) + for (const QString& path : std::as_const(efxPathList)) { QDir directory(path); if (!directory.exists()) @@ -241,8 +241,8 @@ void EffectFileHandler::updateEffects() else { int efxCount = 0; - QStringList filenames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase); - for (const QString& filename : qAsConst(filenames)) + const QStringList filenames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase); + for (const QString& filename : filenames) { EffectDefinition def; if (loadEffectDefinition(path, filename, def)) @@ -268,8 +268,8 @@ void EffectFileHandler::updateEffects() QString schemaPath = path + "schema" + '/'; directory.setPath(schemaPath); - QStringList schemaFileNames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase); - for (const QString& schemaFileName : qAsConst(schemaFileNames)) + const QStringList schemaFileNames = directory.entryList(QStringList() << "*.json", QDir::Files, QDir::Name | QDir::IgnoreCase); + for (const QString& schemaFileName : schemaFileNames) { EffectSchema pyEffect; if (loadEffectSchema(path, directory.filePath(schemaFileName), pyEffect)) @@ -282,7 +282,7 @@ void EffectFileHandler::updateEffects() } } - for (const auto& item : qAsConst(availableEffects)) + for (const auto& item : std::as_const(availableEffects)) { _availableEffects.push_back(item); } diff --git a/libsrc/forwarder/MessageForwarder.cpp b/libsrc/forwarder/MessageForwarder.cpp index b795cbe8..3720ad54 100644 --- a/libsrc/forwarder/MessageForwarder.cpp +++ b/libsrc/forwarder/MessageForwarder.cpp @@ -269,7 +269,7 @@ int MessageForwarder::startJsonTargets(const QJsonObject& config) if (!_jsonTargets.isEmpty()) { - for (const auto& targetHost : qAsConst(_jsonTargets)) + for (const auto& targetHost : std::as_const(_jsonTargets)) { Info(_log, "Forwarding now to JSON-target host: %s port: %u", QSTRING_CSTR(targetHost.host.toString()), targetHost.port); } @@ -286,7 +286,7 @@ void MessageForwarder::stopJsonTargets() if (!_jsonTargets.isEmpty()) { disconnect(_hyperion, &Hyperion::forwardJsonMessage, nullptr, nullptr); - for (const auto& targetHost : qAsConst(_jsonTargets)) + for (const auto& targetHost : std::as_const(_jsonTargets)) { Info(_log, "Stopped forwarding to JSON-target host: %s port: %u", QSTRING_CSTR(targetHost.host.toString()), targetHost.port); } @@ -373,7 +373,7 @@ int MessageForwarder::startFlatbufferTargets(const QJsonObject& config) if (!_flatbufferTargets.isEmpty()) { - for (const auto& targetHost : qAsConst(_flatbufferTargets)) + for (const auto& targetHost : std::as_const(_flatbufferTargets)) { Info(_log, "Forwarding now to Flatbuffer-target host: %s port: %u", QSTRING_CSTR(targetHost.host.toString()), targetHost.port); } @@ -399,7 +399,7 @@ void MessageForwarder::stopFlatbufferTargets() _messageForwarderFlatBufHelper = nullptr; } - for (const auto& targetHost : qAsConst(_flatbufferTargets)) + for (const auto& targetHost : std::as_const(_flatbufferTargets)) { Info(_log, "Stopped forwarding to Flatbuffer-target host: %s port: %u", QSTRING_CSTR(targetHost.host.toString()), targetHost.port); } @@ -412,7 +412,7 @@ void MessageForwarder::forwardJsonMessage(const QJsonObject& message) if (_forwarder_enabled) { QTcpSocket client; - for (const auto& targetHost : qAsConst(_jsonTargets)) + for (const auto& targetHost : std::as_const(_jsonTargets)) { client.connectToHost(targetHost.host, targetHost.port); if (client.waitForConnected(CONNECT_TIMEOUT.count())) diff --git a/libsrc/grabber/audio/AudioGrabber.cpp b/libsrc/grabber/audio/AudioGrabber.cpp index 1e625a1e..4f4eccbd 100644 --- a/libsrc/grabber/audio/AudioGrabber.cpp +++ b/libsrc/grabber/audio/AudioGrabber.cpp @@ -9,6 +9,15 @@ // Constants namespace { const uint16_t RESOLUTION = 255; + + //Constants vuMeter + const QJsonArray DEFAULT_HOTCOLOR { 255,0,0 }; + const QJsonArray DEFAULT_WARNCOLOR { 255,255,0 }; + const QJsonArray DEFAULT_SAFECOLOR { 0,255,0 }; + const int DEFAULT_WARNVALUE { 80 }; + const int DEFAULT_SAFEVALUE { 45 }; + const int DEFAULT_MULTIPLIER { 0 }; + const int DEFAULT_TOLERANCE { 20 }; } #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) @@ -28,12 +37,12 @@ AudioGrabber::AudioGrabber() , _deviceProperties() , _device("none") , _hotColor(QColorConstants::Red) - , _warnValue(80) + , _warnValue(DEFAULT_WARNVALUE) , _warnColor(QColorConstants::Yellow) - , _safeValue(45) + , _safeValue(DEFAULT_SAFEVALUE) , _safeColor(QColorConstants::Green) - , _multiplier(0) - , _tolerance(20) + , _multiplier(DEFAULT_MULTIPLIER) + , _tolerance(DEFAULT_TOLERANCE) , _dynamicMultiplier(INT16_MAX) , _started(false) { @@ -61,18 +70,27 @@ void AudioGrabber::setDevice(const QString& device) void AudioGrabber::setConfiguration(const QJsonObject& config) { - QJsonArray hotColorArray = config["hotColor"].toArray(QJsonArray::fromVariantList(QList({ QVariant(255), QVariant(0), QVariant(0) }))); - QJsonArray warnColorArray = config["warnColor"].toArray(QJsonArray::fromVariantList(QList({ QVariant(255), QVariant(255), QVariant(0) }))); - QJsonArray safeColorArray = config["safeColor"].toArray(QJsonArray::fromVariantList(QList({ QVariant(0), QVariant(255), QVariant(0) }))); + QString audioEffect = config["audioEffect"].toString(); + QJsonObject audioEffectConfig = config[audioEffect].toObject(); - _hotColor = QColor(hotColorArray.at(0).toInt(), hotColorArray.at(1).toInt(), hotColorArray.at(2).toInt()); - _warnColor = QColor(warnColorArray.at(0).toInt(), warnColorArray.at(1).toInt(), warnColorArray.at(2).toInt()); - _safeColor = QColor(safeColorArray.at(0).toInt(), safeColorArray.at(1).toInt(), safeColorArray.at(2).toInt()); + if (audioEffect == "vuMeter") + { + QJsonArray hotColorArray = audioEffectConfig.value("hotColor").toArray(DEFAULT_HOTCOLOR); + QJsonArray warnColorArray = audioEffectConfig.value("warnColor").toArray(DEFAULT_WARNCOLOR); + QJsonArray safeColorArray = audioEffectConfig.value("safeColor").toArray(DEFAULT_SAFECOLOR); - _warnValue = config["warnValue"].toInt(80); - _safeValue = config["safeValue"].toInt(45); - _multiplier = config["multiplier"].toDouble(0); - _tolerance = config["tolerance"].toInt(20); + _hotColor = QColor(hotColorArray.at(0).toInt(), hotColorArray.at(1).toInt(), hotColorArray.at(2).toInt()); + _warnColor = QColor(warnColorArray.at(0).toInt(), warnColorArray.at(1).toInt(), warnColorArray.at(2).toInt()); + _safeColor = QColor(safeColorArray.at(0).toInt(), safeColorArray.at(1).toInt(), safeColorArray.at(2).toInt()); + _warnValue = audioEffectConfig["warnValue"].toInt(DEFAULT_WARNVALUE); + _safeValue = audioEffectConfig["safeValue"].toInt(DEFAULT_SAFEVALUE); + _multiplier = audioEffectConfig["multiplier"].toDouble(DEFAULT_MULTIPLIER); + _tolerance = audioEffectConfig["tolerance"].toInt(DEFAULT_MULTIPLIER); + } + else + { + Error(_log, "Unknow Audio-Effect: \"%s\" configured", QSTRING_CSTR(audioEffect)); + } } void AudioGrabber::resetMultiplier() diff --git a/libsrc/grabber/audio/AudioGrabberWindows.cpp b/libsrc/grabber/audio/AudioGrabberWindows.cpp index 07837bd1..8a2228c3 100644 --- a/libsrc/grabber/audio/AudioGrabberWindows.cpp +++ b/libsrc/grabber/audio/AudioGrabberWindows.cpp @@ -1,4 +1,7 @@ #include + +#include + #include #include #include @@ -61,7 +64,10 @@ bool AudioGrabberWindows::configureCaptureInterface() // wFormatTag, nChannels, nSamplesPerSec, mAvgBytesPerSec, // nBlockAlign, wBitsPerSample, cbSize - notificationSize = max(1024, audioFormat.nAvgBytesPerSec / 8); + #ifdef WIN32 + #undef max + #endif + notificationSize = std::max(static_cast(1024), static_cast(audioFormat.nAvgBytesPerSec / 8)); notificationSize -= notificationSize % audioFormat.nBlockAlign; bufferCaptureSize = notificationSize * AUDIO_NOTIFICATION_COUNT; diff --git a/libsrc/grabber/audio/CMakeLists.txt b/libsrc/grabber/audio/CMakeLists.txt index 187fa9d0..714c5883 100644 --- a/libsrc/grabber/audio/CMakeLists.txt +++ b/libsrc/grabber/audio/CMakeLists.txt @@ -2,8 +2,8 @@ SET( CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber ) SET( CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/audio ) - if (WIN32) + add_definitions(-DUNICODE -D_UNICODE) FILE ( GLOB AUDIO_GRABBER_SOURCES "${CURRENT_HEADER_DIR}/Audio*Windows.h" "${CURRENT_HEADER_DIR}/AudioGrabber.h" "${CURRENT_HEADER_DIR}/AudioWrapper.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*Windows.cpp" "${CURRENT_SOURCE_DIR}/AudioGrabber.cpp" "${CURRENT_SOURCE_DIR}/AudioWrapper.cpp") elseif(${CMAKE_SYSTEM} MATCHES "Linux") FILE ( GLOB AUDIO_GRABBER_SOURCES "${CURRENT_HEADER_DIR}/Audio*Linux.h" "${CURRENT_HEADER_DIR}/AudioGrabber.h" "${CURRENT_HEADER_DIR}/AudioWrapper.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*Linux.cpp" "${CURRENT_SOURCE_DIR}/AudioGrabber.cpp" "${CURRENT_SOURCE_DIR}/AudioWrapper.cpp") diff --git a/libsrc/grabber/qt/QtGrabber.cpp b/libsrc/grabber/qt/QtGrabber.cpp index b3f35182..cb6e0c5c 100644 --- a/libsrc/grabber/qt/QtGrabber.cpp +++ b/libsrc/grabber/qt/QtGrabber.cpp @@ -103,7 +103,7 @@ bool QtGrabber::setupDisplay() Info(_log, "Available Displays:"); int index = 0; - for (auto* screen : qAsConst(screens)) + for (auto* screen : std::as_const(screens)) { const QRect geo = screen->geometry(); Info(_log, "Display %d: Name: %s Resolution: [%dx%d], Geometry: (L,T,R,B) %d,%d,%d,%d Depth:%dbit", index, QSTRING_CSTR(screen->name()), geo.width(), geo.height(), geo.x(), geo.y(), geo.x() + geo.width(), geo.y() + geo.height(), screen->depth()); diff --git a/libsrc/hyperion/ComponentRegister.cpp b/libsrc/hyperion/ComponentRegister.cpp index fd2f261d..9a040caf 100644 --- a/libsrc/hyperion/ComponentRegister.cpp +++ b/libsrc/hyperion/ComponentRegister.cpp @@ -61,7 +61,7 @@ ComponentRegister::ComponentRegister(Hyperion* hyperion) vect << COMP_FORWARDER; #endif - for(auto e : qAsConst(vect)) + for(auto e : std::as_const(vect)) { _componentStates.emplace(e, (e == COMP_ALL)); } diff --git a/libsrc/hyperion/SettingsManager.cpp b/libsrc/hyperion/SettingsManager.cpp index b9d78edc..e9fa5805 100644 --- a/libsrc/hyperion/SettingsManager.cpp +++ b/libsrc/hyperion/SettingsManager.cpp @@ -58,9 +58,9 @@ SettingsManager::SettingsManager(quint8 instance, QObject* parent, bool readonly } // transform json to string lists - QStringList keyList = defaultConfig.keys(); + const QStringList keyList = defaultConfig.keys(); QStringList defValueList; - for (const auto& key : qAsConst(keyList)) + for (const auto& key : keyList) { if (defaultConfig[key].isObject()) { @@ -73,7 +73,7 @@ SettingsManager::SettingsManager(quint8 instance, QObject* parent, bool readonly } // fill database with default data if required - for (const auto& key : qAsConst(keyList)) + for (const auto& key : keyList) { QString val = defValueList.takeFirst(); // prevent overwrite @@ -86,7 +86,7 @@ SettingsManager::SettingsManager(quint8 instance, QObject* parent, bool readonly // need to validate all data in database construct the entire data object // TODO refactor schemaChecker to accept QJsonArray in validate(); QJsonDocument container? To validate them per entry... QJsonObject dbConfig; - for (const auto& key : qAsConst(keyList)) + for (const auto& key : keyList) { QJsonDocument doc = _sTable->getSettingsRecord(key); if (doc.isArray()) @@ -242,9 +242,9 @@ bool SettingsManager::saveSettings(QJsonObject config, bool correct) _qconfig = config; // extract keys and data - QStringList keyList = config.keys(); + const QStringList keyList = config.keys(); QStringList newValueList; - for (const auto& key : qAsConst(keyList)) + for (const auto& key : keyList) { if (config[key].isObject()) { @@ -258,7 +258,7 @@ bool SettingsManager::saveSettings(QJsonObject config, bool correct) bool rc = true; // compare database data with new data to emit/save changes accordingly - for (const auto& key : qAsConst(keyList)) + for (const auto& key : keyList) { QString data = newValueList.takeFirst(); if (_sTable->getSettingsRecordString(key) != data) @@ -269,7 +269,15 @@ bool SettingsManager::saveSettings(QJsonObject config, bool correct) } else { - emit settingsChanged(settings::stringToType(key), QJsonDocument::fromJson(data.toLocal8Bit())); + QJsonParseError error; + QJsonDocument jsonDocument = QJsonDocument::fromJson(data.toUtf8(), &error); + if (error.error != QJsonParseError::NoError) { + Error(_log, "Error parsing JSON: %s", QSTRING_CSTR(error.errorString())); + rc = false; + } + else { + emit settingsChanged(settings::stringToType(key), jsonDocument); + } } } } @@ -618,10 +626,10 @@ bool SettingsManager::handleConfigUpgrade(QJsonObject& config) QJsonArray json; if (newForwarderConfig.contains("json")) { - QJsonArray oldJson = newForwarderConfig["json"].toArray(); + const QJsonArray oldJson = newForwarderConfig["json"].toArray(); QJsonObject newJsonConfig; - for (const QJsonValue& value : qAsConst(oldJson)) + for (const QJsonValue& value : oldJson) { if (value.isString()) { @@ -661,10 +669,10 @@ bool SettingsManager::handleConfigUpgrade(QJsonObject& config) QJsonArray flatbuffer; if (newForwarderConfig.contains("flat")) { - QJsonArray oldFlatbuffer = newForwarderConfig["flat"].toArray(); + const QJsonArray oldFlatbuffer = newForwarderConfig["flat"].toArray(); QJsonObject newFlattbufferConfig; - for (const QJsonValue& value : qAsConst(oldFlatbuffer)) + for (const QJsonValue& value : oldFlatbuffer) { if (value.isString()) { diff --git a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp index 8f73ae5f..ba008ab0 100644 --- a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp @@ -1126,7 +1126,7 @@ bool LedDevicePhilipsHue::setLights() if( !lArray.empty() ) { - for (const QJsonValue &id : qAsConst(lArray)) + for (const QJsonValue &id : std::as_const(lArray)) { int lightId = id.toString().toInt(); if( lightId > 0 ) diff --git a/libsrc/leddevice/dev_net/LedDeviceWled.cpp b/libsrc/leddevice/dev_net/LedDeviceWled.cpp index 3ada5091..e23a8c9b 100644 --- a/libsrc/leddevice/dev_net/LedDeviceWled.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceWled.cpp @@ -352,12 +352,12 @@ bool LedDeviceWled::powerOn() } else { - QJsonArray propertiesSegments = _originalStateProperties[STATE_SEG].toArray(); + const QJsonArray propertiesSegments = _originalStateProperties[STATE_SEG].toArray(); bool isStreamSegmentIdFound { false }; QJsonArray segments; - for (const auto& segmentItem : qAsConst(propertiesSegments)) + for (const auto& segmentItem : propertiesSegments) { QJsonObject segmentObj = segmentItem.toObject(); @@ -505,9 +505,9 @@ bool LedDeviceWled::restoreState() if (_isStreamToSegment) { - QJsonArray propertiesSegments = _originalStateProperties[STATE_SEG].toArray(); + const QJsonArray propertiesSegments = _originalStateProperties[STATE_SEG].toArray(); QJsonArray segments; - for (const auto& segmentItem : qAsConst(propertiesSegments)) + for (const auto& segmentItem : propertiesSegments) { QJsonObject segmentObj = segmentItem.toObject(); diff --git a/libsrc/utils/Logger.cpp b/libsrc/utils/Logger.cpp index 86b4e903..b34981c9 100644 --- a/libsrc/utils/Logger.cpp +++ b/libsrc/utils/Logger.cpp @@ -65,7 +65,7 @@ void Logger::deleteInstance(const QString & name, const QString & subName) if (name.isEmpty()) { - for (auto *logger : qAsConst(LoggerMap)) { + for (auto *logger : std::as_const(LoggerMap)) { delete logger; } diff --git a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp index 685ab4c7..8cd0be75 100644 --- a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp +++ b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp @@ -59,13 +59,13 @@ QPair QJsonSchemaChecker::validate(const QJsonObject& value, bool ig QJsonObject QJsonSchemaChecker::getAutoCorrectedConfig(const QJsonObject& value, bool ignoreRequired) { _ignoreRequired = ignoreRequired; - QStringList sequence = QStringList() << "remove" << "modify" << "create"; + const QStringList sequence = QStringList() << "remove" << "modify" << "create"; _error = false; _schemaError = false; _messages.clear(); _autoCorrected = value; - for (const QString& correct : qAsConst(sequence)) + for (const QString& correct : sequence) { _correct = correct; _currentPath.clear(); diff --git a/src/hyperion-aml/CMakeLists.txt b/src/hyperion-aml/CMakeLists.txt index 37bed4af..cdea66fa 100644 --- a/src/hyperion-aml/CMakeLists.txt +++ b/src/hyperion-aml/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1.0) +cmake_minimum_required(VERSION 3.5.0) project(hyperion-aml) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Widgets REQUIRED) diff --git a/src/hyperion-dispmanx/CMakeLists.txt b/src/hyperion-dispmanx/CMakeLists.txt index 17720483..07862bbd 100644 --- a/src/hyperion-dispmanx/CMakeLists.txt +++ b/src/hyperion-dispmanx/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1.0) +cmake_minimum_required(VERSION 3.5.0) project(hyperion-dispmanx) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Widgets REQUIRED) diff --git a/src/hyperion-framebuffer/CMakeLists.txt b/src/hyperion-framebuffer/CMakeLists.txt index 5667203a..14ea2ce9 100644 --- a/src/hyperion-framebuffer/CMakeLists.txt +++ b/src/hyperion-framebuffer/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1.0) +cmake_minimum_required(VERSION 3.5.0) project(hyperion-framebuffer) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Widgets REQUIRED) diff --git a/src/hyperion-osx/CMakeLists.txt b/src/hyperion-osx/CMakeLists.txt index 12f17a9b..efb15cd1 100644 --- a/src/hyperion-osx/CMakeLists.txt +++ b/src/hyperion-osx/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1.0) +cmake_minimum_required(VERSION 3.5.0) project(hyperion-osx) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui Network Widgets REQUIRED) diff --git a/src/hyperion-qt/CMakeLists.txt b/src/hyperion-qt/CMakeLists.txt index 6cc4b386..f35b8534 100644 --- a/src/hyperion-qt/CMakeLists.txt +++ b/src/hyperion-qt/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1.0) +cmake_minimum_required(VERSION 3.5.0) project(hyperion-qt) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui Network Widgets REQUIRED) diff --git a/src/hyperion-remote/CMakeLists.txt b/src/hyperion-remote/CMakeLists.txt index a7ce5cfe..a2805cad 100644 --- a/src/hyperion-remote/CMakeLists.txt +++ b/src/hyperion-remote/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1.0) +cmake_minimum_required(VERSION 3.5.0) project(hyperion-remote) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Widgets REQUIRED) diff --git a/src/hyperion-remote/hyperion-remote.cpp b/src/hyperion-remote/hyperion-remote.cpp index f47fbb14..e7fdc453 100644 --- a/src/hyperion-remote/hyperion-remote.cpp +++ b/src/hyperion-remote/hyperion-remote.cpp @@ -57,9 +57,9 @@ void showHelp(Option & option){ int getInstaneIdbyName(const QJsonObject & reply, const QString & name){ if(reply.contains("instance")){ - QJsonArray list = reply.value("instance").toArray(); + const QJsonArray list = reply.value("instance").toArray(); - for ( const auto &entry : qAsConst(list) ) { + for ( const auto &entry : list ) { const QJsonObject obj = entry.toObject(); if(obj["friendly_name"] == name && obj["running"].toBool()) { diff --git a/src/hyperion-v4l2/CMakeLists.txt b/src/hyperion-v4l2/CMakeLists.txt index 9c8783dd..1404d2ad 100644 --- a/src/hyperion-v4l2/CMakeLists.txt +++ b/src/hyperion-v4l2/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1.0) +cmake_minimum_required(VERSION 3.5.0) project(hyperion-v4l2) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Widgets REQUIRED) diff --git a/src/hyperion-x11/CMakeLists.txt b/src/hyperion-x11/CMakeLists.txt index a7459ce6..88095ae3 100644 --- a/src/hyperion-x11/CMakeLists.txt +++ b/src/hyperion-x11/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1.0) +cmake_minimum_required(VERSION 3.5.0) project(hyperion-x11) find_package(X11 REQUIRED) diff --git a/src/hyperion-xcb/CMakeLists.txt b/src/hyperion-xcb/CMakeLists.txt index cd374d5a..f378aef2 100644 --- a/src/hyperion-xcb/CMakeLists.txt +++ b/src/hyperion-xcb/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1.0) +cmake_minimum_required(VERSION 3.5.0) project(hyperion-xcb) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Widgets REQUIRED) diff --git a/src/hyperiond/console.h b/src/hyperiond/console.h index 98469960..e61156ae 100644 --- a/src/hyperiond/console.h +++ b/src/hyperiond/console.h @@ -15,4 +15,5 @@ void CreateConsole() freopen_s(&fDummy, "CONOUT$", "w", stderr); freopen_s(&fDummy, "CONIN$", "r", stdin); SetConsoleTitle(TEXT("Hyperion")); + SetConsoleOutputCP(CP_UTF8); } diff --git a/src/hyperiond/main.cpp b/src/hyperiond/main.cpp index 4719cd22..fa9352b9 100644 --- a/src/hyperiond/main.cpp +++ b/src/hyperiond/main.cpp @@ -268,9 +268,9 @@ int main(int argc, char** argv) if (directory.exists() && destDir.exists()) { std::cout << "Extract to folder: " << destDir.absolutePath().toStdString() << std::endl; - QStringList filenames = directory.entryList(QStringList() << "*", QDir::Files, QDir::Name | QDir::IgnoreCase); + const QStringList filenames = directory.entryList(QStringList() << "*", QDir::Files, QDir::Name | QDir::IgnoreCase); QString destFileName; - for (const QString & filename : qAsConst(filenames)) + for (const QString & filename : filenames) { destFileName = destDir.dirName()+"/"+filename; if (QFile::exists(destFileName)) From f49e1a2c0be690d45d0e169e2267318d166f80ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 18:03:57 +0200 Subject: [PATCH 022/117] Bump actions/checkout from 3 to 4 (#1646) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/apt.yml | 6 +++--- .github/workflows/codeql.yml | 2 +- .github/workflows/nightly.yml | 10 +++++----- .github/workflows/pull-request.yml | 6 +++--- .github/workflows/push-master.yml | 8 ++++---- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/apt.yml b/.github/workflows/apt.yml index 66b90cb0..39f440b2 100644 --- a/.github/workflows/apt.yml +++ b/.github/workflows/apt.yml @@ -36,7 +36,7 @@ jobs: name: Setup APT build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set APT matrix id: apt-ppa run: | @@ -54,7 +54,7 @@ jobs: matrix: ${{ fromJson(needs.setup.outputs.apt-matrix) }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.head_sha || github.event.client_payload.head_sha }} submodules: true @@ -107,7 +107,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.head_sha || github.event.client_payload.head_sha }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1565d699..d39190cc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 7151ce2f..70d63aab 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -13,7 +13,7 @@ jobs: if: github.repository_owner == 'hyperion-project' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false fetch-depth: 0 @@ -48,7 +48,7 @@ jobs: if: github.repository_owner == 'hyperion-project' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Check if commit has changed id: build-necessary run: | @@ -66,7 +66,7 @@ jobs: if: ${{ needs.check.outputs.build-nightly == 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set nightly matrix id: nightly-ppa run: | @@ -84,7 +84,7 @@ jobs: matrix: ${{ fromJson(needs.setup.outputs.nightly-matrix) }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive @@ -135,7 +135,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Import GPG key uses: crazy-max/ghaction-import-gpg@v5.3.0 diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 47769bfb..eee7c150 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -76,7 +76,7 @@ jobs: runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -125,7 +125,7 @@ jobs: QT_VERSION: 5.15.2 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml index 8f1b44c3..b0ee7a1d 100644 --- a/.github/workflows/push-master.yml +++ b/.github/workflows/push-master.yml @@ -33,7 +33,7 @@ jobs: platform: amlogic steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive @@ -62,7 +62,7 @@ jobs: name: macOS runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive @@ -97,7 +97,7 @@ jobs: QT_VERSION: 5.15.2 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -162,7 +162,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Generate environment variables - name: Generate environment variables from .version and tag From 41bffecc0b5bc2b651f3d48516536937ad58f331 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 18:04:26 +0200 Subject: [PATCH 023/117] Bump peter-evans/repository-dispatch from 2.1.1 to 2.1.2 (#1645) Bumps [peter-evans/repository-dispatch](https://github.com/peter-evans/repository-dispatch) from 2.1.1 to 2.1.2. - [Release notes](https://github.com/peter-evans/repository-dispatch/releases) - [Commits](https://github.com/peter-evans/repository-dispatch/compare/v2.1.1...v2.1.2) --- updated-dependencies: - dependency-name: peter-evans/repository-dispatch dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d4c8461a..104c2c79 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: steps: # Dispatch event to build new HyperBian image - name: Dispatch HyperBian build - uses: peter-evans/repository-dispatch@v2.1.1 + uses: peter-evans/repository-dispatch@v2.1.2 if: ${{ github.repository_owner == 'hyperion-project'}} with: repository: hyperion-project/HyperBian From 33722c9a09430f0b0f569d56b0a176ee9e4664ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 18:06:39 +0200 Subject: [PATCH 024/117] Bump crazy-max/ghaction-import-gpg from 5.3.0 to 6.0.0 (#1644) Bumps [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) from 5.3.0 to 6.0.0. - [Release notes](https://github.com/crazy-max/ghaction-import-gpg/releases) - [Commits](https://github.com/crazy-max/ghaction-import-gpg/compare/v5.3.0...v6.0.0) --- updated-dependencies: - dependency-name: crazy-max/ghaction-import-gpg dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/apt.yml | 2 +- .github/workflows/nightly.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apt.yml b/.github/workflows/apt.yml index 39f440b2..99bce214 100644 --- a/.github/workflows/apt.yml +++ b/.github/workflows/apt.yml @@ -112,7 +112,7 @@ jobs: ref: ${{ github.event.inputs.head_sha || github.event.client_payload.head_sha }} - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v5.3.0 + uses: crazy-max/ghaction-import-gpg@v6.0.0 with: gpg_private_key: ${{ secrets.APT_GPG }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 70d63aab..2046ba07 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -138,7 +138,7 @@ jobs: uses: actions/checkout@v4 - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v5.3.0 + uses: crazy-max/ghaction-import-gpg@v6.0.0 with: gpg_private_key: ${{ secrets.APT_GPG }} From cd22d4454d99ae7bc84cabb89affcd32766c079c Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Tue, 3 Oct 2023 20:33:11 +0200 Subject: [PATCH 025/117] Philips Hue APIv2 support (#1637) * Support Philips Hue APIv2 and refactoring * Fix MDNSBrower - if timeout during host resolvment occurs * Hue API v2 - Migrate database * Fix macOS build * Handle network timeout before any other error * Address CodeQL findings * Clean-up and Fixes * Only getProperties, if username is available * Option to layout by entertainment area center * Fix Wizard --------- Co-authored-by: Paulchen-Panther <16664240+Paulchen-Panther@users.noreply.github.com> --- assets/webconfig/i18n/en.json | 7 +- assets/webconfig/js/content_leds.js | 16 +- assets/webconfig/js/wizard.js | 720 ++++-- libsrc/hyperion/SettingsManager.cpp | 55 + .../leddevice/dev_net/LedDevicePhilipsHue.cpp | 2018 ++++++++++++----- .../leddevice/dev_net/LedDevicePhilipsHue.h | 203 +- libsrc/leddevice/dev_net/ProviderRestApi.cpp | 203 +- libsrc/leddevice/dev_net/ProviderRestApi.h | 74 +- libsrc/leddevice/dev_net/ProviderUdpSSL.cpp | 10 + libsrc/leddevice/dev_net/ProviderUdpSSL.h | 10 + .../leddevice/schemas/schema-philipshue.json | 108 +- libsrc/mdns/MdnsBrowser.cpp | 1 + resources/ssl/philips_hue_ca.pem | 14 + 13 files changed, 2541 insertions(+), 898 deletions(-) create mode 100644 resources/ssl/philips_hue_ca.pem diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 263c8bd6..d8518fd2 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -86,6 +86,8 @@ "conf_leds_layout_cl_bottomright": "Bottom Right (Corner)", "conf_leds_layout_cl_cornergap": "Corner Gap", "conf_leds_layout_cl_edgegap": "Edge Gap", + "conf_leds_layout_cl_entertainment": "Entertainment Area", + "conf_leds_layout_cl_entertainment_center": "Entertainment Area Center", "conf_leds_layout_cl_gaglength": "Gap length", "conf_leds_layout_cl_gappos": "gap position", "conf_leds_layout_cl_hleddepth": "Horizontal LED depth", @@ -618,7 +620,7 @@ "edt_dev_spec_gpioBcm_title": "GPIO Pin", "edt_dev_spec_gpioMap_title": "GPIO mapping", "edt_dev_spec_gpioNumber_title": "GPIO number", - "edt_dev_spec_groupId_title": "Group ID", + "edt_dev_spec_groupId_title": "Group", "edt_dev_spec_header_title": "Specific Settings", "edt_dev_spec_interpolation_title": "Interpolation", "edt_dev_spec_intervall_title": "Interval", @@ -683,6 +685,7 @@ "edt_dev_spec_transistionTime_title": "Transition time", "edt_dev_spec_uid_title": "UID", "edt_dev_spec_universe_title": "Universe", + "edt_dev_spec_useAPIv2_title": "Use API v2", "edt_dev_spec_useEntertainmentAPI_title": "Use Hue Entertainment API", "edt_dev_spec_useOrbSmoothing_title": "Use orb smoothing", "edt_dev_spec_useRgbwProtocol_title": "Use RGBW protocol", @@ -1087,7 +1090,7 @@ "wiz_cololight_noprops": "Not able to get device properties - Define Hardware LED count manually", "wiz_cololight_title": "Cololight Wizard", "wiz_guideyou": "The $1 will guide you through the settings. Just press the button!", - "wiz_hue_blinkblue": "Let ID $1 light up blue", + "wiz_hue_blinkblue": "Let it light up", "wiz_hue_clientkey": "Clientkey", "wiz_hue_create_user": "Create new User", "wiz_hue_desc1": "1. Hyperion searches automatically for a Hue-Bridge, in case it cannot find one you need to provide the hostname or IP-address and push the reload button.
2. Provide a user ID, if you do not have one create a new one.", diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 1aec94ab..2b0304c5 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -1021,11 +1021,6 @@ $(document).ready(function () { var generalOptions = window.serverSchema.properties.device; var ledType = $(this).val(); - - // philipshueentertainment backward fix - if (ledType == "philipshueentertainment") - ledType = "philipshue"; - var specificOptions = window.serverSchema.properties.alldevices[ledType]; conf_editor = createJsonEditor('editor_container_leddevice', { @@ -1060,13 +1055,10 @@ $(document).ready(function () { $('#btn_led_device_wiz').off(); if (ledType == "philipshue") { - $('#root_specificOptions_useEntertainmentAPI').on("change", function () { - var ledWizardType = (this.checked) ? "philipshueentertainment" : ledType; - var data = { type: ledWizardType }; - var hue_title = (this.checked) ? 'wiz_hue_e_title' : 'wiz_hue_title'; - changeWizard(data, hue_title, startWizardPhilipsHue); - }); - $("#root_specificOptions_useEntertainmentAPI").trigger("change"); + var ledWizardType = ledType; + var data = { type: ledWizardType }; + var hue_title = 'wiz_hue_title'; + changeWizard(data, hue_title, startWizardPhilipsHue); } else if (ledType == "atmoorb") { var ledWizardType = (this.checked) ? "atmoorb" : ledType; diff --git a/assets/webconfig/js/wizard.js b/assets/webconfig/js/wizard.js index 3f88f6db..79fc9182 100755 --- a/assets/webconfig/js/wizard.js +++ b/assets/webconfig/js/wizard.js @@ -604,7 +604,7 @@ var lightPosTopLeft112 = { hmin: 0, hmax: 0.5, vmin: 0, vmax: 0.15 }; var lightPosTopLeft121 = { hmin: 0.5, hmax: 1, vmin: 0, vmax: 0.15 }; var lightPosTopLeftNewMid = { hmin: 0.25, hmax: 0.75, vmin: 0, vmax: 0.15 }; -function assignLightPos(id, pos, name) { +function assignLightPos(pos, name) { var i = null; if (pos === "top") @@ -695,52 +695,50 @@ devicesProperties = {}; var hueIPs = []; var hueIPsinc = 0; -var hueLights = null; -var hueGroups = null; +var hueLights = []; +var hueEntertainmentConfigs = []; +var hueEntertainmentServices = []; var lightLocation = []; var groupLights = []; +var groupChannels = []; var groupLightsLocations = []; -var hueType = "philipshue"; +var isAPIv2Ready = true; +var isEntertainmentReady = true; function startWizardPhilipsHue(e) { - if (typeof e.data.type != "undefined") hueType = e.data.type; - //create html var hue_title = 'wiz_hue_title'; - var hue_intro1 = 'wiz_hue_intro1'; + var hue_intro1 = 'wiz_hue_e_intro1'; var hue_desc1 = 'wiz_hue_desc1'; var hue_create_user = 'wiz_hue_create_user'; - if (hueType == 'philipshueentertainment') { - hue_title = 'wiz_hue_e_title'; - hue_intro1 = 'wiz_hue_e_intro1'; - hue_desc1 = 'wiz_hue_e_desc1'; - hue_create_user = 'wiz_hue_e_create_user'; - } + $('#wiz_header').html('' + $.i18n(hue_title)); $('#wizp1_body').html('

' + $.i18n(hue_title) + '

' + $.i18n(hue_intro1) + '

'); $('#wizp1_footer').html(''); $('#wizp2_body').html('
'); - var hidePort = "hidden-lg"; - if (storedAccess === 'expert') { - hidePort = ""; - } - - $('#wh_topcontainer').append('

' + $.i18n(hue_desc1) + '

' + + var topContainer_html = '

' + $.i18n(hue_desc1) + '

' + '
' + '
' + '

' + $.i18n('wiz_hue_ip') + '

' + '
' + ' ' + - '
' + - '
' + - ' :' + - '
' + - '

' - ); - $('#wh_topcontainer').append(); - $('#wh_topcontainer').append(''); + ' ' + '' + + '
' + + ' ' + + '
'; + + if (storedAccess === 'expert') { + topContainer_html += '
' + + ':' + + '
'; + } + + topContainer_html += '

'; + topContainer_html += ''; + + $('#wh_topcontainer').append(topContainer_html); $('#usrcont').append('

' + $.i18n('wiz_hue_username') + '

' + '
' + @@ -751,23 +749,18 @@ function startWizardPhilipsHue(e) { '
' ); - if (hueType == 'philipshueentertainment') { - $('#usrcont').append('

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


'); - } + $('#usrcont').append('

' + $.i18n('wiz_hue_clientkey') + + '


'); $('#usrcont').append('

<\p>' + ''); - if (hueType == 'philipshueentertainment') { - $('#wizp2_body').append('

'); - createTable("gidsh", "gidsb", "hue_grp_ids_t"); - $('.gidsh').append(createTableRow([$.i18n('edt_dev_spec_groupId_title'), $.i18n('wiz_hue_e_use_group')], true)); - $('#wizp2_body').append(''); - } - else { - $('#wizp2_body').append(''); - } + $('#wizp2_body').append(''); + createTable("gidsh", "gidsb", "hue_grp_ids_t"); + $('.gidsh').append(createTableRow([$.i18n('edt_dev_spec_groupId_title'), ""], true)); + + $('#wizp2_body').append(''); + createTable("lidsh", "lidsb", "hue_ids_t"); $('.lidsh').append(createTableRow([$.i18n('edt_dev_spec_lightid_title'), $.i18n('wiz_pos'), $.i18n('wiz_identify')], true)); $('#wizp2_footer').html(''); @@ -793,13 +786,26 @@ function startWizardPhilipsHue(e) { function checkHueBridge(cb, hueUser) { var usr = (typeof hueUser != "undefined") ? hueUser : 'config'; - if (usr == 'config') $('#wiz_hue_discovered').html(""); + if (usr === 'config') { + $('#wiz_hue_discovered').html(""); + } if (hueIPs[hueIPsinc]) { var host = hueIPs[hueIPsinc].host; var port = hueIPs[hueIPsinc].port; - getProperties_hue_bridge(cb, decodeURIComponent(host), port, usr); + if (usr != '') + { + getProperties_hue_bridge(cb, decodeURIComponent(host), port, usr); + } + else + { + cb(false, usr); + } + + if (isAPIv2Ready) { + $('#port').val(443); + } } } @@ -811,37 +817,51 @@ function checkBridgeResult(reply, usr) { $('#port').val(hueIPs[hueIPsinc].port) $('#usrcont').toggle(true); - checkHueBridge(checkUserResult, $('#user').val() ? $('#user').val() : "newdeveloper"); + + checkHueBridge(checkUserResult, $('#user').val()); } else { - //increment and check again - if (hueIPs.length - 1 > hueIPsinc) { - hueIPsinc++; - checkHueBridge(checkBridgeResult); - } - else { - $('#usrcont').toggle(false); - $('#wiz_hue_ipstate').html($.i18n('wiz_hue_failure_ip')); - } + $('#usrcont').toggle(false); + $('#wiz_hue_ipstate').html($.i18n('wiz_hue_failure_ip')); } }; -function checkUserResult(reply, usr) { +function checkUserResult(reply, username) { $('#usrcont').toggle(true); - if (reply) { - $('#user').val(usr); - if (hueType == 'philipshueentertainment' && $('#clientkey').val() == "") { + var hue_create_user = 'wiz_hue_e_create_user'; + if (!isEntertainmentReady) { + hue_create_user = 'wiz_hue_create_user'; + $('#hue_client_key_r').toggle(false); + } else { + $('#hue_client_key_r').toggle(true); + } + + $('#wiz_hue_create_user').text($.i18n(hue_create_user)); + $('#wiz_hue_create_user').toggle(true); + + if (reply) { + $('#user').val(username); + + if (isEntertainmentReady && $('#clientkey').val() == "") { $('#wiz_hue_usrstate').html($.i18n('wiz_hue_e_clientkey_needed')); $('#wiz_hue_create_user').toggle(true); } else { $('#wiz_hue_usrstate').html(""); $('#wiz_hue_create_user').toggle(false); - if (hueType == 'philipshue') { - get_hue_lights(); - } - if (hueType == 'philipshueentertainment') { - get_hue_groups(); + + if (isEntertainmentReady) { + $('#hue_id_headline').text($.i18n('wiz_hue_e_desc3')); + $('#hue_grp_ids_t').toggle(true); + + get_hue_groups(username); + + } else { + $('#hue_id_headline').text($.i18n('wiz_hue_desc2')); + $('#hue_grp_ids_t').toggle(false); + + get_hue_lights(username); + } } } @@ -852,22 +872,73 @@ function checkUserResult(reply, usr) { } }; -function useGroupId(id) { - $('#groupId').val(id); +function useGroupId(id, username) { + $('#groupId').val(hueEntertainmentConfigs[id].id); + if (isAPIv2Ready) { + var group = hueEntertainmentConfigs[id]; - //Ensure ligthIDs are strings - groupLights = hueGroups[id].lights.map(num => { - return String(num); - }); + groupLights = []; + for (const light of group.light_services) { + groupLights.push(light.rid); + } - groupLightsLocations = hueGroups[id].locations; - get_hue_lights(); + groupChannels = []; + for (const channel of group.channels) { + groupChannels.push(channel); + } + + groupLightsLocations = []; + for (const location of group.locations.service_locations) { + groupLightsLocations.push(location); + } + } else { + //Ensure ligthIDs are strings + groupLights = hueEntertainmentConfigs[id].lights.map(num => { + return String(num); + }); + + var lightLocations = hueEntertainmentConfigs[id].locations; + for (var locationID in lightLocations) { + var lightLocation = {}; + + let position = { + x: lightLocations[locationID][0], + y: lightLocations[locationID][1], + z: lightLocations[locationID][2] + }; + lightLocation.position = position; + + groupLightsLocations.push(lightLocation); + } + } + + get_hue_lights(username); } +function updateBridgeDetails(properties) { + var ledDeviceProperties = properties.config; + + if (!jQuery.isEmptyObject(ledDeviceProperties)) { + isEntertainmentReady = properties.isEntertainmentReady; + isAPIv2Ready = properties.isAPIv2Ready; + + if (ledDeviceProperties.name && ledDeviceProperties.bridgeid && ledDeviceProperties.modelid) { + $('#wiz_hue_discovered').html( + "Bridge: " + ledDeviceProperties.name + + ", Modelid: " + ledDeviceProperties.modelid + + ", Firmware: " + ledDeviceProperties.swversion + "
" + + "API-Version: " + ledDeviceProperties.apiversion + + ", Entertainment: " + (isEntertainmentReady ? "✓" : "-") + + ", APIv2: " + (isAPIv2Ready ? "✓" : "-") + ); + } + } +} async function discover_hue_bridges() { $('#wiz_hue_ipstate').html($.i18n('edt_dev_spec_devices_discovery_inprogress')); - $('#wiz_hue_discovered').html("") + + // $('#wiz_hue_discovered').html("") const res = await requestLedDeviceDiscovery('philipshue'); if (res && !res.error) { const r = res.info; @@ -903,11 +974,6 @@ async function discover_hue_bridges() { port = device.port; } - //Remap https port to http port until Hue-API v2 is supported - if (port == 443) { - port = 80; - } - if (host) { if (!hueIPs.some(item => item.host === host)) { @@ -916,22 +982,39 @@ async function discover_hue_bridges() { } } } + $('#wiz_hue_ipstate').html(""); $('#host').val(hueIPs[hueIPsinc].host) $('#port').val(hueIPs[hueIPsinc].port) - var usr = $('#user').val(); - if (usr != "") { - checkHueBridge(checkUserResult, usr); - } else { - checkHueBridge(checkBridgeResult); + $('#hue_bridge_select').html(""); + + for (var key in hueIPs) { + $('#hue_bridge_select').append(createSelOpt(key, hueIPs[key].host)); } + + $('.hue_bridge_sel_watch').on("click", function () { + hueIPsinc = $(this).val(); + + var name = $("#hue_bridge_select option:selected").text(); + $('#host').val(name); + $('#port').val(hueIPs[hueIPsinc].port) + + var usr = $('#user').val(); + if (usr != "") { + checkHueBridge(checkUserResult, usr); + } else { + checkHueBridge(checkBridgeResult); + } + }); + + $('.hue_bridge_sel_watch').click(); } } } async function getProperties_hue_bridge(cb, hostAddress, port, username, resourceFilter) { - let params = { host: hostAddress, user: username, filter: resourceFilter }; + let params = { host: hostAddress, username: username, filter: resourceFilter }; if (port !== 'undefined') { params.port = parseInt(port); } @@ -945,23 +1028,27 @@ async function getProperties_hue_bridge(cb, hostAddress, port, username, resourc } // Use device's properties, if properties in chache - if (devicesProperties[ledType][key]) { + if (devicesProperties[ledType][key] && devicesProperties[ledType][key][username]) { + updateBridgeDetails(devicesProperties[ledType][key]); cb(true, username); } else { const res = await requestLedDeviceProperties(ledType, params); - - if (res && !res.error) { var ledDeviceProperties = res.info.properties; if (!jQuery.isEmptyObject(ledDeviceProperties)) { + devicesProperties[ledType][key] = {}; + devicesProperties[ledType][key][username] = ledDeviceProperties; + + isAPIv2Ready = res.info.isAPIv2Ready; + devicesProperties[ledType][key].isAPIv2Ready = isAPIv2Ready; + isEntertainmentReady = res.info.isEntertainmentReady; + devicesProperties[ledType][key].isEntertainmentReady = isEntertainmentReady; + + updateBridgeDetails(devicesProperties[ledType][key]); if (username === "config") { - if (ledDeviceProperties.name && ledDeviceProperties.bridgeid && ledDeviceProperties.modelid) { - $('#wiz_hue_discovered').html("Bridge: " + ledDeviceProperties.name + ", Modelid: " + ledDeviceProperties.modelid + ", API-Version: " + ledDeviceProperties.apiversion); - cb(true); - } + cb(true); } else { - devicesProperties[ledType][key] = ledDeviceProperties; cb(true, username); } } else { @@ -973,12 +1060,12 @@ async function getProperties_hue_bridge(cb, hostAddress, port, username, resourc } } -async function identify_hue_device(hostAddress, port, username, id) { +async function identify_hue_device(hostAddress, port, username, name, id, id_v1) { var disabled = $('#btn_wiz_save').is(':disabled'); // Take care that new record cannot be save during background process $('#btn_wiz_save').prop('disabled', true); - let params = { host: decodeURIComponent(hostAddress), user: username, lightId: id }; + let params = { host: decodeURIComponent(hostAddress), username: username, lightName: decodeURIComponent(name), lightId: id, lightId_v1: id_v1 }; if (port !== 'undefined') { params.port = parseInt(port); @@ -1003,11 +1090,9 @@ function beginWizardHue() { $('#user').val(usr); } - if (hueType == 'philipshueentertainment') { - var clkey = eV("clientkey"); - if (clkey != "") { - $('#clientkey').val(clkey); - } + var clkey = eV("clientkey"); + if (clkey != "") { + $('#clientkey').val(clkey); } //check if host is empty/reachable/search for bridge @@ -1022,13 +1107,13 @@ function beginWizardHue() { $('#host').val(host); var port = eV("port"); - if (port == 0) { - $('#port').val(80); - } - else { + if (port > 0) { $('#port').val(port); } - hueIPs.unshift({ host: host, port: port }); + else { + $('#port').val(''); + } + hueIPs.push({ host: host, port: port }); if (usr != "") { checkHueBridge(checkUserResult, usr); @@ -1038,18 +1123,18 @@ function beginWizardHue() { } $('#retry_bridge').off().on('click', function () { + var host = $('#host').val(); + var port = parseInt($('#port').val()); - if ($('#host').val() != "") { + if (host != "") { - hueIPs = []; - hueIPsinc = 0; - - var port = $('#port').val(); - if (isNaN(port) || port < 1 || port > 65535) { - port = 80; - $('#port').val(80); + var idx = hueIPs.findIndex(item => item.host === host && item.port === port); + if (idx === -1) { + hueIPs.push({ host: host, port: port }); + hueIPsinc = hueIPs.length - 1; + } else { + hueIPsinc = idx; } - hueIPs.push({ host: $('#host').val(), port: port }); } else { discover_hue_bridges(); @@ -1064,29 +1149,177 @@ function beginWizardHue() { }); $('#retry_usr').off().on('click', function () { - checkHueBridge(checkUserResult, $('#user').val() ? $('#user').val() : "newdeveloper"); + checkHueBridge(checkUserResult, $('#user').val()); }); $('#wiz_hue_create_user').off().on('click', function () { - if ($('#host').val() != "") { - hueIPs.unshift({ host: $('#host').val(), port: $('#port').val() }); - } createHueUser(); }); + function assignLightEntertainmentPos(isFocusCenter, position, name, id) { + + var x = position.x; + var z = position.z; + + if (isFocusCenter) { + // Map lights as in centered range -0.5 to 0.5 + if (x < -0.5) { + x = -0.5; + } else if (x > 0.5) { + x = 0.5; + } + if (z < -0.5) { + z = -0.5; + } else if (z > 0.5) { + z = 0.5; + } + } else { + // Map lights as in full range -1 to 1 + x /= 2; + z /= 2; + } + + var h = x + 0.5; + var v = -z + 0.5; + + var hmin = h - 0.05; + var hmax = h + 0.05; + var vmin = v - 0.05; + var vmax = v + 0.05; + + let layoutObject = { + hmin: hmin < 0 ? 0 : hmin, + hmax: hmax > 1 ? 1 : hmax, + vmin: vmin < 0 ? 0 : vmin, + vmax: vmax > 1 ? 1 : vmax, + name: name + }; + + if (id) { + layoutObject.name += "_" + id; + } + return layoutObject; + } + + function assignSegmentedLightPos(segment, position, name) { + var layoutObjects = []; + + var segTotalLength = 0; + for (var key in segment) { + + segTotalLength += segment[key].length; + } + + var min; + var max; + var horizontal = true; + + var layoutObject = assignLightPos(position, name); + if (position === "left" || position === "right") { + // vertical distribution + min = layoutObject.vmin; + max = layoutObject.vmax; + horizontal = false; + + } else { + // horizontal distribution + min = layoutObject.hmin; + max = layoutObject.hmax; + } + + var step = (max - min) / segTotalLength; + var start = min; + + for (var key in segment) { + min = start; + max = round(start + segment[key].length * step); + + if (horizontal) { + layoutObject.hmin = min; + layoutObject.hmax = max; + } else { + layoutObject.vmin = min; + layoutObject.vmax = max; + } + layoutObject.name = name + "_" + key; + layoutObjects.push(JSON.parse(JSON.stringify(layoutObject))); + + start = max; + } + + return layoutObjects; + } + $('#btn_wiz_save').off().on("click", function () { var hueLedConfig = []; var finalLightIds = []; + var channelNumber = 0; //create hue led config - for (var key in hueLights) { - if (hueType == 'philipshueentertainment') { - if (groupLights.indexOf(key) == -1) continue; - } - if ($('#hue_' + key).val() != "disabled") { - finalLightIds.push(key); - var idx_content = assignLightPos(key, $('#hue_' + key).val(), hueLights[key].name); - hueLedConfig.push(JSON.parse(JSON.stringify(idx_content))); + for (var key in groupLights) { + var lightId = groupLights[key]; + + if ($('#hue_' + lightId).val() != "disabled") { + finalLightIds.push(lightId); + + var lightName; + if (isAPIv2Ready) { + var light = hueLights.find(light => light.id === lightId); + lightName = light.metadata.name; + } else { + lightName = hueLights[lightId].name; + } + + var position = $('#hue_' + lightId).val(); + var lightIdx = groupLights.indexOf(lightId); + var lightLocation = groupLightsLocations[lightIdx]; + + var serviceID; + if (isAPIv2Ready) { + serviceID = lightLocation.service.rid; + } + + if (position.startsWith("entertainment")) { + + // Layout per entertainment area definition at bridge + var isFocusCenter = false; + if (position === "entertainment_center") { + isFocusCenter = true; + } + + if (isAPIv2Ready) { + + groupChannels.forEach((channel) => { + if (channel.members[0].service.rid === serviceID) { + var layoutObject = assignLightEntertainmentPos(isFocusCenter, channel.position, lightName, channel.channel_id); + hueLedConfig.push(JSON.parse(JSON.stringify(layoutObject))); + ++channelNumber; + } + }); + } else { + var layoutObject = assignLightEntertainmentPos(isFocusCenter, lightLocation.position, lightName); + hueLedConfig.push(JSON.parse(JSON.stringify(layoutObject))); + } + } + else { + // Layout per manual settings + var maxSegments = 1; + + if (isAPIv2Ready) { + var service = hueEntertainmentServices.find(service => service.id === serviceID); + maxSegments = service.segments.max_segments; + } + + if (maxSegments > 1) { + var segment = service.segments.segments; + var layoutObjects = assignSegmentedLightPos(segment, position, lightName); + hueLedConfig.push(...layoutObjects); + } else { + var layoutObject = assignLightPos(position, lightName); + hueLedConfig.push(JSON.parse(JSON.stringify(layoutObject))); + } + channelNumber += maxSegments; + } } } @@ -1121,7 +1354,7 @@ function beginWizardHue() { d.brightnessFactor = parseFloat(eV("brightnessFactor", 1)); d.clientkey = $('#clientkey').val(); - d.groupId = parseInt($('#groupId').val()); + d.groupId = $('#groupId').val(); d.blackLightsTimeout = parseInt(eV("blackLightsTimeout", 5000)); d.brightnessMin = parseFloat(eV("brightnessMin", 0)); d.brightnessMax = parseFloat(eV("brightnessMax", 1)); @@ -1134,8 +1367,16 @@ function beginWizardHue() { d.enableAttempts = parseInt(conf_editor.getEditor("root.generalOptions.enableAttempts").getValue()); d.enableAttemptsInterval = parseInt(conf_editor.getEditor("root.generalOptions.enableAttemptsInterval").getValue()); - if (hueType == 'philipshue') { - d.useEntertainmentAPI = false; + d.useEntertainmentAPI = isEntertainmentReady; + d.useAPIv2 = isAPIv2Ready; + + if (isEntertainmentReady) { + d.hardwareLedCount = channelNumber; + if (window.serverConfig.device.type !== d.type) { + //smoothing on, if new device + sc.smoothing = { enable: true }; + } + } else { d.hardwareLedCount = finalLightIds.length; d.verbose = false; if (window.serverConfig.device.type !== d.type) { @@ -1144,15 +1385,6 @@ function beginWizardHue() { } } - if (hueType == 'philipshueentertainment') { - d.useEntertainmentAPI = true; - d.hardwareLedCount = groupLights.length; - if (window.serverConfig.device.type !== d.type) { - //smoothing on, if new device - sc.smoothing = { enable: true }; - } - } - window.serverConfig.device = d; requestWriteConfig(sc, true); @@ -1163,7 +1395,6 @@ function beginWizardHue() { } function createHueUser() { - var host = hueIPs[hueIPsinc].host; var port = hueIPs[hueIPsinc].port; @@ -1208,7 +1439,8 @@ function createHueUser() { conf_editor.getEditor("root.specificOptions.host").setValue(host); conf_editor.getEditor("root.specificOptions.port").setValue(port); } - if (hueType == 'philipshueentertainment') { + + if (isEntertainmentReady) { var clientkey = response.clientkey; if (clientkey != 'undefined') { $('#clientkey').val(clientkey); @@ -1230,37 +1462,52 @@ function createHueUser() { }, retryInterval * 1000); } -function get_hue_groups() { - +function get_hue_groups(username) { var host = hueIPs[hueIPsinc].host; - if (devicesProperties['philipshue'][host]) { - var ledProperties = devicesProperties['philipshue'][host]; + if (devicesProperties['philipshue'][host] && devicesProperties['philipshue'][host][username]) { + var ledProperties = devicesProperties['philipshue'][host][username]; - if (!jQuery.isEmptyObject(ledProperties)) { - hueGroups = ledProperties.groups; - if (Object.keys(hueGroups).length > 0) { - - $('.lidsb').html(""); - $('#wh_topcontainer').toggle(false); - $('#hue_grp_ids_t').toggle(true); - - var gC = 0; - for (var groupid in hueGroups) { - if (hueGroups[groupid].type == 'Entertainment') { - $('.gidsb').append(createTableRow([groupid + ' (' + hueGroups[groupid].name + ')', ''])); - gC++; - } - } - if (gC == 0) { - noAPISupport('wiz_hue_e_noegrpids'); + if (isAPIv2Ready) { + if (!jQuery.isEmptyObject(ledProperties.data)) { + if (Object.keys(ledProperties.data).length > 0) { + hueEntertainmentConfigs = ledProperties.data.filter(config => { + return config.type === "entertainment_configuration"; + }); + hueEntertainmentServices = ledProperties.data.filter(config => { + return (config.type === "entertainment" && config.renderer === true); + }); } } + } else { + if (!jQuery.isEmptyObject(ledProperties.groups)) { + hueEntertainmentConfigs = []; + var hueGroups = ledProperties.groups; + for (var groupid in hueGroups) { + if (hueGroups[groupid].type == 'Entertainment') { + hueGroups[groupid].id = groupid; + hueEntertainmentConfigs.push(hueGroups[groupid]); + } + } + } + } + + if (Object.keys(hueEntertainmentConfigs).length > 0) { + + $('.lidsb').html(""); + $('#wh_topcontainer').toggle(false); + $('#hue_grp_ids_t').toggle(true); + + for (var groupid in hueEntertainmentConfigs) { + $('.gidsb').append(createTableRow([groupid + ' (' + hueEntertainmentConfigs[groupid].name + ')', ''])); + } + } else { + noAPISupport('wiz_hue_e_noegrpids', username); } } } -function noAPISupport(txt) { +function noAPISupport(txt, username) { showNotification('danger', $.i18n('wiz_hue_e_title'), $.i18n('wiz_hue_e_noapisupport_hint')); conf_editor.getEditor("root.specificOptions.useEntertainmentAPI").setValue(false); $("#root_specificOptions_useEntertainmentAPI").trigger("change"); @@ -1269,51 +1516,82 @@ function noAPISupport(txt) { var txt = (txt) ? $.i18n(txt) : $.i18n('wiz_hue_e_nogrpids'); $('

' + txt + '
' + $.i18n('wiz_hue_e_noapisupport') + '

').insertBefore('#wizp2_body #hue_ids_t'); $('#hue_id_headline').html($.i18n('wiz_hue_desc2')); - hueType = 'philipshue'; - get_hue_lights(); + + get_hue_lights(username); } -function get_hue_lights() { - +function get_hue_lights(username) { var host = hueIPs[hueIPsinc].host; - if (devicesProperties['philipshue'][host]) { - var ledProperties = devicesProperties['philipshue'][host]; + if (devicesProperties['philipshue'][host] && devicesProperties['philipshue'][host][username]) { + var ledProperties = devicesProperties['philipshue'][host][username]; - if (!jQuery.isEmptyObject(ledProperties.lights)) { - hueLights = ledProperties.lights; - if (Object.keys(hueLights).length > 0) { - if (hueType == 'philipshue') { - $('#wh_topcontainer').toggle(false); + if (isAPIv2Ready) { + if (!jQuery.isEmptyObject(ledProperties.data)) { + if (Object.keys(ledProperties.data).length > 0) { + hueLights = ledProperties.data.filter(config => { + return config.type === "light"; + }); } - $('#hue_ids_t, #btn_wiz_save').toggle(true); + } + } else { + if (!jQuery.isEmptyObject(ledProperties.lights)) { + hueLights = ledProperties.lights; + } + } - var 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" - ]; + if (Object.keys(hueLights).length > 0) { + if (!isEntertainmentReady) { + $('#wh_topcontainer').toggle(false); + } + $('#hue_ids_t, #btn_wiz_save').toggle(true); - if (hueType == 'philipshue') { - lightOptions.unshift("disabled"); + var 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" + ]; + + if (isEntertainmentReady) { + lightOptions.unshift("entertainment_center"); + lightOptions.unshift("entertainment"); + } else { + lightOptions.unshift("disabled"); + groupLights = Object.keys(hueLights); + } + + $('.lidsb').html(""); + + var pos = ""; + for (var id in groupLights) { + var lightId = groupLights[id]; + var lightId_v1 = "/lights/" + lightId; + + var lightName; + if (isAPIv2Ready) { + var light = hueLights.find(light => light.id === lightId); + lightName = light.metadata.name; + lightId_v1 = light.id_v1; + } else { + lightName = hueLights[lightId].name; } - $('.lidsb').html(""); - var pos = ""; - for (var lightid in hueLights) { - if (hueType == 'philipshueentertainment') { - if (groupLights.indexOf(lightid) == -1) continue; + if (isEntertainmentReady) { + var lightLocation = {}; + lightLocation = groupLightsLocations[id]; + if (lightLocation) { + if (isAPIv2Ready) { + pos = 0; + } else { + var x = lightLocation.position.x; + var y = lightLocation.position.y; + var z = lightLocation.position.z; - if (groupLightsLocations.hasOwnProperty(lightid)) { - lightLocation = groupLightsLocations[lightid]; - var x = lightLocation[0]; - var y = lightLocation[1]; - var z = lightLocation[2]; var xval = (x < 0) ? "left" : "right"; if (z != 1 && x >= -0.25 && x <= 0.25) xval = ""; switch (z) { @@ -1329,37 +1607,39 @@ function get_hue_lights() { } } } - var options = ""; - for (var opt in lightOptions) { - var val = lightOptions[opt]; - var txt = (val != 'entire' && val != 'disabled') ? 'conf_leds_layout_cl_' : 'wiz_ids_'; - options += '
-
-
-
-
-

-

- -
- + diff --git a/assets/webconfig/i18n/he.json b/assets/webconfig/i18n/he.json new file mode 100644 index 00000000..df977a15 --- /dev/null +++ b/assets/webconfig/i18n/he.json @@ -0,0 +1,155 @@ +{ + "InfoDialog_access_text": "בהתאם לרמת ההגדרות תוכל להתאים אפשרויות נוספות או לקבל גישה לתכונות נוספות. רמת \"ברירת המחדל\" המומלצת היא.", + "InfoDialog_iswitch_text": "אם אתה מפעיל את Hyperion יותר מפעם אחת ברשת המקומית שלך, תוכל לעבור בין תצורות האינטרנט. בחר את המופע של Hyperion למטה והחלף!", + "InfoDialog_nostorage_text": "הדפדפן שלך אינו תומך ב-localStorage. לא ניתן לשמור הגדרת שפה מסוימת (חזרה ל'זיהוי אוטומטי') ורמת גישה (חזרה ל'ברירת מחדל'). קוסמים מסוימים עשויים להיות מוסתרים. אתה עדיין יכול להשתמש בממשק האינטרנט ללא בעיות נוספות", + "InfoDialog_nowrite_text": "Hyperion לא יכול לכתוב לקובץ התצורה הנטען הנוכחי שלך. אנא תקן את הרשאות הקובץ כדי להמשיך.", + "conf_colors_blackborder_intro": "דלג על פסים שחורים באשר הם. כל מצב משתמש באלגוריתם זיהוי אחר המכוון למצבים מיוחדים. תעלה את הסף אם זה לא עובד לך.", + "conf_colors_color_intro": "צור פרופיל כיול אחד או יותר, התאם כל צבע, בהירות, ליניאריזציה ועוד.", + "conf_colors_smoothing_intro": "החלקה משטחת שינויי צבע/בהירות כדי להפחית הסחת דעת מעצבנת.", + "conf_effect_bgeff_intro": "הגדר אפקט/צבע רקע, המוצג בזמן \"בטלה\" של Hyperion. מתחיל תמיד בערוץ עדיפות 255.", + "conf_effect_fgeff_intro": "הגדר אפקט אתחול או צבע, שיוצג במהלך האתחול של Hyperion למשך הזמן שהוגדר.", + "conf_effect_path_intro": "טען אפקטים מהנתיבים המוגדרים. בנוסף, אתה יכול להשבית אפקטים בודדים לפי שם כדי להסתיר אותם מכל רשימות האפקטים.", + "conf_general_impexp_expbtn": "ייצא", + "conf_general_impexp_impbtn": "ייבא", + "conf_general_impexp_l1": "ייבא הגדרה על ידי בחירת קובץ הגדרות למטה ולחץ על \"ייבוא\".", + "conf_general_impexp_l2": "ייצא הגדרה על ידי בחירת קובץ הגדרות למטה ולחץ על \"ייצוא\".", + "conf_general_impexp_title": "תצורת ייבוא/ייצוא", + "conf_general_intro": "הגדרות בסיסיות סביב Hyperion ו-WebUI שאינן מתאימות לקטגוריה אחרת.", + "conf_general_label_title": "הגדרות כלליות", + "conf_grabber_fg_intro": "לכידת מסך היא לכידת המערכת המקומית שלך כמקור קלט, Hyperion מותקן על.", + "conf_grabber_v4l_intro": "לכידת USB היא התקן (לכידה) המחובר באמצעות USB המשמש להזנת תמונות מקור לעיבוד.", + "conf_helptable_expl": "הֶסבֵּר", + "conf_helptable_option": "אפשרות", + "conf_leds_contr_label_contrtype": "סוג בקר", + "conf_leds_device_intro": "Hyperion תומך בהרבה בקרים להעברת נתונים למכשיר היעד שלך. בחר בקר LED מתוך הרשימה הממוינת והגדר אותו. בחרנו את הגדרות ברירת המחדל הטובות ביותר עבור כל מכשיר.", + "conf_leds_layout_btn_checklist": "הצג רשימת בדיקה", + "conf_leds_layout_checkp1": "ה-LED השחור הוא ה-LED הראשון שלך, LED הראשון הוא הנקודה שבה אתה מזין את אות הנתונים שלך.", + "conf_leds_layout_checkp2": "הפריסה היא תמיד המראה הקדמי של הטלוויזיה שלך, לעולם לא התצוגה האחורית.", + "conf_leds_layout_checkp3": "ודא שהכיוון נכון. הנוריות האפורות מציינות את מספר LED 2 ו-3 כדי להמחיש את כיוון הנתונים.", + "conf_leds_layout_checkp4": "בחר פער: בכדי ליצור פער, התעלם ממנו תחילה כאשר אתה מגדיר עליון/תחתון/שמאל/ימין והגדר לאחר מכן את אורך הפער כדי להסיר כמות לדים. שנה את מיקום הפער עד שיתאים.", + "conf_leds_layout_frame": "פריסה קלאסית (מסגרת LED)", + "conf_leds_layout_generatedconf": "תצורת LED שנוצרה/נוכחית", + "conf_leds_layout_intro": "אתה גם צריך פריסת LED, המשקפת את עמדות LED שלך. הפריסה הקלאסית היא מסגרת הטלוויזיה המשמשת בדרך כלל, אך אנו תומכים גם ביצירת מטריצות LED (קירות LED). התצוגה על פריסה זו היא תמיד מחזית הטלוויזיה שלך.", + "conf_leds_layout_matrix": "פריסת מטריצה (קיר LED)", + "conf_leds_layout_textf1": "שדה טקסט זה מציג כברירת מחדל את הפריסה הנטענת הנוכחית שלך וייחלף אם תיצור פריסה חדשה עם האפשרויות שלמעלה. לחלופין, תוכל לבצע עריכות נוספות.", + "conf_leds_nav_label_ledcontroller": "בקר LED", + "conf_leds_nav_label_ledlayout": "פריסת LED", + "conf_leds_optgroup_network": "רשת", + "conf_logging_label_intro": "אזור לבדיקת הודעות יומן, תראה יותר או פחות מידע בהתאם לרמת הרישום שנקבעה.", + "conf_network_forw_intro": "העבר את כל הקלט להתקנה שנייה של Hyperion שניתן להניע עם בקר LED אחר", + "conf_network_proto_intro": "ה-PROTO-Port של כל המופעים של Hyperion, המשמש עבור זרמי תמונות (HyperionScreenCap, Kodi Addon, Android Hyperion Grabber, ...)", + "dashboard_alert_message_confedit": "תצורת ה-Hyperion שלך שונתה. כדי להחיל אותו, הפעל מחדש את Hyperion.", + "dashboard_alert_message_confedit_t": "התצורה השתנתה", + "dashboard_alert_message_confsave_success": "תצורת ה-Hyperion שלך נשמרה בהצלחה. השינויים שלך פעילים כעת.", + "dashboard_alert_message_confsave_success_t": "התצורה נשמרה", + "dashboard_componentbox_label_comp": "רְכִיב", + "dashboard_componentbox_label_status": "מצב", + "dashboard_componentbox_label_title": "מצב הרכיבים", + "dashboard_infobox_label_currenthyp": "גרסת ה-Hyperion שלך:", + "dashboard_infobox_label_instance": "למשל:", + "dashboard_infobox_label_latesthyp": "הגרסה האחרונה של Hyperion:", + "dashboard_infobox_label_platform": "פלטפורמה:", + "dashboard_infobox_label_ports": "יציאות", + "dashboard_infobox_label_smartacc": "גישה חכמה", + "dashboard_infobox_label_statush": "מצב ה-Hyperion:", + "dashboard_infobox_label_title": "מידע", + "dashboard_infobox_message_updatesuccess": "הינך מריץ את הגרסה האחרונה של Hyperion.", + "dashboard_infobox_message_updatewarning": "גרסה חדשה יותר של Hyperion זמינה! ($1)", + "dashboard_label_intro": "לוח המחוונים נותן לך סקירה מהירה על מצב ה- Hyperion", + "dashboard_newsbox_label_title": "בלוג ה-Hyperion", + "dashboard_newsbox_noconn": "לא ניתן להתחבר לשרת Hyperion כדי לאחזר את הפוסטים האחרונים, האם חיבור האינטרנט שלך תקין?", + "dashboard_newsbox_readmore": "קרא עוד", + "dashboard_newsbox_visitblog": "בקר ב-Hyperion בלוג ", + "edt_conf_color_brightnessComp_expl": "מפצה על הבדלי בהירות בין אדום ירוק כחול, ציאן מגנטה צהוב ולבן. 100 פירושו פיצוי מלא, 0 אין פיצוי", + "edt_conf_color_channelAdjustment_header_expl": "צור פרופילי צבע שניתן להקצות לרכיב מסוים. התאם צבע, גמא, בהירות, פיצוי ועוד.", + "edt_conf_v4l2_fpsSoftwareDecimation_expl": "כדי לחסוך במשאבים כל מסגרת n' תעובד בלבד. למשל. אם ה-grabber מוגדר ל-30fps עם אפשרות זו מוגדרת ל-5, התוצאה הסופית תהיה בסביבות 6fps", + "edt_conf_v4l2_signalDetection_expl": "אם מופעל, לכידת USB תושבת זמנית כאשר לא נמצא אות. זה יקרה כאשר התמונה תרד מתחת לערך הסף למשך תקופה של 4 שניות.", + "effectsconfigurator_label_intro": "צור מתוך האפקטים הבסיסיים אפקטים חדשים המותאמים לטעמך. בהתאם לאפקט יש אפשרויות כמו צבע, מהירות, כיוון ועוד.", + "general_access_advanced": "מתקדם", + "general_access_default": "ברירת מחדל", + "general_access_expert": "מומחה", + "general_btn_back": "חזור", + "general_btn_cancel": "בטל", + "general_btn_continue": "המשך", + "general_btn_iswitch": "החלף", + "general_btn_next": "הבא", + "general_btn_off": "כיבוי", + "general_btn_ok": "בסדר", + "general_btn_on": "הדלקה", + "general_btn_restarthyperion": "הפעל מחדש את Hyperion", + "general_btn_save": "שמור", + "general_btn_saveandreload": "שמור וטען מחדש", + "general_btn_yes": "כן", + "general_button_savesettings": "שמור הגדרות", + "general_col_blue": "כחול", + "general_col_green": "ירוק", + "general_col_red": "אדום", + "general_comp_BLACKBORDER": "Blackbar זיהוי", + "general_comp_BOBLIGHTSERVER": "שרת Boblight", + "general_comp_FLATBUFSERVER": "שרת ", + "general_comp_FORWARDER": "שילוח", + "general_comp_GRABBER": "לכידת מסך", + "general_comp_LEDDEVICE": "פלט LED", + "general_comp_PROTOSERVER": "שרת מאגר פרוטוקול", + "general_comp_SMOOTHING": "חלק", + "general_comp_V4L": "לכידת כניסת USB", + "general_country_de": "גרמניה", + "general_country_es": "ספרד", + "general_country_fr": "צרפת", + "general_country_it": "איטליה", + "general_country_nl": "הולנד", + "general_country_uk": "בריטניה", + "general_country_us": "ארצות הברית", + "general_speech_cs": "צ'כית", + "general_speech_de": "גרמנית", + "general_speech_en": "אנגלית", + "general_speech_es": "ספרדית", + "general_speech_it": "איטלקית", + "general_webui_title": "Hyperion - תצורת אינטרנט", + "general_wiki_moreto": "מידע נוסף על \"$1\" בוויקי שלנו", + "info_restart_contus": "אם אתה עדיין משוטט כאן לאחר 20 שניות ואין לך מושג למה, בבקשה פתח נושא חדש בפורום התמיכה שלנו...", + "main_ledsim_btn_togglelednumber": "מספר נורות LED", + "main_ledsim_btn_toggleleds": "הצג אורות", + "main_ledsim_btn_togglelivevideo": "וידאו חי", + "main_ledsim_text": "הדמיה חיה של צבעי LED ואפשרות הזרמת הווידאו הנוכחי של מכשיר הלכידה שלך.", + "main_ledsim_title": "חזותיות LED", + "main_menu_about_token": "אודות Hyperion", + "main_menu_colors_conf_token": "עיבוד תמונה", + "main_menu_dashboard_token": "לוּחַ מַחווָנִים", + "main_menu_effect_conf_token": "אפקטים", + "main_menu_effectsconfigurator_token": "הגדרת אפקטים", + "main_menu_general_conf_token": "כללי", + "main_menu_grabber_conf_token": "לכידת חומרה", + "main_menu_input_selection_token": "בחירת קלט", + "main_menu_leds_conf_token": "יצאת LED", + "main_menu_network_conf_token": "שירותי אינטרנט", + "main_menu_remotecontrol_token": "שלט רחוק", + "main_menu_support_token": "תמיכה", + "main_menu_system_token": "מערכת", + "main_menu_update_token": "עדכון", + "main_menu_webconfig_token": "תצורת אינטרנט", + "remote_input_intro": "Hyperion משתמש במערכת עדיפות לבחירת מקור. לכל מה שתגדירו יש עדיפות (אפקט/צבע/צילום מסך/לכידת USB ומקורות רשת). כברירת מחדל, Hyperion בוחר מקורות בהתאם לעדיפות (המספר הנמוך ביותר משקף את המקור הפעיל הנוכחי). כעת יש לך הזדמנות לבחור מקורות בעצמך. $1", + "support_label_intro": "Hyperion היא תוכנה חינמית ללא מטרות רווח. צוות קטן עובד על זה וזו הסיבה שאנחנו צריכים את התמיכה הקבועה שלכם.", + "update_label_intro": "סקירה כללית על כל גרסאות Hyperion הזמינות. בנוסף, תוכל לעדכן או לשדרג לאחור את הגרסה שלך של Hyperion מתי שתרצה. ממוין מהחדש להכי ישן", + "wiz_atmoorb_desc2": "כעת בחר אילו כדורים יש להוסיף. המיקום מקצה את המנורה למיקום מסוים ב\"תמונה\" שלך. מנורות מושבתות לא יתווספו. לזיהוי מנורות בודדות לחץ על הכפתור בצד ימין.", + "wiz_atmoorb_intro1": "אשף זה מגדיר את Hyperion עבור AtmoOrbs. התכונות הן הזיהוי האוטומטי של AtmoOrb, הגדרת כל אור למיקום ספציפי בתמונה שלך או השבתה וביצוע אופטימיזציה של הגדרות Hyperion באופן אוטומטי! אז בקיצור: כל מה שאתה צריך זה כמה קליקים וסיימת!", + "wiz_cc_adjustgamma": "גמא: מה שאתה צריך לעשות הוא, להתאים את רמות הגמא של כל ערוץ עד שתהיה לך אותה כמות נתפסת של כל ערוץ. רמז: ניטרלי הוא 1.0! לדוגמה, אם האפור שלך קצת אדמדם, זה אומר שאתה צריך להגדיל את הגמא האדום כדי להפחית את כמות האדום (ככל שיותר גמא, כך פחות צבע).", + "wiz_cc_adjustit": "התאם את ה-\"$1\" שלך עד שתסתדר עם זה. שימו לב: ככל שתתרחקו מערך ברירת המחדל, ספקטרום הצבעים יהיה מוגבל (גם עבור כל הצבעים שביניהם). תלוי בקול הטלוויזיה/LED", + "wiz_cc_backlight": "בנוסף, אתה יכול להגדיר תאורה אחורית כדי למיין \"צבעים רעים\" באזורים כמעט כהים או אם אתה לא אוהב את המעבר בין צבע לכבוי במהלך הצפייה. בנוסף אתה יכול להגדיר אם צריך להיות בו קצת צבע או רק לבן. זה מושבת במצב \"כבוי\", \"צבע\" ו\"אפקט\".", + "wiz_cc_intro1": "אשף זה ידריך אותך בכיול ה-LED שלך. אם אתה משתמש בקודי, ניתן לשלוח את תמונות הכיול וסרטוני הווידאו ישירות אליו. תנאי מוקדם: עליך להפעיל את \"אפשר שליטה מרחוק מיישומים במערכות אחרות\" בקודי.
לחלופין, ייתכן שתרצה להוריד את הקבצים האלה בעצמך ולהציג אותם כשהאשף יבקש ממך להתאים את ההגדרה.", + "wiz_cc_kodidiscon": "Kodi לא נמצא, המשך ללא תמיכת Kodi (אנא בדוק אם שלט רחוק על ידי מערכות אחרות מופעלת).", + "wiz_cc_summary": "מסקנה של ההגדרות שלך. במהלך הפעלת וידאו, תוכל לשנות או לבדוק שוב ערכים. אם סיימת, לחץ על שמור.", + "wiz_cololight_intro1": "אשף זה מגדיר את Hyperion עבור מערכת Cololight. התכונות הן הזיהוי האוטומטי של Cololight וכוונון את הגדרות Hyperion באופן אוטומטי! בקיצור: כל מה שאתה צריך זה כמה קליקים וסיימת!
הערה: במקרה של Cololight Strip, ייתכן שיהיה עליך לתקן ידנית את ספירת ה-LED ואת הפריסה.", + "wiz_hue_desc1": "1. Hyperion מחפש אוטומטית עבור Hue-Bridge, במקרה שהוא לא יכול למצוא אחד, עליך לספק את שם המארח או כתובת ה-IP וללחוץ על כפתור הטעינה מחדש. \n2. ספק מזהה משתמש, אם אין לך צור אחד חדש.", + "wiz_hue_desc2": "3. כעת בחרו אילו מנורות יש להוסיף. המיקום מקצה את המנורה למיקום מסוים ב\"תמונה\" שלך. מנורות מושבתות לא יתווספו. לזיהוי מנורות בודדות לחץ על הכפתור בצד ימין", + "wiz_hue_e_desc1": "1. Hyperion מחפש אוטומטית עבור Hue-Bridge, במקרה שהוא לא יכול למצוא אחד, עליך לספק את שם המארח או כתובת ה-IP וללחוץ על כפתור הטעינה מחדש.
2. ספק מזהה משתמש ומפתח הלקוח, אם אין לך את שניהם, צור חדשים.", + "wiz_hue_e_desc3": "4. בחר באיזה מיקום המנורה המתאימה צריכה להיות \"בתמונה\". בחירה מראש של העמדה נעשתה בהתבסס על המיקומים המוגדרים של האורות בקבוצת הבידור. זו רק המלצה וניתן להתאים אישית לפי הרצון. לכן אתה יכול להדגיש אותם בקצרה על ידי לחיצה על הכפתור הימני כדי לשפר את הבחירה.", + "wiz_hue_e_intro1": "אשף זה מגדיר את Hyperion עבור מערכת Philips Hue Entertainment הידועה. התכונות הן: זיהוי אוטומטי של Hue Bridge, יצירת מפתחות משתמש ולקוח, בחירת קבוצת בידור, הגדרת אורות קבוצה למיקום ספציפי בתמונה שלך ואופטימיזציה של הגדרות Hyperion באופן אוטומטי! אז בקיצור: כל מה שאתה צריך זה כמה קליקים וסיימת!", + "wiz_hue_failure_user": "המשתמש לא נמצא, צור אחד חדש עם הכפתור למטה או הזן מזהה משתמש חוקי ולחץ על הסמל \"טען מחדש\".", + "wiz_hue_intro1": "אשף זה מגדיר את Hyperion עבור מערכת Philips Hue הידועה. התכונות הן זיהוי אוטומטי של Hue Bridge, יצירת משתמש, הגדר כל אור גוון למיקום מסוים בתמונה שלך או השבת אותו וכוונו את הגדרות Hyperion באופן אוטומטי! אז בקיצור: כל מה שאתה צריך זה כמה קליקים וסיימת!", + "wiz_rgb_expl": "נקודת הצבע מחליפה כל x שניות את הצבע (אדום, ירוק), ובאותו הזמן גם נוריות ה-LED שלך מחליפות את הצבע. ענה על השאלות בתחתית כדי לבדוק/לתקן את סדר הביטים שלך.", + "wiz_rgb_intro1": "אשף זה ידריך אותך בתהליך מציאת סדר הצבעים הנכון עבור הלדים שלך. לחץ על המשך כדי להתחיל.", + "wiz_rgb_intro2": "מתי אתה צריך את האשף הזה? דוגמה: אתה קובע את הצבע האדום, אבל אתה מקבל ירוק או כחול. אתה יכול גם להשתמש בו עבור תצורה ראשונה.", + "wiz_yeelight_desc2": "כעת בחר אילו מנורות יש להוסיף. המיקום מקצה את המנורה למיקום מסוים ב\"תמונה\" שלך. מנורות מושבתות לא יתווספו. לזיהוי מנורות בודדות לחץ על הכפתור בצד ימין.", + "wiz_yeelight_intro1": "אשף זה מגדיר את Hyperion עבור מערכת Yeelight. התכונות הן הזיהוי האוטומטי של Yeelighs, הגדרת כל אור למיקום מסוים בתמונה שלך או להשבית אותו ולכוון את הגדרות Hyperion באופן אוטומטי! אז בקיצור: כל מה שאתה צריך זה כמה קליקים וסיימת!" +} \ No newline at end of file diff --git a/assets/webconfig/js/languages.js b/assets/webconfig/js/languages.js index 21ecf191..b417b55b 100644 --- a/assets/webconfig/js/languages.js +++ b/assets/webconfig/js/languages.js @@ -1,6 +1,6 @@ var storedLang; -var availLang = ['ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'nl', 'nb', 'pl', 'pt', 'ro', 'sv', 'vi', 'ru', 'tr', 'zh-CN']; -var availLangText = ['Català', 'Čeština', 'Dansk', 'Deutsch', 'Ελληνική', 'English', 'Español', 'Français', 'Magyar', 'Italiano', '日本語', 'Nederlands', 'Norsk Bokmål', 'Polski', 'Português', 'Română', 'Svenska', 'Tiếng Việt', 'русский', 'Türkçe', '汉语']; +var availLang = ['ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'fr', 'he', 'hu', 'it', 'ja', 'nl', 'nb', 'pl', 'pt', 'ro', 'sv', 'vi', 'ru', 'tr', 'zh-CN']; +var availLangText = ['Català', 'Čeština', 'Dansk', 'Deutsch', 'Ελληνική', 'English', 'Español', 'Français', 'עִברִית' ,'Magyar', 'Italiano', '日本語', 'Nederlands', 'Norsk Bokmål', 'Polski', 'Português', 'Română', 'Svenska', 'Tiếng Việt', 'русский', 'Türkçe', '汉语']; //$.i18n.debug = true; diff --git a/bin/scripts/install_pr.sh b/bin/scripts/install_pr.sh index bab790a0..1f654f23 100755 --- a/bin/scripts/install_pr.sh +++ b/bin/scripts/install_pr.sh @@ -12,6 +12,8 @@ hasPython3=$? type python > /dev/null 2> /dev/null hasPython2=$? +BASE_PATH='.'; + if [[ "${hasWget}" -ne 0 ]] && [[ "${hasCurl}" -ne 0 ]]; then echo '---> Critical Error: wget or curl required to download pull request artifacts' exit 1 @@ -171,20 +173,20 @@ fi # Download packed PR artifact echo "---> Downloading the Pull Request #$pr_number" if [ $hasCurl -eq 0 ]; then - curl -skH "Authorization: token ${PR_TOKEN}" -o $HOME/temp.zip -L --get $archive_download_url + curl -skH "Authorization: token ${PR_TOKEN}" -o $BASE_PATH/temp.zip -L --get $archive_download_url elif [ $hasWget -eq 0 ]; then echo "wget" - wget --quiet --header="Authorization: token ${PR_TOKEN}" -O $HOME/temp.zip $archive_download_url + wget --quiet --header="Authorization: token ${PR_TOKEN}" -O $BASE_PATH/temp.zip $archive_download_url fi # Create new folder & extract PR artifact echo "---> Extracting packed Artifact" -mkdir -p $HOME/hyperion_pr$pr_number -unzip -p $HOME/temp.zip | tar --strip-components=2 -C $HOME/hyperion_pr$pr_number share/hyperion/ -xz +mkdir -p $BASE_PATH/hyperion_pr$pr_number +unzip -p $BASE_PATH/temp.zip | tar --strip-components=2 -C $BASE_PATH/hyperion_pr$pr_number share/hyperion/ -xz # Delete PR artifact echo '---> Remove temporary files' -rm $HOME/temp.zip 2>/dev/null +rm $BASE_PATH/temp.zip 2>/dev/null # Create the startup script echo '---> Create startup script' @@ -209,7 +211,7 @@ if [[ ! -z ${CURRENT_SERVICE} ]]; then fi fi'"" -TARGET_CONFIGDIR="$HOME/hyperion_pr$pr_number/config" +TARGET_CONFIGDIR="$BASE_PATH/config" if [[ ! -z ${CONFIGDIR} ]]; then STARTUP_SCRIPT+=" @@ -221,19 +223,19 @@ fi STARTUP_SCRIPT+=" # Start PR artifact -cd $HOME/hyperion_pr$pr_number +cd $BASE_PATH/hyperion_pr$pr_number ./bin/hyperiond -d -u $TARGET_CONFIGDIR" # Place startup script -echo "$STARTUP_SCRIPT" > $HOME/hyperion_pr$pr_number/$pr_number.sh +echo "$STARTUP_SCRIPT" > $BASE_PATH/hyperion_pr$pr_number/$pr_number.sh # Set the executen bit -chmod +x -R $HOME/hyperion_pr$pr_number/$pr_number.sh +chmod +x -R $BASE_PATH/hyperion_pr$pr_number/$pr_number.sh echo "*******************************************************************************" echo "Download finished!" $REBOOTMESSAGE -echo "You can test the pull request with this command: ~/hyperion_pr$pr_number/$pr_number.sh" -echo "Remove the test installation with: rm -R ~/hyperion_pr$pr_number" +echo "You can test the pull request with this command: $BASE_PATH/hyperion_pr$pr_number/$pr_number.sh" +echo "Remove the test installation with: rm -R $BASE_PATH/hyperion_pr$pr_number" echo "Feedback is welcome at https://github.com/hyperion-project/hyperion.ng/pull/$pr_number" echo "*******************************************************************************" diff --git a/effects/matrix.json b/effects/matrix.json index 2a2f295c..55c42c24 100644 --- a/effects/matrix.json +++ b/effects/matrix.json @@ -11,6 +11,6 @@ "grayscale": false, "imageSource": "file", "reverse": false, - "file": "matrix.gif" + "file": ":matrix.gif" } } diff --git a/libsrc/grabber/video/v4l2/V4L2Grabber.cpp b/libsrc/grabber/video/v4l2/V4L2Grabber.cpp index a0caa19d..8f504464 100644 --- a/libsrc/grabber/video/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/video/v4l2/V4L2Grabber.cpp @@ -1290,11 +1290,11 @@ QJsonArray V4L2Grabber::discover(const QJsonObject& params) format["format"] = pixelFormatToString(encodingFormat); QMap, QSet> combined = QMap, QSet>(); - for (auto enc : input.value().encodingFormats.values(encodingFormat)) + for (const auto &enc : input.value().encodingFormats.values(encodingFormat)) { std::pair width_height{enc.width, enc.height}; auto &com = combined[width_height]; - for (auto framerate : qAsConst(enc.framerates)) + for (auto framerate : enc.framerates) { com.insert(framerate); } @@ -1326,7 +1326,7 @@ QJsonArray V4L2Grabber::discover(const QJsonObject& params) device["video_inputs"] = video_inputs; QJsonObject controls, controls_default; - for (const auto &control : qAsConst(_deviceControls[device_property.key()])) + for (const auto &control : std::as_const(_deviceControls[device_property.key()])) { QJsonObject property; property["minValue"] = control.minValue; diff --git a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp index d4d75e9f..14f9c39a 100644 --- a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp @@ -446,11 +446,11 @@ bool LedDevicePhilipsHueBridge::checkApiError(const QJsonDocument &response, boo QJsonObject obj = response.object(); if (obj.contains(API_ERRORS)) { - QJsonArray errorList = obj.value(API_ERRORS).toArray(); + const QJsonArray errorList = obj.value(API_ERRORS).toArray(); if (!errorList.isEmpty()) { QStringList errors; - for (const QJsonValue &error : qAsConst(errorList)) + for (const QJsonValue &error : errorList) { QString errorString = error.toObject()[API_ERROR_DESCRIPTION].toString(); if (!errorString.contains("may not have effect")) @@ -909,9 +909,9 @@ void LedDevicePhilipsHueBridge::setDevicesMap(const QJsonDocument &doc) if (_useApiV2) { - QJsonArray devices = doc.array(); + const QJsonArray devices = doc.array(); - for (const QJsonValue &device : qAsConst(devices)) + for (const QJsonValue &device : devices) { QString deviceId = device.toObject().value("id").toString(); _devicesMap.insert(deviceId, device.toObject()); @@ -925,9 +925,9 @@ void LedDevicePhilipsHueBridge::setLightsMap(const QJsonDocument &doc) if (_useApiV2) { - QJsonArray lights = doc.array(); + const QJsonArray lights = doc.array(); - for (const QJsonValue &light : qAsConst(lights)) + for (const QJsonValue &light : lights) { QString lightId = light.toObject().value("id").toString(); _lightsMap.insert(lightId, light.toObject()); @@ -965,9 +965,9 @@ void LedDevicePhilipsHueBridge::setGroupMap(const QJsonDocument &doc) _groupsMap.clear(); if (_useApiV2) { - QJsonArray groups = doc.array(); + const QJsonArray groups = doc.array(); - for (const QJsonValue &group : qAsConst(groups)) + for (const QJsonValue &group : groups) { QString groupId = group.toObject().value("id").toString(); _groupsMap.insert(groupId, group.toObject()); @@ -995,9 +995,9 @@ void LedDevicePhilipsHueBridge::setEntertainmentSrvMap(const QJsonDocument &doc) if (_useApiV2) { - QJsonArray entertainmentSrvs = doc.array(); + const QJsonArray entertainmentSrvs = doc.array(); - for (const QJsonValue &entertainmentSrv : qAsConst(entertainmentSrvs)) + for (const QJsonValue &entertainmentSrv : entertainmentSrvs) { QString entertainmentSrvId = entertainmentSrv.toObject().value("id").toString(); _entertainmentMap.insert(entertainmentSrvId, entertainmentSrv.toObject()); @@ -1093,8 +1093,8 @@ QStringList LedDevicePhilipsHueBridge::getGroupLights(const QString& groupId) co { if (_useApiV2) { - QJsonArray lightServices = group.value( API_LIGHT_SERVICES ).toArray(); - for (const QJsonValue &light : qAsConst(lightServices)) + const QJsonArray lightServices = group.value( API_LIGHT_SERVICES ).toArray(); + for (const QJsonValue &light : lightServices) { groupLights.append( light.toObject().value(API_RID).toString()); } @@ -1140,7 +1140,7 @@ QJsonObject LedDevicePhilipsHueBridge::getEntertainmentSrvDetails(const QString& DebugIf( verbose, _log, "getEntertainmentSrvDetails [%s]", QSTRING_CSTR(deviceId) ); QJsonObject details; - for (const QJsonObject& entertainmentSrv : qAsConst(_entertainmentMap)) + for (const QJsonObject& entertainmentSrv : std::as_const(_entertainmentMap)) { QJsonObject owner = entertainmentSrv[API_OWNER].toObject(); @@ -1933,7 +1933,7 @@ bool LedDevicePhilipsHue::updateLights(const QMap &map) if(!_lightIds.empty()) { _lights.reserve(static_cast(_lightIds.size())); - for(const auto &id : qAsConst(_lightIds)) + for(const auto &id : std::as_const(_lightIds)) { if (map.contains(id)) { From b73e9f4996b81008613251092def2a6579fa1660 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:12:59 +0100 Subject: [PATCH 053/117] Qt 6.7 (#1650) * Do not validate values for options without value * Clean-up * ws281x include files workaround * Revert "ws281x include files workaround" This reverts commit 1b983087183e3c563a191edd34a318cfd0cdace1. * Use https://github.com/hyperion-project/rpi_ws281x while fix is applied in original repository --- .gitmodules | 4 ++-- CMakeLists.txt | 11 +++++------ libsrc/commandline/Parser.cpp | 25 ++++++++++++++----------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/.gitmodules b/.gitmodules index 624c175c..2fd17349 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "dependencies/external/rpi_ws281x"] path = dependencies/external/rpi_ws281x - url = https://github.com/jgarff/rpi_ws281x - branch = master + url = https://github.com/hyperion-project/rpi_ws281x + branch = main [submodule "dependencies/external/flatbuffers"] path = dependencies/external/flatbuffers url = https://github.com/google/flatbuffers diff --git a/CMakeLists.txt b/CMakeLists.txt index 813cb750..32bbb7d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -222,10 +222,11 @@ if (HYPERION_LIGHT) SET ( DEFAULT_OSX OFF ) SET ( DEFAULT_QT OFF ) SET ( DEFAULT_V4L2 OFF ) - SET ( DEFAULT_AUDIO OFF ) SET ( DEFAULT_X11 OFF ) SET ( DEFAULT_XCB OFF ) + SET ( DEFAULT_AUDIO OFF ) + # Disable Input Servers SET ( DEFAULT_BOBLIGHT_SERVER OFF ) SET ( DEFAULT_CEC OFF ) @@ -274,15 +275,13 @@ message(STATUS "ENABLE_V4L2 = ${ENABLE_V4L2}") option(ENABLE_X11 "Enable the X11 grabber" ${DEFAULT_X11}) message(STATUS "ENABLE_X11 = ${ENABLE_X11}") -option(ENABLE_AUDIO "Enable the AUDIO grabber" ${DEFAULT_AUDIO}) -message(STATUS "ENABLE_AUDIO = ${ENABLE_AUDIO}") - -option(ENABLE_WS281XPWM "Enable the WS281x-PWM device" ${DEFAULT_WS281XPWM} ) -message(STATUS "ENABLE_WS281XPWM = ${ENABLE_WS281XPWM}") option(ENABLE_XCB "Enable the XCB grabber" ${DEFAULT_XCB}) message(STATUS "ENABLE_XCB = ${ENABLE_XCB}") +option(ENABLE_AUDIO "Enable the AUDIO grabber" ${DEFAULT_AUDIO}) +message(STATUS "ENABLE_AUDIO = ${ENABLE_AUDIO}") + removeIndent() message(STATUS "Input options:") diff --git a/libsrc/commandline/Parser.cpp b/libsrc/commandline/Parser.cpp index 0bceb682..df58fe1f 100644 --- a/libsrc/commandline/Parser.cpp +++ b/libsrc/commandline/Parser.cpp @@ -14,19 +14,22 @@ bool Parser::parse(const QStringList &arguments) return false; } - for(Option * option : _options) + for(Option * option : std::as_const(_options)) { - QString value = this->value(*option); - if (!option->validate(*this, value)) { - const QString error = option->getError(); - if (!error.isEmpty()) { - _errorText = tr("\"%1\" is not a valid option for %2, %3").arg(value, option->name(), error); + if (!option->valueName().isEmpty()) + { + QString value = this->value(*option); + if (!option->validate(*this, value)) { + const QString error = option->getError(); + if (!error.isEmpty()) { + _errorText = tr("\"%1\" is not a valid option for %2, %3").arg(value, option->name(), error); + } + else + { + _errorText = tr("\"%1\" is not a valid option for %2").arg(value, option->name()); + } + return false; } - else - { - _errorText = tr("\"%1\" is not a valid option for %2").arg(value, option->name()); - } - return false; } } return true; From 27027b224cc8f4db4ad3f30b1af0afec37eb2adf Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:13:34 +0100 Subject: [PATCH 054/117] Fix self-signed certificate handling (#1649) --- CHANGELOG.md | 1 + .../leddevice/dev_net/LedDevicePhilipsHue.cpp | 6 +- libsrc/leddevice/dev_net/ProviderRestApi.cpp | 73 ++++++++++++++++++- libsrc/leddevice/dev_net/ProviderRestApi.h | 2 + 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 061f7b09..e4024253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ Note: The wizard will configure an APIv2 capable bridge always with Entertainmen - Changed default build from Stretch to Buster - Support Qt 6.7, Update to Protobuf 23.4.0, Update mbedTLS to v3.4.0, Update flatbuffers to v23.5.26 - Use C++17 standard as default +- Added Pull Request (PR) installation script, allowing users to test development builds savely on Linux - Fixed missing include limits in QJsonSchemaChecker - Thanks @Portisch - Fixed dependencies for deb packages in Debian Bookworm (#1579) - Thanks @hg42, @Psirus - Fixed git version identification when run in docker and local code diff --git a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp index 14f9c39a..77712594 100644 --- a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp @@ -584,11 +584,7 @@ int LedDevicePhilipsHueBridge::close() bool LedDevicePhilipsHueBridge::configureSsl() { _restApi->setAlternateServerIdentity(_deviceBridgeId); - - if (_isDiyHue) - { - _restApi->acceptSelfSignedCertificates(true); - } + _restApi->acceptSelfSignedCertificates(true); bool success = _restApi->setCaCertificate(API_SSL_CA_CERTIFICATE_RESSOURCE); if (!success) diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.cpp b/libsrc/leddevice/dev_net/ProviderRestApi.cpp index e2d07475..7321810f 100644 --- a/libsrc/leddevice/dev_net/ProviderRestApi.cpp +++ b/libsrc/leddevice/dev_net/ProviderRestApi.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include @@ -451,6 +453,63 @@ bool ProviderRestApi::checkServerIdentity(const QSslConfiguration& sslConfig) co return isServerIdentified; } +bool ProviderRestApi::matchesPinnedCertificate(const QSslCertificate& certificate) +{ + bool isMatching {false}; + + QList certificateInfos = certificate.subjectInfo(QSslCertificate::CommonName); + + if (certificateInfos.isEmpty()) + { + return false; + } + QString identifier = certificateInfos.constFirst(); + + QString appDataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + QString certDir = appDataDir + "/certificates"; + QDir().mkpath(certDir); + + QString filePath(certDir + "/" + identifier + ".pem"); + QFile file(filePath); + if (file.open(QIODevice::ReadOnly)) + { + QList certificates = QSslCertificate::fromDevice(&file, QSsl::Pem); + if (!certificates.isEmpty()) + { + Debug (_log,"First used certificate loaded successfully"); + QSslCertificate pinnedeCertificate = certificates.constFirst(); + if (pinnedeCertificate == certificate) + { + isMatching = true; + } + } + else + { + Debug (_log,"Error reading first used certificate file: %s", QSTRING_CSTR(filePath)); + } + file.close(); + } + else + { + if (file.open(QIODevice::WriteOnly)) + { + QByteArray pemData = certificate.toPem(); + qint64 bytesWritten = file.write(pemData); + if (bytesWritten == pemData.size()) + { + Debug (_log,"First used certificate saved to file: %s", QSTRING_CSTR(filePath)); + isMatching = true; + } + else + { + Debug (_log,"Error writing first used certificate file: %s", QSTRING_CSTR(filePath)); + } + file.close(); + } + } + return isMatching; +} + void ProviderRestApi::onSslErrors(QNetworkReply* reply, const QList& errors) { int ignoredErrorCount {0}; @@ -466,11 +525,21 @@ void ProviderRestApi::onSslErrors(QNetworkReply* reply, const QList& } break; case QSslError::SelfSignedCertificate : - if (_isSeflSignedCertificateAccpeted) + if (_isSeflSignedCertificateAccpeted) + { + // Get the peer certificate associated with the error + QSslCertificate certificate = error.certificate(); + if (matchesPinnedCertificate(certificate)) { + Debug (_log,"'Trust on first use' - Certificate received matches pinned certificate"); ignoreSslError = true; } - break; + else + { + Error (_log,"'Trust on first use' - Certificate received does not match pinned certificate"); + } + } + break; default: break; } diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.h b/libsrc/leddevice/dev_net/ProviderRestApi.h index b93d13ea..db4f9bd7 100644 --- a/libsrc/leddevice/dev_net/ProviderRestApi.h +++ b/libsrc/leddevice/dev_net/ProviderRestApi.h @@ -444,6 +444,8 @@ private: bool checkServerIdentity(const QSslConfiguration& sslConfig) const; + bool matchesPinnedCertificate(const QSslCertificate& certificate); + Logger* _log; /// QNetworkAccessManager object for sending REST-requests. From 8a54eff6563f5b47c7dd5dcef1fb2f41507e8cda Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Mon, 30 Oct 2023 00:21:59 +0000 Subject: [PATCH 055/117] Update submodule rpi_ws281x --- dependencies/external/rpi_ws281x | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/external/rpi_ws281x b/dependencies/external/rpi_ws281x index 1f47b59e..49086d39 160000 --- a/dependencies/external/rpi_ws281x +++ b/dependencies/external/rpi_ws281x @@ -1 +1 @@ -Subproject commit 1f47b59ed603223d1376d36c788c89af67ae2fdc +Subproject commit 49086d3913367d2fb014a615f9d958a47867bc39 From f57c4f84acd92dede4a2e5e1c8eef6786eaff99c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 14:38:23 +0100 Subject: [PATCH 056/117] Bump jurplel/install-qt-action from 3 to 4 (#1652) Bumps [jurplel/install-qt-action](https://github.com/jurplel/install-qt-action) from 3 to 4. --- .github/workflows/pull-request.yml | 2 +- .github/workflows/push-master.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index eee7c150..12d69ae4 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -137,7 +137,7 @@ jobs: echo -n "+PR${{ github.event.pull_request.number }}" >> .version - name: Install Qt - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{env.QT_VERSION}} target: 'desktop' diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml index b0ee7a1d..23c0abc8 100644 --- a/.github/workflows/push-master.yml +++ b/.github/workflows/push-master.yml @@ -102,7 +102,7 @@ jobs: submodules: recursive - name: Install Qt - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: ${{env.QT_VERSION}} target: 'desktop' From c9518db59742ddae6da7c93a49d3eaf3fe5d7ab5 Mon Sep 17 00:00:00 2001 From: LordGrey Date: Fri, 3 Nov 2023 19:54:59 +0100 Subject: [PATCH 057/117] Revert "Bump jurplel/install-qt-action from 3 to 4 (#1652)" This reverts commit f57c4f84acd92dede4a2e5e1c8eef6786eaff99c. --- .github/workflows/pull-request.yml | 2 +- .github/workflows/push-master.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 12d69ae4..eee7c150 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -137,7 +137,7 @@ jobs: echo -n "+PR${{ github.event.pull_request.number }}" >> .version - name: Install Qt - uses: jurplel/install-qt-action@v4 + uses: jurplel/install-qt-action@v3 with: version: ${{env.QT_VERSION}} target: 'desktop' diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml index 23c0abc8..b0ee7a1d 100644 --- a/.github/workflows/push-master.yml +++ b/.github/workflows/push-master.yml @@ -102,7 +102,7 @@ jobs: submodules: recursive - name: Install Qt - uses: jurplel/install-qt-action@v4 + uses: jurplel/install-qt-action@v3 with: version: ${{env.QT_VERSION}} target: 'desktop' From 91270966f9c7012f8eafca6bd1825e985e4c9501 Mon Sep 17 00:00:00 2001 From: Paulchen-Panther <16664240+Paulchen-Panther@users.noreply.github.com> Date: Thu, 16 Nov 2023 21:05:56 +0100 Subject: [PATCH 058/117] Refactor/Create APT/DNF Repository (#1648) --- .ci/ci_build.sh | 71 --- .ci/ci_install.sh | 40 -- .devcontainer.json | 37 +- .github/scripts/build.sh | 59 +++ .github/workflows/apt.yml | 154 ------- .github/workflows/apt/amd64.json | 58 --- .github/workflows/apt/arm64.json | 51 --- .github/workflows/apt/armhf.json | 51 --- .github/workflows/cleanup.yml | 4 +- .github/workflows/codeql.yml | 25 +- .github/workflows/nightly.yml | 187 -------- .github/workflows/pull-request.yml | 193 -------- .github/workflows/push-master.yml | 201 --------- .github/workflows/push_pull.yml | 49 ++ .github/workflows/qt5_6.yml | 250 +++++++++++ .github/workflows/release.yml | 9 +- CMakeLists.txt | 423 +++++++++--------- bin/scripts/docker-compile.sh | 153 +++++-- bin/scripts/install_pr.sh | 12 +- bin/service/hyperion.systemd | 2 +- bin/service/hyperion.xml | 22 + cmake/Dependencies.cmake | 51 ++- cmake/Findqmdnsengine.cmake | 8 +- cmake/LDGold.cmake | 17 - .../{hyperiond.desktop => hyperion.desktop} | 4 +- cmake/desktop/hyperion.metainfo.xml | 48 ++ cmake/desktop/hyperiond_128.png | Bin 16979 -> 0 bytes cmake/package-scripts/postinst | 8 +- cmake/package-scripts/prerm | 28 +- cmake/packages.cmake | 2 +- cmake/win/win_rc.cmake | 15 - debian/control.in | 12 - debian/distributions | 58 --- debian/rules.in | 32 -- dependencies/CMakeLists-qmdnsengine.txt.in | 26 -- dependencies/CMakeLists.txt | 86 ++-- dependencies/external/mbedtls | 2 +- include/grabber/OsxFrameGrabberMock.h | 93 ---- .../grabber/{ => amlogic}/AmlogicGrabber.h | 2 +- .../grabber/{ => amlogic}/AmlogicWrapper.h | 2 +- include/grabber/{ => audio}/AudioGrabber.h | 0 .../grabber/{ => audio}/AudioGrabberLinux.h | 31 +- .../grabber/{ => audio}/AudioGrabberWindows.h | 4 +- include/grabber/{ => audio}/AudioWrapper.h | 12 +- .../grabber/{ => directx}/DirectXGrabber.h | 0 .../grabber/{ => directx}/DirectXWrapper.h | 2 +- .../{ => dispmanx}/DispmanxFrameGrabber.h | 0 .../grabber/{ => dispmanx}/DispmanxWrapper.h | 2 +- .../FramebufferFrameGrabber.h | 0 .../{ => framebuffer}/FramebufferWrapper.h | 2 +- include/grabber/{ => osx}/OsxFrameGrabber.h | 4 - include/grabber/{ => osx}/OsxWrapper.h | 2 +- include/grabber/{ => qt}/QtGrabber.h | 0 include/grabber/{ => qt}/QtWrapper.h | 2 +- include/grabber/{ => video}/EncoderThread.h | 12 +- include/grabber/{ => video}/VideoWrapper.h | 4 +- .../{ => video/mediafoundation}/MFGrabber.h | 2 +- .../grabber/{ => video/v4l2}/V4L2Grabber.h | 2 +- include/grabber/{ => x11}/X11Grabber.h | 0 include/grabber/{ => x11}/X11Wrapper.h | 2 +- include/grabber/{ => xcb}/XcbGrabber.h | 0 include/grabber/{ => xcb}/XcbWrapper.h | 2 +- include/hyperion/ImageToLedsMap.h | 2 +- include/utils/Process.h | 9 +- libsrc/CMakeLists.txt | 6 +- libsrc/api/CMakeLists.txt | 32 +- libsrc/api/JsonAPI.cpp | 26 +- libsrc/blackborder/CMakeLists.txt | 14 +- libsrc/boblightserver/CMakeLists.txt | 15 +- libsrc/cec/CMakeLists.txt | 23 +- libsrc/commandline/CMakeLists.txt | 31 +- libsrc/db/CMakeLists.txt | 14 +- libsrc/effectengine/CMakeLists.txt | 57 +-- libsrc/effectengine/EffectEngine.qrc.in | 2 +- libsrc/flatbufserver/CMakeLists.txt | 97 ++-- libsrc/forwarder/CMakeLists.txt | 17 +- libsrc/grabber/CMakeLists.txt | 22 +- libsrc/grabber/amlogic/AmlogicGrabber.cpp | 2 +- libsrc/grabber/amlogic/AmlogicWrapper.cpp | 2 +- libsrc/grabber/amlogic/CMakeLists.txt | 22 +- libsrc/grabber/audio/AudioGrabber.cpp | 2 +- libsrc/grabber/audio/AudioGrabberLinux.cpp | 61 ++- libsrc/grabber/audio/AudioGrabberWindows.cpp | 28 +- libsrc/grabber/audio/AudioWrapper.cpp | 2 +- libsrc/grabber/audio/CMakeLists.txt | 61 +-- libsrc/grabber/directx/CMakeLists.txt | 23 +- libsrc/grabber/directx/DirectXGrabber.cpp | 2 +- libsrc/grabber/directx/DirectXWrapper.cpp | 2 +- libsrc/grabber/dispmanx/CMakeLists.txt | 25 +- .../grabber/dispmanx/DispmanxFrameGrabber.cpp | 2 +- libsrc/grabber/dispmanx/DispmanxWrapper.cpp | 2 +- libsrc/grabber/framebuffer/CMakeLists.txt | 15 +- .../framebuffer/FramebufferFrameGrabber.cpp | 2 +- .../framebuffer/FramebufferWrapper.cpp | 2 +- libsrc/grabber/osx/CMakeLists.txt | 15 +- libsrc/grabber/osx/OsxFrameGrabber.cpp | 2 +- libsrc/grabber/osx/OsxFrameGrabberMock.cpp | 159 ------- libsrc/grabber/osx/OsxWrapper.cpp | 2 +- libsrc/grabber/qt/CMakeLists.txt | 15 +- libsrc/grabber/qt/QtGrabber.cpp | 4 +- libsrc/grabber/qt/QtWrapper.cpp | 2 +- libsrc/grabber/video/CMakeLists.txt | 51 ++- libsrc/grabber/video/EncoderThread.cpp | 2 +- libsrc/grabber/video/VideoWrapper.cpp | 2 +- .../video/mediafoundation/MFGrabber.cpp | 4 +- .../video/mediafoundation/MFSourceReaderCB.h | 2 +- libsrc/grabber/video/v4l2/V4L2Grabber.cpp | 2 +- libsrc/grabber/x11/CMakeLists.txt | 29 +- libsrc/grabber/x11/X11Grabber.cpp | 2 +- libsrc/grabber/x11/X11Wrapper.cpp | 2 +- libsrc/grabber/xcb/CMakeLists.txt | 26 +- libsrc/grabber/xcb/XcbGrabber.cpp | 2 +- libsrc/grabber/xcb/XcbWrapper.cpp | 2 +- libsrc/hyperion/CMakeLists.txt | 64 ++- libsrc/jsonserver/CMakeLists.txt | 17 +- libsrc/leddevice/CMakeLists.txt | 89 ++-- libsrc/mdns/CMakeLists.txt | 25 +- libsrc/protoserver/CMakeLists.txt | 49 +- libsrc/python/CMakeLists.txt | 48 +- libsrc/ssdp/CMakeLists.txt | 15 +- libsrc/utils/CMakeLists.txt | 114 +++-- libsrc/utils/Process.cpp | 145 +++--- libsrc/webserver/CMakeLists.txt | 44 +- resources/CMakeLists.txt | 14 +- resources/icons/hyperion-128px.png | Bin 0 -> 10834 bytes resources/icons/hyperion-16px.png | Bin 0 -> 717 bytes resources/icons/hyperion-192px.png | Bin 0 -> 21900 bytes resources/icons/hyperion-22px.png | Bin 0 -> 1728 bytes resources/icons/hyperion-24px.png | Bin 0 -> 1894 bytes resources/icons/hyperion-256px.png | Bin 0 -> 31694 bytes resources/icons/hyperion-32px.png | Bin 0 -> 1459 bytes resources/icons/hyperion-36px.png | Bin 0 -> 2462 bytes resources/icons/hyperion-48px.png | Bin 0 -> 2556 bytes resources/icons/hyperion-512px.png | Bin 0 -> 86035 bytes resources/icons/hyperion-64px.png | Bin 0 -> 3845 bytes resources/icons/hyperion-72px.png | Bin 0 -> 4451 bytes resources/icons/hyperion-96px.png | Bin 0 -> 7921 bytes resources/icons/hyperion-icon-32px.png | Bin 1454 -> 0 bytes resources/icons/hyperion-icon-512px.png | Bin 69597 -> 0 bytes snap/snapcraft.yaml | 4 +- src/CMakeLists.txt | 6 +- src/hyperion-aml/AmlogicWrapper.h | 2 +- src/hyperion-aml/CMakeLists.txt | 41 +- src/hyperion-dispmanx/CMakeLists.txt | 35 +- src/hyperion-dispmanx/DispmanxWrapper.h | 4 +- src/hyperion-framebuffer/CMakeLists.txt | 42 +- src/hyperion-framebuffer/FramebufferWrapper.h | 2 +- src/hyperion-osx/CMakeLists.txt | 30 +- src/hyperion-osx/OsxWrapper.h | 2 +- src/hyperion-qt/CMakeLists.txt | 51 +-- src/hyperion-qt/QtWrapper.h | 2 +- src/hyperion-remote/CMakeLists.txt | 58 +-- src/hyperion-v4l2/CMakeLists.txt | 41 +- src/hyperion-v4l2/hyperion-v4l2.cpp | 4 +- src/hyperion-x11/CMakeLists.txt | 42 +- src/hyperion-x11/X11Wrapper.h | 2 +- src/hyperion-xcb/CMakeLists.txt | 29 +- src/hyperion-xcb/XcbWrapper.h | 2 +- src/hyperiond/CMakeLists.txt | 194 ++++---- src/hyperiond/hyperiond.h | 22 +- src/hyperiond/main.cpp | 2 +- src/hyperiond/systray.cpp | 4 +- test/CMakeLists.txt | 14 +- test/dispmanx2png/CMakeLists.txt | 4 +- test/dispmanx2png/dispmanx2png.cpp | 2 +- 165 files changed, 1918 insertions(+), 2924 deletions(-) delete mode 100755 .ci/ci_build.sh delete mode 100755 .ci/ci_install.sh create mode 100755 .github/scripts/build.sh delete mode 100644 .github/workflows/apt.yml delete mode 100644 .github/workflows/apt/amd64.json delete mode 100644 .github/workflows/apt/arm64.json delete mode 100644 .github/workflows/apt/armhf.json delete mode 100644 .github/workflows/nightly.yml delete mode 100644 .github/workflows/pull-request.yml delete mode 100644 .github/workflows/push-master.yml create mode 100644 .github/workflows/push_pull.yml create mode 100644 .github/workflows/qt5_6.yml create mode 100644 bin/service/hyperion.xml delete mode 100644 cmake/LDGold.cmake rename cmake/desktop/{hyperiond.desktop => hyperion.desktop} (61%) create mode 100644 cmake/desktop/hyperion.metainfo.xml delete mode 100644 cmake/desktop/hyperiond_128.png delete mode 100644 cmake/win/win_rc.cmake delete mode 100644 debian/control.in delete mode 100644 debian/distributions delete mode 100644 debian/rules.in delete mode 100644 dependencies/CMakeLists-qmdnsengine.txt.in delete mode 100644 include/grabber/OsxFrameGrabberMock.h rename include/grabber/{ => amlogic}/AmlogicGrabber.h (97%) rename include/grabber/{ => amlogic}/AmlogicWrapper.h (95%) rename include/grabber/{ => audio}/AudioGrabber.h (100%) rename include/grabber/{ => audio}/AudioGrabberLinux.h (86%) rename include/grabber/{ => audio}/AudioGrabberWindows.h (98%) rename include/grabber/{ => audio}/AudioWrapper.h (90%) rename include/grabber/{ => directx}/DirectXGrabber.h (100%) rename include/grabber/{ => directx}/DirectXWrapper.h (96%) rename include/grabber/{ => dispmanx}/DispmanxFrameGrabber.h (100%) rename include/grabber/{ => dispmanx}/DispmanxWrapper.h (95%) rename include/grabber/{ => framebuffer}/FramebufferFrameGrabber.h (100%) rename include/grabber/{ => framebuffer}/FramebufferWrapper.h (94%) rename include/grabber/{ => osx}/OsxFrameGrabber.h (95%) rename include/grabber/{ => osx}/OsxWrapper.h (95%) rename include/grabber/{ => qt}/QtGrabber.h (100%) rename include/grabber/{ => qt}/QtWrapper.h (97%) rename include/grabber/{ => video}/EncoderThread.h (91%) rename include/grabber/{ => video}/VideoWrapper.h (89%) rename include/grabber/{ => video/mediafoundation}/MFGrabber.h (98%) rename include/grabber/{ => video/v4l2}/V4L2Grabber.h (99%) rename include/grabber/{ => x11}/X11Grabber.h (100%) rename include/grabber/{ => x11}/X11Wrapper.h (97%) rename include/grabber/{ => xcb}/XcbGrabber.h (100%) rename include/grabber/{ => xcb}/XcbWrapper.h (94%) delete mode 100644 libsrc/grabber/osx/OsxFrameGrabberMock.cpp create mode 100644 resources/icons/hyperion-128px.png create mode 100644 resources/icons/hyperion-16px.png create mode 100644 resources/icons/hyperion-192px.png create mode 100644 resources/icons/hyperion-22px.png create mode 100644 resources/icons/hyperion-24px.png create mode 100644 resources/icons/hyperion-256px.png create mode 100644 resources/icons/hyperion-32px.png create mode 100644 resources/icons/hyperion-36px.png create mode 100644 resources/icons/hyperion-48px.png create mode 100644 resources/icons/hyperion-512px.png create mode 100644 resources/icons/hyperion-64px.png create mode 100644 resources/icons/hyperion-72px.png create mode 100644 resources/icons/hyperion-96px.png delete mode 100644 resources/icons/hyperion-icon-32px.png delete mode 100644 resources/icons/hyperion-icon-512px.png diff --git a/.ci/ci_build.sh b/.ci/ci_build.sh deleted file mode 100755 index df276f00..00000000 --- a/.ci/ci_build.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash - -# detect CI -if [ "$HOME" != "" ]; then - # GitHub Actions - echo "Github Actions detected" - CI_NAME="$(uname -s | tr '[:upper:]' '[:lower:]')" - CI_BUILD_DIR="$GITHUB_WORKSPACE" -else - # for executing in non ci environment - CI_NAME="$(uname -s | tr '[:upper:]' '[:lower:]')" -fi - -# set environment variables if not exists -[ -z "${BUILD_TYPE}" ] && BUILD_TYPE="Debug" - -# Determine cmake build type; tag builds are Release, else Debug (-dev appends to platform) -if [[ $BUILD_SOURCEBRANCH == *"refs/tags"* || $GITHUB_REF == *"refs/tags"* ]]; then - BUILD_TYPE=Release -else - PLATFORM=${PLATFORM}-dev -fi - -echo "Platform: ${PLATFORM}, build type: ${BUILD_TYPE}, CI_NAME: $CI_NAME, docker image: ${DOCKER_IMAGE}, docker type: ${DOCKER_TAG}" - -# Build the package on osx or linux -if [[ "$CI_NAME" == 'osx' || "$CI_NAME" == 'darwin' ]]; then - echo "Compile Hyperion on OSX or Darwin" - # compile prepare - mkdir build || exit 1 - cd build - cmake -DPLATFORM=${PLATFORM} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DCMAKE_INSTALL_PREFIX:PATH=/usr/local ../ || exit 2 - make -j $(sysctl -n hw.ncpu) package || exit 3 - cd ${CI_BUILD_DIR} && source /${CI_BUILD_DIR}/test/testrunner.sh || exit 4 - exit 0; - exit 1 || { echo "---> Hyperion compilation failed! Abort"; exit 5; } -elif [[ $CI_NAME == *"mingw64_nt"* || "$CI_NAME" == 'windows_nt' ]]; then - echo "Compile Hyperion on Windows" - # compile prepare - echo "Number of Cores $NUMBER_OF_PROCESSORS" - mkdir build || exit 1 - cd build - cmake -G "Visual Studio 17 2022" -A x64 -DPLATFORM=${PLATFORM} -DCMAKE_BUILD_TYPE="Release" ../ || exit 2 - cmake --build . --target package --config "Release" -- -nologo -v:m -maxcpucount || exit 3 - exit 0; - exit 1 || { echo "---> Hyperion compilation failed! Abort"; exit 5; } -elif [[ "$CI_NAME" == 'linux' ]]; then - echo "Compile Hyperion with DOCKER_IMAGE = ${DOCKER_IMAGE}, DOCKER_TAG = ${DOCKER_TAG} and friendly name DOCKER_NAME = ${DOCKER_NAME}" - # set GitHub Container Registry url - REGISTRY_URL="ghcr.io/hyperion-project/${DOCKER_IMAGE}" - # take ownership of deploy dir - mkdir ${CI_BUILD_DIR}/deploy - - # run docker - docker run --rm \ - -v "${CI_BUILD_DIR}/deploy:/deploy" \ - -v "${CI_BUILD_DIR}:/source:ro" \ - $REGISTRY_URL:$DOCKER_TAG \ - /bin/bash -c "mkdir hyperion && cp -r source/. /hyperion && - cd /hyperion && mkdir build && cd build && - cmake -DPLATFORM=${PLATFORM} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ../ || exit 2 && - make -j $(nproc) package || exit 3 && - cp /hyperion/build/bin/h* /deploy/ 2>/dev/null || : && - cp /hyperion/build/Hyperion-* /deploy/ 2>/dev/null || : && - cd /hyperion && source /hyperion/test/testrunner.sh || exit 4 && - exit 0; - exit 1 " || { echo "---> Hyperion compilation failed! Abort"; exit 5; } - - # overwrite file owner to current user - sudo chown -fR $(stat -c "%U:%G" ${CI_BUILD_DIR}/deploy) ${CI_BUILD_DIR}/deploy -fi diff --git a/.ci/ci_install.sh b/.ci/ci_install.sh deleted file mode 100755 index 86c6ffd4..00000000 --- a/.ci/ci_install.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -# detect CI -if [ "$HOME" != "" ]; then - # GitHub Actions - CI_NAME="$(uname -s | tr '[:upper:]' '[:lower:]')" - CI_BUILD_DIR="$GITHUB_WORKSPACE" -else - # for executing in non ci environment - CI_NAME="$(uname -s | tr '[:upper:]' '[:lower:]')" -fi - -function installAndUpgrade() -{ - arr=("$@") - for i in "${arr[@]}"; - do - list_output=`brew list --formula | grep $i` - outdated_output=`brew outdated | grep $i` - - if [[ ! -z "$list_output" ]]; then - if [[ ! -z "$outdated_output" ]]; then - echo "Outdated package: ${outdated_output}" - brew unlink ${outdated_output} - brew upgrade $i - brew link --overwrite $i - fi - else - brew install $i - fi - done -} - -# install osx deps for hyperion compile -if [[ $CI_NAME == 'osx' || $CI_NAME == 'darwin' ]]; then - echo "Install dependencies" - brew update - dependencies=("qt5" "python" "libusb" "cmake" "doxygen") - installAndUpgrade "${dependencies[@]}" -fi diff --git a/.devcontainer.json b/.devcontainer.json index 4cce9be7..966a658b 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,20 +1,25 @@ { "name": "Hyperion.ng Linux", - "extensions": [ - "twxs.cmake", - "ms-vscode.cpptools", - "ms-vscode.cmake-tools", - "spmeesseman.vscode-taskexplorer", - "yzhang.markdown-all-in-one", - "CoenraadS.bracket-pair-colorizer", - "vscode-icons-team.vscode-icons", - "editorconfig.editorconfig" - ], - "settings": { - "editor.formatOnSave": false, - "cmake.environment": { - }, - }, + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + "extensions": [ + "twxs.cmake", + "ms-vscode.cpptools", + "ms-vscode.cmake-tools", + "spmeesseman.vscode-taskexplorer", + "yzhang.markdown-all-in-one", + "CoenraadS.bracket-pair-colorizer", + "vscode-icons-team.vscode-icons", + "editorconfig.editorconfig", + "RVSmartPorting.rpm-spec-ext" + ], + "settings": { + "editor.formatOnSave": false, + "cmake.environment": { } + } + } + }, "forwardPorts": [8090, 8092], - "postCreateCommand": "git submodule update --recursive --init && sudo apt-get update && sudo apt-get install -y 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 libjpeg-dev libturbojpeg0-dev libssl-dev" + "postCreateCommand": "git submodule update --recursive --init && sudo apt-get update && sudo apt-get install -y 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 libjpeg-dev libturbojpeg0-dev libssl-dev libasound2-dev" } diff --git a/.github/scripts/build.sh b/.github/scripts/build.sh new file mode 100755 index 00000000..5880abb9 --- /dev/null +++ b/.github/scripts/build.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# set environment variables if not exists +[ -z "${BUILD_TYPE}" ] && BUILD_TYPE="Debug" +[ -z "${TARGET_ARCH}" ] && TARGET_ARCH="linux/amd64" +[ -z "${PLATFORM}" ] && PLATFORM="x11" + +# Determine cmake build type; tag builds are Release, else Debug (-dev appends to platform) +if [[ $GITHUB_REF == *"refs/tags"* ]]; then + BUILD_TYPE=Release +else + PLATFORM=${PLATFORM}-dev +fi + +echo "Compile Hyperion on '${RUNNER_OS}' with build type '${BUILD_TYPE}' and platform '${PLATFORM}'" + +# Build the package on MacOS, Windows or Linux +if [[ "$RUNNER_OS" == 'macOS' ]]; then + mkdir build || exit 1 + cd build + cmake -DPLATFORM=${PLATFORM} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DCMAKE_INSTALL_PREFIX:PATH=/usr/local ../ || exit 2 + make -j $(sysctl -n hw.ncpu) package || exit 3 + cd ${GITHUB_WORKSPACE} && source /${GITHUB_WORKSPACE}/test/testrunner.sh || exit 4 + exit 0; + exit 1 || { echo "---> Hyperion compilation failed! Abort"; exit 5; } +elif [[ $RUNNER_OS == "Windows" ]]; then + echo "Number of Cores $NUMBER_OF_PROCESSORS" + mkdir build || exit 1 + cd build + cmake -G "Visual Studio 17 2022" -A x64 -DPLATFORM=${PLATFORM} -DCMAKE_BUILD_TYPE="Release" ../ || exit 2 + cmake --build . --target package --config "Release" -- -nologo -v:m -maxcpucount || exit 3 + exit 0; + exit 1 || { echo "---> Hyperion compilation failed! Abort"; exit 5; } +elif [[ "$RUNNER_OS" == 'Linux' ]]; then + echo "Docker arguments used: DOCKER_IMAGE=${DOCKER_IMAGE}, DOCKER_TAG=${DOCKER_TAG}, TARGET_ARCH=${TARGET_ARCH}" + # verification bypass of external dependencies + git config --global --add safe.directory "${GITHUB_WORKSPACE}/dependencies/external/*" + # set GitHub Container Registry url + REGISTRY_URL="ghcr.io/hyperion-project/${DOCKER_IMAGE}" + # take ownership of deploy dir + mkdir ${GITHUB_WORKSPACE}/deploy + + # run docker + docker run --rm --platform=${TARGET_ARCH} \ + -v "${GITHUB_WORKSPACE}/deploy:/deploy" \ + -v "${GITHUB_WORKSPACE}:/source:rw" \ + $REGISTRY_URL:$DOCKER_TAG \ + /bin/bash -c "mkdir -p /source/build && cd /source/build && + cmake -DPLATFORM=${PLATFORM} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ../ || exit 2 && + cmake --build /source/build --target package -- -j $(nproc) || exit 3 && + cp /source/build/bin/h* /deploy/ 2>/dev/null || : && + cp /source/build/Hyperion-* /deploy/ 2>/dev/null || : && + cd /source && source /source/test/testrunner.sh || exit 5 && + exit 0; + exit 1 " || { echo "---> Hyperion compilation failed! Abort"; exit 5; } + + # overwrite file owner to current user + sudo chown -fR $(stat -c "%U:%G" ${GITHUB_WORKSPACE}/deploy) ${GITHUB_WORKSPACE}/deploy +fi diff --git a/.github/workflows/apt.yml b/.github/workflows/apt.yml deleted file mode 100644 index 99bce214..00000000 --- a/.github/workflows/apt.yml +++ /dev/null @@ -1,154 +0,0 @@ -name: Hyperion APT Build -on: - workflow_call: - inputs: - head_sha: - type: string - description: The branch, tag or SHA to checkout - required: true - secrets: - APT_GPG: - required: true - APT_USER: - required: true - APT_PASSWORD: - required: true - APT_DRAFT: - required: true - workflow_dispatch: - inputs: - head_sha: - type: string - description: The branch, tag or SHA to checkout - required: true - secrets: - APT_GPG: - required: true - APT_USER: - required: true - APT_PASSWORD: - required: true - APT_DRAFT: - required: true - -jobs: - setup: - name: Setup APT build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set APT matrix - id: apt-ppa - run: | - APT=$(jq -n '.include |= [ inputs[] | select(.["exclude"] != true)]' .github/workflows/apt/*.json --compact-output) - echo "apt=$APT" >> $GITHUB_OUTPUT - - outputs: - apt-matrix: ${{ steps.apt-ppa.outputs.apt }} - - build: - name: ${{ matrix.description }} - needs: [setup] - runs-on: ubuntu-latest - strategy: - matrix: ${{ fromJson(needs.setup.outputs.apt-matrix) }} - - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.head_sha || github.event.client_payload.head_sha }} - submodules: true - - - name: Generate environment variables - run: | - tr -d '\n' < .version > temp && mv temp .version - VERSION=$(cat .version) - echo VERSION=${VERSION} >> $GITHUB_ENV - if [[ $VERSION == *"-"* ]]; then - echo STANDARDS_VERSION=$(echo ${VERSION%-*}) >> $GITHUB_ENV - echo TARBALL_VERSION=$(echo ${VERSION%-*}) >> $GITHUB_ENV - echo DEBIAN_FORMAT='3.0 (quilt)' >> $GITHUB_ENV - else - echo STANDARDS_VERSION=$(echo ${VERSION%+*}) >> $GITHUB_ENV - echo TARBALL_VERSION=${VERSION}~$(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV - echo DEBIAN_FORMAT='3.0 (native)' >> $GITHUB_ENV - fi - echo DISTRIBUTION=$(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV - - - name: Build package - shell: bash - run: | - mkdir -p "${GITHUB_WORKSPACE}/deploy" - docker run --rm \ - -v "${GITHUB_WORKSPACE}/deploy:/deploy" \ - -v "${GITHUB_WORKSPACE}:/source:rw" \ - ghcr.io/hyperion-project/${{ matrix.architecture }}:${{ env.DISTRIBUTION }} \ - /bin/bash -c "cd /source && \ - mkdir -p debian/source && echo '${{ env.DEBIAN_FORMAT }}' > debian/source/format && \ - dch --create --distribution ${{ env.DISTRIBUTION }} --package 'hyperion' -v '${{ env.VERSION }}~${{ env.DISTRIBUTION }}' '${{ github.event.commits[0].message }}' && \ - cp -fr LICENSE debian/copyright && \ - sed 's/@BUILD_DEPENDS@/${{ matrix.build-depends }}/g; s/@DEPENDS@/${{ matrix.package-depends }}/g; s/@ARCHITECTURE@/${{ matrix.architecture }}/g; s/@STANDARDS_VERSION@/${{ env.STANDARDS_VERSION }}/g' debian/control.in > debian/control && \ - sed 's/@CMAKE_ENVIRONMENT@/${{ matrix.cmake-environment }}/g' debian/rules.in > debian/rules && \ - tar -cJf ../hyperion_${{ env.TARBALL_VERSION }}.orig.tar.xz . && \ - debuild --no-lintian -uc -us && \ - cp ../hyperion_*.deb /deploy" - - - name: Upload package artifact - if: ${{ startsWith(github.event.ref, 'refs/tags') || github.event_name == 'workflow_dispatch' }} - uses: actions/upload-artifact@v3 - with: - path: deploy - retention-days: 1 - - publish: - name: Publish APT packages - if: ${{ startsWith(github.event.ref, 'refs/tags') || github.event_name == 'workflow_dispatch' }} - needs: [setup, build] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.head_sha || github.event.client_payload.head_sha }} - - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6.0.0 - with: - gpg_private_key: ${{ secrets.APT_GPG }} - - - name: Install reprepro - run: sudo apt -y install reprepro - - - name: Make build folders, export public GPG key and copy distributions file - run: | - mkdir -p apt/{conf,dists,db} - gpg --armor --output apt/hyperion.pub.key --export 'admin@hyperion-project.org' - cp debian/distributions apt/conf/distributions - - - name: Create initial structure/packages files and symbolic links - run: | - reprepro -Vb apt createsymlinks - reprepro -Vb apt export - - - name: Download artifacts - uses: actions/download-artifact@v3.0.2 - - - name: Include artifacts into the package source - run: | - for file in artifact/hyperion_*.deb; do - if [ -f "$file" ]; then - dist=${file#*~} - dist=${dist%_*} - reprepro -Vb apt/ includedeb "$dist" "$file" - fi - done - - - name: Upload packages to APT server (DRAFT) - uses: SamKirkland/FTP-Deploy-Action@v4.3.4 - with: - server: apt.hyperion-project.org - username: ${{ secrets.APT_USER }} - password: ${{ secrets.APT_PASSWORD }} - local-dir: "./apt/" - server-dir: ${{ secrets.APT_DRAFT }} - dangerous-clean-slate: true diff --git a/.github/workflows/apt/amd64.json b/.github/workflows/apt/amd64.json deleted file mode 100644 index ecfa56c3..00000000 --- a/.github/workflows/apt/amd64.json +++ /dev/null @@ -1,58 +0,0 @@ -[ - { - "distribution": "Focal", - "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.8, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg, libcec4", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Ubuntu 20.04 (Focal Fossa) (amd64)" - }, - { - "distribution": "Jammy", - "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Ubuntu 22.04 (Jammy Jellyfish) (amd64)" - }, - { - "distribution": "Kinetic", - "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Ubuntu 22.10 (Kinetic Kudu) (amd64)" - }, - { - "distribution": "Lunar", - "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.11, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Ubuntu 23.04 (Lunar Lobster) (amd64)" - }, - { - "distribution": "Buster", - "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.7, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec4", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Debian 10.x (Buster) (amd64)" - }, - { - "distribution": "Bullseye", - "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Debian 11.x (Bullseye) (amd64)" - }, - { - "distribution": "Bookworm", - "architecture": "amd64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.11, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg0, libcec6", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Debian 12.x (Bookworm) (amd64)" - } -] diff --git a/.github/workflows/apt/arm64.json b/.github/workflows/apt/arm64.json deleted file mode 100644 index 8cf6325e..00000000 --- a/.github/workflows/apt/arm64.json +++ /dev/null @@ -1,51 +0,0 @@ -[ - { - "distribution": "Focal", - "architecture": "arm64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.8, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg, libcec4", - "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Ubuntu 20.04 (Focal Fossa) (arm64)" - }, - { - "distribution": "Jammy", - "architecture": "arm64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", - "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Ubuntu 22.04 (Jammy Jellyfish) (arm64)" - }, - { - "distribution": "Kinetic", - "architecture": "arm64", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", - "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Ubuntu 22.10 (Kinetic Kudu) (arm64)" - }, - { - "distribution": "Buster", - "architecture": "arm64", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.7, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec4", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Debian 10.x (Buster) (arm64)" - }, - { - "distribution": "Bullseye", - "architecture": "arm64", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Debian 11.x (Bullseye) (arm64)" - }, - { - "distribution": "Bookworm", - "architecture": "arm64", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.11, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg0, libcec6", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Debian 12.x (Bookworm) (arm64)", - "exclude" : true - } -] diff --git a/.github/workflows/apt/armhf.json b/.github/workflows/apt/armhf.json deleted file mode 100644 index ed4b9b4d..00000000 --- a/.github/workflows/apt/armhf.json +++ /dev/null @@ -1,51 +0,0 @@ -[ - { - "distribution": "Focal", - "architecture": "armhf", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.8, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg, libcec4", - "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Ubuntu 20.04 (Focal Fossa) (armhf)" - }, - { - "distribution": "Jammy", - "architecture": "armhf", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", - "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Ubuntu 22.04 (Jammy Jellyfish) (armhf)" - }, - { - "distribution": "Kinetic", - "architecture": "armhf", - "build-depends": "git, cmake, build-essential, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, libqt5x11extras5-dev, libusb-1.0-0-dev, python3-dev, libcec-dev, libxcb-image0-dev, libxcb-util0-dev, libxcb-shm0-dev, libxcb-render0-dev, libxcb-randr0-dev, libxrandr-dev, libxrender-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libssl-dev, libmbedtls-dev", - "package-depends": "libpython3.10, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg, libcec6", - "cmake-environment": "-DENABLE_DISPMANX=OFF -DENABLE_X11=ON -DENABLE_XCB=ON -DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Ubuntu 22.10 (Kinetic Kudu) (armhf)" - }, - { - "distribution": "Buster", - "architecture": "armhf", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl1.0-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.7, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec4", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Debian 10.x (Buster) (armhf)" - }, - { - "distribution": "Bullseye", - "architecture": "armhf", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.9, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls12, libasound2, libturbojpeg0, libcec6", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Debian 11.x (Bullseye) (armhf)" - }, - { - "distribution": "Bookworm", - "architecture": "armhf", - "build-depends": "git, cmake, python3-dev, qtbase5-dev, libqt5serialport5-dev, libqt5sql5-sqlite, libqt5svg5-dev, build-essential, libusb-1.0-0-dev, libcec-dev, libssl-dev, libraspberrypi-dev, libasound2-dev, libturbojpeg0-dev, libjpeg-dev, libmbedtls-dev", - "package-depends": "libpython3.11, libusb-1.0-0, libqt5widgets5, libqt5x11extras5, libqt5sql5, libqt5sql5-sqlite, libqt5serialport5, libmbedtls14, libasound2, libturbojpeg0, libcec6", - "cmake-environment": "-DUSE_SYSTEM_MBEDTLS_LIBS=ON -DENABLE_DEPLOY_DEPENDENCIES=OFF -DCMAKE_BUILD_TYPE=Release", - "description": "Debian 12.x (Bookworm) (armhf)", - "exclude" : true - } -] diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index 95ab9fd7..48c42fb0 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -1,4 +1,4 @@ -name: Clean artifacts +name: 🧹 Cleanup old artifacts # Run cleanup workflow at the end of every day on: @@ -9,7 +9,7 @@ jobs: clean: runs-on: ubuntu-latest steps: - - name: cleanup + - name: 🧹 Cleanup old workflow artifacts uses: kolpav/purge-artifacts-action@v1 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d39190cc..9ed68369 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,4 +1,8 @@ -name: "CodeQL" +name: 📊 CodeQL +run-name: | + ${{ github.event_name == 'schedule' && '⏰ Scheduled CodeQL run' || '' }} + ${{ github.event_name == 'push' && format('📊 Pushed CodeQL run - {0}', github.event.head_commit.message) || '' }} + ${{ github.event_name == 'pull_request' && format('📊 CodeQL run for PR {0} - {1}', github.event.pull_request.number, github.event.pull_request.title) || github.event.head_commit.message }} on: push: @@ -10,7 +14,7 @@ on: jobs: analyze: - name: Analyze + name: 📊 Analyze runs-on: ubuntu-latest permissions: actions: read @@ -23,35 +27,35 @@ jobs: language: [ python, javascript, cpp ] steps: - - name: Checkout + - name: ⬇ Checkout uses: actions/checkout@v4 with: submodules: recursive - - name: Install Packages (cpp) + - name: 📥 Install Packages (cpp) if: ${{ matrix.language == 'cpp' }} run: | sudo apt-get update sudo apt-get install --yes git cmake build-essential qtbase5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5x11extras5-dev libusb-1.0-0-dev python3-dev libcec-dev libxcb-image0-dev libxcb-util0-dev libxcb-shm0-dev libxcb-render0-dev libxcb-randr0-dev libxrandr-dev libxrender-dev libavahi-core-dev libavahi-compat-libdnssd-dev libasound2-dev libturbojpeg0-dev libjpeg-dev libssl-dev - - name: Initialize CodeQL + - name: 🔁 Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} queries: +security-and-quality config-file: ./.github/config/codeql.yml - - name: Autobuild + - name: 👷 Autobuild uses: github/codeql-action/autobuild@v2 - - name: Perform CodeQL Analysis + - name: 🏃 Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 with: category: "/language:${{ matrix.language }}" upload: False output: sarif-results - - name: Filter SARIF + - name: 🆔 Filter SARIF uses: advanced-security/filter-sarif@v1 with: patterns: | @@ -63,11 +67,12 @@ jobs: input: sarif-results/${{ matrix.language }}.sarif output: sarif-results/${{ matrix.language }}.sarif - - name: Upload SARIF + - name: 📦 Upload SARIF uses: github/codeql-action/upload-sarif@v2 with: sarif_file: sarif-results/${{ matrix.language }}.sarif - - name: Upload loc as a Build Artifact + + - name: 📦 Upload loc as a Build Artifact uses: actions/upload-artifact@v3 with: name: sarif-results diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml deleted file mode 100644 index 2046ba07..00000000 --- a/.github/workflows/nightly.yml +++ /dev/null @@ -1,187 +0,0 @@ -name: Nightly build - -# Create nightly builds at the end of every day -on: - schedule: - - cron: '0 0 * * *' - repository_dispatch: - types: [hyperion_nightly_push] - -jobs: - update: - name: Update Submodule rpi_ws281x - if: github.repository_owner == 'hyperion-project' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - fetch-depth: 0 - submodules: recursive - - - name: Update Submodule rpi_ws281x - id: update - run: git submodule update --remote --recursive dependencies/external/rpi_ws281x - - - name: Check git status - id: status - run: echo "status=$(git status -s)" >> $GITHUB_OUTPUT - - - name: Add and commit changes - if: ${{ steps.status.outputs.status }} - run: | - git config --local user.email "20935312+Hyperion-Bot@users.noreply.github.com" - git config --local user.name "Hyperion-Bot" - git config --local diff.ignoreSubmodules dirty - git commit -am "Update submodule rpi_ws281x" - - - name: Push changes - if: ${{ steps.status.outputs.status }} - uses: ad-m/github-push-action@master - with: - github_token: ${{ secrets.HYPERION_BOT_TOKEN }} - branch: ${{ github.ref }} - - check: - name: Compare local <-> nightly - needs: [update] - if: github.repository_owner == 'hyperion-project' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Check if commit has changed - id: build-necessary - run: | - if wget --spider "https://nightly.apt.hyperion-project.org/$(git rev-parse --short HEAD)" 2>/dev/null; then - echo "commit-has-changed=false" >> $GITHUB_OUTPUT - else - echo "commit-has-changed=true" >> $GITHUB_OUTPUT - fi - outputs: - build-nightly: ${{ steps.build-necessary.outputs.commit-has-changed }} - - setup: - name: Setup nightly build - needs: [check] - if: ${{ needs.check.outputs.build-nightly == 'true' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set nightly matrix - id: nightly-ppa - run: | - NIGHTLY=$(jq -n '.include |= [ inputs[] | select(.["exclude"] != true)]' .github/workflows/apt/*.json --compact-output) - echo "nightly=$NIGHTLY" >> $GITHUB_OUTPUT - - outputs: - nightly-matrix: ${{ steps.nightly-ppa.outputs.nightly }} - - build: - name: ${{ matrix.description }} - needs: [setup] - runs-on: ubuntu-latest - strategy: - matrix: ${{ fromJson(needs.setup.outputs.nightly-matrix) }} - - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Generate environment variables - run: | - echo "$(tr -d '\n' < .version)+nightly$(date '+%Y%m%d')$(git rev-parse --short HEAD)" > .version - VERSION=$(cat .version) - echo VERSION=${VERSION} >> $GITHUB_ENV - if [[ $VERSION == *"-"* ]]; then - echo STANDARDS_VERSION=$(echo ${VERSION%-*}) >> $GITHUB_ENV - echo TARBALL_VERSION=$(echo ${VERSION%-*}) >> $GITHUB_ENV - echo DEBIAN_FORMAT='3.0 (quilt)' >> $GITHUB_ENV - else - echo STANDARDS_VERSION=$(echo ${VERSION%+*}) >> $GITHUB_ENV - echo TARBALL_VERSION=${VERSION}~$(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV - echo DEBIAN_FORMAT='3.0 (native)' >> $GITHUB_ENV - fi - echo DISTRIBUTION=$(echo ${{ matrix.distribution }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV - - - name: Build package - shell: bash - run: | - mkdir -p "${GITHUB_WORKSPACE}/deploy" - docker run --rm \ - -v "${GITHUB_WORKSPACE}/deploy:/deploy" \ - -v "${GITHUB_WORKSPACE}:/source:rw" \ - ghcr.io/hyperion-project/${{ matrix.architecture }}:${{ env.DISTRIBUTION }} \ - /bin/bash -c "cd /source && \ - mkdir -p debian/source && echo '${{ env.DEBIAN_FORMAT }}' > debian/source/format && \ - dch --create --distribution ${{ env.DISTRIBUTION }} --package 'hyperion' -v '${{ env.VERSION }}~${{ env.DISTRIBUTION }}' '${{ github.event.commits[0].message }}' && \ - cp -fr LICENSE debian/copyright && \ - sed 's/@BUILD_DEPENDS@/${{ matrix.build-depends }}/g; s/@DEPENDS@/${{ matrix.package-depends }}/g; s/@ARCHITECTURE@/${{ matrix.architecture }}/g; s/@STANDARDS_VERSION@/${{ env.STANDARDS_VERSION }}/g' debian/control.in > debian/control && \ - sed 's/@CMAKE_ENVIRONMENT@/${{ matrix.cmake-environment }}/g' debian/rules.in > debian/rules && \ - tar -cJf ../hyperion_${{ env.TARBALL_VERSION }}.orig.tar.xz . && \ - debuild --no-lintian -uc -us && \ - cp ../hyperion_*.deb /deploy" - - - name: Upload package artifact - uses: actions/upload-artifact@v3 - with: - path: deploy - retention-days: 1 - - publish: - name: Publish nightly packages - needs: [setup, build] - if: github.repository_owner == 'hyperion-project' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6.0.0 - with: - gpg_private_key: ${{ secrets.APT_GPG }} - - - name: Install reprepro - run: sudo apt -y install reprepro - - - name: Make build folders, export public GPG key, copy distributions file and create short sha file for nightly build check - run: | - mkdir -p nightly/{conf,dists,db} - gpg --armor --output nightly/hyperion.pub.key --export 'admin@hyperion-project.org' - cp debian/distributions nightly/conf/distributions - touch "nightly/$(git rev-parse --short HEAD)" - - - name: Create initial structure/packages files and symbolic links - run: | - reprepro -Vb nightly createsymlinks - reprepro -Vb nightly export - - - name: Download artifacts - uses: actions/download-artifact@v3.0.2 - - - name: Include artifacts into the package source - run: | - for file in artifact/*.deb; do - if [ -f "$file" ]; then - dist=${file#*~} - dist=${dist%_*} - reprepro -Vb nightly/ includedeb "$dist" "$file" - fi - done - - - name: Upload packages to nightly server - uses: SamKirkland/FTP-Deploy-Action@v4.3.4 - with: - server: nightly.apt.hyperion-project.org - username: ${{ secrets.NIGHTLY_USER }} - password: ${{ secrets.NIGHTLY_PASSWORD }} - local-dir: "./nightly/" - server-dir: "./" - dangerous-clean-slate: true - - - name: Remove intermediate artifacts - uses: geekyeggo/delete-artifact@v2 - with: - name: artifact - failOnError: false diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml deleted file mode 100644 index eee7c150..00000000 --- a/.github/workflows/pull-request.yml +++ /dev/null @@ -1,193 +0,0 @@ -name: Hyperion PR Build -on: - pull_request: - branches: - - master - -jobs: - -###################### -###### Linux ######### -###################### - - Linux: - name: ${{ matrix.dockerName }} - runs-on: ubuntu-latest - strategy: - matrix: - dockerImage: [ x86_64, armv6l, armv7l, aarch64 ] - include: - - dockerImage: x86_64 - dockerName: Debian Buster (x86_64) - platform: x11 - - dockerImage: armv6l - dockerName: Debian Buster (Raspberry Pi v1 & ZERO) - platform: rpi - - dockerImage: armv7l - dockerName: Debian Buster (Raspberry Pi 2 & 3) - platform: rpi - - dockerImage: aarch64 - dockerName: Debian Buster (Generic AARCH64) - platform: amlogic - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - # Append PR number to .version - - name: Append PR number to version - shell: bash - run: | - tr -d '\n' < .version > temp && mv temp .version - echo -n "+PR${{ github.event.pull_request.number }}" >> .version - - # Build packages - - name: Build packages - env: - DOCKER_IMAGE: ${{ matrix.dockerImage }} - DOCKER_TAG: buster - DOCKER_NAME: ${{ matrix.dockerName }} - PLATFORM: ${{ matrix.platform }} - shell: bash - run: ./.ci/ci_build.sh - - # Collecting deployable artifacts - - name: Collecting deployable artifacts - shell: bash - run: | - mkdir -p ${{ matrix.dockerImage }} - mv deploy/*.tar.gz ${{ matrix.dockerImage }} - - # Upload artifacts - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: ${{ matrix.dockerImage }} - path: ${{ matrix.dockerImage }} - -###################### -###### macOS ######### -###################### - - macOS: - name: macOS - runs-on: macos-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - # Append PR number to .version - - name: Append PR number to version - shell: bash - run: | - tr -d '\n' < .version > temp && mv temp .version - echo -n "+PR${{ github.event.pull_request.number }}" >> .version - - # Install dependencies - - name: Install dependencies - shell: bash - run: ./.ci/ci_install.sh - - # Build packages - - name: Build packages - env: - PLATFORM: osx - shell: bash - run: ./.ci/ci_build.sh - - # Collecting deployable artifacts - - name: Collecting deployable artifacts - shell: bash - run: | - mkdir -p macOS - mv build/*.dmg macOS - - # Upload artifacts - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: macOS - path: macOS - -###################### -###### Windows ####### -###################### - - windows: - name: Windows - runs-on: windows-2022 - env: - VCINSTALLDIR: 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC' - QT_VERSION: 5.15.2 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - # Append PR number to .version - - name: Append PR number to version - shell: bash - run: | - tr -d '\n' < .version > temp && mv temp .version - echo -n "+PR${{ github.event.pull_request.number }}" >> .version - - - name: Install Qt - uses: jurplel/install-qt-action@v3 - with: - version: ${{env.QT_VERSION}} - target: 'desktop' - arch: 'win64_msvc2019_64' - cache: 'true' - cache-key-prefix: 'cache-qt-windows' - - - name: Cache Chocolatey downloads - uses: actions/cache@v3 - with: - path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey - key: ${{ runner.os }}-chocolatey - -# - name: Install Python -# shell: powershell -# run: | -# choco install --no-progress python -y - - - name: Install OpenSSL, DirectX SDK - shell: powershell - run: | - choco install --no-progress openssl --version=1.1.1.2100 -y - choco install --no-progress directx-sdk -y - - - name: Install libjpeg-turbo - run: | - Invoke-WebRequest https://netcologne.dl.sourceforge.net/project/libjpeg-turbo/2.0.6/libjpeg-turbo-2.0.6-vc64.exe -OutFile libjpeg-turbo.exe -UserAgent NativeHost - .\libjpeg-turbo /S - - - name: Set up x64 build architecture environment - shell: cmd - run: call "${{env.VCINSTALLDIR}}\Auxiliary\Build\vcvars64.bat" - - # Build packages - - name: Build packages - env: - PLATFORM: windows - shell: bash - run: ./.ci/ci_build.sh - - # Collecting deployable artifacts - - name: Collecting deployable artifacts - shell: bash - run: | - mkdir -p windows - mv build/*.exe windows - - # Upload artifacts - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: windows - path: windows diff --git a/.github/workflows/push-master.yml b/.github/workflows/push-master.yml deleted file mode 100644 index b0ee7a1d..00000000 --- a/.github/workflows/push-master.yml +++ /dev/null @@ -1,201 +0,0 @@ -name: Hyperion CI Build -on: - push: - branches: - - '**' - tags: - - '*' - -jobs: - -################### -###### Linux ###### -################### - - Linux: - name: ${{ matrix.dockerName }} - runs-on: ubuntu-latest - strategy: - matrix: - dockerImage: [ x86_64, armv6l, armv7l, aarch64 ] - include: - - dockerImage: x86_64 - dockerName: Debian Buster (x86_64) - platform: x11 - - dockerImage: armv6l - dockerName: Debian Buster (Raspberry Pi v1 & ZERO) - platform: rpi - - dockerImage: armv7l - dockerName: Debian Buster (Raspberry Pi 2 & 3) - platform: rpi - - dockerImage: aarch64 - dockerName: Debian Buster (Generic AARCH64) - platform: amlogic - - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - # Build process - - name: Build packages - env: - DOCKER_IMAGE: ${{ matrix.dockerImage }} - DOCKER_TAG: buster - DOCKER_NAME: ${{ matrix.dockerName }} - PLATFORM: ${{ matrix.platform }} - shell: bash - run: ./.ci/ci_build.sh - - # Upload artifacts (only on tagged commit) - - name: Upload artifacts - if: startsWith(github.event.ref, 'refs/tags') - uses: actions/upload-artifact@v3 - with: - path: deploy/Hyperion-* - -################### -###### macOS ###### -################### - - macOS: - name: macOS - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - # Install dependencies - - name: Install dependencies - shell: bash - run: ./.ci/ci_install.sh - - # Build process - - name: Build packages - env: - PLATFORM: osx - shell: bash - run: ./.ci/ci_build.sh - - # Upload artifacts (only on tagged commit) - - name: Upload artifacts - if: startsWith(github.event.ref, 'refs/tags') - uses: actions/upload-artifact@v3 - with: - path: build/Hyperion-* - -##################### -###### Windows ###### -##################### - - windows: - name: Windows - runs-on: windows-2022 - env: - VCINSTALLDIR: 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC' - QT_VERSION: 5.15.2 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install Qt - uses: jurplel/install-qt-action@v3 - with: - version: ${{env.QT_VERSION}} - target: 'desktop' - arch: 'win64_msvc2019_64' - cache: 'true' - cache-key-prefix: 'cache-qt-windows' - - - name: Cache Chocolatey downloads - uses: actions/cache@v3 - with: - path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey - key: ${{ runner.os }}-chocolatey - -# - name: Install Python -# shell: powershell -# run: | -# choco install --no-progress python -y - - - name: Install OpenSSL, DirectX SDK - shell: powershell - run: | - choco install --no-progress openssl --version=1.1.1.2100 -y - choco install --no-progress directx-sdk -y - - - name: Install libjpeg-turbo - run: | - Invoke-WebRequest https://netcologne.dl.sourceforge.net/project/libjpeg-turbo/2.0.6/libjpeg-turbo-2.0.6-vc64.exe -OutFile libjpeg-turbo.exe -UserAgent NativeHost - .\libjpeg-turbo /S - - - name: Set up x64 build architecture environment - shell: cmd - run: call "${{env.VCINSTALLDIR}}\Auxiliary\Build\vcvars64.bat" - - # Build packages - - name: Build packages - env: - PLATFORM: windows - shell: bash - run: ./.ci/ci_build.sh - - # Upload artifacts (only on tagged commit) - - name: Upload artifacts - if: startsWith(github.event.ref, 'refs/tags') - uses: actions/upload-artifact@v3 - with: - path: build/Hyperion-* - retention-days: 1 - -##################################### -###### Publish GitHub Releases ###### -##################################### - - github_publish: - name: Publish GitHub Releases - if: startsWith(github.event.ref, 'refs/tags') - needs: [Linux, macOS, windows] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - # Generate environment variables - - name: Generate environment variables from .version and tag - run: | - echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV - echo "VERSION=$(tr -d '\n' < .version)" >> $GITHUB_ENV - - # Download artifacts from previous build process - - name: Download artifacts - uses: actions/download-artifact@v3.0.2 - with: - path: artifacts - - # Create draft release and upload artifacts - - name: Create draft release - uses: softprops/action-gh-release@v1 - with: - name: Hyperion ${{ env.VERSION }} - tag_name: ${{ env.TAG }} - files: "artifacts/**" - draft: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -########################## -###### APT workflow ###### -########################## - - apt_build: - name: APT Build GitHub Releases - if: startsWith(github.event.ref, 'refs/tags') - needs: [Linux, macOS, windows] - uses: ./.github/workflows/apt.yml - secrets: inherit - with: - head_sha: master diff --git a/.github/workflows/push_pull.yml b/.github/workflows/push_pull.yml new file mode 100644 index 00000000..add2c944 --- /dev/null +++ b/.github/workflows/push_pull.yml @@ -0,0 +1,49 @@ +name: Hyperion CI/PR Builds +run-name: | + ${{ github.event_name == 'push' && '🌱 Push build -' || '' }} + ${{ github.event_name == 'pull_request' && format('📦 Artifacts build for PR {0} - {1}', github.event.pull_request.number, github.event.pull_request.title) || github.event.head_commit.message }} + +on: + push: + branches: + - '**' + tags: + - '*' + pull_request: + branches: + - 'master' + +jobs: + + # GitHub Push/Pull Request (Release only on tagged commits) + github_build: + name: Qt ${{ matrix.qt_version }} Build ${{ matrix.qt_version == '6' && '(Testing))' || '' }} + strategy: + fail-fast: false + matrix: + qt_version: ['5', '6'] + uses: ./.github/workflows/qt5_6.yml + secrets: inherit + with: + qt_version: ${{ matrix.qt_version }} + event_name: ${{ github.event_name }} + pull_request_number: ${{ github.event.pull_request.number }} + publish: ${{ startsWith(github.event.ref, 'refs/tags') }} + + # Build DEB/RPM Packages for APT/DNF Repository (runs only on tagged commits) + repo_build: + name: 🚀 Let Hyperion build its own repository (APT/DNF) + if: startsWith(github.event.ref, 'refs/tags') + needs: [ github_build ] + runs-on: ubuntu-latest + steps: + - name: 📲 Dispatch APT/DNF build + if: ${{ env.SECRET_HYPERION_BOT_TOKEN != null }} + uses: peter-evans/repository-dispatch@v2.1.2 + with: + repository: hyperion-project/hyperion.releases-ci + token: ${{ secrets.HYPERION_BOT_TOKEN }} + event-type: releases_repo_build + client-payload: '{ "head_sha": "${{ github.sha }}" }' + env: + SECRET_HYPERION_BOT_TOKEN: ${{ secrets.HYPERION_BOT_TOKEN }} diff --git a/.github/workflows/qt5_6.yml b/.github/workflows/qt5_6.yml new file mode 100644 index 00000000..ac4622b3 --- /dev/null +++ b/.github/workflows/qt5_6.yml @@ -0,0 +1,250 @@ +name: GitHub Qt5/6 Builds + +on: + # Reusable from push_pull.yml + workflow_call: + inputs: + qt_version: + type: string + description: Build with this Qt version + default: '5' + required: false + event_name: + type: string + description: The event name + default: '' + required: false + pull_request_number: + type: string + description: The corresponding PR number + default: '' + required: false + publish: + type: boolean + description: Package publishing + default: false + required: false + +env: + ghcr: hyperion-project + +jobs: + +###################### +###### Linux ######### +###################### + + Linux: + name: 🐧 ${{ matrix.os.description }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: [ + { distribution: debian, codename: buster, description: Debian Buster (x86_64), architecture: [ amd64, linux/amd64 ] }, + { distribution: debian, codename: bullseye, description: Debian Bullseye (x86_64), architecture: [ amd64, linux/amd64 ] }, + { distribution: debian, codename: buster, description: Debian Buster (Raspberry Pi 1/ZERO), architecture: [ armv6, linux/arm/v5 ] }, + { distribution: debian, codename: buster, description: Debian Buster (Raspberry Pi 2/3/4), architecture: [ armv7, linux/arm/v7 ] }, + { distribution: debian, codename: bullseye, description: Debian Bullseye (Raspberry Pi 2/3/4), architecture: [ armv7, linux/arm/v7 ] }, + { distribution: debian, codename: buster, description: Debian Buster (Generic AARCH64), architecture: [ aarch64, linux/arm64 ] }, + { distribution: debian, codename: bullseye, description: Debian Bullseye (Generic AARCH64), architecture: [ aarch64, linux/arm64 ] } + ] + isQt5: + - ${{ inputs.qt_version == '5' }} + include: + - os.architecture[0]: amd64 + platform: x11 + - os.architecture[0]: armv6 + platform: rpi + - os.architecture[0]: armv7 + platform: rpi + - os.architecture[0]: aarch64 + platform: amlogic + exclude: + - isQt5: true + os: { distribution: debian, codename: bullseye } + - isQt5: false + os: { distribution: debian, codename: buster } + + steps: + - name: ⬇ Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: 🔧 Prepare + shell: bash + run: | + echo '::group::Append PR number to version (PR only)' + if [[ "${{ inputs.event_name }}" = "pull_request" ]]; then + tr -d '\n' < .version > temp && mv temp .version + echo -n "+PR${{ inputs.pull_request_number }}" >> .version + fi + echo '::endgroup::' + + - name: 👷 Build + shell: bash + run: ./.github/scripts/build.sh + env: + DOCKER_IMAGE: ${{ matrix.os.distribution }} + DOCKER_TAG: ${{ matrix.os.codename }}${{ inputs.qt_version == '6' && '-qt6' || '' }} + PLATFORM: ${{ matrix.platform }} + TARGET_ARCH: ${{ matrix.os.architecture[1] }} + + - name: 📦 Upload + if: ${{ inputs.publish || inputs.event_name == 'pull_request' }} + uses: actions/upload-artifact@v3 + with: + name: ${{ inputs.event_name == 'pull_request' && env.NAME || 'artifact' }} + path: ${{ inputs.event_name == 'pull_request' && 'deploy/*.tar.gz' || 'deploy/Hyperion-*' }} + env: + NAME: ${{ format('{0}_{1}_{2}{3}', matrix.os.distribution, matrix.os.codename, matrix.os.architecture[0], inputs.qt_version == '6' && '_qt6' || '') }} + +###################### +###### macOS ######### +###################### + + macOS: + name: 🍏 macOS x64 + runs-on: macos-latest + steps: + - name: ⬇ Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: 🔧 Prepare + shell: bash + run: | + echo '::group::Append PR number to version (PR only)' + if [[ "${{ inputs.event_name }}" = "pull_request" ]]; then + tr -d '\n' < .version > temp && mv temp .version + echo -n "+PR${{ inputs.pull_request_number }}" >> .version + fi + echo '::endgroup::' + + echo '::group::Update/Install dependencies' + brew update || true + brew install --overwrite qt${{ inputs.qt_version }} libusb + brew link --overwrite --force qt${{ inputs.qt_version }} + echo '::endgroup::' + + - name: 👷 Build + shell: bash + run: ./.github/scripts/build.sh + env: + PLATFORM: osx + + - name: 📦 Upload + if: ${{ inputs.publish || inputs.event_name == 'pull_request' }} + uses: actions/upload-artifact@v3 + with: + name: ${{ inputs.event_name == 'pull_request' && env.NAME || 'artifact' }} + path: 'build/Hyperion-*' + env: + NAME: ${{ inputs.qt_version == '6' && 'macOS_x64_qt6' || 'macOS_x64' }} + +###################### +###### Windows ####### +###################### + + windows: + name: 🪟 Windows x64 + runs-on: windows-2022 + env: + VCINSTALLDIR: 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC' + steps: + - name: ⬇ Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: 🔧 Prepare PR + if: ${{ inputs.event_name == 'pull_request' }} + shell: bash + run: | + echo '::group::Append PR number to version' + tr -d '\n' < .version > temp && mv temp .version + echo -n "+PR${{ inputs.pull_request_number }}" >> .version + echo '::endgroup::' + + - name: 💾 Cache/Restore + uses: actions/cache@v3 + with: + path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey + key: ${{ runner.os }}${{ inputs.qt_version == '6' && '-chocolatey-qt6' || '-chocolatey' }} + + - name: 📥 Install DirectX SDK, OpenSSL, libjpeg-turbo ${{ inputs.qt_version == '6' && 'and Vulkan-SDK' || '' }} + shell: powershell + run: | + choco install --no-progress directx-sdk ${{env.VULKAN_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 + with: + version: ${{ inputs.qt_version == '6' && '6.5.2' || '5.15.2' }} + target: 'desktop' + modules: ${{ inputs.qt_version == '6' && 'qtserialport' || '' }} + arch: 'win64_msvc2019_64' + cache: 'true' + cache-key-prefix: 'cache-qt-windows' + + - name: 🛠️ Setup MSVC + shell: cmd + run: call "${{env.VCINSTALLDIR}}\Auxiliary\Build\vcvars64.bat" + + - name: 👷 Build + shell: bash + run: ./.github/scripts/build.sh + env: + PLATFORM: windows + + - name: 📦 Upload + if: ${{ inputs.publish || inputs.event_name == 'pull_request' }} + uses: actions/upload-artifact@v3 + with: + name: ${{ inputs.event_name == 'pull_request' && env.NAME || 'artifact' }} + path: ${{ inputs.event_name == 'pull_request' && 'build/*.exe' || 'build/Hyperion-*' }} + env: + NAME: ${{ inputs.qt_version == '6' && 'windows_x64_qt6' || 'windows_x64' }} + +##################################### +###### Publish GitHub Releases ###### +##################################### + + github_publish: + name: 🚀 Publish GitHub Releases + if: ${{ inputs.qt_version == '5' && inputs.publish }} + needs: [Linux, macOS, windows] + runs-on: ubuntu-latest + steps: + - name: ⬇ Checkout + uses: actions/checkout@v4 + + - name: 🔧 Prepare + run: | + echo '::group::Generate environment variables from .version and tag' + echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV + echo "VERSION=$(tr -d '\n' < .version)" >> $GITHUB_ENV + echo '::endgroup::' + + - name: 💾 Artifact download + uses: actions/download-artifact@v3.0.2 + with: + path: artifacts + + - name: 📦 Upload + uses: softprops/action-gh-release@v1 + with: + name: Hyperion ${{ env.VERSION }} + tag_name: ${{ env.TAG }} + files: "artifacts/**" + draft: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 104c2c79..941e07dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,18 @@ -name: Release Actions +name: 🚀 Release Actions +run-name: 🚀 Let HyperBian create + on: release: types: [published] jobs: + hyperbian: - name: Let HyperBian create + name: 🚀 Let HyperBian create runs-on: ubuntu-latest steps: # Dispatch event to build new HyperBian image - - name: Dispatch HyperBian build + - name: 📲 Dispatch HyperBian build uses: peter-evans/repository-dispatch@v2.1.2 if: ${{ github.repository_owner == 'hyperion-project'}} with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 32bbb7d2..8d88c41e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,17 +1,17 @@ cmake_minimum_required(VERSION 3.5.0) -message( STATUS "CMake Version: ${CMAKE_VERSION}" ) +message(STATUS "CMake Version: ${CMAKE_VERSION}") macro(addIndent text) -if(${CMAKE_VERSION} VERSION_GREATER "3.16.0") - list(APPEND CMAKE_MESSAGE_INDENT ${text}) -endif() + if(${CMAKE_VERSION} VERSION_GREATER "3.16.0") + list(APPEND CMAKE_MESSAGE_INDENT ${text}) + endif() endmacro() macro(removeIndent) -if(${CMAKE_VERSION} VERSION_GREATER "3.16.0") - list(POP_BACK CMAKE_MESSAGE_INDENT) -endif() + if(${CMAKE_VERSION} VERSION_GREATER "3.16.0") + list(POP_BACK CMAKE_MESSAGE_INDENT) + endif() endmacro() PROJECT(hyperion) @@ -31,9 +31,16 @@ set(CMAKE_AUTOMOC ON) # auto prepare .qrc files set(CMAKE_AUTORCC ON) -# Configure CCache if available +# multicore compiling +include(ProcessorCount) +ProcessorCount(NCORES) +if(NOT NCORES EQUAL 0) + set(CMAKE_BUILD_PARALLEL_LEVEL NCORES) +endif() + +# Configure CCache ifavailable find_program(CCACHE_FOUND ccache) -if ( CCACHE_FOUND ) +if(CCACHE_FOUND) set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache) set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache) endif(CCACHE_FOUND) @@ -58,160 +65,163 @@ set(CMAKE_CXX_EXTENSIONS OFF) # Set build variables # Grabber -SET ( DEFAULT_AMLOGIC OFF ) -SET ( DEFAULT_DISPMANX OFF ) -SET ( DEFAULT_DX OFF ) -SET ( DEFAULT_MF OFF ) -SET ( DEFAULT_OSX OFF ) -SET ( DEFAULT_QT ON ) -SET ( DEFAULT_V4L2 OFF ) -SET ( DEFAULT_AUDIO ON ) -SET ( DEFAULT_X11 OFF ) -SET ( DEFAULT_XCB OFF ) +set(DEFAULT_AMLOGIC OFF) +set(DEFAULT_DISPMANX OFF) +set(DEFAULT_DX OFF) +set(DEFAULT_MF OFF) +set(DEFAULT_OSX OFF) +set(DEFAULT_QT ON ) +set(DEFAULT_V4L2 OFF) +set(DEFAULT_AUDIO ON ) +set(DEFAULT_X11 OFF) +set(DEFAULT_XCB OFF) # Input -SET ( DEFAULT_BOBLIGHT_SERVER ON ) -SET ( DEFAULT_CEC OFF ) -SET ( DEFAULT_FLATBUF_SERVER ON ) -SET ( DEFAULT_PROTOBUF_SERVER ON ) +set(DEFAULT_BOBLIGHT_SERVER ON ) +set(DEFAULT_CEC OFF) +set(DEFAULT_FLATBUF_SERVER ON ) +set(DEFAULT_PROTOBUF_SERVER ON ) # Output -SET ( DEFAULT_FORWARDER ON ) -SET ( DEFAULT_FLATBUF_CONNECT ON ) +set(DEFAULT_FORWARDER ON ) +set(DEFAULT_FLATBUF_CONNECT ON ) # LED-Devices -SET ( DEFAULT_DEV_NETWORK ON ) -SET ( DEFAULT_DEV_SERIAL ON ) -SET ( DEFAULT_DEV_SPI OFF ) -SET ( DEFAULT_DEV_TINKERFORGE OFF ) -SET ( DEFAULT_DEV_USB_HID OFF ) -SET ( DEFAULT_DEV_WS281XPWM OFF ) +set(DEFAULT_DEV_NETWORK ON ) +set(DEFAULT_DEV_SERIAL ON ) +set(DEFAULT_DEV_SPI OFF) +set(DEFAULT_DEV_TINKERFORGE OFF) +set(DEFAULT_DEV_USB_HID OFF) +set(DEFAULT_DEV_WS281XPWM OFF) # Services -SET ( DEFAULT_EFFECTENGINE ON ) -SET ( DEFAULT_EXPERIMENTAL OFF ) -SET ( DEFAULT_MDNS ON ) -SET ( DEFAULT_REMOTE_CTL ON ) +set(DEFAULT_EFFECTENGINE ON ) +set(DEFAULT_EXPERIMENTAL OFF) +set(DEFAULT_MDNS ON ) +set(DEFAULT_REMOTE_CTL ON ) # Build -SET ( DEFAULT_JSONCHECKS ON ) -SET ( DEFAULT_DEPLOY_DEPENDENCIES ON ) -SET ( DEFAULT_USE_SYSTEM_FLATBUFFERS_LIBS OFF ) -SET ( DEFAULT_USE_SYSTEM_PROTO_LIBS OFF ) -SET ( DEFAULT_USE_SYSTEM_MBEDTLS_LIBS OFF ) -SET ( DEFAULT_USE_SYSTEM_QMDNS_LIBS OFF ) -SET ( DEFAULT_TESTS OFF ) +set(DEFAULT_JSONCHECKS ON ) +set(DEFAULT_DEPLOY_DEPENDENCIES ON ) +set(DEFAULT_USE_SYSTEM_FLATBUFFERS_LIBS OFF) +set(DEFAULT_USE_SYSTEM_PROTO_LIBS OFF) +set(DEFAULT_USE_SYSTEM_MBEDTLS_LIBS OFF) +set(DEFAULT_USE_SYSTEM_QMDNS_LIBS OFF) +set(DEFAULT_TESTS OFF) # Build Hyperion with a reduced set of functionality, overwrites other default values -SET ( DEFAULT_HYPERION_LIGHT OFF ) +set(DEFAULT_HYPERION_LIGHT OFF) -IF ( ${CMAKE_SYSTEM} MATCHES "Linux" ) - SET ( DEFAULT_FB ON ) - SET ( DEFAULT_V4L2 ON ) - SET ( DEFAULT_DEV_SPI ON ) - SET ( DEFAULT_DEV_TINKERFORGE ON ) - SET ( DEFAULT_DEV_USB_HID ON ) - SET ( DEFAULT_CEC ON ) -ELSEIF ( WIN32 ) - SET ( DEFAULT_DX ON ) - SET ( DEFAULT_MF ON ) -ELSE() - SET ( DEFAULT_FB OFF ) - SET ( DEFAULT_V4L2 OFF ) - SET ( DEFAULT_DEV_SPI OFF ) - SET ( DEFAULT_DEV_TINKERFORGE OFF ) - SET ( DEFAULT_DEV_USB_HID OFF ) - SET ( DEFAULT_CEC OFF ) -ENDIF() +if(${CMAKE_SYSTEM} MATCHES "Linux") + set(DEFAULT_FB ON) + set(DEFAULT_V4L2 ON) + set(DEFAULT_DEV_SPI ON) + set(DEFAULT_DEV_TINKERFORGE ON) + set(DEFAULT_DEV_USB_HID ON) + set(DEFAULT_CEC ON) +elseif (WIN32) + set(DEFAULT_DX ON) + set(DEFAULT_MF ON) +else() + set(DEFAULT_FB OFF) + set(DEFAULT_V4L2 OFF) + set(DEFAULT_DEV_SPI OFF) + set(DEFAULT_DEV_TINKERFORGE OFF) + set(DEFAULT_DEV_USB_HID OFF) + set(DEFAULT_CEC OFF) +endif() -if ( NOT DEFINED PLATFORM ) - if ( APPLE ) - SET( PLATFORM "osx") - elseif ( WIN32 ) - SET( PLATFORM "windows") - elseif ( "${CMAKE_SYSTEM_PROCESSOR}" MATCHES "x86" ) - SET( PLATFORM "x11") - elseif ( "${CMAKE_SYSTEM_PROCESSOR}" MATCHES "arm" OR "${CMAKE_SYSTEM_PROCESSOR}" MATCHES "aarch64") - SET( PLATFORM "rpi") - FILE( READ /proc/cpuinfo SYSTEM_CPUINFO ) - STRING ( TOLOWER "${SYSTEM_CPUINFO}" SYSTEM_CPUINFO ) - if ( "${SYSTEM_CPUINFO}" MATCHES "amlogic" AND ${CMAKE_SIZEOF_VOID_P} EQUAL 4 ) - SET( PLATFORM "amlogic" ) - elseif ( ("${SYSTEM_CPUINFO}" MATCHES "amlogic" OR "${SYSTEM_CPUINFO}" MATCHES "odroid-c2" OR "${SYSTEM_CPUINFO}" MATCHES "vero4k") AND ${CMAKE_SIZEOF_VOID_P} EQUAL 8 ) - SET( PLATFORM "amlogic64" ) +if(NOT DEFINED PLATFORM) + if(APPLE) + set(PLATFORM "osx") + elseif (WIN32) + set(PLATFORM "windows") + elseif ("${CMAKE_SYSTEM_PROCESSOR}" MATCHES "x86") + set(PLATFORM "x11") + elseif ("${CMAKE_SYSTEM_PROCESSOR}" MATCHES "arm" OR "${CMAKE_SYSTEM_PROCESSOR}" MATCHES "aarch64") + set(PLATFORM "rpi") + file(READ /proc/cpuinfo SYSTEM_CPUINFO) + STRING (TOLOWER "${SYSTEM_CPUINFO}" SYSTEM_CPUINFO) + if("${SYSTEM_CPUINFO}" MATCHES "amlogic" AND ${CMAKE_SIZEOF_VOID_P} EQUAL 4) + set(PLATFORM "amlogic") + elseif (("${SYSTEM_CPUINFO}" MATCHES "amlogic" OR "${SYSTEM_CPUINFO}" MATCHES "odroid-c2" OR "${SYSTEM_CPUINFO}" MATCHES "vero4k") AND ${CMAKE_SIZEOF_VOID_P} EQUAL 8) + set(PLATFORM "amlogic64") endif() endif() - if ( PLATFORM ) - message( STATUS "PLATFORM is not defined, evaluated platform: ${PLATFORM}") + if(PLATFORM) + message(STATUS "PLATFORM is not defined, evaluated platform: ${PLATFORM}") else() - message( FATAL_ERROR "PLATFORM is not defined and could not be evaluated. Set -DPLATFORM=") + message(FATAL_ERROR "PLATFORM is not defined and could not be evaluated. Set -DPLATFORM=") endif() endif() -message( STATUS "PLATFORM: ${PLATFORM}") +message(STATUS "PLATFORM: ${PLATFORM}") # Macro to get path of first sub dir of a dir, used for MAC OSX lib/header searching -MACRO(FIRSTSUBDIR result curdir) - FILE(GLOB children RELATIVE ${curdir} ${curdir}/*) - SET(dirlist "") - FOREACH(child ${children}) - IF(IS_DIRECTORY ${curdir}/${child}) - LIST(APPEND dirlist "${curdir}/${child}") - BREAK() - ENDIF() - ENDFOREACH() - SET(${result} ${dirlist}) -ENDMACRO() +macro(FIRSTSUBDIR result curdir) + file(GLOB children RELATIVE ${curdir} ${curdir}/*) + set(dirlist "") + foreach(child ${children}) + if(IS_DIRECTORY ${curdir}/${child}) + list(APPEND dirlist "${curdir}/${child}") + break() + endif() + endforeach() + set(${result} ${dirlist}) +endmacro() + +if("${PLATFORM}" MATCHES "osx") + # specify the min version of the target platform (only GitHub Actions) + if(DEFINED ENV{GITHUB_WORKSPACE}) + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15") + endif() -if ( "${PLATFORM}" MATCHES "osx" ) - # specify the min version of the target platform - SET ( CMAKE_OSX_DEPLOYMENT_TARGET "10.15" ) # add specific prefix paths FIRSTSUBDIR(SUBDIRPY "/usr/local/opt/python3/Frameworks/Python.framework/Versions") set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${SUBDIRPY}) include_directories("/opt/X11/include/") - SET ( DEFAULT_OSX ON ) - SET ( DEFAULT_AUDIO OFF ) - SET ( DEFAULT_DEV_USB_HID ON ) + set(DEFAULT_OSX ON ) + set(DEFAULT_AUDIO OFF) + set(DEFAULT_DEV_USB_HID ON ) -elseif ( "${PLATFORM}" MATCHES "rpi" ) - SET ( DEFAULT_DISPMANX ON ) - SET ( DEFAULT_DEV_WS281XPWM ON ) -elseif ( "${PLATFORM}" MATCHES "^amlogic" ) - SET ( DEFAULT_AMLOGIC ON ) - if ( "${PLATFORM}" MATCHES "-dev$" ) - SET ( DEFAULT_AMLOGIC ON ) - SET ( DEFAULT_DISPMANX OFF ) - SET ( DEFAULT_QT OFF ) - SET ( DEFAULT_CEC OFF ) +elseif ("${PLATFORM}" MATCHES "rpi") + set(DEFAULT_DISPMANX ON) + set(DEFAULT_DEV_WS281XPWM ON) +elseif ("${PLATFORM}" MATCHES "^amlogic") + set(DEFAULT_AMLOGIC ON) + if("${PLATFORM}" MATCHES "-dev$") + set(DEFAULT_AMLOGIC ON) + set(DEFAULT_DISPMANX OFF) + set(DEFAULT_QT OFF) + set(DEFAULT_CEC OFF) endif() -elseif ( "${PLATFORM}" MATCHES "^x11" ) - SET ( DEFAULT_X11 ON ) - SET ( DEFAULT_XCB ON ) - if ( "${PLATFORM}" MATCHES "-dev$" ) - SET ( DEFAULT_AMLOGIC ON) - SET ( DEFAULT_DEV_WS281XPWM ON ) +elseif ("${PLATFORM}" MATCHES "^x11") + set(DEFAULT_X11 ON) + set(DEFAULT_XCB ON) + if("${PLATFORM}" MATCHES "-dev$") + set(DEFAULT_AMLOGIC ON) + set(DEFAULT_DEV_WS281XPWM ON) endif() -elseif ( "${PLATFORM}" STREQUAL "imx6" ) - SET ( DEFAULT_FB ON ) +elseif ("${PLATFORM}" STREQUAL "imx6") + set(DEFAULT_FB ON) endif() # enable tests for -dev builds -if ( "${PLATFORM}" MATCHES "-dev$" ) - SET ( DEFAULT_TESTS ON ) +if("${PLATFORM}" MATCHES "-dev$") + set(DEFAULT_TESTS ON) endif() -STRING( TOUPPER "-DPLATFORM_${PLATFORM}" PLATFORM_DEFINE) -STRING( REPLACE "-DEV" "" PLATFORM_DEFINE "${PLATFORM_DEFINE}" ) -ADD_DEFINITIONS( ${PLATFORM_DEFINE} ) +string(TOUPPER "-DPLATFORM_${PLATFORM}" PLATFORM_DEFINE) +string(REPLACE "-DEV" "" PLATFORM_DEFINE "${PLATFORM_DEFINE}") +ADD_DEFINITIONS(${PLATFORM_DEFINE}) # set the build options -option(HYPERION_LIGHT "Build Hyperion with a reduced set of functionality" ${DEFAULT_HYPERION_LIGHT} ) +option(HYPERION_LIGHT "Build Hyperion with a reduced set of functionality" ${DEFAULT_HYPERION_LIGHT}) message(STATUS "HYPERION_LIGHT = ${HYPERION_LIGHT}") -if (HYPERION_LIGHT) +if(HYPERION_LIGHT) message(STATUS "HYPERION_LIGHT: Hyperion is build with a reduced set of functionality.") # Disable Grabbers SET ( DEFAULT_AMLOGIC OFF ) @@ -228,43 +238,43 @@ if (HYPERION_LIGHT) SET ( DEFAULT_AUDIO OFF ) # Disable Input Servers - SET ( DEFAULT_BOBLIGHT_SERVER OFF ) - SET ( DEFAULT_CEC OFF ) - SET ( DEFAULT_FLATBUF_SERVER OFF ) - SET ( DEFAULT_PROTOBUF_SERVER OFF ) + set(DEFAULT_BOBLIGHT_SERVER OFF) + set(DEFAULT_CEC OFF) + set(DEFAULT_FLATBUF_SERVER OFF) + set(DEFAULT_PROTOBUF_SERVER OFF) # Disable Output Connectors - SET ( DEFAULT_FORWARDER OFF ) - SET ( DEFAULT_FLATBUF_CONNECT OFF ) + set(DEFAULT_FORWARDER OFF) + set(DEFAULT_FLATBUF_CONNECT OFF) # Disable Services - SET ( DEFAULT_EFFECTENGINE OFF ) + set(DEFAULT_EFFECTENGINE OFF) endif() message(STATUS "Grabber options:") addIndent(" - ") -option(ENABLE_AMLOGIC "Enable the AMLOGIC video grabber" ${DEFAULT_AMLOGIC} ) +option(ENABLE_AMLOGIC "Enable the AMLOGIC video grabber" ${DEFAULT_AMLOGIC}) message(STATUS "ENABLE_AMLOGIC = ${ENABLE_AMLOGIC}") -option(ENABLE_DISPMANX "Enable the RPi dispmanx grabber" ${DEFAULT_DISPMANX} ) +option(ENABLE_DISPMANX "Enable the RPi dispmanx grabber" ${DEFAULT_DISPMANX}) message(STATUS "ENABLE_DISPMANX = ${ENABLE_DISPMANX}") option(ENABLE_DX "Enable the DirectX grabber" ${DEFAULT_DX}) message(STATUS "ENABLE_DX = ${ENABLE_DX}") -if (ENABLE_AMLOGIC) - SET(ENABLE_FB ON) +if(ENABLE_AMLOGIC) + set(ENABLE_FB ON) else() - option(ENABLE_FB " Enable the framebuffer grabber" ${DEFAULT_FB} ) + option(ENABLE_FB " Enable the framebuffer grabber" ${DEFAULT_FB}) endif() message(STATUS "ENABLE_FB = ${ENABLE_FB}") option(ENABLE_MF "Enable the Media Foundation grabber" ${DEFAULT_MF}) message(STATUS "ENABLE_MF = ${ENABLE_MF}") -option(ENABLE_OSX "Enable the OSX grabber" ${DEFAULT_OSX} ) +option(ENABLE_OSX "Enable the OSX grabber" ${DEFAULT_OSX}) message(STATUS "ENABLE_OSX = ${ENABLE_OSX}") option(ENABLE_QT "Enable the Qt grabber" ${DEFAULT_QT}) @@ -287,16 +297,16 @@ removeIndent() message(STATUS "Input options:") addIndent(" - ") -option(ENABLE_BOBLIGHT_SERVER "Enable BOBLIGHT server" ${DEFAULT_BOBLIGHT_SERVER} ) +option(ENABLE_BOBLIGHT_SERVER "Enable BOBLIGHT server" ${DEFAULT_BOBLIGHT_SERVER}) message(STATUS "ENABLE_BOBLIGHT_SERVER = ${ENABLE_BOBLIGHT_SERVER}") -option(ENABLE_CEC "Enable the libcec and CEC control" ${DEFAULT_CEC} ) +option(ENABLE_CEC "Enable the libcec and CEC control" ${DEFAULT_CEC}) message(STATUS "ENABLE_CEC = ${ENABLE_CEC}") -option(ENABLE_FLATBUF_SERVER "Enable Flatbuffers server" ${DEFAULT_FLATBUF_SERVER} ) +option(ENABLE_FLATBUF_SERVER "Enable Flatbuffers server" ${DEFAULT_FLATBUF_SERVER}) message(STATUS "ENABLE_FLATBUF_SERVER = ${ENABLE_FLATBUF_SERVER}") -option(ENABLE_PROTOBUF_SERVER "Enable Protocol Buffers server" ${DEFAULT_PROTOBUF_SERVER} ) +option(ENABLE_PROTOBUF_SERVER "Enable Protocol Buffers server" ${DEFAULT_PROTOBUF_SERVER}) message(STATUS "ENABLE_PROTOBUF_SERVER = ${ENABLE_PROTOBUF_SERVER}") removeIndent() @@ -304,13 +314,13 @@ removeIndent() message(STATUS "Output options:") addIndent(" - ") -option(ENABLE_FORWARDER "Enable Hyperion forwarding" ${DEFAULT_FORWARDER} ) +option(ENABLE_FORWARDER "Enable Hyperion forwarding" ${DEFAULT_FORWARDER}) message(STATUS "ENABLE_FORWARDER = ${ENABLE_FORWARDER}") -if (ENABLE_FORWARDER) - SET(ENABLE_FLATBUF_CONNECT ON) +if(ENABLE_FORWARDER) + set(ENABLE_FLATBUF_CONNECT ON) else() - option(ENABLE_FLATBUF_CONNECT "Enable Flatbuffers connecting remotely" ${DEFAULT_FLATBUF_CONNECT} ) + option(ENABLE_FLATBUF_CONNECT "Enable Flatbuffers connecting remotely" ${DEFAULT_FLATBUF_CONNECT}) endif() message(STATUS "ENABLE_FLATBUF_CONNECT = ${ENABLE_FLATBUF_CONNECT}") @@ -319,22 +329,22 @@ removeIndent() message(STATUS "LED-Device options:") addIndent(" - ") -option(ENABLE_DEV_NETWORK "Enable the Network devices" ${DEFAULT_DEV_NETWORK} ) +option(ENABLE_DEV_NETWORK "Enable the Network devices" ${DEFAULT_DEV_NETWORK}) message(STATUS "ENABLE_DEV_NETWORK = ${ENABLE_DEV_NETWORK}") -option(ENABLE_DEV_SERIAL "Enable the Serial devices" ${DEFAULT_DEV_SERIAL} ) +option(ENABLE_DEV_SERIAL "Enable the Serial devices" ${DEFAULT_DEV_SERIAL}) message(STATUS "ENABLE_DEV_SERIAL = ${ENABLE_DEV_SERIAL}") -option(ENABLE_DEV_SPI "Enable the SPI device" ${DEFAULT_DEV_SPI} ) +option(ENABLE_DEV_SPI "Enable the SPI device" ${DEFAULT_DEV_SPI}) message(STATUS "ENABLE_DEV_SPI = ${ENABLE_DEV_SPI}") option(ENABLE_DEV_TINKERFORGE "Enable the TINKERFORGE device" ${DEFAULT_DEV_TINKERFORGE}) message(STATUS "ENABLE_DEV_TINKERFORGE = ${ENABLE_DEV_TINKERFORGE}") -option(ENABLE_DEV_USB_HID "Enable the libusb and hid devices" ${DEFAULT_DEV_USB_HID} ) +option(ENABLE_DEV_USB_HID "Enable the libusb and hid devices" ${DEFAULT_DEV_USB_HID}) message(STATUS "ENABLE_DEV_USB_HID = ${ENABLE_DEV_USB_HID}") -option(ENABLE_DEV_WS281XPWM "Enable the WS281x-PWM device" ${DEFAULT_DEV_WS281XPWM} ) +option(ENABLE_DEV_WS281XPWM "Enable the WS281x-PWM device" ${DEFAULT_DEV_WS281XPWM}) message(STATUS "ENABLE_DEV_WS281XPWM = ${ENABLE_DEV_WS281XPWM}") removeIndent() @@ -375,7 +385,7 @@ endif() message(STATUS "DEFAULT_USE_SYSTEM_MBEDTLS_LIBS = ${DEFAULT_USE_SYSTEM_MBEDTLS_LIBS}") -if (ENABLE_MDNS) +if(ENABLE_MDNS) message(STATUS "DEFAULT_USE_SYSTEM_QMDNS_LIBS = ${DEFAULT_USE_SYSTEM_QMDNS_LIBS}") endif() @@ -388,14 +398,14 @@ message(STATUS "ENABLE_TESTS = ${ENABLE_TESTS}") removeIndent() -SET ( FLATBUFFERS_INSTALL_BIN_DIR ${CMAKE_BINARY_DIR}/flatbuf ) -SET ( FLATBUFFERS_INSTALL_LIB_DIR ${CMAKE_BINARY_DIR}/flatbuf ) +set(FLATBUFFERS_INSTALL_BIN_DIR ${CMAKE_BINARY_DIR}/flatbuf) +set(FLATBUFFERS_INSTALL_LIB_DIR ${CMAKE_BINARY_DIR}/flatbuf) -SET ( PROTOBUF_INSTALL_BIN_DIR ${CMAKE_BINARY_DIR}/proto ) -SET ( PROTOBUF_INSTALL_LIB_DIR ${CMAKE_BINARY_DIR}/proto ) +set(PROTOBUF_INSTALL_BIN_DIR ${CMAKE_BINARY_DIR}/proto) +set(PROTOBUF_INSTALL_LIB_DIR ${CMAKE_BINARY_DIR}/proto) if(ENABLE_JSONCHECKS OR ENABLE_EFFECTENGINE) - if ("${CMAKE_VERSION}" VERSION_LESS "3.12.0") + if("${CMAKE_VERSION}" VERSION_LESS "3.12.0") set(Python_ADDITIONAL_VERSIONS 3.5) find_package(PythonInterp 3.5 REQUIRED) else() @@ -408,38 +418,38 @@ endif() if(ENABLE_JSONCHECKS) # check all json files - FILE ( GLOB_RECURSE HYPERION_SCHEMAS RELATIVE ${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/libsrc/*schema*.json ) - SET( JSON_FILES ${CMAKE_BINARY_DIR}/config/hyperion.config.json.default ${HYPERION_SCHEMAS}) + file (GLOB_RECURSE HYPERION_SCHEMAS RELATIVE ${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/libsrc/*schema*.json) + set(JSON_FILES ${CMAKE_BINARY_DIR}/config/hyperion.config.json.default ${HYPERION_SCHEMAS}) - EXECUTE_PROCESS ( + execute_process ( COMMAND ${PYTHON_EXECUTABLE} test/jsonchecks/checkjson.py ${JSON_FILES} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} RESULT_VARIABLE CHECK_JSON_FAILED ) - IF ( ${CHECK_JSON_FAILED} ) - MESSAGE (FATAL_ERROR "check of json files failed" ) - ENDIF () + if(${CHECK_JSON_FAILED}) + message (FATAL_ERROR "check of json files failed") + endif() if(ENABLE_EFFECTENGINE) - EXECUTE_PROCESS ( + execute_process ( COMMAND ${PYTHON_EXECUTABLE} test/jsonchecks/checkeffects.py effects effects/schema WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} RESULT_VARIABLE CHECK_EFFECTS_FAILED ) - IF ( ${CHECK_EFFECTS_FAILED} ) - MESSAGE (FATAL_ERROR "check of json effect files failed" ) - ENDIF () + if(${CHECK_EFFECTS_FAILED}) + message (FATAL_ERROR "check of json effect files failed") + endif() endif() - EXECUTE_PROCESS ( + execute_process ( COMMAND ${PYTHON_EXECUTABLE} test/jsonchecks/checkschema.py ${CMAKE_BINARY_DIR}/config/hyperion.config.json.default libsrc/hyperion/hyperion.schema.json WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} RESULT_VARIABLE CHECK_CONFIG_FAILED ) - IF ( ${CHECK_CONFIG_FAILED} ) - MESSAGE (FATAL_ERROR "check of json default config failed" ) - ENDIF () + if(${CHECK_CONFIG_FAILED}) + message (FATAL_ERROR "check of json default config failed") + endif() endif(ENABLE_JSONCHECKS) # Add project specific cmake modules (find, etc) @@ -453,8 +463,8 @@ configure_file("${PROJECT_SOURCE_DIR}/HyperionConfig.h.in" "${PROJECT_BINARY_DIR include_directories("${PROJECT_BINARY_DIR}") # Define the global output path of binaries -SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib) -SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) +set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib) +set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) file(MAKE_DIRECTORY ${LIBRARY_OUTPUT_PATH}) file(MAKE_DIRECTORY ${EXECUTABLE_OUTPUT_PATH}) @@ -467,32 +477,21 @@ include_directories(${CMAKE_SOURCE_DIR}/include) #set(CMAKE_FIND_LIBRARY_SUFFIXES ".a;.so") # MSVC options -if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") +if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") # Search for Windows SDK find_package(WindowsSDK REQUIRED) message(STATUS "WINDOWS SDK: ${WINDOWSSDK_LATEST_DIR} ${WINDOWSSDK_LATEST_NAME}") message(STATUS "MSVC VERSION: ${MSVC_VERSION}") - - # Search for DirectX9 - if (ENABLE_DX) - find_package(DirectX9 REQUIRED) - endif(ENABLE_DX) - -endif() - -# Use GNU gold linker if available -if (NOT WIN32 AND NOT APPLE) - include (${CMAKE_CURRENT_SOURCE_DIR}/cmake/LDGold.cmake) endif() # Don't create new dynamic tags (RUNPATH) and setup -rpath to search for shared libs in BINARY/../lib folder (only for Unix) -if (ENABLE_DEPLOY_DEPENDENCIES AND UNIX AND NOT APPLE) +if(ENABLE_DEPLOY_DEPENDENCIES AND UNIX AND NOT APPLE) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--disable-new-dtags") - SET(CMAKE_SKIP_BUILD_RPATH FALSE) - SET(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) - SET(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_RPATH}:$ORIGIN/../lib") - SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) -endif () + set(CMAKE_SKIP_BUILD_RPATH FALSE) + set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) + set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib") + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +endif() if(APPLE) set(CMAKE_EXE_LINKER_FLAGS "-framework CoreGraphics") @@ -503,18 +502,18 @@ find_package(Threads REQUIRED) # Allow to overwrite QT base directory # Either supply QTDIR as -DQTDIR= to cmake or set and environment variable QTDIR pointing to the Qt installation # For Windows and OSX, the default Qt installation path are tried to resolved automatically -if (NOT DEFINED QTDIR) - if (DEFINED ENV{QTDIR}) +if(NOT DEFINED QTDIR) + if(DEFINED ENV{QTDIR}) set(QTDIR $ENV{QTDIR}) else() - if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") FIRSTSUBDIR(SUBDIRQT "C:/Qt") - if (NOT ${SUBDIRQT} STREQUAL "") + if(NOT ${SUBDIRQT} STREQUAL "") set(QTDIR "${SUBDIRQT}/msvc2019_64") endif() - elseif ( "${PLATFORM}" MATCHES "osx" ) + elseif ("${PLATFORM}" MATCHES "osx") # QT6 x86_64 location - if (EXISTS /usr/local/opt/qt6) + if(EXISTS /usr/local/opt/qt6) set(QTDIR "/usr/local/opt/qt6") # QT6 arm64 location elseif (EXISTS /opt/homebrew/opt/qt@6) @@ -530,34 +529,34 @@ if (NOT DEFINED QTDIR) endif() endif() -if (DEFINED QTDIR) +if(DEFINED QTDIR) message(STATUS "Add QTDIR: ${QTDIR} to CMAKE_PREFIX_PATH") list(PREPEND CMAKE_PREFIX_PATH ${QTDIR} "${QTDIR}/lib") endif() -if (CMAKE_PREFIX_PATH) - message( STATUS "CMAKE_PREFIX_PATH used: ${CMAKE_PREFIX_PATH}" ) +if(CMAKE_PREFIX_PATH) + message(STATUS "CMAKE_PREFIX_PATH used: ${CMAKE_PREFIX_PATH}") endif() # find QT libs find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Gui Network Sql Widgets REQUIRED) -message( STATUS "Found Qt Version: ${QT_VERSION}" ) +message(STATUS "Found Qt Version: ${QT_VERSION}") -if (${QT_VERSION_MAJOR} GREATER_EQUAL 6 ) - SET(QT_MIN_VERSION "6.2.2") +if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) + set(QT_MIN_VERSION "6.2.2") else() - SET(QT_MIN_VERSION "5.5.0") + set(QT_MIN_VERSION "5.5.0") endif() -if ( "${QT_VERSION}" VERSION_LESS "${QT_MIN_VERSION}" ) - message( FATAL_ERROR "Your Qt version is to old! Minimum required ${QT_MIN_VERSION}" ) +if("${QT_VERSION}" VERSION_LESS "${QT_MIN_VERSION}") + message(FATAL_ERROR "Your Qt version is to old! Minimum required ${QT_MIN_VERSION}") endif() find_package(Qt${QT_VERSION_MAJOR} ${QT_VERSION} COMPONENTS Core Gui Network Sql Widgets REQUIRED) -message( STATUS "Qt version used: ${QT_VERSION}" ) +message(STATUS "Qt version used: ${QT_VERSION}") -if (APPLE AND (${QT_VERSION_MAJOR} GREATER_EQUAL 6) ) +if(APPLE AND (${QT_VERSION_MAJOR} GREATER_EQUAL 6)) set(OPENSSL_ROOT_DIR /usr/local/opt/openssl) endif() @@ -569,29 +568,29 @@ add_definitions(${QT_DEFINITIONS}) add_subdirectory(dependencies) add_subdirectory(libsrc) add_subdirectory(src) -if (ENABLE_TESTS) +if(ENABLE_TESTS) add_subdirectory(test) -endif () +endif() # Add resources directory add_subdirectory(resources) # remove generated files on make cleaan too -LIST( APPEND GENERATED_QRC +list(APPEND GENERATED_QRC ${CMAKE_BINARY_DIR}/WebConfig.qrc ${CMAKE_BINARY_DIR}/HyperionConfig.h ) if(ENABLE_EFFECTENGINE) -LIST( APPEND GENERATED_QRC +list(APPEND GENERATED_QRC ${CMAKE_BINARY_DIR}/EffectEngine.qrc ) -endif () +endif() -set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${GENERATED_QRC}" ) +set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${GENERATED_QRC}") # uninstall target -configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" IMMEDIATE @ONLY) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" IMMEDIATE @ONLY) add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) # enable make package - no code after this line ! diff --git a/bin/scripts/docker-compile.sh b/bin/scripts/docker-compile.sh index ce298b96..bafde87f 100755 --- a/bin/scripts/docker-compile.sh +++ b/bin/scripts/docker-compile.sh @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash DOCKER="docker" # Git repo url of Hyperion @@ -7,20 +7,21 @@ GIT_REPO_URL="https://github.com/hyperion-project/hyperion.ng.git" REGISTRY_URL="ghcr.io/hyperion-project" # cmake build type BUILD_TYPE="Release" -# the docker image at GitHub Container Registry -BUILD_IMAGE="x86_64" -# the docker tag at GitHub Container Registry -BUILD_TAG="bullseye" +DISTRIBUTION="debian" +CODENAME="bullseye" +ARCHITECTURE="amd64" # build packages (.deb .zip ...) BUILD_PACKAGES=true # packages string inserted to cmake cmd PACKAGES="" # platform string inserted to cmake cmd BUILD_PLATFORM="" +#Run build with Qt6 or Qt5 +BUILD_WITH_QT5=false #Run build using GitHub code files -BUILD_LOCAL=0 +BUILD_LOCAL=false #Build from scratch -BUILD_INCREMENTAL=0 +BUILD_INCREMENTAL=false #Verbose output _VERBOSE=0 #Additional args @@ -37,11 +38,7 @@ cd `dirname ${BASE_PATH}` > /dev/null BASE_PATH=`pwd`; popd > /dev/null -BASE_PATH=`pwd`;function log () { - if [[ $_V -eq 1 ]]; then - echo "$@" - fi -} +BASE_PATH=`pwd`; set +e ${DOCKER} ps >/dev/null 2>&1 @@ -62,7 +59,7 @@ function printHelp { echo "######################################################## ## A script to compile Hyperion inside a docker container ## Requires installed Docker: https://www.docker.com/ -## Without arguments it will compile Hyperion for Debian Bullseye (x86_64) and uses Hyperion code from GitHub repository. +## Without arguments it will compile Hyperion for ${DISTRIBUTION}:${CODENAME}, ${ARCHITECTURE} architecture and uses Hyperion code from GitHub repository. ## For all images and tags currently available, see https://github.com/orgs/hyperion-project/packages ## ## Homepage: https://www.hyperion-project.org @@ -71,16 +68,17 @@ echo "######################################################## # These are possible arguments to modify the script behaviour with their default values # # docker-compile.sh -h, --help # Show this help message -# docker-compile.sh -i, --image # The docker image, e.g., x86_64, armv6l, armv7l, aarch64 -# docker-compile.sh -t, --tag # The docker tag, e.g., buster, bullseye, bookworm +# docker-compile.sh -n, --name # The distribution's codename, e.g., buster, bullseye, bookworm, jammy, trixie, lunar, mantic; Note: for Fedora it is the version number +# docker-compile.sh -a, --architecture # The output architecture, e.g., amd64, arm64, arm/v7 # docker-compile.sh -b, --type # Release or Debug build # docker-compile.sh -p, --packages # If true, build packages with CPack +# docker-compile.sh --qt5 # Build with Qt5, otherwise build with Qt6 +# docker-compile.sh -f, --platform # cmake PLATFORM parameter, e.g. x11, amlogic-dev # docker-compile.sh -l, --local # Run build using local code files # docker-compile.sh -c, --incremental # Run incremental build, i.e. do not delete files created during previous build -# docker-compile.sh -f, --platform # cmake PLATFORM parameter, e.g. x11, amlogic-dev # docker-compile.sh -v, --verbose # Run the script in verbose mode # docker-compile.sh -- args # Additonal cmake arguments, e.g., -DHYPERION_LIGHT=ON -# More informations to docker tags at: https://github.com/Hyperion-Project/hyperion.docker-ci" +# More informations to docker containers available at: https://github.com/Hyperion-Project/hyperion.docker-ci" } function log () { @@ -89,48 +87,63 @@ function log () { fi } +function check_distribution () { + url=${REGISTRY_URL}/$1:${CODENAME} + + log "Check for distribution at: $url" + if $($DOCKER buildx imagetools inspect "$url" 2>&1 | grep -q $2) ; then + rc=0 + else + rc=1 + fi + return $rc +} + echo "Compile Hyperion using a Docker container" -options=$(getopt -l "image:,tag:,type:,packages:,platform:,local,incremental,verbose,help" -o "i:t:b:p:f:lcvh" -a -- "$@") +options=$(getopt -l "architecture:,name:,type:,packages:,platform:,qt5,local,incremental,verbose,help" -o "a:n:b:p:f:lcvh" -a -- "$@") eval set -- "$options" while true do case $1 in - -i|--image) + -a|--architecture) shift - BUILD_IMAGE=$1 + ARCHITECTURE=`echo $1 | tr '[:upper:]' '[:lower:]'` ;; - -t|--tag) + -n|--name) shift - BUILD_TAG=$1 + CODENAME=`echo $1 | tr '[:upper:]' '[:lower:]'` ;; - -b|--type) + -b|--type) shift BUILD_TYPE=$1 ;; - -p|--packages) + -p|--packages) shift BUILD_PACKAGES=$1 ;; - -f|--platform) + -f|--platform) shift BUILD_PLATFORM=$1 ;; - -l|--local) - BUILD_LOCAL=1 + --qt5) + BUILD_WITH_QT5=true ;; - -c|--incremental) - BUILD_INCREMENTAL=1 + -l|--local) + BUILD_LOCAL=true ;; - -v|--verbose) + -i|--incremental) + BUILD_INCREMENTAL=true + ;; + -v|--verbose) _VERBOSE=1 ;; - -h|--help) + -h|--help) printHelp exit 0 ;; --) - shift + shift break;; esac shift @@ -148,7 +161,66 @@ if [[ ! -z ${BUILD_PLATFORM} ]]; then PLATFORM="-DPLATFORM=${BUILD_PLATFORM}" fi -echo "---> Initialize with IMAGE:TAG=${BUILD_IMAGE}:${BUILD_TAG}, BUILD_TYPE=${BUILD_TYPE}, BUILD_PACKAGES=${BUILD_PACKAGES}, PLATFORM=${BUILD_PLATFORM}, BUILD_LOCAL=${BUILD_LOCAL}, BUILD_INCREMENTAL=${BUILD_INCREMENTAL}" +PLATFORM_ARCHITECTURE="linux/"${ARCHITECTURE} + +QTVERSION="5" +if [ ${BUILD_WITH_QT5} == false ]; then + QTVERSION="6" + CODENAME="${CODENAME}-qt6" +fi + +echo "---> Evaluate distribution for codename:${CODENAME} on platform architecture ${PLATFORM_ARCHITECTURE}" +DISTRIBUTION="debian" +if ! check_distribution ${DISTRIBUTION} ${PLATFORM_ARCHITECTURE} ; then + DISTRIBUTION="ubuntu" + if ! check_distribution ${DISTRIBUTION} ${PLATFORM_ARCHITECTURE} ; then + DISTRIBUTION="fedora" + if ! check_distribution ${DISTRIBUTION} ${PLATFORM_ARCHITECTURE} ; then + echo "No docker image found for a distribution with codename: ${CODENAME} to be build on platform architecture ${PLATFORM_ARCHITECTURE}" + exit 1 + fi + fi +fi + +echo "---> Build with -> Distribution: ${DISTRIBUTION}, Codename: ${CODENAME}, Architecture: ${ARCHITECTURE}, Type: ${BUILD_TYPE}, Platform: ${BUILD_PLATFORM}, QT Version: ${QTVERSION}, Build Packages: ${BUILD_PACKAGES}, Build local: ${BUILD_LOCAL}, Build incremental: ${BUILD_INCREMENTAL}" + +# Determine the current architecture +CURRENT_ARCHITECTURE=`uname -m` + +#Test if multiarchitecture setup, i.e. user-space is 32bit +if [ ${CURRENT_ARCHITECTURE} == "aarch64" ]; then + CURRENT_ARCHITECTURE="arm64" + USER_ARCHITECTURE=$CURRENT_ARCHITECTURE + IS_V7L=`cat /proc/$$/maps |grep -m1 -c v7l` + if [ $IS_V7L -ne 0 ]; then + USER_ARCHITECTURE="arm/v7" + else + IS_V6L=`cat /proc/$$/maps |grep -m1 -c v6l` + if [ $IS_V6L -ne 0 ]; then + USER_ARCHITECTURE="arm/v6" + fi + fi + if [ $ARCHITECTURE != $USER_ARCHITECTURE ]; then + log "Identified user space current architecture: $USER_ARCHITECTURE" + CURRENT_ARCHITECTURE=$USER_ARCHITECTURE + fi +else + CURRENT_ARCHITECTURE=${CURRENT_ARCHITECTURE//x86_/amd} +fi + +log "Identified kernel current architecture: $CURRENT_ARCHITECTURE" +if [ $ARCHITECTURE != $CURRENT_ARCHITECTURE ]; then + echo "---> Build is not for the same architecturem, enable emulation for ${PLATFORM_ARCHITECTURE}" + ENTRYPOINT_OPTION= + + if [ $CURRENT_ARCHITECTURE != "amd64" ]; then + echo "---> Emulation builds can only be executed on linux/amd64, linux/x86_64 platforms, current architecture is ${CURRENT_ARCHITECTURE}" + exit 1 + fi +else + log "Build natively for platform architecture: ${PLATFORM_ARCHITECTURE}" + ENTRYPOINT_OPTION="--entrypoint=""" +fi log "---> BASE_PATH = ${BASE_PATH}" CODE_PATH=${BASE_PATH}; @@ -163,9 +235,11 @@ git clone --recursive --depth 1 -q ${GIT_REPO_URL} ${CODE_PATH} || { echo "---> fi log "---> CODE_PATH = ${CODE_PATH}" -BUILD_DIR="build-${BUILD_IMAGE}-${BUILD_TAG}" +ARCHITECTURE_PATH=${ARCHITECTURE//\//_} + +BUILD_DIR="build-${CODENAME}-${ARCHITECTURE_PATH}" BUILD_PATH="${CODE_PATH}/${BUILD_DIR}" -DEPLOY_DIR="deploy/${BUILD_IMAGE}/${BUILD_TAG}" +DEPLOY_DIR="deploy/${CODENAME}/${ARCHITECTURE}" DEPLOY_PATH="${CODE_PATH}/${DEPLOY_DIR}" log "---> BUILD_DIR = ${BUILD_DIR}" @@ -178,7 +252,7 @@ sudo rm -fr "${DEPLOY_PATH}" >/dev/null 2>&1 mkdir -p "${DEPLOY_PATH}" >/dev/null 2>&1 #Remove previous build area, if no incremental build -if [ ${BUILD_INCREMENTAL} != 1 ]; then +if [ ${BUILD_INCREMENTAL} != true ]; then sudo rm -fr "${BUILD_PATH}" >/dev/null 2>&1 fi mkdir -p "${BUILD_PATH}" >/dev/null 2>&1 @@ -194,10 +268,11 @@ echo "---> Compiling Hyperion from source code at ${CODE_PATH}" # execute inside container all commands on bash echo "---> Startup docker..." -$DOCKER run --rm \ +$DOCKER run --rm --platform=${PLATFORM_ARCHITECTURE} \ + ${ENTRYPOINT_OPTION} \ -v "${DEPLOY_PATH}:/deploy" \ -v "${CODE_PATH}/:/source:rw" \ - ${REGISTRY_URL}/${BUILD_IMAGE}:${BUILD_TAG} \ + ${REGISTRY_URL}/${DISTRIBUTION}:${CODENAME} \ /bin/bash -c "mkdir -p /source/${BUILD_DIR} && cd /source/${BUILD_DIR} && cmake -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${PLATFORM} ${BUILD_ARGS} .. || exit 2 && make -j $(nproc) ${PACKAGES} || exit 3 || : && @@ -210,7 +285,7 @@ DOCKERRC=${?} sudo chown -fR $(stat -c "%U:%G" ${BASE_PATH}) ${BUILD_PATH} if [ ${DOCKERRC} == 0 ]; then - if [ ${BUILD_LOCAL} == 1 ]; then + if [ ${BUILD_LOCAL} == true ]; then echo "---> Find compiled binaries in: ${BUILD_PATH}/bin" fi diff --git a/bin/scripts/install_pr.sh b/bin/scripts/install_pr.sh index 1f654f23..cd023504 100755 --- a/bin/scripts/install_pr.sh +++ b/bin/scripts/install_pr.sh @@ -73,13 +73,13 @@ if [ ${ARCHITECTURE} == "aarch64" ]; then IS_V7L=`cat /proc/$$/maps |grep -m1 -c v7l` if [ $IS_V7L -ne 0 ]; then USER_ARCHITECTURE="armv7l" - else + else IS_V6L=`cat /proc/$$/maps |grep -m1 -c v6l` if [ $IS_V6L -ne 0 ]; then USER_ARCHITECTURE="armv6l" fi fi - if [ $ARCHITECTURE != $USER_ARCHITECTURE ]; then + if [ $ARCHITECTURE != $USER_ARCHITECTURE ]; then echo "---> Identified kernel target architecture: $ARCHITECTURE" echo "---> Identified user space target architecture: $USER_ARCHITECTURE" ARCHITECTURE=$USER_ARCHITECTURE @@ -134,11 +134,11 @@ if [ -z "$run_id" ]; then # Determine run_id from head_sha runs=$(request_call "$api_url/actions/runs?head_sha=$head_sha") run_id=$(echo "$runs" | tr '\r\n' ' ' | ${pythonCmd} -c """ -import json,sys +import json,sys,os data = json.load(sys.stdin) for i in data['workflow_runs']: - if i['name'] == 'Hyperion PR Build': + if os.path.basename(i['path']) == 'push_pull.yml': print(i['id']) break """ 2>/dev/null) @@ -198,11 +198,11 @@ if [[ ! -z ${CURRENT_SERVICE} ]]; then echo "---> Stop current service: ${CURRENT_SERVICE}" STOPCMD="systemctl stop --quiet ${CURRENT_SERVICE} --now" - USERNAME=${SUDO_USER:-$(whoami)} + USERNAME=${SUDO_USER:-$(whoami)} if [ ${USERNAME} != "root" ]; then STOPCMD="sudo ${STOPCMD}" fi - + ${STOPCMD} >/dev/null 2>&1 if [ $? -ne 0 ]; then echo "---> Critical Error: Failed to stop service: ${CURRENT_SERVICE}, Hyperion may not be started. Stop Hyperion manually." diff --git a/bin/service/hyperion.systemd b/bin/service/hyperion.systemd index adee0eed..7087d6b0 100644 --- a/bin/service/hyperion.systemd +++ b/bin/service/hyperion.systemd @@ -1,5 +1,5 @@ [Unit] -Description=Hyperion ambient light systemd service for user %i +Description=Hyperion ambient light systemd service for user %i Documentation=https://docs.hyperion-project.org Requisite=network.target Wants=network-online.target diff --git a/bin/service/hyperion.xml b/bin/service/hyperion.xml new file mode 100644 index 00000000..1ecc4e7e --- /dev/null +++ b/bin/service/hyperion.xml @@ -0,0 +1,22 @@ + + + Hyperion + Hyperion.NG firewall rules + + + + + + + + + + + + + + + + + + diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index 40ada3e1..847c4c40 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -14,7 +14,7 @@ macro(DeployMacOS TARGET) install(CODE "set(PLUGIN_DIR \"${QT_PLUGIN_DIR}\")" COMPONENT "Hyperion") install(CODE "set(BUILD_DIR \"${CMAKE_BINARY_DIR}\")" COMPONENT "Hyperion") install(CODE "set(ENABLE_EFFECTENGINE \"${ENABLE_EFFECTENGINE}\")" COMPONENT "Hyperion") - + install(CODE [[ file(GET_RUNTIME_DEPENDENCIES @@ -36,6 +36,7 @@ macro(DeployMacOS TARGET) FILES "${dependency}" DESTINATION "${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}/Contents/lib" TYPE SHARED_LIBRARY + FOLLOW_SYMLINK_CHAIN ) endif() endforeach() @@ -48,7 +49,6 @@ macro(DeployMacOS TARGET) foreach(PLUGIN "platforms" "sqldrivers" "imageformats") if(EXISTS ${PLUGIN_DIR}/${PLUGIN}) file(GLOB files "${PLUGIN_DIR}/${PLUGIN}/*") - list(FILTER files EXCLUDE REGEX ".*libqwebp\\.dylib$") foreach(file ${files}) file(GET_RUNTIME_DEPENDENCIES EXECUTABLES ${file} @@ -61,6 +61,7 @@ macro(DeployMacOS TARGET) DESTINATION "${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}/Contents/lib" TYPE SHARED_LIBRARY FILES ${DEPENDENCY} + FOLLOW_SYMLINK_CHAIN ) endforeach() @@ -76,25 +77,27 @@ macro(DeployMacOS TARGET) endif() endforeach() - include(BundleUtilities) + include(BundleUtilities) fixup_bundle("${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}" "${QT_PLUGINS}" "${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}/Contents/lib" IGNORE_ITEM "python;python3;Python;Python3;.Python;.Python3") if(ENABLE_EFFECTENGINE) # Detect the Python version and modules directory - find_package(Python3 3.5 REQUIRED) - execute_process( - COMMAND ${Python3_EXECUTABLE} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(standard_lib=True))" - OUTPUT_VARIABLE PYTHON_MODULES_DIR - OUTPUT_STRIP_TRAILING_WHITESPACE - ) + if(NOT CMAKE_VERSION VERSION_LESS "3.12") + find_package(Python3 COMPONENTS Interpreter Development REQUIRED) + set(PYTHON_VERSION_MAJOR_MINOR "${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}") + set(PYTHON_MODULES_DIR ${Python3_STDLIB}) + else() + find_package (PythonLibs ${PYTHON_VERSION_STRING} EXACT) + set(PYTHON_VERSION_MAJOR_MINOR "${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}") + set(PYTHON_MODULES_DIR ${Python_STDLIB}) + endif() MESSAGE("Add Python ${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR} to bundle") MESSAGE("PYTHON_MODULES_DIR: ${PYTHON_MODULES_DIR}") # Copy Python modules to '/../Frameworks/Python.framework/Versions/Current/lib/PythonMAJOR.MINOR' and ignore the unnecessary stuff listed below if (PYTHON_MODULES_DIR) - set(PYTHON_VERSION_MAJOR_MINOR "${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}") file( COPY ${PYTHON_MODULES_DIR}/ DESTINATION "${CMAKE_INSTALL_PREFIX}/${TARGET_BUNDLE_NAME}/Contents/Frameworks/Python.framework/Versions/Current/lib/python${PYTHON_VERSION_MAJOR_MINOR}" @@ -167,9 +170,9 @@ macro(DeployLinux TARGET) # Extract dependencies ignoring the system ones get_prerequisites(${TARGET_FILE} DEPENDENCIES 0 1 "" "") - + message(STATUS "Dependencies for target file: ${DEPENDENCIES}") - + # Append symlink and non-symlink dependencies to the list set(PREREQUISITE_LIBS "") foreach(DEPENDENCY ${DEPENDENCIES}) @@ -276,15 +279,13 @@ macro(DeployLinux TARGET) if(ENABLE_EFFECTENGINE) # Detect the Python version and modules directory if (NOT CMAKE_VERSION VERSION_LESS "3.12") + find_package(Python3 COMPONENTS Interpreter Development REQUIRED) set(PYTHON_VERSION_MAJOR_MINOR "${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}") - set(PYTHON_MODULES_DIR "${Python3_STDLIB}") + set(PYTHON_MODULES_DIR ${Python3_STDLIB}) else() + find_package (PythonLibs ${PYTHON_VERSION_STRING} EXACT) set(PYTHON_VERSION_MAJOR_MINOR "${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}") - execute_process( - COMMAND ${PYTHON_EXECUTABLE} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(standard_lib=True))" - OUTPUT_VARIABLE PYTHON_MODULES_DIR - OUTPUT_STRIP_TRAILING_WHITESPACE - ) + set(PYTHON_MODULES_DIR ${Python_STDLIB}) endif() # Copy Python modules to 'share/hyperion/lib/pythonMAJOR.MINOR' and ignore the unnecessary stuff listed below @@ -381,19 +382,25 @@ macro(DeployWindows TARGET) list(GET openssl_versions 0 openssl_version_major) list(GET openssl_versions 1 openssl_version_minor) - set(library_suffix "-${openssl_version_major}_${openssl_version_minor}") + set(open_ssl_version_suffix) + if (openssl_version_major VERSION_EQUAL 1 AND openssl_version_minor VERSION_EQUAL 1) + set(open_ssl_version_suffix "-1_1") + else() + set(open_ssl_version_suffix "-3") + endif() + if (CMAKE_SIZEOF_VOID_P EQUAL 8) - string(APPEND library_suffix "-x64") + string(APPEND open_ssl_version_suffix "-x64") endif() find_file(OPENSSL_SSL - NAMES "libssl${library_suffix}.dll" + NAMES "libssl${open_ssl_version_suffix}.dll" PATHS ${OPENSSL_INCLUDE_DIR}/.. ${OPENSSL_INCLUDE_DIR}/../bin NO_DEFAULT_PATH ) find_file(OPENSSL_CRYPTO - NAMES "libcrypto${library_suffix}.dll" + NAMES "libcrypto${open_ssl_version_suffix}.dll" PATHS ${OPENSSL_INCLUDE_DIR}/.. ${OPENSSL_INCLUDE_DIR}/../bin NO_DEFAULT_PATH ) diff --git a/cmake/Findqmdnsengine.cmake b/cmake/Findqmdnsengine.cmake index ceb7a6b8..dac67533 100644 --- a/cmake/Findqmdnsengine.cmake +++ b/cmake/Findqmdnsengine.cmake @@ -19,4 +19,10 @@ find_package_handle_standard_args(qmdnsengine REQUIRED_VARS QMDNS_INCLUDE_DIR QMDNS_LIBRARIES ) -mark_as_advanced(QMDNS_INCLUDE_DIR QMDNS_LIBRARIES) +if(QMDNSENGINE_FOUND) + add_library(qmdnsengine STATIC IMPORTED GLOBAL) + set_target_properties(qmdnsengine PROPERTIES + IMPORTED_LOCATION ${QMDNS_LIBRARIES} + INTERFACE_INCLUDE_DIRECTORIES ${QMDNS_INCLUDE_DIR} + ) +endif() diff --git a/cmake/LDGold.cmake b/cmake/LDGold.cmake deleted file mode 100644 index 92e59694..00000000 --- a/cmake/LDGold.cmake +++ /dev/null @@ -1,17 +0,0 @@ -option(ENABLE_LDGOLD "Use GNU gold linker" ON) - -set(LDGOLD_FOUND FALSE) -if(ENABLE_LDGOLD) - execute_process(COMMAND ${CMAKE_C_COMPILER} -fuse-ld=gold -Wl,--version ERROR_QUIET OUTPUT_VARIABLE LD_VERSION) - if(LD_VERSION MATCHES "GNU gold") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=gold") - set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=gold") - set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=gold") - set(LDGOLD_FOUND TRUE) - message(STATUS "Linker: GNU gold") - else() - message(STATUS "GNU gold linker is not available, falling back to default system linker") - endif() -else() - message(STATUS "Linker: Default system linker") -endif() diff --git a/cmake/desktop/hyperiond.desktop b/cmake/desktop/hyperion.desktop similarity index 61% rename from cmake/desktop/hyperiond.desktop rename to cmake/desktop/hyperion.desktop index d47a38d3..67d02cbd 100644 --- a/cmake/desktop/hyperiond.desktop +++ b/cmake/desktop/hyperion.desktop @@ -1,8 +1,8 @@ [Desktop Entry] Name=Hyperion GenericName=Hyperion Ambient Lighting -Comment=Hyperion mimics the well known Ambilight from Philips -Icon=/usr/share/pixmaps/hyperion/hyperiond_128.png +Comment=Hyperion is an opensource Bias or Ambient Lighting implementation +Icon=hyperion Terminal=false TryExec=hyperiond Exec=hyperiond diff --git a/cmake/desktop/hyperion.metainfo.xml b/cmake/desktop/hyperion.metainfo.xml new file mode 100644 index 00000000..bc1ac7e6 --- /dev/null +++ b/cmake/desktop/hyperion.metainfo.xml @@ -0,0 +1,48 @@ + + + + com.hyperion-project.hyperion + MIT + MIT + Hyperion + The successor to Hyperion aka Hyperion Next Generation. + + +

+ Hyperion is an opensource Bias or Ambient Lighting implementation which you might know from TV manufacturers. + It supports many LED devices and video grabbers. +

+
+ + https://hyperion-project.org + https://github.com/hyperion-project/hyperion.ng/issues + https://hyperion-project.org + https://docs.hyperion-project.org/ + https://www.paypal.me/HyperionAmbi + https://poeditor.com/join/project/Y4F6vHRFjA + + + + + + + + + + + The multi language web interface + https://raw.githubusercontent.com/hyperion-project/hyperion.ng/master/doc/screenshot.png + + + + + Application + + + com.hyperion-project.hyperion.desktop + + Hyperion Project + admin@hyperion-project.org + + +
diff --git a/cmake/desktop/hyperiond_128.png b/cmake/desktop/hyperiond_128.png deleted file mode 100644 index dc400fc7043854e980d540f9bff4f13ebff772ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16979 zcmeIZXH*p3)-GC222pYlK@bE%M9CQ%MKVYZk~20rXXpk&kSGFzWQmeRl4MjeA|j#5 zNKTS7bU20Ye)qTc8T*XyocrgVamVduRn4mP%vy8K^{h2lRW&a)o+w-;VITnj;Hr}1 zV=ZuxxGY2j;P$JuMLz&Qf?%?;8Zav>0N_jrN{~|gD#O@mW~3rl!s<=#)}tZul$ypZ z=`QW(M=}DVliViRlt@-*Dz2dKtV$R)Lzw{(QK|Jufmcvz=VYe)M#9 zp8oe6q`O2N9;(A?3s)E3s%bAat20`%YjQ~6xjDQZ+7WM@!Wg`0Tr<%xAnVT|q|V4n z8^v_bi_xxrj{WMWbOWiK#X_Lo^y_9Cz=CWe@!V_6LiVqLvx} z_;Y|A6b1n2mmReR0A9QRuwf1W;;8^Y=bF)|DG62(SgI&I2C$dkoR*?Q@C>o5;!_U* zxI%eZAU|hF=#o`Gp#cYpTRG4Rx~@L_Rvcd>;z+OT-~x!SP& z>3`AK0&Sx>##^MQRcf)&y=t*1WP)1T@DO^^gUJcSbc08c{B`|>ifs;RZ7+%~LM?nl z_1K-{+XZhq`Cq?5@{y0V=a$?PybmhVQe*h9stHRgvjn-XUw5Ulq+-(EHI@zy-!+H`-Fb{1y7xJ)EjeIOOccm?Y4%OEk{BaV-8 zi()s_>?m%Es+CwwnzE`*Rim@{MeHv~_MXdj(Zf*Ly*?#w=4Pk($d$d_2WNeU zbpOnpw?i`8f;wG}Gm1BWb!$=fw2zEn_momyn3~iD$6JkyV!oP1KqpQSrq3+TdEjWn z|GT`s!^JKuz$p3s=-c<=(dwi%3U;1{ii*o@+Dkv5YeH8Xrfc@Q!wekHw^9S7({R&F zginMOa;5xq>fh3-N8eI>8$!+hT-*6UDp^oq5R;E!I&xF^T zHu(3F+T-PnZE0^aN1rK3LLJU$@6dKF>qxwL)?2rsat%aRuH?%{MTWg%eQYWtxf`l+ zn|crU*#717OPZSq$z8-g&O+gvNE$g#;+IU5aWk2%Cle{PB`c?Xsnz!7wS&@A9C3qh zriJR1bxZYhw`#{uSD{^u9^d#Z669!~TK^wx&YqA5af`sO*PPqlN8NTMnehPU0Z-zh zN~xvR9m1^U`LC4}u#8ehTS6vS6&;)Nf~St3N30u@?jrbxvPKpGr*Qe7!j) z+5RB}C#L=E5W{|qP$@Cn3dExsCv|Nb**x)r0#7waI@oW|&rO8lo*1n~oTK{Ju_UW| zy8><1U)>XG82@l%m^Y$T1f(GeR9VuF_A;P*XY--M_uQ6K~8Zr+aLP%7oXp~eGb9r=A?ewIgU9R z#)wF1VF+Jn^oCe(%8jaotKOqaYaf#jh|-&n{A4yBu(4}=5wpM#cEa_U{lYbGiHe3c z<36gdM5R1D@rFiEs^@ceH+c|Ov08M{T0G`RAb)5Xy*5=VPpSqP`cAA#&cup%_AB!E zn-bM9de}kam7K(mH}-Y`*Hn4KIf#VDAs5;6C>wLUjA^NNzbbIIZ`MRdMjW?y=^7 z8LGHcSmqzE)G^yoQezC)8(j_1`Z!XdUae)Nw;ShWduRovzqV9oOUgN@Yp}#l~LwYp&zh`sW!fSaF(!e-km@ zYIuD2+V32qSWR9IYKG{l^id6W#R5*{{NGwL@sc-P)K&Iij>y&J?-D6{`xo;Km5hSP zvOw82qYG)x6{*cM3MMJ0@US%^e=Uifi46|bh_^`@rS7^AP57v4q*Oss3vK2HyJOzl zgfC^%gxuSKVng+vdQ_LrEVAj_&FF7^8HO!)fiOE--f?#WyicDNKHeoSd9=)%7@D`m z7hA14a_|^m);%@y_!L1&-szrWNfd1vN#6Oeic`4YX%?lerzPj-XXBRgi7M^%cfSg! zROLH2O->Ja*D@gK?1C4iRztd5~z5 zsqpRb1n%>6%kjUl0Vp|ZO3gsUY#zuE`T$$ANtSsFizp(t5h z;uGYCe-M08I>=y4R292hP%uC*mM7R_S$#h}uV2?EA+Fh^WufIk3vRnr#8T=>o!69J zHI$k0R-CJZK8>UkcWB&W7YDf?R2#+*HA5_xTlBIWHb?VE`Y5Y^vj6tKS^U(xS9P_v zoALKC6;*2HTbVHLn4FoZfgb%PQ&^A!`ZbwB&QCu9;;8p((;+c7B8@J6HSS(3yx#6CG%r| zgM~wz?=9pt+sj?gC--B`WG4@Bk0C!0S`E$oUmkC;IOv_JQVPIS>w;H}UYt~Zu6~EPm4#CxQ3Db>gW#`{k*ZLRld%%Gnx}?W0=(~IcB0GUyDQ;%gXKPnF8c4R=fECy?svV7Unx0xsAAj06rD{D_O$TJcP=@)kj|3mX3uMG5(TxFe1O2 z>#+o1&E4#q`5FZqT|8)WtCO$tvo2XKTaNT6C3R^LIjL9hLDnI(YvPeNf08Uv_c6KJ zAbwGcvJb?3ASnN!$Iq=h{NwsjPOxpLhKNLno_6!?!C)Ep_jRaq?gHCqKGN+KN_qs( z>ShVT-A11stZ-PFK9z~x8EvlUA+HckcO5a@B1QD#bB!jH?g$ZZzUjB7d6@7-{{K6FS;}zal4?3^x-SX8Vy9l5n9YY!WCjW^W<|Nz?xnlQG>1FHMmv|{^ zpuJee4~eU&@T<;k@>{#fZ;7LfI6j47Oh6pEA^Sq{yhj15sBD?w5*@hk9G=zYeyl%- zvTHjgYdbAn#cs#Sh=o$;Oy=o@2~N(wNvdnV``D~3BO`H!S@MBY53o6QRr^K7TSQF- zX%1D7&#lZy;vt@)Vx1ZL9@*H|!>XN&AGs(O?CFel(GGmU1^$~ET^umO+(xJD!@9hv zcqW>pzWHW5DJed)hTo57!(Z!(tTIgJ3EJD292g~T7U@)`^t`TRcAcf1h54a0%t(NE zoqFF5H8K}u$b8mANV_a4?iU!ev!p$Dq|HpSJgm6>gp)bqr)It`{@Ws+ zScpF-4`xSKJMaogfzWzX9+J@ z^lH3M=}YEo?qJf{}eGt%n73nA2`3#w8>LAyR~jk{cAUg?Y7` z{6b?F%+LqW6D0IV1hap1|BbFY@r|&1Fvo6QA<@s(piB3;ePL4te&R44BL3{Kvxyl>361 z?r6|y=^H^yj~hic$i-CUm5sjIG9e+qh(0}7^J4cNt9>&^Sy5w-N0p;+^Nx3UY;jqQQk<@-uPnAk|R>P z+{FLLZuHg(tCr$MTw{b6?hcWB?X3`kFfOWR`^0PQJd|^}`y!{nZ%XG1=jdrtgm4Ad z-1wEAAd&*(ElIBjUKJcHR_>9%^R3wvqPv2Fn1I4}@X{`TgNpY8r=b>;W+ajM6*e?c ze}jxHg|Cv5GrUI=*19c+4|odG+^nUoDRr~Z-*z1e~Rf&|dr+PF@AUO}?XwhyU9 zU#pf_c^S&MULpFBv(yTaf!QKR!tUFXcw9?*kKVDqgHPP(o=Ln`_-?Ys4yT0e8UJHv zzac7t{BVdf&5fv$f>*C^5Rp#AQlu>+8D*?=4!DlL@CE>i_8bVQ&J`y5<`jl)VUe7z zmKfaWD17pyk$z>alIJNguhr@p$Tc2uK08ayOxoAX9P=bXRCGSeiNnufqEGT}ZPz7N zov@CUw8t9}ZC6gne|qLl!hOqK2=e+}by7Qt>=Vjm2yoa$OnZ?Cz4hgc4E5^m^#w9G|DSXoRD924iur7nQZ*Sptq{)0(!|FLAa+Z?s z>4ulu5;So16>q$U;8?K7g!9MH=|fzVGZ}S{QNYX6;V*$d&J%55koNKrD*~B*Z?X}? z#&lxW0Ck*Oqu28~s6a4|vFcU$$IXDmA`Ub2C%<*lpH`(JjE*4w8d~q4=s4Qua10A; z2+EtM86;k}=SM#0kQph*&wxe5HZFL4LN@`r&w+?Z*Uj~ z9nwh0nJD{)sPP@+GK=4yBqk%D>FZbEnA(J80gD_h0SQZD}2QMf}J7I=v{k`ORf2&HPR19=%an=n3_`v4ObO+mY1VmROX$l4J?C(A#q?v3keXn_i>z=#O?;nc|YeSOW<0aH< zeS7(}hrCojD3TghC<*VIr716uvYMnt-SpjqI1l6fn2Nwp3~I-nJt4lS${S>E*&|B# zQ;0)uYpY040=8;W)2Dw`)UeP$sHr&+j_dl#=St@eN!EbcOhCZd2L&G<;PH?7xBT|6 zV3p^SsNDwnSxX$}w@l(I8w#T=e1I)W8TsUnD(mqeB$ExNM_Z0eW2ZU^Kj~guvlrrP z*jhHUan|ykCG7rA8B=)^hb8h2RfcW4^a5IgJ+iKlwCtpUrzup-zI$~3QUf*AXF!I7Fim8OHhmM_( zMkQG0X7(2+oYeyFd)`MIJ#*jxGG_3UCAnc)9bd?ySo;CTFYiEvxapMI+h2S>eK|UF zlIp7~1>_qQ9!#hyT$#bBs!oW4GM`_@`?*gBwfPMNAGpdfXzOdV4N_F(UY5JY{f)Y{ zx)Zn^6&pO>nULe>5rA*Hmx+C-YKhpAvx>pvoL*9D2Y-{NIf5P^I2XqnL%Lx4v%UnM zYXIfy6<8ES{w7<93r_ryJyKTGB9C(HS!!Q0UH$02!p*`_K3bDNiR8dUid6a>O}GuB z@zqFyPb7I5D}hE7FGx*v(q$TJnaH44fL z?(&x0_WxacG%Io1P8bLv`sf_Q>#k{i<2F?7t2~AaGUVy1JQxtX2gX_&p>|Eth7d7! zKIFOvh#QLVnxoA*Ph$)T0&h2{+Vm&0+AsG$GAXxA1vJa}1o=LTbQD)i&^DM-l2r`( zK3~#;@YZ^F!DyPiCCeQqWL~vEJ8JZ~MXTR3oj0xk;6tBBq=glFn#j`P;D^Pf`b+7m z2nUK@m{K>5N^Fix;he~WpUaWywh@hYp|KKub{TYH$qF5x4#*-)pUM&f@8|Ow@oBV7 z8t&#-@gOGN@C}&@)X|iFlyi#}GUKKg(vH3<{oNUVq|+Kqpjl~!K|YLKdFgC;er14b zL6DyesmCVWS5wCJYBJYMDg*m|pMk4oS#1`i4x1h1UIS@Pibm$pxlvvf6<;h3MaDKW*eXYf@ zXy~!iy7G<1atorLW4=^B`?N}B&Rmt-0#3Fsq)!?K(G!b{iy{}TZHl$qnJF9l$h3Y~ zcDI|cnW?Gt@we*1;FDMm8Q7KLyR);b!T~Jfb*Am6#yOu>@Ju1?Vj(>lRo9Fs>17z4 zu0EN3F=1fuFjmm77bp1iwzD~c41W4`P#TLm7`(rMrgi)x%b+rFI4FB`I`KPc2cj08 z6TnYaP=^^pc0o+-7Vj7!`LNV#XMB%atgJk*KXa&#&5tebZ~4i#d~$$KdxjfHsc58l z;|KdG&NxD3fNY$RA2G8}coPpnOyF*G^DQTDnxK417Sz?klOoq8ntVwZm9KTok?~-^ zCqMOP5**Sf1AA^MYs5v`3@bE$uhNr@NPO;&hDC%qZQkP`D~M)Q{w&KkMLxx9660!` zKk&?i)!N>a7{<0@N4nv)$GhaY0U zdA&{NdGqtcsk{FL6tg&oc|hk9e?m7ZNx2-~<|~XE%yz>ttdNQlet)$s+h}(EmXnN8 z&yBSgC4o`^;yERsk=`M7z*>}ELztyQ0nf4DLlbUUc{OI_Gd=9!2Y{xq6J{-Mc zcK*5Tq;-`U`wIJPyCzFT`?nk-h&{$_*`SpJXCuNGK2u!*$9SCtQq8AENRgOfF`qGM z4%ax029U;0sb~uZv&>D+zMgcS9d!1f={XWx{BzT~uAO}aXS4Yu#4b|cN1?MLH+iA+ zuXn~K{9UdVDOJ>s5O9t-jr+_+nx{xQb;v_Y}t0`hwvNqJd5yXsi!<`-f z$igc;DSDJG&9u{e+tK}4jI1bC2ynfmEXur>?tv>5KEp;w^ zOvj9>%({|~J3=ot`3rX_%{!W-PengUN2J)8*jfnu32X?`%bNsWjhokn-A}mBK$T%} zpXj@iv5vy4s}V=oP_3JcS40;gd5l!)-VSL>%I}+NMZIA&tqv75bd2QyGVmG0H$&xU zOJz!BGRqlwlH5hrynZKY(!JVLM)u`|4IBUHsWlE#aZ6TJZn9EicPKn5Ii6(wQENNf zy!&Y-_aY9j06y;$>l3_xCq8@Mc;kD(fI8{~TNSieiGJKHe~A0ULS!<7HaiPPSP0(g z+!S7%hwjj%T5$*>NPmW`4baKnJadF0smq26f6-2*l%VSr@Ssw99*Gef?<9=s^rxi_lBOia zKYezrFJITBCAY#@Q@{FsUtHaFEzPu48Jq9UNgM4)o*9$5J<8tPFU4^SoU)Q<_;prw zm+r7%>6$fWlsY>6;E(wl&kT(@Yug_cSln|GZD?n3z4mCf{DyjYBYF9b@!*t~(E3@k ztnRv;li0Idv@ZQ`Oq9ctE*WtiMW2FmO3+u5TUX@2(r`epdW^|El!EcjV=qvc)I) zIU40gEdkLcLC2Iqfq(ei$+e{$m!4uW~L8;#%E9wp%7d#@vX z(LnrX!Ie!xaTqE0l;hVXA-t$jZDet8oqNuGjx-PJFtKBQ3cbb9&T}qujq=5ZS_Pry0#mzakv6Gk1 zWi+XdkD4ZUCB!2vwHcQhGPj<$CmP=99hTA5HM=&;Y_x#huIR&gXkYm#Jc(oopcK%s zSE+303gCf{A3!*OQ^PwNkrIJCvkS6#C*&@En`+0)ZbW3oY!ci`p7)iXa9L>aaZxI9 z^m5^fg)x5Dk$8aARrt8kqv@<2Yvd5e=xe`+r<{DDVX|}0(*Ec+hxhy93VSO3q6Q7{ zR`Kq89FJN2%2__M*+cYt>>{N}eM7sMf4Jy5iL~DppDFQ+LbcZHpB7gh+Lw#>#_%95 zAMiydq{Y1DZ745G;`JK3mbh$hYW?V;rcG!lakBk=onx^T^PS`DyU%qnyrUsyxSl+8 zyClH(LFTb5EaG+Re60+t&2$Gh3DfDhNE)fF?#9T;8);fA^S{yPJDeQR>L7l7H-5R$ z99PNUZuzeDfHnQW8z+YGLLONLdO1YkpWVT(IdS)v79Q84@pR!XmmC&u6@JhL0QW zJo_IxExIJ3Awxj!_>RqCdhTkuP+6-&X4OVS&+`3r*TS;rH zy^2y}FU*Yyi){!@_H9qDRjynFwgg0^=+iYfOuA3t8_C^hojiJ>U;G5RnOk#S)AaZR zmd#n~^CQ(q*|jRPf42D}No!rQv_^$qpE%%?=m7RatgUv5__6KCo?0>6f8{jJunTZe{7Vb0lti{GJtI)xzNZ)ABXm8#!^e9lo<}j3hKH zaiJNQ#ZHytMcHa6UKCPka}^&7o4v%QnJ%;)x$NFhuxkz$bjNyHH#;ORPMVG=|73n6 zVBzxd;(17Rgsm!3ZuR1|n3Jx(#5*F_J|wxZWBV3w1|KOUNhnPb-gPjp&;9Z>^ic#- z&AERq<3TSvNUvzebw|o*_5{T~hkb5@S;{8M3jXfc+<$cv$LF~Hm5H`PW6R@EqH$vZ zg;h+D?>P)EuvekLK5Q6vr#%2s1l@b7E_pEwMGrH>w;NV5ueNmi*-*F_j&<5x1G6}XGjTt?F&tEa~-Lr!q z+re-6UiYQ>G&f>$dM)`S3o|Z;ch@d>o-TNPCp$BxwMN6T*#OY*Z!69C|vgTkv|L#Qda zbk1r0tM(`FY7x(-ndsS|ZiqQEp8lKMApIxwGh|b7gnQ>Z8dR>CvY%8Le0e>adRU|H zef;0wVB7Ywz~<2q%Q8a-6aa1**cd3;DkH5MY7)i;M%o1#rQ0 zFTg?x%9ohS!T`blvyTDg8~>1#K$-I&7#vXMgWv#H!F>i;_(7Q(+?Rj_7V%eGt)Tn^ zEJT04ilB6P<-hj|N@^M`{P+2I@ALD6`n>#n;=DrQe1a@|V&Vc~;{1Xj8j|y07J;gl zzWl@X%S-+3$EEzYAHtxF`wts0e{DqYFEY(P1j$?lT z#Kgp;S4iovUZv;1#dwSV|M-J# z2PlXxZNi1H0yq>9TnY%b17HU2#E1O({N>7}qqunZ1cXGyBv(K{E%{}naB=W(@$vES zz}g_N9>Al(r@Y1Ykbp|lf{@jXnm;7r9TD53s?Ri9L%X*HEZswiNoeWluQRZ7+~MT9 zD<~u^A}S^>`&dq1K~YIrTSr$<-{7gCm9>qn-7}cIho_gfkFTHq^RVz25s@#WUMD6c zze!1bo0glGUr<=|zPRLLbxmzu{ilY;FJC)4ySjUN`+g3OjE*74Cno0?7MGS+R@c@y z_Vy1BkB(1H&(1IXy7cq!&tIPXPkvE=e&OKZ;o=cq`US!9xpbTY5C0Y)0p&wYLJK!4 zR{jto>PHFhsy-95325!oSh^39(B2lDXWzSY?T=^wdya+vKl1FKj{TQklK?3$1nfLq z3IGb6VWK#5aQj5SRzRw1aRU(Kjno_Nnb*Yp4e(hMQY8ol=jce-h zpsgfxjFZO`F8{WDv$vl$O|$AY{4;dJ>JaE9w3_9?_*lHD?9Oa~Fi|C{AE$%IMuB-210fhTtcoAKJ?g{*}Btz6Wv2erfs0J5G?uaV~ zWd2=G(QV6fi&y~YGM4D8Ek1>RmUhc+&5P~#Dov1&xPHCD&rRh1VcWjTDdD}3c1)|6 z0FB(%rnp(QQGwI6kr(nL)Qk`V^=LouJS>n1KQ0p7$Cwmc!gzCAC*<#xtG-jW@7zmn zTkgiFVS&k1Np2JMNY2VnB$Wk%A?qq=4=hl`d?tbg>}B6M7j+2gPpQhRYu1F36m;=m zf&O?@Iu@wCHLOy+r$%$E!g_7=O1$!@n++CN9{h8`jBPCwXyvEV{iyT3nF!hVFQsn_ zn7hwOhTLS~`>R-h0}E`a&geD5&D{1)XpZE1sgn6o7yjOz8fjy+-TlMUV?zrPJ1T#G z2xfyQ!|lKUxu6>y+0GqRs)^h1u6M%1Ur(XhM)%OrGoMo|&@SNnsUl*KTK|U&%vq;0 z1&PixAZ%~FA$89U3piP0frU~7yQH~pf)*hxa4X22N&QA{izTQhE;#mD0Y*2mB043! z8q^$vDri__ZJsk4hE^T<{g&L3=$ks&n#y=D#UJF*b|pUVrPHI6-vh<^;bvhC^U>V1eEB-R0z$uzQFv=`s`gYbXueRd{^D3&^X%c1(*DFM|PUBbeFMX)N2y=y#e7Q zh9$cnR9wYBMs0+COr5G5XYt+kHhOg^ZBMqT*~=Apca_J#W}SQFwP{5s{eVg2=*aGi zM)7ehu%rX}Q8E%tJC;(Of{|0-E3=CzMOAJ~ZlpF#V1bl`iy>1g{dvJtlvwGty6iIw zXjesN&W2TaJ?635UWIt7;am@9#c$M&+H}~=pGfW5BYV*rCmFALEYOWa+5a{)S=%JU zNVhE-3-Nkkfduro1N*T;%+HJ7Tp@1(jqakExGMPKSCsvu<4gW0Y zTynbDm?48kRz9s@n|DFI9K!;;576{SQ^?vEp|R{%`;Nyg%Ad|NwvW9{BRMPR^iLb+ znaFh0)kcJvIX0M@hI^8$l1asg6dp;8J~%Zs#lr$Q-53>hEYP*-(Ia&;>LC8#&#qyC z@a13X=cMqZ8Z3ZvM*RPne~a20l(Ct3N`DaDbQ!}n+@Z}I9fp#I<=dnNte671;th%-a$}!M3SAqpz zW;39`*uBmygHJJ|DU&<(9rj!>%HCjj9N~w``ZNz$@u@Xs{=35Lb?SqW(VGK1fM_hH zZDYXB_ur52hqZ<6p=;9ZO=_AP`i>QrwaR4c8eAfy6hd9lP#-LiN7;z3?F*FGk|cY7 zbFK*t$V)@*f}OlTe3gau2RjM}3hBNqc3X4DgIK_}qOJjo=h=n@>fjebUbTmwT(fC* zhpS{wHh%&^Mg1u^W(?DF>=*BPY6UVQ0}2{;b+e}8!F)U_%JolJL@ItZmqZOad_$Fj zxiz@-4>$hJb(6}@wll8(;>yF-@c+|LBxP?5*#1jsE|2lr!uHL%yoV75-83m5p$ti> z1EAxI^t<=p{o($hy}n7*QY$;z={py2y^)QHRdy?~^QgY((=~(zsNsTs@uBK;;28N6 ziF%=)Q_jV|<5DSfD9ULXV^+BW*Ra;EGgI@&0_&>Y&jv&U!;qGR{&DZNv#)@J*N(t| z)XKZrWVj$r2k$8$LxE@IrM!<0Qp4>(nmVEe`F*-{gB?$i3v;NXEh`L|#@CW6?lzf4 z!}qmo<1fId=4yQ^=~y7)%ticnMS6302Zbna0|z8UITv&^ZQx&zrRQ@}cC&Q2#ur{` zmsSl!X&qJd+4cl5k3n8uay?J031STIn3W_8`YS$`dL9w?j3nIKPBx$m@hg5lq2Hxv z{+>2GjI*DAzq*RTy`)LLP{5g|%ZunE)C8I@aiosm3i80JcOg;m+fOQ!4Z^^Zti9dp zY@)AN00$&oyh*3)97)@f#^1q7J7YAg^*gWr#rOR*Eq>rX3>Ale9V*7FoSJgfz<*O9 zCuR}wC48qpZT^UbDV&&BGO}HVH&jm5d6>Ay^KmjwheJ;D%<7|onb@B)%)~#y_^mz;D zPugZmQE^mX2${hq5%Axm!>dutk{qr`vp*}om_5^0y~^R} zuwkW2wa1%YWWlG};0W^kYi^xJW(Km3m}h@2w=Tcf40NHFboL|KS~rb0ql=4<9{pj-cn4MEnRYUnb9$ zqn5<9=7v_s64Q>u6{Fufp z>H<~b@tNuKkj#2;6^@_v55Hq zT=eyVsj6$U_hlhpahyshM!gdYyzRZmLvsul7>n;#>PEF=fqh5#UL)*uOB4&FGGo-@ zF(Rh&YVwA4aS8}5kPVLp?alIpl_zX=FLo?q%+1b(p{FyqPLD3}WSA#pAYQsF&&o*Q zR{tET31xPcQ5~b#UBw(<9e)O1Vd)px7oNj=7&kDYWT*^glykFVTFltcJgOE<;BA*C z^@}*g+d^wd7cn;A;`0*H7;Sdb_xth0eI685>z(gon9(phLMG&7t7F0k?%!sFF}{Z1 z;9!n>SL^$9g$_66eZXC=`kDU21akMp>80+NW{?4oL@>HJ;N6u&M$75^dl>TL(^+4S zzAQ;+^?GhA7K)yDCZbA*%Ga&8>ZDoh9oCoOYxxz%Y4nm_KFa0v(*I@z3(#TgG9`6ws-=>p z1)pGn0|KxUz|_^Dv%VK>3r0*GqZ*H4Z4SLtx+mqXe!IQ=-D%r_Xo=MxH!S!HD&9an z>OchbWe?;U*xiw(#Czi4ET8HP-TDN2!rlMQzl{pXxSzo~$$Z%{FA5!EjMR6{mi(0i z<=(a}e`P)%hwlmTpPqwlr941IK~K{TPSYy7M*Ah}N$cVlu)w$Z;>^XdQ7Aq}!M#-k z)Il;I{{VGVG?C6id{r-cut3A-!<~5h)jT2CRm%^au>Lf@kps7Vp0;>m)+(fe}A zLM-ofMoI^ToNnHJwXts&n|%OUTOW@uGOaaL+ACW=)LdT$)3f+e7aYgz!MEd24jB2Q zwyhmBdzQY78&BD$AkXEa7|2%K5!i{u`e-qbC}x;ik0kwM1scPM-DRtzNzuj;s#vk7 z(9PlrxHByKU=9l~y6om-ribCHi+|(_@u)*upD2^o#qyO}vk9~Kv&?`^f7grZRSYmU zuZo%aup@#IpPM(%q$%@#PMO?bbpkyQcw?d4GyyjgC=(wD!2)lv!0949Trb-w#P?7_y+g(uaWq)%2N_*`EQsd;;Dnpl7;?HloHdGT& zewzIBFc!K+@@a4!}!qQC@BSps?19P>63Riyhz?1Wl z(XInyE1y-{Yh|;)&!{jo$hfvOV({UP27FH^d$%GGG($!mmG&G9L{(OD7FIig4>wX` zA~*(L!RL)mH!+CpGZ8{8K%;~ONT#}>hnEBX6ZEv?91~T!72|?JS=YSXTgtvF>8c?9 z0HY?01v>Rdv*=!^z^_eq-re$=OLzs3Ej5i=>aZ-eWbm?aRpJS4wbox6wyrs_F2k~?mbNF9YYgLA#0^2T$^Ow<$Q7LJbvj^6a z>~ziQ$tho?q0IlRe0pfc2>T$^ITrhoePhlZanfzIW2U1ymUp2*g<6s%xV}TRWCC5l zyW4JDF|_$QD2TREu{mh|1HqO8A?6bppN_HY?rg~9lmf^$sTfnE?9&U0mvfjB!HsFF ztmm7ighmR&9qe{?8-7Qh;KaIfy5J4*fWY+PY52TF#zrbgZa?^G+Q)2)z&r8j`rYUz z74VwKkRhd&XXN=a7njWbXQ1}Y_DXnn%k*0{kNiG47k z(;SS_knxqAKiFKcJypQLjIP3}yQB_cvLUdd$$(%+&zdYWnWkCmxAs`iZBB zhSIl?+g|d|djx)?XVK%6@BDxH`|Ih_4C+&8RlFWQaz~{TBLYXXS86L}%S|!a2u(Sf zkd1#X$Q@4uI{lI1&bPDWS}yLrw({FgI6%l;v||BZj9082-~-=nkPN^A55eUuNVdp4 z4Bef0_?iT0@hkWq%@<7>ra9Do^_w7@!J83w7Ld)H;PZms#kuoq;JD3aM#;G*X%K>S zR}#D14nfxUDnd`Ap!4p&Cz$8pZ30MA3S)L^$P5F z!O6uMX6yIARv;ZbeF!QrT()523$qn}1oQOJuyNP6aB_9D0YvYyT>epRN#6lc6#qoY zx;VPHYr9%l*#LZG={QPY<*k2KJ_b$HfjRwU=Ku7v?ZX;Z83?BOCs+<_RNdCr!^RWf z6XR8lJGlH`aq53oD>+%%*{Iq$+j%|%`1nO1dVa0_H}yaD`Y`Lis$UZgi-Ohv<&CzB zt*4KLyAANKZvoeCu5N><+y89D^O={ErLzUh@jrXda^p}N#54W}ysV3ptGkVd2h7D8 z5I(bU)dz7m{{!w{-QX7z)^O#$d^yPUze4`y5D1JUIKO<`$@w3E${KPCO3t1(?pDt% zob7DnUEG~4JOKedVI!?^2M!OT>ac(cF#Nk z4KGVam=()o7bgptb6omHS+E9Bl6&&F;*oji{{ZQx B0#N_} diff --git a/cmake/package-scripts/postinst b/cmake/package-scripts/postinst index 7bc63322..54cd3901 100644 --- a/cmake/package-scripts/postinst +++ b/cmake/package-scripts/postinst @@ -105,9 +105,9 @@ ln -fs $BINSP/scripts/updateHyperionUser.sh $BINTP/updateHyperionUser 2>/dev/nul if [ "$IS_UPGRADE" = false ]; then if hash desktop-file-install 2>/dev/null; then echo "---> Install Hyperion desktop icons" - mkdir /usr/share/pixmaps/hyperion 2>/dev/null - cp /usr/share/hyperion/desktop/*.png /usr/share/pixmaps/hyperion 2>/dev/null - desktop-file-install /usr/share/hyperion/desktop/hyperiond.desktop 2>/dev/null + cp -R /usr/share/hyperion/icons /usr/share/icons/hicolor 2>/dev/null + cp /usr/share/hyperion/desktop/hyperion.metainfo.xml /usr/share/metainfo/hyperion.metainfo.xml 2>/dev/null + desktop-file-install /usr/share/hyperion/desktop/hyperion.desktop 2>/dev/null fi fi @@ -149,7 +149,7 @@ $REBOOTMESSAGE echo "-----------------------------------------------------------------------------" echo "Webpage: www.hyperion-project.org" echo "Forum: www.hyperion-project.org" -echo "Documenation: docs.hyperion-project.org" +echo "Documentation: docs.hyperion-project.org" echo "-----------------------------------------------------------------------------" diff --git a/cmake/package-scripts/prerm b/cmake/package-scripts/prerm index 648c9a2d..ea2b9d66 100644 --- a/cmake/package-scripts/prerm +++ b/cmake/package-scripts/prerm @@ -59,11 +59,29 @@ fi # In case we don't use a service kill all instances killall hyperiond 2> /dev/null -# delete desktop icons; desktop-file-edit is a workaround to hide the entry and delete it afterwards manual. -# TODO Better way for deletion and keep the desktop in sync without logout/login or desktop dependend cmds? -echo "---> Delete Hyperion desktop icons" -desktop-file-edit --set-key=NoDisplay --set-value=true /usr/share/applications/hyperiond.desktop 2> /dev/null +# remove desktop/appstream file rm -v /usr/share/applications/hyperion* 2> /dev/null -rm -rv /usr/share/pixmaps/hyperion 2> /dev/null +rm -v /usr/share/metainfo/hyperion* 2> /dev/null + +# update desktop-database (if exists) +if [ -x /usr/bin/update-desktop-database ]; then + update-desktop-database -q /usr/share/applications +fi + +# remove Hyperion icons +for i in 16x16 22x22 24x24 32x32 36x36 48x48 64x64 72x72 96x96 128x128 192x192 256x256 512x512 + rm -v usr/share/icons/hicolor/$i/apps/hyperion.png 2> /dev/null +done + +# update icon-cache +if [ -e /usr/share/icons/hicolor/icon-theme.cache ] ; then + # touch it, just in case we cannot find the binary... + touch --no-create /usr/share/icons/hicolor + if hash gtk-update-icon-cache 2>/dev/null; then + gtk-update-icon-cache /usr/share/icons/hicolor + fi + # ignore errors + true +fi exit 0 diff --git a/cmake/packages.cmake b/cmake/packages.cmake index 046cefe7..a6701545 100644 --- a/cmake/packages.cmake +++ b/cmake/packages.cmake @@ -52,7 +52,7 @@ SET ( CPACK_PACKAGE_CONTACT "packages@hyperion-project.org") SET ( CPACK_PACKAGE_VENDOR "hyperion-project") SET ( CPACK_PACKAGE_EXECUTABLES "hyperiond;Hyperion" ) SET ( CPACK_PACKAGE_INSTALL_DIRECTORY "Hyperion" ) -SET ( CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/resources/icons/hyperion-icon-32px.png" ) +SET ( CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/resources/icons/hyperion-32px.png" ) SET ( CPACK_PACKAGE_VERSION_MAJOR "${HYPERION_VERSION_MAJOR}") SET ( CPACK_PACKAGE_VERSION_MINOR "${HYPERION_VERSION_MINOR}") diff --git a/cmake/win/win_rc.cmake b/cmake/win/win_rc.cmake deleted file mode 100644 index 203e3794..00000000 --- a/cmake/win/win_rc.cmake +++ /dev/null @@ -1,15 +0,0 @@ -# process a .rc file for windows -# Provides (BINARY_NAME)_WIN_RC_PATH with path to generated file -function(generate_win_rc_file BINARY_NAME) - # target path to store generated files - set(TARGET_PATH ${CMAKE_BINARY_DIR}/win_rc_file/${BINARY_NAME}) - # assets - string(REPLACE "/" "\\\\" WIN_RC_ICON_PATH ${CMAKE_SOURCE_DIR}/cmake/nsis/installer.ico) - # configure the rc file - configure_file( - ${CMAKE_SOURCE_DIR}/cmake/win/win.rc.in - ${TARGET_PATH}/win.rc - ) - # provide var for parent scope - set(${BINARY_NAME}_WIN_RC_PATH ${TARGET_PATH}/win.rc PARENT_SCOPE) -endfunction() diff --git a/debian/control.in b/debian/control.in deleted file mode 100644 index e52b6935..00000000 --- a/debian/control.in +++ /dev/null @@ -1,12 +0,0 @@ -Source: hyperion -Section: devel -Priority: optional -Build-Depends: @BUILD_DEPENDS@ -Standards-Version: @STANDARDS_VERSION@ -Maintainer: Hyperion Project -Homepage: https://hyperion-project.org/ - -Package: hyperion -Architecture: @ARCHITECTURE@ -Depends: @DEPENDS@ -Description: Hyperion is an opensource Bias or Ambient Lighting implementation which you might know from TV manufactures. It supports many LED devices and video grabbers. diff --git a/debian/distributions b/debian/distributions deleted file mode 100644 index 12da7848..00000000 --- a/debian/distributions +++ /dev/null @@ -1,58 +0,0 @@ -Origin: Hyperion-Project -Label: apt.hyperion-project.org -Codename: focal -Architectures: amd64 armhf arm64 -Components: main -Description: Official APT Repository by Hyperion Project -SignWith: yes - -Origin: Hyperion-Project -Label: apt.hyperion-project.org -Codename: jammy -Architectures: amd64 armhf arm64 -Components: main -Description: Official APT Repository by Hyperion Project -SignWith: yes - -Origin: Hyperion-Project -Label: apt.hyperion-project.org -Codename: kinetic -Architectures: amd64 armhf arm64 -Components: main -Description: Official APT Repository by Hyperion Project -SignWith: yes - -Origin: Hyperion-Project -Label: apt.hyperion-project.org -Codename: lunar -Architectures: amd64 -Components: main -Description: Official APT Repository by Hyperion Project -SignWith: yes - -Origin: Hyperion-Project -Label: apt.hyperion-project.org -Suite: oldstable -Codename: buster -Architectures: amd64 armhf arm64 -Components: main -Description: Official APT Repository by Hyperion Project -SignWith: yes - -Origin: Hyperion-Project -Label: apt.hyperion-project.org -Suite: stable -Codename: bullseye -Architectures: amd64 armhf arm64 -Components: main -Description: Official APT Repository by Hyperion Project -SignWith: yes - -Origin: Hyperion-Project -Label: apt.hyperion-project.org -Suite: unstable -Codename: bookworm -Architectures: amd64 -Components: main -Description: Official APT Repository by Hyperion Project -SignWith: yes diff --git a/debian/rules.in b/debian/rules.in deleted file mode 100644 index a8d28d84..00000000 --- a/debian/rules.in +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/make -f -export DH_VERBOSE = 1 - -BUILDDIR = build - -build: - mkdir $(BUILDDIR); - cd $(BUILDDIR); cmake @CMAKE_ENVIRONMENT@ -DCMAKE_INSTALL_PREFIX=../debian/tmp/usr .. - make -j4 -C $(BUILDDIR) - -binary: binary-indep binary-arch - -binary-indep: - -binary-arch: - cd $(BUILDDIR); cmake -P cmake_install.cmake - rm -rf debian/tmp/usr/include debian/tmp/usr/lib debian/tmp/usr/bin/flatc - mkdir debian/tmp/DEBIAN - cp cmake/package-scripts/postinst debian/tmp/DEBIAN - chmod 0775 debian/tmp/DEBIAN/postinst - cp cmake/package-scripts/preinst debian/tmp/DEBIAN - chmod 0775 debian/tmp/DEBIAN/preinst - cp cmake/package-scripts/prerm debian/tmp/DEBIAN - chmod 0775 debian/tmp/DEBIAN/prerm - dpkg-gencontrol -phyperion - dpkg --build debian/tmp .. - rm -rf debian/tmp $(BUILDDIR) - -clean: - rm -rf $(BUILDDIR) - -.PHONY: build binary binary-arch binary-indep clean diff --git a/dependencies/CMakeLists-qmdnsengine.txt.in b/dependencies/CMakeLists-qmdnsengine.txt.in deleted file mode 100644 index b3f0812a..00000000 --- a/dependencies/CMakeLists-qmdnsengine.txt.in +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.5) - -project(qmdnsengine) - -set(WORK_DIR "@QMDNS_WORK_DIR@") -set(SOURCE_DIR "@QMDNS_SOURCE_DIR@") -set(INSTALL_DIR "@QMDNS_INSTALL_DIR@") -set(CMAKE_ARGS "@QMDNS_CMAKE_ARGS@") -set(QMDNS_LOGGING "@QMDNS_LOGGING@") - -include(ExternalProject) - -ExternalProject_Add(qmdnsengine - PREFIX ${WORK_DIR} - BUILD_ALWAYS OFF - DOWNLOAD_COMMAND "" - SOURCE_DIR ${SOURCE_DIR} - INSTALL_DIR ${INSTALL_DIR} - CMAKE_ARGS ${CMAKE_ARGS} - LOG_DOWNLOAD ${QMDNS_LOGGING} - LOG_UPDATE ${QMDNS_LOGGING} - LOG_CONFIGURE ${QMDNS_LOGGING} - LOG_BUILD ${QMDNS_LOGGING} - LOG_INSTALL ${QMDNS_LOGGING} - LOG_TEST ${QMDNS_LOGGING} -) diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index bc5044be..28ba5d04 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -24,46 +24,46 @@ if (ENABLE_MDNS) if (USE_SYSTEM_QMDNS_LIBS) find_package(qmdnsengine REQUIRED) else () - if (NOT DEFINED BUILD_QMDNS_ONCE) - set(BUILD_QMDNS_ONCE CACHE INTERNAL "Done") - set(QMDNS_WORK_DIR "${CMAKE_BINARY_DIR}/dependencies/external/qmdnsengine") - set(QMDNS_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/qmdnsengine") - set(QMDNS_INSTALL_DIR ${CMAKE_BINARY_DIR}) - set(QMDNS_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} - -Wno-dev - ) - - if(${CMAKE_BUILD_TYPE} AND ${CMAKE_BUILD_TYPE} EQUAL "Debug") - set(QMDNS_LOGGING 1) - else () - set(QMDNS_LOGGING 0) - endif () - - configure_file(${CMAKE_SOURCE_DIR}/dependencies/CMakeLists-qmdnsengine.txt.in ${QMDNS_WORK_DIR}/CMakeLists.txt @ONLY) - execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" . WORKING_DIRECTORY ${QMDNS_WORK_DIR}) - execute_process(COMMAND ${CMAKE_COMMAND} --build . --config "${CMAKE_BUILD_TYPE}" WORKING_DIRECTORY ${QMDNS_WORK_DIR}) - endif() - - set(QMDNS_INCLUDE_DIR "${CMAKE_BINARY_DIR}/include") - if(WIN32) set(QMDNS_LIBRARIES ${CMAKE_BINARY_DIR}/lib/qmdnsengine${CMAKE_STATIC_LIBRARY_SUFFIX}) else() set(QMDNS_LIBRARIES ${CMAKE_BINARY_DIR}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}qmdnsengine${CMAKE_STATIC_LIBRARY_SUFFIX}) endif() - mark_as_advanced (QMDNS_INCLUDE_DIR QMDNS_LIBRARIES) - endif () + get_property(isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + set(QMDNS_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_BUILD_TYPE=${CMAKE_BUILD_TYPE}> + -Wno-dev + ) - set(QMDNS_INCLUDE_DIR ${QMDNS_INCLUDE_DIR} PARENT_SCOPE) - set(QMDNS_LIBRARIES ${QMDNS_LIBRARIES} PARENT_SCOPE) - include_directories(${QMDNS_INCLUDE_DIR}) + include(ExternalProject) + ExternalProject_Add(qmdns + PREFIX ${CMAKE_BINARY_DIR}/dependencies/external/qmdnsengine + BUILD_ALWAYS OFF + DOWNLOAD_COMMAND "" + SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/qmdnsengine + BINARY_DIR ${CMAKE_BINARY_DIR}/dependencies/external/qmdnsengine/build + CMAKE_ARGS ${QMDNS_CMAKE_ARGS} + CONFIGURE_COMMAND ${CMAKE_COMMAND} -S -B ${QMDNS_CMAKE_ARGS} -G ${CMAKE_GENERATOR} + BUILD_COMMAND ${CMAKE_COMMAND} --build --config $ + INSTALL_DIR ${CMAKE_BINARY_DIR} + BUILD_BYPRODUCTS ${QMDNS_LIBRARIES} + ) + + add_library(qmdnsengine STATIC IMPORTED GLOBAL) + set_target_properties(qmdnsengine PROPERTIES + IMPORTED_LOCATION ${QMDNS_LIBRARIES} + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_BINARY_DIR}/include + ) + + add_dependencies(qmdnsengine qmdns) + endif () endif() #============================================================================= @@ -75,8 +75,18 @@ if(ENABLE_FLATBUF_SERVER OR ENABLE_FLATBUF_CONNECT) if (USE_SYSTEM_FLATBUFFERS_LIBS) find_program(FLATBUFFERS_FLATC_EXECUTABLE NAMES flatc REQUIRED) - find_package(Flatbuffers REQUIRED) - else () + find_package(Flatbuffers QUIET) + if (NOT Flatbuffers_FOUND) + find_package(FlatBuffers QUIET) + if (NOT FlatBuffers_FOUND) + message(STATUS "Could not find Flatbuffers system library, build static Flatbuffers library") + set(DEFAULT_USE_SYSTEM_FLATBUFFERS_LIBS OFF PARENT_SCOPE) + set(USE_SYSTEM_FLATBUFFERS_LIBS OFF) + endif() + endif() + endif() + + if (NOT USE_SYSTEM_FLATBUFFERS_LIBS) set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared flatbuffers library") set(FLATBUFFERS_BUILD_TESTS OFF CACHE BOOL "Build Flatbuffers with tests") add_subdirectory(external/flatbuffers) @@ -133,9 +143,9 @@ endif() if(ENABLE_PROTOBUF_SERVER) set(USE_SYSTEM_PROTO_LIBS ${DEFAULT_USE_SYSTEM_PROTO_LIBS} CACHE BOOL "use protobuf library from system") - + # defines for 3rd party sub-modules - set(ABSL_PROPAGATE_CXX_STD ON CACHE BOOL "Build abseil-cpp with C++ version requirements propagated") + set(ABSL_PROPAGATE_CXX_STD ON CACHE BOOL "Build abseil-cpp with C++ version requirements propagated") if (USE_SYSTEM_PROTO_LIBS) find_package(Protobuf REQUIRED) @@ -163,7 +173,7 @@ if(ENABLE_PROTOBUF_SERVER) # define the protobuf library set(PROTOBUF_LIBRARIES protobuf::libprotobuf) - + endif() # redefine at parent scope diff --git a/dependencies/external/mbedtls b/dependencies/external/mbedtls index 1873d3bf..edb8fec9 160000 --- a/dependencies/external/mbedtls +++ b/dependencies/external/mbedtls @@ -1 +1 @@ -Subproject commit 1873d3bfc2da771672bd8e7e8f41f57e0af77f33 +Subproject commit edb8fec9882084344a314368ac7fd957a187519c diff --git a/include/grabber/OsxFrameGrabberMock.h b/include/grabber/OsxFrameGrabberMock.h deleted file mode 100644 index bdc622c4..00000000 --- a/include/grabber/OsxFrameGrabberMock.h +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once -#ifndef __APPLE__ - -/* - * this is a mock up for compiling and testing osx wrapper on no osx platform. - * this will show a test image and rotate the colors. - * - * see https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.8.sdk/System/Library/Frameworks/CoreGraphics.framework/Versions/A/Headers - * - */ - -#include -#include - -enum _CGError { - kCGErrorSuccess = 0, - kCGErrorFailure = 1000, - kCGErrorIllegalArgument = 1001, - kCGErrorInvalidConnection = 1002, - kCGErrorInvalidContext = 1003, - kCGErrorCannotComplete = 1004, - kCGErrorNotImplemented = 1006, - kCGErrorRangeCheck = 1007, - kCGErrorTypeCheck = 1008, - kCGErrorInvalidOperation = 1010, - kCGErrorNoneAvailable = 1011, - - /* Obsolete errors. */ - kCGErrorNameTooLong = 1005, - kCGErrorNoCurrentPoint = 1009, - kCGErrorApplicationRequiresNewerSystem = 1015, - kCGErrorApplicationNotPermittedToExecute = 1016, - kCGErrorApplicationIncorrectExecutableFormatFound = 1023, - kCGErrorApplicationIsLaunching = 1024, - kCGErrorApplicationAlreadyRunning = 1025, - kCGErrorApplicationCanOnlyBeRunInOneSessionAtATime = 1026, - kCGErrorClassicApplicationsMustBeLaunchedByClassic = 1027, - kCGErrorForkFailed = 1028, - kCGErrorRetryRegistration = 1029, - kCGErrorFirst = 1000, - kCGErrorLast = 1029 -}; -typedef int32_t CGError; -typedef double CGFloat; - -struct CGSize { - CGFloat width; - CGFloat height; -}; -typedef struct CGSize CGSize; - -struct CGPoint { - float x; - float y; -}; -typedef struct CGPoint CGPoint; - -struct CGRect { - CGPoint origin; - CGSize size; -}; -typedef struct CGRect CGRect; - -typedef CGError CGDisplayErr; -typedef uint32_t CGDirectDisplayID; -typedef uint32_t CGDisplayCount;; -typedef struct CGDisplayMode *CGDisplayModeRef; - -typedef Image CGImage; -typedef CGImage* CGImageRef; -typedef unsigned char CFData; -typedef CFData* CFDataRef; - -const int kCGDirectMainDisplay = 0; - -CGError CGGetActiveDisplayList(uint32_t maxDisplays, CGDirectDisplayID *activeDisplays, uint32_t *displayCount); -CGDisplayModeRef CGDisplayCopyDisplayMode(CGDirectDisplayID display); -CGRect CGDisplayBounds(CGDirectDisplayID display); -void CGDisplayModeRelease(CGDisplayModeRef mode); - -CGImageRef CGDisplayCreateImage(CGDirectDisplayID display); -void CGImageRelease(CGImageRef image); -CGImageRef CGImageGetDataProvider(CGImageRef image); -CFDataRef CGDataProviderCopyData(CGImageRef image); -unsigned char* CFDataGetBytePtr(CFDataRef imgData); -unsigned CGImageGetWidth(CGImageRef image); -unsigned CGImageGetHeight(CGImageRef image); -unsigned CGImageGetBitsPerPixel(CGImageRef image); -unsigned CGImageGetBytesPerRow(CGImageRef image); -void CFRelease(CFDataRef imgData); - - -#endif diff --git a/include/grabber/AmlogicGrabber.h b/include/grabber/amlogic/AmlogicGrabber.h similarity index 97% rename from include/grabber/AmlogicGrabber.h rename to include/grabber/amlogic/AmlogicGrabber.h index 97231354..f61742aa 100644 --- a/include/grabber/AmlogicGrabber.h +++ b/include/grabber/amlogic/AmlogicGrabber.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include /// /// diff --git a/include/grabber/AmlogicWrapper.h b/include/grabber/amlogic/AmlogicWrapper.h similarity index 95% rename from include/grabber/AmlogicWrapper.h rename to include/grabber/amlogic/AmlogicWrapper.h index 87796bcd..d97c45c1 100644 --- a/include/grabber/AmlogicWrapper.h +++ b/include/grabber/amlogic/AmlogicWrapper.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include /// /// The Amlogic uses an instance of the AmlogicGrabber to obtain ImageRgb's from the diff --git a/include/grabber/AudioGrabber.h b/include/grabber/audio/AudioGrabber.h similarity index 100% rename from include/grabber/AudioGrabber.h rename to include/grabber/audio/AudioGrabber.h diff --git a/include/grabber/AudioGrabberLinux.h b/include/grabber/audio/AudioGrabberLinux.h similarity index 86% rename from include/grabber/AudioGrabberLinux.h rename to include/grabber/audio/AudioGrabberLinux.h index 0f19ae6c..272ab2f5 100644 --- a/include/grabber/AudioGrabberLinux.h +++ b/include/grabber/audio/AudioGrabberLinux.h @@ -6,7 +6,7 @@ #include // Hyperion-utils includes -#include +#include /// /// @brief The Linux Audio capture implementation @@ -18,74 +18,69 @@ class AudioGrabberLinux : public AudioGrabber AudioGrabberLinux(); ~AudioGrabberLinux() override; - /// + /// /// Process audio buffer /// void processAudioBuffer(snd_pcm_sframes_t frames); - /// + /// /// Is Running Flag /// std::atomic _isRunning; - /// + /// /// Current capture device /// snd_pcm_t * _captureDevice; public slots: - /// + /// /// Start audio capturing session /// /// @returns true if successful bool start() override; - /// + /// /// Stop audio capturing session /// void stop() override; - /// + /// /// Discovery audio devices /// QJsonArray discover(const QJsonObject& params) override; private: - /// + /// /// Refresh audio devices /// void refreshDevices(); - /// + /// /// Configure current audio capture interface /// bool configureCaptureInterface(); - /// + /// /// Get device name from path /// QString getDeviceName(const QString& devicePath) const; - /// + /// /// Current sample rate /// unsigned int _sampleRate; - /// + /// /// Audio capture thread /// pthread_t _audioThread; - /// + /// /// ALSA device configuration parameters /// snd_pcm_hw_params_t * _captureDeviceConfig; }; -/// -/// Audio processing thread function -/// -static void* AudioThreadRunner(void* params); - #endif // AUDIOGRABBERLINUX_H diff --git a/include/grabber/AudioGrabberWindows.h b/include/grabber/audio/AudioGrabberWindows.h similarity index 98% rename from include/grabber/AudioGrabberWindows.h rename to include/grabber/audio/AudioGrabberWindows.h index 9c3945b6..ee7f53c4 100644 --- a/include/grabber/AudioGrabberWindows.h +++ b/include/grabber/audio/AudioGrabberWindows.h @@ -2,7 +2,7 @@ #define AUDIOGRABBERWINDOWS_H // Hyperion-utils includes -#include +#include #include /// @@ -14,7 +14,7 @@ class AudioGrabberWindows : public AudioGrabber AudioGrabberWindows(); ~AudioGrabberWindows() override; - + public slots: bool start() override; void stop() override; diff --git a/include/grabber/AudioWrapper.h b/include/grabber/audio/AudioWrapper.h similarity index 90% rename from include/grabber/AudioWrapper.h rename to include/grabber/audio/AudioWrapper.h index 9e13c933..4f301790 100644 --- a/include/grabber/AudioWrapper.h +++ b/include/grabber/audio/AudioWrapper.h @@ -3,14 +3,14 @@ #include #ifdef WIN32 - #include + #include #endif #ifdef __linux__ - #include + #include #endif -/// +/// /// Audio Grabber wrapper /// class AudioWrapper : public GrabberWrapper @@ -32,7 +32,7 @@ class AudioWrapper : public GrabberWrapper /// ~AudioWrapper() override; - /// + /// /// Settings update handler /// void handleSettingsUpdate(settings::type type, const QJsonDocument& config) override; @@ -43,13 +43,13 @@ class AudioWrapper : public GrabberWrapper /// void action() override; - /// + /// /// Start audio capturing session /// /// @returns true if successful bool start() override; - /// + /// /// Stop audio capturing session /// void stop() override; diff --git a/include/grabber/DirectXGrabber.h b/include/grabber/directx/DirectXGrabber.h similarity index 100% rename from include/grabber/DirectXGrabber.h rename to include/grabber/directx/DirectXGrabber.h diff --git a/include/grabber/DirectXWrapper.h b/include/grabber/directx/DirectXWrapper.h similarity index 96% rename from include/grabber/DirectXWrapper.h rename to include/grabber/directx/DirectXWrapper.h index d063497d..56ec6879 100644 --- a/include/grabber/DirectXWrapper.h +++ b/include/grabber/directx/DirectXWrapper.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include class DirectXWrapper: public GrabberWrapper { diff --git a/include/grabber/DispmanxFrameGrabber.h b/include/grabber/dispmanx/DispmanxFrameGrabber.h similarity index 100% rename from include/grabber/DispmanxFrameGrabber.h rename to include/grabber/dispmanx/DispmanxFrameGrabber.h diff --git a/include/grabber/DispmanxWrapper.h b/include/grabber/dispmanx/DispmanxWrapper.h similarity index 95% rename from include/grabber/DispmanxWrapper.h rename to include/grabber/dispmanx/DispmanxWrapper.h index 303391fa..499316fc 100644 --- a/include/grabber/DispmanxWrapper.h +++ b/include/grabber/dispmanx/DispmanxWrapper.h @@ -3,7 +3,7 @@ // Utils includes #include #include -#include +#include /// /// The DispmanxWrapper uses an instance of the DispmanxFrameGrabber to obtain ImageRgb's from the diff --git a/include/grabber/FramebufferFrameGrabber.h b/include/grabber/framebuffer/FramebufferFrameGrabber.h similarity index 100% rename from include/grabber/FramebufferFrameGrabber.h rename to include/grabber/framebuffer/FramebufferFrameGrabber.h diff --git a/include/grabber/FramebufferWrapper.h b/include/grabber/framebuffer/FramebufferWrapper.h similarity index 94% rename from include/grabber/FramebufferWrapper.h rename to include/grabber/framebuffer/FramebufferWrapper.h index 2098d362..084eb96c 100644 --- a/include/grabber/FramebufferWrapper.h +++ b/include/grabber/framebuffer/FramebufferWrapper.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include /// /// The FramebufferWrapper uses an instance of the FramebufferFrameGrabber to obtain ImageRgb's from the diff --git a/include/grabber/OsxFrameGrabber.h b/include/grabber/osx/OsxFrameGrabber.h similarity index 95% rename from include/grabber/OsxFrameGrabber.h rename to include/grabber/osx/OsxFrameGrabber.h index 17530888..afb430fc 100644 --- a/include/grabber/OsxFrameGrabber.h +++ b/include/grabber/osx/OsxFrameGrabber.h @@ -1,11 +1,7 @@ #pragma once // OSX includes -#ifdef __APPLE__ #include -#else -#include -#endif // Utils includes #include diff --git a/include/grabber/OsxWrapper.h b/include/grabber/osx/OsxWrapper.h similarity index 95% rename from include/grabber/OsxWrapper.h rename to include/grabber/osx/OsxWrapper.h index c9520f7e..fd367184 100644 --- a/include/grabber/OsxWrapper.h +++ b/include/grabber/osx/OsxWrapper.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include /// /// The OsxWrapper uses an instance of the OsxFrameGrabber to obtain ImageRgb's from the displayed content. diff --git a/include/grabber/QtGrabber.h b/include/grabber/qt/QtGrabber.h similarity index 100% rename from include/grabber/QtGrabber.h rename to include/grabber/qt/QtGrabber.h diff --git a/include/grabber/QtWrapper.h b/include/grabber/qt/QtWrapper.h similarity index 97% rename from include/grabber/QtWrapper.h rename to include/grabber/qt/QtWrapper.h index 3bba4a82..3df2a64e 100644 --- a/include/grabber/QtWrapper.h +++ b/include/grabber/qt/QtWrapper.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include /// /// The QtWrapper uses QtFramework API's to get a picture from system diff --git a/include/grabber/EncoderThread.h b/include/grabber/video/EncoderThread.h similarity index 91% rename from include/grabber/EncoderThread.h rename to include/grabber/video/EncoderThread.h index 93a3ed88..51008d3e 100644 --- a/include/grabber/EncoderThread.h +++ b/include/grabber/video/EncoderThread.h @@ -136,11 +136,11 @@ class EncoderThreadManager : public QObject public: explicit EncoderThreadManager(QObject *parent = nullptr) : QObject(parent) - , _threadCount(static_cast(qMax(QThread::idealThreadCount(), DEFAULT_THREAD_COUNT))) + , _threadCount(qMax(QThread::idealThreadCount(), DEFAULT_THREAD_COUNT)) , _threads(nullptr) { _threads = new Thread*[_threadCount]; - for (unsigned long i = 0; i < _threadCount; i++) + for (int i = 0; i < _threadCount; i++) { _threads[i] = new Thread(new EncoderThread, this); _threads[i]->setObjectName("Encoder " + QString::number(i)); @@ -151,7 +151,7 @@ public: { if (_threads != nullptr) { - for(unsigned long i = 0; i < _threadCount; i++) + for(int i = 0; i < _threadCount; i++) { _threads[i]->deleteLater(); _threads[i] = nullptr; @@ -165,18 +165,18 @@ public: void start() { if (_threads != nullptr) - for (unsigned long i = 0; i < _threadCount; i++) + for (int i = 0; i < _threadCount; i++) connect(_threads[i]->thread(), &EncoderThread::newFrame, this, &EncoderThreadManager::newFrame); } void stop() { if (_threads != nullptr) - for(unsigned long i = 0; i < _threadCount; i++) + for(int i = 0; i < _threadCount; i++) disconnect(_threads[i]->thread(), nullptr, nullptr, nullptr); } - unsigned long _threadCount; + int _threadCount; Thread** _threads; signals: diff --git a/include/grabber/VideoWrapper.h b/include/grabber/video/VideoWrapper.h similarity index 89% rename from include/grabber/VideoWrapper.h rename to include/grabber/video/VideoWrapper.h index 932ff1ab..19422f25 100644 --- a/include/grabber/VideoWrapper.h +++ b/include/grabber/video/VideoWrapper.h @@ -4,9 +4,9 @@ #include #if defined(ENABLE_MF) - #include + #include #elif defined(ENABLE_V4L2) - #include + #include #endif #if defined(ENABLE_CEC) diff --git a/include/grabber/MFGrabber.h b/include/grabber/video/mediafoundation/MFGrabber.h similarity index 98% rename from include/grabber/MFGrabber.h rename to include/grabber/video/mediafoundation/MFGrabber.h index f778f8e8..30142069 100644 --- a/include/grabber/MFGrabber.h +++ b/include/grabber/video/mediafoundation/MFGrabber.h @@ -21,7 +21,7 @@ #include // decoder thread includes -#include +#include /// Forward class declaration class SourceReaderCB; diff --git a/include/grabber/V4L2Grabber.h b/include/grabber/video/v4l2/V4L2Grabber.h similarity index 99% rename from include/grabber/V4L2Grabber.h rename to include/grabber/video/v4l2/V4L2Grabber.h index 22f8cdba..6c72be94 100644 --- a/include/grabber/V4L2Grabber.h +++ b/include/grabber/video/v4l2/V4L2Grabber.h @@ -19,7 +19,7 @@ #include // decoder thread includes -#include +#include // Determine the cmake options #include diff --git a/include/grabber/X11Grabber.h b/include/grabber/x11/X11Grabber.h similarity index 100% rename from include/grabber/X11Grabber.h rename to include/grabber/x11/X11Grabber.h diff --git a/include/grabber/X11Wrapper.h b/include/grabber/x11/X11Wrapper.h similarity index 97% rename from include/grabber/X11Wrapper.h rename to include/grabber/x11/X11Wrapper.h index 79b6da92..dae62ad7 100644 --- a/include/grabber/X11Wrapper.h +++ b/include/grabber/x11/X11Wrapper.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include // some include of xorg defines "None" this is also used by QT and has to be undefined to avoid collisions #ifdef None #undef None diff --git a/include/grabber/XcbGrabber.h b/include/grabber/xcb/XcbGrabber.h similarity index 100% rename from include/grabber/XcbGrabber.h rename to include/grabber/xcb/XcbGrabber.h diff --git a/include/grabber/XcbWrapper.h b/include/grabber/xcb/XcbWrapper.h similarity index 94% rename from include/grabber/XcbWrapper.h rename to include/grabber/xcb/XcbWrapper.h index 71bb70ea..0292937c 100644 --- a/include/grabber/XcbWrapper.h +++ b/include/grabber/xcb/XcbWrapper.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include // some include of xorg defines "None" this is also used by QT and has to be undefined to avoid collisions #ifdef None diff --git a/include/hyperion/ImageToLedsMap.h b/include/hyperion/ImageToLedsMap.h index 45e7bb5a..d720581a 100644 --- a/include/hyperion/ImageToLedsMap.h +++ b/include/hyperion/ImageToLedsMap.h @@ -555,7 +555,7 @@ namespace hyperion if (pixelNum > 0) { // initial cluster with different colors - auto clusters = std::unique_ptr< ColorCluster >(new ColorCluster[_clusterCount]); + std::unique_ptr[]> clusters(new ColorCluster[_clusterCount]); for(int k = 0; k < _clusterCount; ++k) { clusters.get()[k].newColor = DEFAULT_CLUSTER_COLORS[k]; diff --git a/include/utils/Process.h b/include/utils/Process.h index f8da8e65..07df2fe7 100644 --- a/include/utils/Process.h +++ b/include/utils/Process.h @@ -3,9 +3,8 @@ #include #include -namespace Process { - -void restartHyperion(int exitCode = 0); -QByteArray command_exec(const QString& cmd, const QByteArray& data = {}); - +namespace Process +{ + void restartHyperion(int exitCode = 0); + QByteArray command_exec(const QString& cmd, const QByteArray& data = {}); } diff --git a/libsrc/CMakeLists.txt b/libsrc/CMakeLists.txt index ba5716ba..7acad42a 100644 --- a/libsrc/CMakeLists.txt +++ b/libsrc/CMakeLists.txt @@ -1,7 +1,7 @@ # Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc) +set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include) +set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc) add_subdirectory(hyperion) add_subdirectory(commandline) @@ -34,7 +34,7 @@ add_subdirectory(ssdp) if(ENABLE_MDNS) add_subdirectory(mdns) -endif() +endif() if(ENABLE_EFFECTENGINE) add_subdirectory(effectengine) diff --git a/libsrc/api/CMakeLists.txt b/libsrc/api/CMakeLists.txt index 3fd822ff..ac5cdcfd 100644 --- a/libsrc/api/CMakeLists.txt +++ b/libsrc/api/CMakeLists.txt @@ -1,26 +1,20 @@ -# Define the current source locations - -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/api) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/api) - -FILE ( GLOB_RECURSE Api_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -set(Api_RESOURCES ${CURRENT_SOURCE_DIR}/JSONRPC_schemas.qrc ) - add_library(hyperion-api - ${Api_SOURCES} - ${Api_RESOURCES} + ${CMAKE_SOURCE_DIR}/include/api/apiStructs.h + ${CMAKE_SOURCE_DIR}/include/api/API.h + ${CMAKE_SOURCE_DIR}/include/api/JsonAPI.h + ${CMAKE_SOURCE_DIR}/include/api/JsonCB.h + ${CMAKE_SOURCE_DIR}/libsrc/api/JsonAPI.cpp + ${CMAKE_SOURCE_DIR}/libsrc/api/API.cpp + ${CMAKE_SOURCE_DIR}/libsrc/api/JsonCB.cpp + ${CMAKE_SOURCE_DIR}/libsrc/api/JSONRPC_schemas.qrc ) -if(ENABLE_DX) - include_directories(${DIRECTX9_INCLUDE_DIRS}) - target_link_libraries(hyperion-api ${DIRECTX9_LIBRARIES}) -endif(ENABLE_DX) - target_link_libraries(hyperion-api hyperion hyperion-utils - Qt${QT_VERSION_MAJOR}::Core - Qt${QT_VERSION_MAJOR}::Gui - Qt${QT_VERSION_MAJOR}::Network + ${DIRECTX9_LIBRARIES} +) + +target_include_directories(hyperion-api PRIVATE + ${DIRECTX9_INCLUDE_DIRS} ) diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 75863ca2..abc62a6f 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -19,54 +19,54 @@ #include // Required to determine the cmake options #include -#include +#include #include #if defined(ENABLE_MF) - #include + #include #elif defined(ENABLE_V4L2) - #include + #include #endif #if defined(ENABLE_AUDIO) - #include + #include #ifdef WIN32 - #include + #include #endif #ifdef __linux__ - #include + #include #endif #endif #if defined(ENABLE_X11) - #include + #include #endif #if defined(ENABLE_XCB) - #include + #include #endif #if defined(ENABLE_DX) - #include + #include #endif #if defined(ENABLE_FB) - #include + #include #endif #if defined(ENABLE_DISPMANX) - #include + #include #endif #if defined(ENABLE_AMLOGIC) - #include + #include #endif #if defined(ENABLE_OSX) - #include + #include #endif #include diff --git a/libsrc/blackborder/CMakeLists.txt b/libsrc/blackborder/CMakeLists.txt index e93c5e47..d49454d8 100644 --- a/libsrc/blackborder/CMakeLists.txt +++ b/libsrc/blackborder/CMakeLists.txt @@ -1,11 +1,9 @@ - -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/blackborder) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/blackborder) - -FILE ( GLOB Blackborder_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -add_library(blackborder ${Blackborder_SOURCES} ) +add_library(blackborder + ${CMAKE_SOURCE_DIR}/include/blackborder/BlackBorderDetector.h + ${CMAKE_SOURCE_DIR}/include/blackborder/BlackBorderProcessor.h + ${CMAKE_SOURCE_DIR}/libsrc/blackborder/BlackBorderDetector.cpp + ${CMAKE_SOURCE_DIR}/libsrc/blackborder/BlackBorderProcessor.cpp +) target_link_libraries(blackborder hyperion-utils diff --git a/libsrc/boblightserver/CMakeLists.txt b/libsrc/boblightserver/CMakeLists.txt index 1587d4df..44ff5162 100644 --- a/libsrc/boblightserver/CMakeLists.txt +++ b/libsrc/boblightserver/CMakeLists.txt @@ -1,14 +1,11 @@ - -# Define the current source locations -set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/boblightserver) -set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/boblightserver) - -FILE ( GLOB BoblightServer_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -add_library(boblightserver ${BoblightServer_SOURCES} ) +add_library(boblightserver + ${CMAKE_SOURCE_DIR}/include/boblightserver/BoblightServer.h + ${CMAKE_SOURCE_DIR}/libsrc/boblightserver/BoblightServer.cpp + ${CMAKE_SOURCE_DIR}/libsrc/boblightserver/BoblightClientConnection.h + ${CMAKE_SOURCE_DIR}/libsrc/boblightserver/BoblightClientConnection.cpp +) target_link_libraries(boblightserver hyperion hyperion-utils - ${QT_LIBRARIES} ) diff --git a/libsrc/cec/CMakeLists.txt b/libsrc/cec/CMakeLists.txt index dfd80e7d..711c33d1 100644 --- a/libsrc/cec/CMakeLists.txt +++ b/libsrc/cec/CMakeLists.txt @@ -1,19 +1,18 @@ -# Define the current source locations find_package(CEC REQUIRED) +if(CEC_FOUND) + list(GET CEC_LIBRARIES 0 CEC_LIBRARIES) + add_definitions(-DCEC_LIBRARY="${CEC_LIBRARIES}") +else() + message(FATAL_ERROR "libCEC not found") +endif() -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/cec) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/cec) - -FILE (GLOB CEC_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp") - -add_library(cechandler ${CEC_SOURCES}) -list(GET CEC_LIBRARIES 0 CEC_LIBRARIES) -add_definitions(-DCEC_LIBRARY="${CEC_LIBRARIES}") - -include_directories(${CEC_INCLUDE_DIRS}) +add_library(cechandler + ${CMAKE_SOURCE_DIR}/include/cec/CECEvent.h + ${CMAKE_SOURCE_DIR}/include/cec/CECHandler.h + ${CMAKE_SOURCE_DIR}/libsrc/cec/CECHandler.cpp +) target_link_libraries(cechandler Qt${QT_VERSION_MAJOR}::Core ${CMAKE_DL_LIBS} ) - diff --git a/libsrc/commandline/CMakeLists.txt b/libsrc/commandline/CMakeLists.txt index 63a403ac..cd764913 100644 --- a/libsrc/commandline/CMakeLists.txt +++ b/libsrc/commandline/CMakeLists.txt @@ -1,10 +1,27 @@ -# Define the current source locations -set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/commandline) -set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/commandline) - -FILE ( GLOB Parser_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -add_library(commandline ${Parser_SOURCES} ) +add_library(commandline + ${CMAKE_SOURCE_DIR}/include/commandline/BooleanOption.h + ${CMAKE_SOURCE_DIR}/include/commandline/ColorOption.h + ${CMAKE_SOURCE_DIR}/include/commandline/ColorsOption.h + ${CMAKE_SOURCE_DIR}/include/commandline/DoubleOption.h + ${CMAKE_SOURCE_DIR}/include/commandline/ImageOption.h + ${CMAKE_SOURCE_DIR}/include/commandline/IntOption.h + ${CMAKE_SOURCE_DIR}/include/commandline/Option.h + ${CMAKE_SOURCE_DIR}/include/commandline/Parser.h + ${CMAKE_SOURCE_DIR}/include/commandline/RegularExpressionOption.h + ${CMAKE_SOURCE_DIR}/include/commandline/SwitchOption.h + ${CMAKE_SOURCE_DIR}/include/commandline/ValidatorOption.h + ${CMAKE_SOURCE_DIR}/libsrc/commandline/BooleanOption.cpp + ${CMAKE_SOURCE_DIR}/libsrc/commandline/ColorOption.cpp + ${CMAKE_SOURCE_DIR}/libsrc/commandline/ColorsOption.cpp + ${CMAKE_SOURCE_DIR}/libsrc/commandline/DoubleOption.cpp + ${CMAKE_SOURCE_DIR}/libsrc/commandline/ImageOption.cpp + ${CMAKE_SOURCE_DIR}/libsrc/commandline/IntOption.cpp + ${CMAKE_SOURCE_DIR}/libsrc/commandline/Option.cpp + ${CMAKE_SOURCE_DIR}/libsrc/commandline/Parser.cpp + ${CMAKE_SOURCE_DIR}/libsrc/commandline/RegularExpressionOption.cpp + ${CMAKE_SOURCE_DIR}/libsrc/commandline/SwitchOption.cpp + ${CMAKE_SOURCE_DIR}/libsrc/commandline/ValidatorOption.cpp +) target_link_libraries(commandline hyperion diff --git a/libsrc/db/CMakeLists.txt b/libsrc/db/CMakeLists.txt index b81991e6..1beb3fe5 100644 --- a/libsrc/db/CMakeLists.txt +++ b/libsrc/db/CMakeLists.txt @@ -1,16 +1,14 @@ -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/db) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/db) - -FILE ( GLOB DB_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - add_library(database - ${DB_SOURCES} + ${CMAKE_SOURCE_DIR}/include/db/AuthTable.h + ${CMAKE_SOURCE_DIR}/include/db/DBManager.h + ${CMAKE_SOURCE_DIR}/include/db/InstanceTable.h + ${CMAKE_SOURCE_DIR}/include/db/MetaTable.h + ${CMAKE_SOURCE_DIR}/include/db/SettingsTable.h + ${CMAKE_SOURCE_DIR}/libsrc/db/DBManager.cpp ) target_link_libraries(database hyperion hyperion-utils - Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Sql ) diff --git a/libsrc/effectengine/CMakeLists.txt b/libsrc/effectengine/CMakeLists.txt index 0059648e..27ba48c0 100644 --- a/libsrc/effectengine/CMakeLists.txt +++ b/libsrc/effectengine/CMakeLists.txt @@ -1,47 +1,30 @@ -if (NOT CMAKE_VERSION VERSION_LESS "3.12") - find_package(Python3 COMPONENTS Interpreter Development REQUIRED) -else() - find_package (PythonLibs ${PYTHON_VERSION_STRING} EXACT) # Maps PythonLibs to the PythonInterp version of the main cmake -endif() +file(GLOB effectFiles RELATIVE ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/effects/*) +set(HYPERION_EFFECTS_RES "") +foreach(f ${effectFiles}) + get_filename_component(fname ${f} NAME) + set(HYPERION_EFFECTS_RES "${HYPERION_EFFECTS_RES}\n\t\t${f}") +endforeach() -# Include the python directory. Also include the parent (which is for example /usr/include) -# which may be required when it is not includes by the (cross-) compiler by default. -if (NOT CMAKE_VERSION VERSION_LESS "3.12") - include_directories(${Python3_INCLUDE_DIRS} ${Python3_INCLUDE_DIRS}/..) -else() - include_directories(${PYTHON_INCLUDE_DIRS} ${PYTHON_INCLUDE_DIRS}/..) -endif() - -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/effectengine) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/effectengine) - -FILE ( GLOB EffectEngineSOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) -FILE ( GLOB effectFiles RELATIVE ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/effects/* ) - -SET ( HYPERION_EFFECTS_RES "") -FOREACH( f ${effectFiles} ) - GET_FILENAME_COMPONENT(fname ${f} NAME) - SET(HYPERION_EFFECTS_RES "${HYPERION_EFFECTS_RES}\n\t\t${f}") -ENDFOREACH() -CONFIGURE_FILE(${CURRENT_SOURCE_DIR}/EffectEngine.qrc.in ${CMAKE_BINARY_DIR}/EffectEngine.qrc ) - -SET(EffectEngine_RESOURCES ${CMAKE_BINARY_DIR}/EffectEngine.qrc) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/EffectEngine.qrc.in ${CMAKE_BINARY_DIR}/EffectEngine.qrc) add_library(effectengine - ${EffectEngine_RESOURCES} - ${EffectEngineSOURCES} + ${CMAKE_BINARY_DIR}/EffectEngine.qrc + ${CMAKE_SOURCE_DIR}/include/effectengine/ActiveEffectDefinition.h + ${CMAKE_SOURCE_DIR}/include/effectengine/Effect.h + ${CMAKE_SOURCE_DIR}/include/effectengine/EffectDefinition.h + ${CMAKE_SOURCE_DIR}/include/effectengine/EffectEngine.h + ${CMAKE_SOURCE_DIR}/include/effectengine/EffectFileHandler.h + ${CMAKE_SOURCE_DIR}/include/effectengine/EffectModule.h + ${CMAKE_SOURCE_DIR}/include/effectengine/EffectSchema.h + ${CMAKE_SOURCE_DIR}/libsrc/effectengine/Effect.cpp + ${CMAKE_SOURCE_DIR}/libsrc/effectengine/EffectEngine.cpp + ${CMAKE_SOURCE_DIR}/libsrc/effectengine/EffectFileHandler.cpp + ${CMAKE_SOURCE_DIR}/libsrc/effectengine/EffectModule.cpp ) target_link_libraries(effectengine - hyperion python + hyperion Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui ) - -if (NOT CMAKE_VERSION VERSION_LESS "3.12") - target_link_libraries( effectengine ${Python3_LIBRARIES} ) -else() - target_link_libraries( effectengine ${PYTHON_LIBRARIES} ) -endif() diff --git a/libsrc/effectengine/EffectEngine.qrc.in b/libsrc/effectengine/EffectEngine.qrc.in index 2816f96b..2e43ab6d 100644 --- a/libsrc/effectengine/EffectEngine.qrc.in +++ b/libsrc/effectengine/EffectEngine.qrc.in @@ -1,6 +1,6 @@ - ${CURRENT_SOURCE_DIR}/EffectDefinition.schema.json + ${CMAKE_CURRENT_SOURCE_DIR}/EffectDefinition.schema.json ${HYPERION_EFFECTS_RES} diff --git a/libsrc/flatbufserver/CMakeLists.txt b/libsrc/flatbufserver/CMakeLists.txt index 836652f8..5e879517 100644 --- a/libsrc/flatbufserver/CMakeLists.txt +++ b/libsrc/flatbufserver/CMakeLists.txt @@ -1,69 +1,54 @@ +# set (compiled) Flatbuffer schema names +set(FBS_Request "hyperion_request") +set(FBS_Reply "hyperion_reply") -# Define the current source locations -set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/flatbufserver) -set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/flatbufserver) - -include_directories( - ${CMAKE_CURRENT_BINARY_DIR} - ${FLATBUFFERS_INCLUDE_DIRS} -) - -set(Flatbuffer_GENERATED_FBS - hyperion_reply_generated.h - hyperion_request_generated.h -) - -set(Flatbuffer_FBS - ${CURRENT_SOURCE_DIR}/hyperion_reply.fbs - ${CURRENT_SOURCE_DIR}/hyperion_request.fbs -) -FOREACH(FBS_FILE ${Flatbuffer_FBS}) - compile_flattbuffer_schema(${FBS_FILE} ${CMAKE_CURRENT_SOURCE_DIR}) -ENDFOREACH(FBS_FILE) +# define and compile flatbuffer schemas +list(APPEND Compiled_FBS ${FBS_Request}_generated.h) +list(APPEND Compiled_FBS ${FBS_Reply}_generated.h) +compile_flattbuffer_schema(${CMAKE_CURRENT_SOURCE_DIR}/${FBS_Request}.fbs ${CMAKE_CURRENT_SOURCE_DIR}) +compile_flattbuffer_schema(${CMAKE_CURRENT_SOURCE_DIR}/${FBS_Reply}.fbs ${CMAKE_CURRENT_SOURCE_DIR}) # let cmake know about new generated source files -set_source_files_properties( - ${Flatbuffer_GENERATED_FBS} PROPERTIES GENERATED TRUE -) - -### Split flatbufconnect from flatbufserver as flatbufserver relates to HyperionDaemon +set_source_files_properties(${Compiled_FBS} PROPERTIES GENERATED TRUE) +# split flatbufconnect from flatbufserver as flatbufserver relates to HyperionDaemon if(ENABLE_FLATBUF_CONNECT) -add_library(flatbufconnect - ${CURRENT_HEADER_DIR}/FlatBufferConnection.h - ${CURRENT_SOURCE_DIR}/FlatBufferConnection.cpp - ${FLATBUFSERVER_SOURCES} - ${Flatbuffer_GENERATED_FBS} + add_library(flatbufconnect + ${CMAKE_SOURCE_DIR}/include/flatbufserver/FlatBufferConnection.h + ${CMAKE_SOURCE_DIR}/libsrc/flatbufserver/FlatBufferConnection.cpp + ${Compiled_FBS} + ) -) -target_link_libraries(flatbufconnect - hyperion-utils - flatbuffers - Qt${QT_VERSION_MAJOR}::Network - Qt${QT_VERSION_MAJOR}::Core -) + target_link_libraries(flatbufconnect + hyperion-utils + flatbuffers + ) + + target_include_directories(flatbufconnect PUBLIC + ${FLATBUFFERS_INCLUDE_DIRS} + ) endif() if(ENABLE_FLATBUF_SERVER) -add_library(flatbufserver - ${CURRENT_HEADER_DIR}/FlatBufferServer.h - ${CURRENT_SOURCE_DIR}/FlatBufferServer.cpp - ${CURRENT_SOURCE_DIR}/FlatBufferClient.h - ${CURRENT_SOURCE_DIR}/FlatBufferClient.cpp - ${FLATBUFSERVER_SOURCES} - ${Flatbuffer_GENERATED_FBS} -) + add_library(flatbufserver + ${CMAKE_SOURCE_DIR}/include/flatbufserver/FlatBufferServer.h + ${CMAKE_SOURCE_DIR}/libsrc/flatbufserver/FlatBufferServer.cpp + ${CMAKE_SOURCE_DIR}/libsrc/flatbufserver/FlatBufferClient.h + ${CMAKE_SOURCE_DIR}/libsrc/flatbufserver/FlatBufferClient.cpp + ${Compiled_FBS} + ) -target_link_libraries(flatbufserver -hyperion-utils -flatbuffers -Qt${QT_VERSION_MAJOR}::Network -Qt${QT_VERSION_MAJOR}::Core -) - -if(ENABLE_MDNS) - target_link_libraries(flatbufserver mdns) -endif() + target_link_libraries(flatbufserver + hyperion-utils + flatbuffers + ) + target_include_directories(flatbufserver PUBLIC + ${FLATBUFFERS_INCLUDE_DIRS} + ) + + if(ENABLE_MDNS) + target_link_libraries(flatbufserver mdns) + endif() endif() diff --git a/libsrc/forwarder/CMakeLists.txt b/libsrc/forwarder/CMakeLists.txt index 60dc5a6b..a9ee88c1 100644 --- a/libsrc/forwarder/CMakeLists.txt +++ b/libsrc/forwarder/CMakeLists.txt @@ -1,22 +1,11 @@ - -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/forwarder) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/forwarder) - -if(ENABLE_FLATBUF_CONNECT) -include_directories( - ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/flatbufserver +add_library(forwarder + ${CMAKE_SOURCE_DIR}/include/forwarder/MessageForwarder.h + ${CMAKE_SOURCE_DIR}/libsrc/forwarder/MessageForwarder.cpp ) -endif() - -FILE ( GLOB Forwarder_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -add_library(forwarder ${Forwarder_SOURCES} ) target_link_libraries(forwarder hyperion hyperion-utils - ${QT_LIBRARIES} ) if(ENABLE_FLATBUF_CONNECT) diff --git a/libsrc/grabber/CMakeLists.txt b/libsrc/grabber/CMakeLists.txt index c729ad0a..75f8c529 100644 --- a/libsrc/grabber/CMakeLists.txt +++ b/libsrc/grabber/CMakeLists.txt @@ -1,39 +1,39 @@ -if (ENABLE_AMLOGIC) +if(ENABLE_AMLOGIC) add_subdirectory(amlogic) endif (ENABLE_AMLOGIC) -if (ENABLE_DISPMANX) +if(ENABLE_DISPMANX) add_subdirectory(dispmanx) endif (ENABLE_DISPMANX) -if (ENABLE_FB) +if(ENABLE_FB) add_subdirectory(framebuffer) endif (ENABLE_FB) -if (ENABLE_OSX) +if(ENABLE_OSX) add_subdirectory(osx) endif(ENABLE_OSX) -if (ENABLE_V4L2 OR ENABLE_MF) +if(ENABLE_V4L2 OR ENABLE_MF) add_subdirectory(video) -endif () +endif() -if (ENABLE_X11) +if(ENABLE_X11) add_subdirectory(x11) endif(ENABLE_X11) -if (ENABLE_XCB) +if(ENABLE_XCB) add_subdirectory(xcb) endif(ENABLE_XCB) -if (ENABLE_QT) +if(ENABLE_QT) add_subdirectory(qt) endif(ENABLE_QT) -if (ENABLE_DX) +if(ENABLE_DX) add_subdirectory(directx) endif(ENABLE_DX) -if (ENABLE_AUDIO) +if(ENABLE_AUDIO) add_subdirectory(audio) endif() diff --git a/libsrc/grabber/amlogic/AmlogicGrabber.cpp b/libsrc/grabber/amlogic/AmlogicGrabber.cpp index 4c590d0c..0f6b0587 100644 --- a/libsrc/grabber/amlogic/AmlogicGrabber.cpp +++ b/libsrc/grabber/amlogic/AmlogicGrabber.cpp @@ -20,7 +20,7 @@ // Local includes #include -#include +#include #include "Amvideocap.h" // Constants diff --git a/libsrc/grabber/amlogic/AmlogicWrapper.cpp b/libsrc/grabber/amlogic/AmlogicWrapper.cpp index 25581b2c..ac371ba0 100644 --- a/libsrc/grabber/amlogic/AmlogicWrapper.cpp +++ b/libsrc/grabber/amlogic/AmlogicWrapper.cpp @@ -1,4 +1,4 @@ -#include +#include AmlogicWrapper::AmlogicWrapper(int pixelDecimation, int updateRate_Hz) : GrabberWrapper("Amlogic", &_grabber, updateRate_Hz) diff --git a/libsrc/grabber/amlogic/CMakeLists.txt b/libsrc/grabber/amlogic/CMakeLists.txt index 284f8b5f..d59066d0 100644 --- a/libsrc/grabber/amlogic/CMakeLists.txt +++ b/libsrc/grabber/amlogic/CMakeLists.txt @@ -1,13 +1,15 @@ -INCLUDE (CheckIncludeFiles) - -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/amlogic) - -FILE ( GLOB AmlogicSOURCES "${CURRENT_HEADER_DIR}/Amlogic*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -add_library(amlogic-grabber ${AmlogicSOURCES} ) +add_library(amlogic-grabber + ${CMAKE_SOURCE_DIR}/include/grabber/amlogic/AmlogicGrabber.h + ${CMAKE_SOURCE_DIR}/include/grabber/amlogic/AmlogicWrapper.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/amlogic/AmlogicGrabber.cpp + ${CMAKE_SOURCE_DIR}/libsrc/grabber/amlogic/AmlogicWrapper.cpp + ${CMAKE_SOURCE_DIR}/libsrc/grabber/amlogic/Amvideocap.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/amlogic/ion.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/amlogic/meson_ion.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/amlogic/IonBuffer.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/amlogic/IonBuffer.cpp +) target_link_libraries(amlogic-grabber hyperion - ${QT_LIBRARIES}) +) diff --git a/libsrc/grabber/audio/AudioGrabber.cpp b/libsrc/grabber/audio/AudioGrabber.cpp index 4f4eccbd..995a1b7c 100644 --- a/libsrc/grabber/audio/AudioGrabber.cpp +++ b/libsrc/grabber/audio/AudioGrabber.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/libsrc/grabber/audio/AudioGrabberLinux.cpp b/libsrc/grabber/audio/AudioGrabberLinux.cpp index 8938e043..944b317d 100644 --- a/libsrc/grabber/audio/AudioGrabberLinux.cpp +++ b/libsrc/grabber/audio/AudioGrabberLinux.cpp @@ -1,11 +1,31 @@ -#include +#include #include #include #include -typedef void* (*THREADFUNCPTR)(void*); +static void * AudioThreadRunner(void* params) +{ + AudioGrabberLinux* This = static_cast(params); + + Debug(This->getLog(), "Audio Thread Started"); + + snd_pcm_sframes_t framesAvailable = 0; + + while (This->_isRunning.load(std::memory_order_acquire)) + { + snd_pcm_wait(This->_captureDevice, 1000); + + if ((framesAvailable = snd_pcm_avail(This->_captureDevice)) > 0) + This->processAudioBuffer(framesAvailable); + + sched_yield(); + } + + Debug(This->getLog(), "Audio Thread Shutting Down"); + return nullptr; +} AudioGrabberLinux::AudioGrabberLinux() : AudioGrabber() @@ -121,7 +141,7 @@ bool AudioGrabberLinux::configureCaptureInterface() snd_pcm_close(_captureDevice); return false; } - + if ((error = snd_pcm_hw_params_set_access(_captureDevice, _captureDeviceConfig, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { Error(_log, "Failed to configure interleaved mode: %s", snd_strerror(error)); @@ -129,7 +149,7 @@ bool AudioGrabberLinux::configureCaptureInterface() snd_pcm_close(_captureDevice); return false; } - + if ((error = snd_pcm_hw_params_set_format(_captureDevice, _captureDeviceConfig, SND_PCM_FORMAT_S16_LE)) < 0) { Error(_log, "Failed to configure capture format: %s", snd_strerror(error)); @@ -169,7 +189,7 @@ bool AudioGrabberLinux::configureCaptureInterface() snd_pcm_close(_captureDevice); return false; } - + return true; } @@ -189,11 +209,6 @@ bool AudioGrabberLinux::start() _isRunning.store(true, std::memory_order_release); pthread_attr_t threadAttributes; - int threadPriority = 1; - - sched_param schedulerParameter; - schedulerParameter.sched_priority = threadPriority; - if (pthread_attr_init(&threadAttributes) != 0) { Debug(_log, "Failed to create thread attributes"); @@ -201,7 +216,7 @@ bool AudioGrabberLinux::start() return false; } - if (pthread_create(&_audioThread, &threadAttributes, static_cast(&AudioThreadRunner), static_cast(this)) != 0) + if (pthread_create(&_audioThread, &threadAttributes, &AudioThreadRunner, static_cast(this)) != 0) { Debug(_log, "Failed to create audio capture thread"); stop(); @@ -239,7 +254,7 @@ void AudioGrabberLinux::processAudioBuffer(snd_pcm_sframes_t frames) ssize_t bytes = snd_pcm_frames_to_bytes(_captureDevice, frames); int16_t * buffer = static_cast(calloc(static_cast(bytes / 2), sizeof(int16_t))); - + if (frames == 0) { buffer[0] = 0; @@ -293,25 +308,3 @@ QString AudioGrabberLinux::getDeviceName(const QString& devicePath) const return _deviceProperties.value(devicePath).name; } - -static void * AudioThreadRunner(void* params) -{ - AudioGrabberLinux* This = static_cast(params); - - Debug(This->getLog(), "Audio Thread Started"); - - snd_pcm_sframes_t framesAvailable = 0; - - while (This->_isRunning.load(std::memory_order_acquire)) - { - snd_pcm_wait(This->_captureDevice, 1000); - - if ((framesAvailable = snd_pcm_avail(This->_captureDevice)) > 0) - This->processAudioBuffer(framesAvailable); - - sched_yield(); - } - - Debug(This->getLog(), "Audio Thread Shutting Down"); - return nullptr; -} diff --git a/libsrc/grabber/audio/AudioGrabberWindows.cpp b/libsrc/grabber/audio/AudioGrabberWindows.cpp index 8a2228c3..d3d37597 100644 --- a/libsrc/grabber/audio/AudioGrabberWindows.cpp +++ b/libsrc/grabber/audio/AudioGrabberWindows.cpp @@ -1,4 +1,4 @@ -#include +#include #include @@ -71,7 +71,7 @@ bool AudioGrabberWindows::configureCaptureInterface() notificationSize -= notificationSize % audioFormat.nBlockAlign; bufferCaptureSize = notificationSize * AUDIO_NOTIFICATION_COUNT; - + DSCBUFFERDESC bufferDesc; bufferDesc.dwSize = sizeof(DSCBUFFERDESC); bufferDesc.dwFlags = 0; @@ -80,7 +80,7 @@ bool AudioGrabberWindows::configureCaptureInterface() bufferDesc.lpwfxFormat = &audioFormat; bufferDesc.dwFXCount = 0; bufferDesc.lpDSCFXDesc = NULL; - + // Create Capture Device's Buffer LPDIRECTSOUNDCAPTUREBUFFER preBuffer; if (FAILED(recordingDevice->CreateCaptureBuffer(&bufferDesc, &preBuffer, NULL))) @@ -101,7 +101,7 @@ bool AudioGrabberWindows::configureCaptureInterface() } preBuffer->Release(); - + // Create Notifications LPDIRECTSOUNDNOTIFY8 notify; @@ -112,7 +112,7 @@ bool AudioGrabberWindows::configureCaptureInterface() recordingBuffer->Release(); return false; } - + // Create Events notificationEvent = CreateEvent(NULL, TRUE, FALSE, NULL); @@ -133,11 +133,11 @@ bool AudioGrabberWindows::configureCaptureInterface() positionNotify[i].dwOffset = (notificationSize * i) + notificationSize - 1; positionNotify[i].hEventNotify = notificationEvent; } - + // Set Notifications notify->SetNotificationPositions(AUDIO_NOTIFICATION_COUNT, positionNotify); notify->Release(); - + return true; } @@ -162,12 +162,12 @@ bool AudioGrabberWindows::start() } Info(_log, "Capture audio from %s", QSTRING_CSTR(getDeviceName(_device))); - + if (!this->configureCaptureInterface()) { return false; } - + if (FAILED(recordingBuffer->Start(DSCBSTART_LOOPING))) { Error(_log, "Failed starting audio capture from '%s'", QSTRING_CSTR(getDeviceName(_device))); @@ -214,7 +214,7 @@ void AudioGrabberWindows::stop() { Error(_log, "Audio capture failed to stop: '%s'", QSTRING_CSTR(getDeviceName(_device))); } - + if (FAILED(recordingBuffer->Release())) { Error(_log, "Failed to release recording buffer: '%s'", QSTRING_CSTR(getDeviceName(_device))); @@ -306,7 +306,7 @@ void AudioGrabberWindows::processAudioBuffer() // Buffer wrapped around, read second position if (capturedAudio2 != NULL) - { + { bufferCapturePosition += capturedAudio2Length; bufferCapturePosition %= bufferCaptureSize; // Circular Buffer } @@ -318,13 +318,13 @@ void AudioGrabberWindows::processAudioBuffer() { CopyMemory(readBuffer + capturedAudioLength, capturedAudio2, capturedAudio2Length); } - + // Release Buffer Lock recordingBuffer->Unlock(capturedAudio, capturedAudioLength, capturedAudio2, capturedAudio2Length); - + // Process Audio Frame this->processAudioFrame(readBuffer, frameSize); - + delete[] readBuffer; } diff --git a/libsrc/grabber/audio/AudioWrapper.cpp b/libsrc/grabber/audio/AudioWrapper.cpp index 0dd624de..2c47a3b8 100644 --- a/libsrc/grabber/audio/AudioWrapper.cpp +++ b/libsrc/grabber/audio/AudioWrapper.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/libsrc/grabber/audio/CMakeLists.txt b/libsrc/grabber/audio/CMakeLists.txt index 714c5883..f60ee775 100644 --- a/libsrc/grabber/audio/CMakeLists.txt +++ b/libsrc/grabber/audio/CMakeLists.txt @@ -1,35 +1,38 @@ -# Define the current source locations -SET( CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber ) -SET( CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/audio ) - -if (WIN32) - add_definitions(-DUNICODE -D_UNICODE) - FILE ( GLOB AUDIO_GRABBER_SOURCES "${CURRENT_HEADER_DIR}/Audio*Windows.h" "${CURRENT_HEADER_DIR}/AudioGrabber.h" "${CURRENT_HEADER_DIR}/AudioWrapper.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*Windows.cpp" "${CURRENT_SOURCE_DIR}/AudioGrabber.cpp" "${CURRENT_SOURCE_DIR}/AudioWrapper.cpp") -elseif(${CMAKE_SYSTEM} MATCHES "Linux") - FILE ( GLOB AUDIO_GRABBER_SOURCES "${CURRENT_HEADER_DIR}/Audio*Linux.h" "${CURRENT_HEADER_DIR}/AudioGrabber.h" "${CURRENT_HEADER_DIR}/AudioWrapper.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*Linux.cpp" "${CURRENT_SOURCE_DIR}/AudioGrabber.cpp" "${CURRENT_SOURCE_DIR}/AudioWrapper.cpp") -elseif (APPLE) - #TODO - #FILE ( GLOB AUDIO_GRABBER_SOURCES "${CURRENT_HEADER_DIR}/Audio*Apple.h" "${CURRENT_HEADER_DIR}/AudioGrabber.h" "${CURRENT_HEADER_DIR}/AudioWrapper.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*Apple.cpp" "${CURRENT_SOURCE_DIR}/AudioGrabber.cpp" "${CURRENT_SOURCE_DIR}/AudioWrapper.cpp") +if(WIN32) + add_definitions(-DUNICODE -D_UNICODE) + set(AUDIO_GRABBER_SOURCES + ${CMAKE_SOURCE_DIR}/include/grabber/audio/AudioGrabberWindows.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/audio/AudioGrabberWindows.cpp + ) +elseif(CMAKE_HOST_UNIX AND NOT APPLE) + set(AUDIO_GRABBER_SOURCES + ${CMAKE_SOURCE_DIR}/include/grabber/audio/AudioGrabberLinux.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/audio/AudioGrabberLinux.cpp + ) endif() -add_library( audio-grabber ${AUDIO_GRABBER_SOURCES} ) +add_library(audio-grabber + ${CMAKE_SOURCE_DIR}/include/grabber/audio/AudioGrabber.h + ${CMAKE_SOURCE_DIR}/include/grabber/audio/AudioWrapper.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/audio/AudioGrabber.cpp + ${CMAKE_SOURCE_DIR}/libsrc/grabber/audio/AudioWrapper.cpp + ${AUDIO_GRABBER_SOURCES} +) -set(AUDIO_LIBS hyperion) - - -if (WIN32) - set(AUDIO_LIBS ${AUDIO_LIBS} DSound) -elseif(${CMAKE_SYSTEM} MATCHES "Linux") - find_package(ALSA REQUIRED) - if (ALSA_FOUND) - include_directories(${ALSA_INCLUDE_DIRS}) - set(AUDIO_LIBS ${AUDIO_LIBS} ${ALSA_LIBRARIES}) - endif(ALSA_FOUND) - - set(THREADS_PREFER_PTHREAD_FLAG ON) - find_package(Threads REQUIRED) - set(AUDIO_LIBS ${AUDIO_LIBS} Threads::Threads) # PRIVATE +if(WIN32) + set(AUDIO_LIBS DSound) +elseif(CMAKE_HOST_UNIX AND NOT APPLE) + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(ALSA REQUIRED) + find_package(Threads REQUIRED) + set(AUDIO_LIBS ${ALSA_LIBRARIES} Threads::Threads) endif() +target_link_libraries(audio-grabber + hyperion + ${AUDIO_LIBS} +) -target_link_libraries(audio-grabber ${AUDIO_LIBS} ${QT_LIBRARIES}) +if(CMAKE_HOST_UNIX AND NOT APPLE) + target_include_directories(audio-grabber PUBLIC ${ALSA_INCLUDE_DIRS}) +endif() diff --git a/libsrc/grabber/directx/CMakeLists.txt b/libsrc/grabber/directx/CMakeLists.txt index 16db7dd2..3d86e897 100644 --- a/libsrc/grabber/directx/CMakeLists.txt +++ b/libsrc/grabber/directx/CMakeLists.txt @@ -1,14 +1,17 @@ -# Define the current source locations -SET( CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber ) -SET( CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/directx ) +find_package(DirectX9 REQUIRED) -include_directories(${DIRECTX9_INCLUDE_DIRS}) - -FILE ( GLOB DIRECTX_GRAB_SOURCES "${CURRENT_HEADER_DIR}/DirectX*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -add_library( directx-grabber ${DIRECTX_GRAB_SOURCES} ) +add_library(directx-grabber + ${CMAKE_SOURCE_DIR}/include/grabber/directx/DirectXGrabber.h + ${CMAKE_SOURCE_DIR}/include/grabber/directx/DirectXWrapper.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/directx/DirectXGrabber.cpp + ${CMAKE_SOURCE_DIR}/libsrc/grabber/directx/DirectXWrapper.cpp +) target_link_libraries(directx-grabber - hyperion - ${DIRECTX9_LIBRARIES} + hyperion + ${DIRECTX9_LIBRARIES} +) + +target_include_directories(directx-grabber PUBLIC + ${DIRECTX9_INCLUDE_DIRS} ) diff --git a/libsrc/grabber/directx/DirectXGrabber.cpp b/libsrc/grabber/directx/DirectXGrabber.cpp index ae909888..24db4b4f 100644 --- a/libsrc/grabber/directx/DirectXGrabber.cpp +++ b/libsrc/grabber/directx/DirectXGrabber.cpp @@ -1,5 +1,5 @@ #include -#include +#include #pragma comment(lib, "d3d9.lib") #pragma comment(lib,"d3dx9.lib") diff --git a/libsrc/grabber/directx/DirectXWrapper.cpp b/libsrc/grabber/directx/DirectXWrapper.cpp index 3c4188e5..cddf19ce 100644 --- a/libsrc/grabber/directx/DirectXWrapper.cpp +++ b/libsrc/grabber/directx/DirectXWrapper.cpp @@ -1,4 +1,4 @@ -#include +#include DirectXWrapper::DirectXWrapper( int updateRate_Hz, int display, diff --git a/libsrc/grabber/dispmanx/CMakeLists.txt b/libsrc/grabber/dispmanx/CMakeLists.txt index 51292da1..93082b9c 100644 --- a/libsrc/grabber/dispmanx/CMakeLists.txt +++ b/libsrc/grabber/dispmanx/CMakeLists.txt @@ -1,5 +1,5 @@ # Find the BCM-package (VC control) -if( "${PLATFORM}" MATCHES rpi) +if("${PLATFORM}" MATCHES rpi) find_package(BCM) if(BCM_FOUND) add_definitions(-DBCM_FOUND) @@ -9,21 +9,20 @@ else() set(BCM_LIBRARY "") endif() -# Define the current source locations -set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) -set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/dispmanx) +add_library(dispmanx-grabber + ${CMAKE_SOURCE_DIR}/include/grabber/dispmanx/DispmanxFrameGrabber.h + ${CMAKE_SOURCE_DIR}/include/grabber/dispmanx/DispmanxWrapper.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/dispmanx/DispmanxFrameGrabber.cpp + ${CMAKE_SOURCE_DIR}/libsrc/grabber/dispmanx/DispmanxWrapper.cpp +) -FILE ( GLOB DispmanxGrabberSOURCES "${CURRENT_HEADER_DIR}/Dispmanx*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -add_library(dispmanx-grabber ${DispmanxGrabberSOURCES}) add_definitions(-DBCM_LIBRARY="${BCM_LIBRARY}") +target_link_libraries(dispmanx-grabber + hyperion + ${CMAKE_DL_LIBS} +) + target_include_directories(dispmanx-grabber PUBLIC ${BCM_INCLUDE_DIR} ) - -target_link_libraries(dispmanx-grabber - hyperion - ${QT_LIBRARIES} - ${CMAKE_DL_LIBS} -) diff --git a/libsrc/grabber/dispmanx/DispmanxFrameGrabber.cpp b/libsrc/grabber/dispmanx/DispmanxFrameGrabber.cpp index b0a46fe4..d0486cdf 100644 --- a/libsrc/grabber/dispmanx/DispmanxFrameGrabber.cpp +++ b/libsrc/grabber/dispmanx/DispmanxFrameGrabber.cpp @@ -16,7 +16,7 @@ namespace { } //End of constants // Local includes -#include "grabber/DispmanxFrameGrabber.h" +#include "grabber/dispmanx/DispmanxFrameGrabber.h" DispmanxFrameGrabber::DispmanxFrameGrabber() : Grabber("DISPMANXGRABBER") diff --git a/libsrc/grabber/dispmanx/DispmanxWrapper.cpp b/libsrc/grabber/dispmanx/DispmanxWrapper.cpp index 6ffe3354..19fb1b02 100644 --- a/libsrc/grabber/dispmanx/DispmanxWrapper.cpp +++ b/libsrc/grabber/dispmanx/DispmanxWrapper.cpp @@ -1,4 +1,4 @@ -#include +#include DispmanxWrapper::DispmanxWrapper( int updateRate_Hz, int pixelDecimation diff --git a/libsrc/grabber/framebuffer/CMakeLists.txt b/libsrc/grabber/framebuffer/CMakeLists.txt index af1368ff..bcaf4675 100644 --- a/libsrc/grabber/framebuffer/CMakeLists.txt +++ b/libsrc/grabber/framebuffer/CMakeLists.txt @@ -1,11 +1,10 @@ -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/framebuffer) - -FILE ( GLOB FramebufferGrabberSOURCES "${CURRENT_HEADER_DIR}/Framebuffer*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -add_library(framebuffer-grabber ${FramebufferGrabberSOURCES} ) +add_library(framebuffer-grabber + ${CMAKE_SOURCE_DIR}/include/grabber/framebuffer/FramebufferFrameGrabber.h + ${CMAKE_SOURCE_DIR}/include/grabber/framebuffer/FramebufferWrapper.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/framebuffer/FramebufferFrameGrabber.cpp + ${CMAKE_SOURCE_DIR}/libsrc/grabber/framebuffer/FramebufferWrapper.cpp +) target_link_libraries(framebuffer-grabber hyperion - ${QT_LIBRARIES}) +) diff --git a/libsrc/grabber/framebuffer/FramebufferFrameGrabber.cpp b/libsrc/grabber/framebuffer/FramebufferFrameGrabber.cpp index 554f8166..c4129341 100644 --- a/libsrc/grabber/framebuffer/FramebufferFrameGrabber.cpp +++ b/libsrc/grabber/framebuffer/FramebufferFrameGrabber.cpp @@ -28,7 +28,7 @@ const char DISCOVERY_FILEPATTERN[] = "fb?"; } //End of constants // Local includes -#include +#include FramebufferFrameGrabber::FramebufferFrameGrabber(const QString & device) : Grabber("FRAMEBUFFERGRABBER") diff --git a/libsrc/grabber/framebuffer/FramebufferWrapper.cpp b/libsrc/grabber/framebuffer/FramebufferWrapper.cpp index 7d99f527..9cb40046 100644 --- a/libsrc/grabber/framebuffer/FramebufferWrapper.cpp +++ b/libsrc/grabber/framebuffer/FramebufferWrapper.cpp @@ -1,4 +1,4 @@ -#include +#include FramebufferWrapper::FramebufferWrapper( int updateRate_Hz, const QString & device, diff --git a/libsrc/grabber/osx/CMakeLists.txt b/libsrc/grabber/osx/CMakeLists.txt index 3a5690c6..7d18e8d3 100644 --- a/libsrc/grabber/osx/CMakeLists.txt +++ b/libsrc/grabber/osx/CMakeLists.txt @@ -1,11 +1,10 @@ -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/osx) - -FILE ( GLOB OsxGrabberSOURCES "${CURRENT_HEADER_DIR}/Osx*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -add_library(osx-grabber ${OsxGrabberSOURCES} ) +add_library(osx-grabber + ${CMAKE_SOURCE_DIR}/include/grabber/osx/OsxFrameGrabber.h + ${CMAKE_SOURCE_DIR}/include/grabber/osx/OsxWrapper.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/osx/OsxFrameGrabber.cpp + ${CMAKE_SOURCE_DIR}/libsrc/grabber/osx/OsxWrapper.cpp +) target_link_libraries(osx-grabber hyperion - ${QT_LIBRARIES}) +) diff --git a/libsrc/grabber/osx/OsxFrameGrabber.cpp b/libsrc/grabber/osx/OsxFrameGrabber.cpp index 54ea28f7..0e42e529 100644 --- a/libsrc/grabber/osx/OsxFrameGrabber.cpp +++ b/libsrc/grabber/osx/OsxFrameGrabber.cpp @@ -3,7 +3,7 @@ #include // Local includes -#include +#include //Qt #include diff --git a/libsrc/grabber/osx/OsxFrameGrabberMock.cpp b/libsrc/grabber/osx/OsxFrameGrabberMock.cpp deleted file mode 100644 index 2bcab484..00000000 --- a/libsrc/grabber/osx/OsxFrameGrabberMock.cpp +++ /dev/null @@ -1,159 +0,0 @@ -#ifndef __APPLE__ -#include - -unsigned __osx_frame_counter = 0; -const int __screenWidth = 800; -const int __screenHeight = 600; - -CGError CGGetActiveDisplayList(uint32_t maxDisplays, CGDirectDisplayID *activeDisplays, uint32_t *displayCount) -{ - if (maxDisplays == 0 || activeDisplays == nullptr) - { - *displayCount = 2; - } - else - { - displayCount = &maxDisplays; - if (activeDisplays != nullptr) - { - for (CGDirectDisplayID i = 0; i < maxDisplays; ++i) - { - activeDisplays[i] = i; - } - } - else - { - return kCGErrorFailure; - } - } - return kCGErrorSuccess; -} - -CGImageRef CGDisplayCreateImage(CGDirectDisplayID display) -{ - CGImageRef image = new CGImage(__screenWidth / (display+1), __screenHeight / (display+1)); - - return image; -} - -void CGImageRelease(CGImageRef image) -{ - delete image; -} - -CGImageRef CGImageGetDataProvider(CGImageRef image) -{ - __osx_frame_counter++; - if (__osx_frame_counter > 100) - { - __osx_frame_counter = 0; - } - - ColorRgb color[4] = {ColorRgb::RED, ColorRgb::BLUE, ColorRgb::GREEN, ColorRgb::WHITE}; - if (__osx_frame_counter < 25) - { - color[0] = ColorRgb::WHITE; - color[1] = ColorRgb::RED; - color[2] = ColorRgb::BLUE; - color[3] = ColorRgb::GREEN; - } - else if(__osx_frame_counter < 50) - { - color[1] = ColorRgb::WHITE; - color[2] = ColorRgb::RED; - color[3] = ColorRgb::BLUE; - color[0] = ColorRgb::GREEN; - } - else if(__osx_frame_counter < 75) - { - color[2] = ColorRgb::WHITE; - color[3] = ColorRgb::RED; - color[0] = ColorRgb::BLUE; - color[1] = ColorRgb::GREEN; - } - unsigned w = image->width(); - unsigned h = image->height(); - - for (unsigned y=0; y= h/2) id = 2; - if (x >= w/2 && y < h/2) id = 3; - - image->memptr()[y*w + x] = color[id]; - } - } - - return image; -} - -CFDataRef CGDataProviderCopyData(CGImageRef image) -{ - const unsigned indexMax = image->width() * image->height() * CGImageGetBitsPerPixel(image); - CFDataRef data = new CFData[indexMax]; - int lineLength = CGImageGetBytesPerRow(image); - - for (unsigned y=0; yheight(); y++) - { - for (unsigned x=0; xwidth(); x++) - { - int index = lineLength * y + x * CGImageGetBitsPerPixel(image); - - data[index ] = (*image)(x,y).blue; - data[index+1] = (*image)(x,y).green; - data[index+2] = (*image)(x,y).red; - data[index+3] = 0; - } - } - return data; -} - -unsigned char* CFDataGetBytePtr(CFDataRef imgData) -{ - return imgData; -} - -unsigned CGImageGetWidth(CGImageRef image) -{ - return image->width(); -} - -unsigned CGImageGetHeight(CGImageRef image) -{ - return image->height(); -} - -unsigned CGImageGetBytesPerRow(CGImageRef image) -{ - return image->width()*CGImageGetBitsPerPixel(image); -} - -unsigned CGImageGetBitsPerPixel(CGImageRef) -{ - return 4; -} - -void CFRelease(CFDataRef imgData) -{ - delete imgData; -} - -CGDisplayModeRef CGDisplayCopyDisplayMode(CGDirectDisplayID display) -{ - return nullptr; -} -CGRect CGDisplayBounds(CGDirectDisplayID display) -{ - CGRect rect; - rect.size.width = __screenWidth / (display+1); - rect.size.height = __screenHeight / (display+1); - return rect; -} -void CGDisplayModeRelease(CGDisplayModeRef mode) -{ -} - -#endif diff --git a/libsrc/grabber/osx/OsxWrapper.cpp b/libsrc/grabber/osx/OsxWrapper.cpp index 724be2fa..408f7a69 100644 --- a/libsrc/grabber/osx/OsxWrapper.cpp +++ b/libsrc/grabber/osx/OsxWrapper.cpp @@ -1,4 +1,4 @@ -#include +#include OsxWrapper::OsxWrapper( int updateRate_Hz, int display, diff --git a/libsrc/grabber/qt/CMakeLists.txt b/libsrc/grabber/qt/CMakeLists.txt index 1b0111de..6ece8bbc 100644 --- a/libsrc/grabber/qt/CMakeLists.txt +++ b/libsrc/grabber/qt/CMakeLists.txt @@ -1,13 +1,10 @@ -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/qt) - - -FILE ( GLOB QT_GRAB_SOURCES "${CURRENT_HEADER_DIR}/Qt*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -add_library(qt-grabber ${QT_GRAB_SOURCES} ) +add_library(qt-grabber + ${CMAKE_SOURCE_DIR}/include/grabber/qt/QtGrabber.h + ${CMAKE_SOURCE_DIR}/include/grabber/qt/QtWrapper.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/qt/QtGrabber.cpp + ${CMAKE_SOURCE_DIR}/libsrc/grabber/qt/QtWrapper.cpp +) target_link_libraries(qt-grabber hyperion - ${QT_LIBRARIES} ) diff --git a/libsrc/grabber/qt/QtGrabber.cpp b/libsrc/grabber/qt/QtGrabber.cpp index cb6e0c5c..3d2cd51c 100644 --- a/libsrc/grabber/qt/QtGrabber.cpp +++ b/libsrc/grabber/qt/QtGrabber.cpp @@ -1,5 +1,5 @@ // proj -#include +#include // qt #include @@ -226,7 +226,7 @@ int QtGrabber::grabFrame(Image& image) QPixmap originalPixmap = grabWindow(0, _src_x, _src_y, _src_x_max, _src_y_max); #else QPixmap originalPixmap = _screen->grabWindow(0, _src_x, _src_y, _src_x_max, _src_y_max); -#endif +#endif if (originalPixmap.isNull()) { rc = -1; diff --git a/libsrc/grabber/qt/QtWrapper.cpp b/libsrc/grabber/qt/QtWrapper.cpp index 90cb489b..689f25c5 100644 --- a/libsrc/grabber/qt/QtWrapper.cpp +++ b/libsrc/grabber/qt/QtWrapper.cpp @@ -1,4 +1,4 @@ -#include +#include QtWrapper::QtWrapper( int updateRate_Hz, int display, diff --git a/libsrc/grabber/video/CMakeLists.txt b/libsrc/grabber/video/CMakeLists.txt index 43a0e580..c4c2f896 100644 --- a/libsrc/grabber/video/CMakeLists.txt +++ b/libsrc/grabber/video/CMakeLists.txt @@ -1,33 +1,38 @@ # Common cmake definition for external video grabber -# Add Turbo JPEG library -if (ENABLE_V4L2 OR ENABLE_MF) - find_package(TurboJPEG) - if (TURBOJPEG_FOUND) - add_definitions(-DHAVE_TURBO_JPEG) - message( STATUS "Using Turbo JPEG library: ${TurboJPEG_LIBRARY}") - include_directories(${TurboJPEG_INCLUDE_DIRS}) - else () - message( STATUS "Turbo JPEG library not found, MJPEG camera format won't work.") - endif () -endif() +set(MF-grabber mediafoundation) +set(V4L2-grabber v4l2) -# Define the wrapper/header/source locations and collect them -SET(WRAPPER_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/video) -SET(HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) -if (ENABLE_MF) +if(ENABLE_MF) project(mf-grabber) - SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/video/mediafoundation) - FILE (GLOB SOURCES "${WRAPPER_DIR}/*.cpp" "${HEADER_DIR}/Video*.h" "${HEADER_DIR}/MF*.h" "${HEADER_DIR}/Encoder*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp") + set(grabber_project MF) + set(MediaFoundationSourceReaderCallBack ${CMAKE_SOURCE_DIR}/libsrc/grabber/video/mediafoundation/MFSourceReaderCB.h) elseif(ENABLE_V4L2) project(v4l2-grabber) - SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/video/v4l2) - FILE (GLOB SOURCES "${WRAPPER_DIR}/*.cpp" "${HEADER_DIR}/Video*.h" "${HEADER_DIR}/V4L2*.h" "${HEADER_DIR}/Encoder*.h" "${CURRENT_SOURCE_DIR}/*.cpp") + set(grabber_project V4L2) endif() -add_library(${PROJECT_NAME} ${SOURCES}) -target_link_libraries(${PROJECT_NAME} hyperion ${QT_LIBRARIES}) +add_library(${PROJECT_NAME} + ${CMAKE_SOURCE_DIR}/include/grabber/video/EncoderThread.h + ${CMAKE_SOURCE_DIR}/include/grabber/video/VideoWrapper.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/video/EncoderThread.cpp + ${CMAKE_SOURCE_DIR}/libsrc/grabber/video/VideoWrapper.cpp + ${CMAKE_SOURCE_DIR}/include/grabber/video/${${grabber_project}-grabber}/${grabber_project}Grabber.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/video/${${grabber_project}-grabber}/${grabber_project}Grabber.cpp + ${MediaFoundationSourceReaderCallBack} +) -if(TURBOJPEG_FOUND) - target_link_libraries(${PROJECT_NAME} ${TurboJPEG_LIBRARY}) +target_link_libraries(${PROJECT_NAME} hyperion) + +# Add Turbo JPEG library +if(ENABLE_V4L2 OR ENABLE_MF) + find_package(TurboJPEG) + if(TURBOJPEG_FOUND) + add_definitions(-DHAVE_TURBO_JPEG) + message(STATUS "Using Turbo JPEG library: ${TurboJPEG_LIBRARY}") + target_link_libraries(${PROJECT_NAME} ${TurboJPEG_LIBRARY}) + target_include_directories(${PROJECT_NAME} PUBLIC ${TurboJPEG_INCLUDE_DIRS}) + else () + message(STATUS "Turbo JPEG library not found, MJPEG camera format won't work.") + endif() endif() diff --git a/libsrc/grabber/video/EncoderThread.cpp b/libsrc/grabber/video/EncoderThread.cpp index 1de36ff7..3555bb9f 100644 --- a/libsrc/grabber/video/EncoderThread.cpp +++ b/libsrc/grabber/video/EncoderThread.cpp @@ -1,4 +1,4 @@ -#include "grabber/EncoderThread.h" +#include "grabber/video/EncoderThread.h" #include diff --git a/libsrc/grabber/video/VideoWrapper.cpp b/libsrc/grabber/video/VideoWrapper.cpp index 7a3ed201..3b95f561 100644 --- a/libsrc/grabber/video/VideoWrapper.cpp +++ b/libsrc/grabber/video/VideoWrapper.cpp @@ -1,6 +1,6 @@ #include -#include +#include // qt includes #include diff --git a/libsrc/grabber/video/mediafoundation/MFGrabber.cpp b/libsrc/grabber/video/mediafoundation/MFGrabber.cpp index 5e67e6ef..ca2b6a35 100644 --- a/libsrc/grabber/video/mediafoundation/MFGrabber.cpp +++ b/libsrc/grabber/video/mediafoundation/MFGrabber.cpp @@ -1,5 +1,5 @@ #include "MFSourceReaderCB.h" -#include "grabber/MFGrabber.h" +#include "grabber/video/mediafoundation/MFGrabber.h" // Constants namespace { const bool verbose = false; } @@ -537,7 +537,7 @@ void MFGrabber::process_image(const void *frameImageBuffer, int size) Error(_log, "Frame too small: %d != %d", size, _frameByteSize); else if (_threadManager != nullptr) { - for (unsigned long i = 0; i < _threadManager->_threadCount; i++) + for (int i = 0; i < _threadManager->_threadCount; i++) { if (!_threadManager->_threads[i]->isBusy()) { diff --git a/libsrc/grabber/video/mediafoundation/MFSourceReaderCB.h b/libsrc/grabber/video/mediafoundation/MFSourceReaderCB.h index 8cb10ba4..2bcef437 100644 --- a/libsrc/grabber/video/mediafoundation/MFSourceReaderCB.h +++ b/libsrc/grabber/video/mediafoundation/MFSourceReaderCB.h @@ -19,7 +19,7 @@ #pragma comment (lib, "strmiids.lib") #pragma comment (lib, "wmcodecdspuuid.lib") -#include +#include #define SAFE_RELEASE(x) if(x) { x->Release(); x = nullptr; } diff --git a/libsrc/grabber/video/v4l2/V4L2Grabber.cpp b/libsrc/grabber/video/v4l2/V4L2Grabber.cpp index 8f504464..cd0d273f 100644 --- a/libsrc/grabber/video/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/video/v4l2/V4L2Grabber.cpp @@ -23,7 +23,7 @@ #include #include -#include "grabber/V4L2Grabber.h" +#include "grabber/video/v4l2/V4L2Grabber.h" #define CLEAR(x) memset(&(x), 0, sizeof(x)) diff --git a/libsrc/grabber/x11/CMakeLists.txt b/libsrc/grabber/x11/CMakeLists.txt index 08183aff..d5cd3831 100644 --- a/libsrc/grabber/x11/CMakeLists.txt +++ b/libsrc/grabber/x11/CMakeLists.txt @@ -1,24 +1,23 @@ -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/x11) - -# Find X11 find_package(X11 REQUIRED) -include_directories( ${X11_INCLUDES} ) - -if(APPLE) - include_directories("/opt/X11/include") -endif(APPLE) - -FILE ( GLOB X11_SOURCES "${CURRENT_HEADER_DIR}/X11*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -add_library(x11-grabber ${X11_SOURCES} ) +add_library(x11-grabber + ${CMAKE_SOURCE_DIR}/include/grabber/x11/X11Grabber.h + ${CMAKE_SOURCE_DIR}/include/grabber/x11/X11Wrapper.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/x11/X11Grabber.cpp + ${CMAKE_SOURCE_DIR}/libsrc/grabber/x11/X11Wrapper.cpp +) target_link_libraries(x11-grabber hyperion ${X11_LIBRARIES} ${X11_Xrandr_LIB} ${X11_Xrender_LIB} - ${QT_LIBRARIES} +) + +if(APPLE) + list(APPEND X11_INCLUDES "/opt/X11/include") +endif() + +target_include_directories(x11-grabber PUBLIC + ${X11_INCLUDES} ) diff --git a/libsrc/grabber/x11/X11Grabber.cpp b/libsrc/grabber/x11/X11Grabber.cpp index 46c7faa4..cf24eea2 100644 --- a/libsrc/grabber/x11/X11Grabber.cpp +++ b/libsrc/grabber/x11/X11Grabber.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include diff --git a/libsrc/grabber/x11/X11Wrapper.cpp b/libsrc/grabber/x11/X11Wrapper.cpp index a453fc19..a7e36b71 100644 --- a/libsrc/grabber/x11/X11Wrapper.cpp +++ b/libsrc/grabber/x11/X11Wrapper.cpp @@ -1,4 +1,4 @@ -#include +#include X11Wrapper::X11Wrapper( int updateRate_Hz, int pixelDecimation, diff --git a/libsrc/grabber/xcb/CMakeLists.txt b/libsrc/grabber/xcb/CMakeLists.txt index 97f1ee91..d353f2bd 100644 --- a/libsrc/grabber/xcb/CMakeLists.txt +++ b/libsrc/grabber/xcb/CMakeLists.txt @@ -1,23 +1,19 @@ -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/xcb) - find_package(XCB COMPONENTS SHM IMAGE RENDER RANDR REQUIRED) -include_directories(${XCB_INCLUDE_DIRS}) - -FILE (GLOB XCB_SOURCES "${CURRENT_HEADER_DIR}/Xcb*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -add_library(xcb-grabber ${XCB_SOURCES}) +add_library(xcb-grabber + ${CMAKE_SOURCE_DIR}/include/grabber/xcb/XcbGrabber.h + ${CMAKE_SOURCE_DIR}/include/grabber/xcb/XcbWrapper.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/xcb/XcbCommandExecutor.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/xcb/XcbCommands.h + ${CMAKE_SOURCE_DIR}/libsrc/grabber/xcb/XcbGrabber.cpp + ${CMAKE_SOURCE_DIR}/libsrc/grabber/xcb/XcbWrapper.cpp +) target_link_libraries(xcb-grabber hyperion ${XCB_LIBRARIES} - ${QT_LIBRARIES} ) -if (NOT APPLE) - target_link_libraries( - xcb-grabber - ) -endif() +target_include_directories(xcb-grabber PUBLIC + ${XCB_INCLUDE_DIRS} +) diff --git a/libsrc/grabber/xcb/XcbGrabber.cpp b/libsrc/grabber/xcb/XcbGrabber.cpp index a5fc97f9..bf58770d 100644 --- a/libsrc/grabber/xcb/XcbGrabber.cpp +++ b/libsrc/grabber/xcb/XcbGrabber.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include "XcbCommands.h" #include "XcbCommandExecutor.h" diff --git a/libsrc/grabber/xcb/XcbWrapper.cpp b/libsrc/grabber/xcb/XcbWrapper.cpp index 339cb4e8..129217e1 100644 --- a/libsrc/grabber/xcb/XcbWrapper.cpp +++ b/libsrc/grabber/xcb/XcbWrapper.cpp @@ -1,4 +1,4 @@ -#include +#include XcbWrapper::XcbWrapper( int updateRate_Hz, int pixelDecimation, diff --git a/libsrc/hyperion/CMakeLists.txt b/libsrc/hyperion/CMakeLists.txt index 3a880a91..77600086 100644 --- a/libsrc/hyperion/CMakeLists.txt +++ b/libsrc/hyperion/CMakeLists.txt @@ -1,21 +1,50 @@ - -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/hyperion) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/hyperion) - -if(ENABLE_FLATBUF_SERVER) -include_directories( - ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/flatbufserver -) -endif() - -FILE ( GLOB Hyperion_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -SET(Hyperion_RESOURCES ${CURRENT_SOURCE_DIR}/resource.qrc) - add_library(hyperion - ${Hyperion_SOURCES} - ${Hyperion_RESOURCES} + # Authorization Manager + ${CMAKE_SOURCE_DIR}/include/hyperion/AuthManager.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/AuthManager.cpp + # Background Effect Handler + ${CMAKE_SOURCE_DIR}/include/hyperion/BGEffectHandler.h + # Capture Control class + ${CMAKE_SOURCE_DIR}/include/hyperion/CaptureCont.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/CaptureCont.cpp + # Color Adjustment + ${CMAKE_SOURCE_DIR}/include/hyperion/ColorAdjustment.h + # Component Register + ${CMAKE_SOURCE_DIR}/include/hyperion/ComponentRegister.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/ComponentRegister.cpp + # Grabber/Wrapper classes + ${CMAKE_SOURCE_DIR}/include/hyperion/Grabber.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/Grabber.cpp + ${CMAKE_SOURCE_DIR}/include/hyperion/GrabberWrapper.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/GrabberWrapper.cpp + # Hyperion + Resources + ${CMAKE_SOURCE_DIR}/include/hyperion/Hyperion.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/Hyperion.cpp + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/resource.qrc + # Instance Manager + ${CMAKE_SOURCE_DIR}/include/hyperion/HyperionIManager.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/HyperionIManager.cpp + # Image Processor + ${CMAKE_SOURCE_DIR}/include/hyperion/ImageProcessor.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/ImageProcessor.cpp + # ImageToLedsMap class + ${CMAKE_SOURCE_DIR}/include/hyperion/ImageToLedsMap.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/ImageToLedsMap.cpp + # Led String + ${CMAKE_SOURCE_DIR}/include/hyperion/LedString.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/LedString.cpp + # Linear Color Smoothing + ${CMAKE_SOURCE_DIR}/include/hyperion/LinearColorSmoothing.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/LinearColorSmoothing.cpp + # Led Color Transform + ${CMAKE_SOURCE_DIR}/include/hyperion/MultiColorAdjustment.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/MultiColorAdjustment.cpp + # Priority Muxer + ${CMAKE_SOURCE_DIR}/include/hyperion/PriorityMuxer.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/PriorityMuxer.cpp + # Settings Manager + ${CMAKE_SOURCE_DIR}/include/hyperion/SettingsManager.h + ${CMAKE_SOURCE_DIR}/libsrc/hyperion/SettingsManager.cpp ) target_link_libraries(hyperion @@ -23,7 +52,6 @@ target_link_libraries(hyperion hyperion-utils leddevice database - ${QT_LIBRARIES} ) if(ENABLE_BOBLIGHT_SERVER) diff --git a/libsrc/jsonserver/CMakeLists.txt b/libsrc/jsonserver/CMakeLists.txt index 30ca1373..b8b3c5b7 100644 --- a/libsrc/jsonserver/CMakeLists.txt +++ b/libsrc/jsonserver/CMakeLists.txt @@ -1,20 +1,15 @@ - -# Define the current source locations -set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/jsonserver) -set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/jsonserver) - -FILE ( GLOB JsonServer_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -add_library(jsonserver ${JsonServer_SOURCES} ) +add_library(jsonserver + ${CMAKE_SOURCE_DIR}/include/jsonserver/JsonServer.h + ${CMAKE_SOURCE_DIR}/libsrc/jsonserver/JsonServer.cpp + ${CMAKE_SOURCE_DIR}/libsrc/jsonserver/JsonClientConnection.h + ${CMAKE_SOURCE_DIR}/libsrc/jsonserver/JsonClientConnection.cpp +) target_link_libraries(jsonserver hyperion-api hyperion - Qt${QT_VERSION_MAJOR}::Network - Qt${QT_VERSION_MAJOR}::Gui ) if(ENABLE_MDNS) target_link_libraries(jsonserver mdns) endif() - diff --git a/libsrc/leddevice/CMakeLists.txt b/libsrc/leddevice/CMakeLists.txt index 233bb9fd..259ebf14 100644 --- a/libsrc/leddevice/CMakeLists.txt +++ b/libsrc/leddevice/CMakeLists.txt @@ -1,13 +1,13 @@ # Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/leddevice) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/leddevice) +set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/leddevice) +set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/leddevice) -if ( ENABLE_DEV_NETWORK ) +if(ENABLE_DEV_NETWORK) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Network REQUIRED) endif() -if ( ENABLE_DEV_SERIAL ) +if(ENABLE_DEV_SERIAL) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS SerialPort REQUIRED) endif() @@ -21,7 +21,7 @@ include_directories( dev_tinker ) -FILE ( GLOB Leddevice_SOURCES +file (GLOB Leddevice_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" @@ -29,49 +29,48 @@ FILE ( GLOB Leddevice_SOURCES "${CURRENT_SOURCE_DIR}/dev_other/*.cpp" ) -if ( ENABLE_OSX OR WIN32 ) +if(ENABLE_OSX OR WIN32) list(REMOVE_ITEM Leddevice_SOURCES "${CURRENT_SOURCE_DIR}/dev_other/LedDevicePiBlaster.h") list(REMOVE_ITEM Leddevice_SOURCES "${CURRENT_SOURCE_DIR}/dev_other/LedDevicePiBlaster.cpp") endif() -if ( ENABLE_DEV_NETWORK ) - FILE ( GLOB Leddevice_NETWORK_SOURCES "${CURRENT_SOURCE_DIR}/dev_net/*.h" "${CURRENT_SOURCE_DIR}/dev_net/*.cpp") +if(ENABLE_DEV_NETWORK) + file (GLOB Leddevice_NETWORK_SOURCES "${CURRENT_SOURCE_DIR}/dev_net/*.h" "${CURRENT_SOURCE_DIR}/dev_net/*.cpp") endif() -if ( ENABLE_DEV_SERIAL ) - FILE ( GLOB Leddevice_SERIAL_SOURCES "${CURRENT_SOURCE_DIR}/dev_serial/*.h" "${CURRENT_SOURCE_DIR}/dev_serial/*.cpp") +if(ENABLE_DEV_SERIAL) + file (GLOB Leddevice_SERIAL_SOURCES "${CURRENT_SOURCE_DIR}/dev_serial/*.h" "${CURRENT_SOURCE_DIR}/dev_serial/*.cpp") endif() -if ( ENABLE_DEV_SPI ) - FILE ( GLOB Leddevice_SPI_SOURCES "${CURRENT_SOURCE_DIR}/dev_spi/*.h" "${CURRENT_SOURCE_DIR}/dev_spi/*.cpp") +if(ENABLE_DEV_SPI) + file (GLOB Leddevice_SPI_SOURCES "${CURRENT_SOURCE_DIR}/dev_spi/*.h" "${CURRENT_SOURCE_DIR}/dev_spi/*.cpp") endif() -if ( ENABLE_DEV_TINKERFORGE ) - FILE ( GLOB Leddevice_TINKER_SOURCES "${CURRENT_SOURCE_DIR}/dev_tinker/*.h" "${CURRENT_SOURCE_DIR}/dev_tinker/*.cpp") +if(ENABLE_DEV_TINKERFORGE) + file (GLOB Leddevice_TINKER_SOURCES "${CURRENT_SOURCE_DIR}/dev_tinker/*.h" "${CURRENT_SOURCE_DIR}/dev_tinker/*.cpp") endif() -if ( ENABLE_DEV_USB_HID ) +if(ENABLE_DEV_USB_HID) find_package(libusb-1.0 REQUIRED) include_directories( ${CMAKE_SOURCE_DIR}/include/hidapi ${LIBUSB_1_INCLUDE_DIRS} ) - FILE ( GLOB Leddevice_USB_HID_SOURCES "${CURRENT_SOURCE_DIR}/dev_hid/*.h" "${CURRENT_SOURCE_DIR}/dev_hid/*.cpp") + file (GLOB Leddevice_USB_HID_SOURCES "${CURRENT_SOURCE_DIR}/dev_hid/*.h" "${CURRENT_SOURCE_DIR}/dev_hid/*.cpp") endif() -if ( ENABLE_DEV_WS281XPWM ) - include_directories(../../dependencies/external/rpi_ws281x) - FILE ( GLOB Leddevice_PWM_SOURCES "${CURRENT_SOURCE_DIR}/dev_rpi_pwm/*.h" "${CURRENT_SOURCE_DIR}/dev_rpi_pwm/*.cpp") +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 ) +set(LedDevice_RESOURCES ${CURRENT_SOURCE_DIR}/LedDeviceSchemas.qrc) -SET( Leddevice_SOURCES +set(Leddevice_SOURCES ${Leddevice_SOURCES} ${LedDevice_RESOURCES} ${Leddevice_NETWORK_SOURCES} ${Leddevice_PWM_SOURCES} - ${Leddevice_SERIAL_SOURCES} + ${Leddevice_SERIAL_SOURCES} ${Leddevice_SPI_SOURCES} ${Leddevice_TINKER_SOURCES} ${Leddevice_USB_HID_SOURCES} @@ -79,20 +78,20 @@ SET( Leddevice_SOURCES # auto generate header file that include all available leddevice headers # auto generate cpp file for register() calls -FILE ( WRITE "${CMAKE_BINARY_DIR}/LedDevice_headers.h" "#pragma once\n\n//this file is autogenerated, don't touch it\n\n" ) -FILE ( WRITE "${CMAKE_BINARY_DIR}/LedDevice_register.cpp" "//this file is autogenerated, don't touch it\n\n" ) -FOREACH( f ${Leddevice_SOURCES} ) - # MESSAGE (STATUS "Add led device: ${f}") - if ( "${f}" MATCHES "dev_.*/Led.evice.+h$" ) +file (WRITE "${CMAKE_BINARY_DIR}/LedDevice_headers.h" "#pragma once\n\n//this file is autogenerated, don't touch it\n\n") +file (WRITE "${CMAKE_BINARY_DIR}/LedDevice_register.cpp" "//this file is autogenerated, don't touch it\n\n") +foreach(f ${Leddevice_SOURCES}) + # message (STATUS "Add led device: ${f}") + if("${f}" MATCHES "dev_.*/Led.evice.+h$") GET_FILENAME_COMPONENT(fname ${f} NAME) - FILE ( APPEND "${CMAKE_BINARY_DIR}/LedDevice_headers.h" "#include \"${fname}\"\n" ) - STRING( SUBSTRING ${fname} 9 -1 dname) - STRING( REPLACE ".h" "" dname "${dname}" ) - FILE ( APPEND "${CMAKE_BINARY_DIR}/LedDevice_register.cpp" "REGISTER(${dname});\n" ) + file (APPEND "${CMAKE_BINARY_DIR}/LedDevice_headers.h" "#include \"${fname}\"\n") + string(SUBSTRING ${fname} 9 -1 dname) + string(REPLACE ".h" "" dname "${dname}") + file (APPEND "${CMAKE_BINARY_DIR}/LedDevice_register.cpp" "REGISTER(${dname});\n") endif() -ENDFOREACH() +endforeach() -add_library(leddevice ${CMAKE_BINARY_DIR}/LedDevice_headers.h ${Leddevice_SOURCES} ) +add_library(leddevice ${CMAKE_BINARY_DIR}/LedDevice_headers.h ${Leddevice_SOURCES}) target_link_libraries(leddevice hyperion @@ -106,19 +105,19 @@ endif() if(ENABLE_DEV_NETWORK) target_link_libraries(leddevice Qt${QT_VERSION_MAJOR}::Network ssdp) - - if (NOT DEFAULT_USE_SYSTEM_MBEDTLS_LIBS) - if (MBEDTLS_LIBRARIES) + + if(NOT DEFAULT_USE_SYSTEM_MBEDTLS_LIBS) + if(MBEDTLS_LIBRARIES) include_directories(${MBEDTLS_INCLUDE_DIR}) target_link_libraries(leddevice ${MBEDTLS_LIBRARIES}) target_include_directories(leddevice PRIVATE ${MBEDTLS_INCLUDE_DIR}) endif (MBEDTLS_LIBRARIES) - endif () + endif() string(REGEX MATCH "[0-9]+|-([A-Za-z0-9_.]+)" MBEDTLS_MAJOR ${MBEDTLS_VERSION}) - if (MBEDTLS_MAJOR EQUAL "3") + if(MBEDTLS_MAJOR EQUAL "3") target_compile_definitions(leddevice PRIVATE USE_MBEDTLS3) - if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") target_compile_features(leddevice PRIVATE cxx_std_20) endif() endif() @@ -133,10 +132,11 @@ if(ENABLE_DEV_TINKERFORGE) endif() if(ENABLE_DEV_WS281XPWM) + target_include_directories(leddevice PUBLIC "${CMAKE_SOURCE_DIR}/dependencies/external/rpi_ws281x") target_link_libraries(leddevice ws281x) endif() -if (ENABLE_DEV_USB_HID) +if(ENABLE_DEV_USB_HID) if(APPLE) target_link_libraries(leddevice ${LIBUSB_1_LIBRARIES} hidapi-mac) else() @@ -150,17 +150,18 @@ if (ENABLE_DEV_USB_HID) #include int main() { - struct timespec t; - return clock_gettime(CLOCK_REALTIME, &t); + struct timespec t; + return clock_gettime(CLOCK_REALTIME, &t); } " GLIBC_HAS_CLOCK_GETTIME) - IF(NOT GLIBC_HAS_CLOCK_GETTIME) + if(NOT GLIBC_HAS_CLOCK_GETTIME) target_link_libraries(leddevice rt) endif() endif() endif() if(ENABLE_MDNS) - target_link_libraries(leddevice mdns) + + target_link_libraries(leddevice mdns) endif() diff --git a/libsrc/mdns/CMakeLists.txt b/libsrc/mdns/CMakeLists.txt index 4fdd5ed5..58bd4bcf 100644 --- a/libsrc/mdns/CMakeLists.txt +++ b/libsrc/mdns/CMakeLists.txt @@ -1,16 +1,13 @@ - -# Define the current source locations -set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/mdns) -set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/mdns) - -FILE ( GLOB MDNS_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) -add_library(mdns ${MDNS_SOURCES}) - -include_directories(${QMDNS_INCLUDE_DIR}) - -target_link_libraries(mdns -hyperion-utils -${QMDNS_LIBRARIES} +add_library(mdns + ${CMAKE_SOURCE_DIR}/include/mdns/MdnsServiceRegister.h + ${CMAKE_SOURCE_DIR}/include/mdns/MdnsBrowser.h + ${CMAKE_SOURCE_DIR}/include/mdns/MdnsProvider.h + ${CMAKE_SOURCE_DIR}/libsrc/mdns/MdnsBrowser.cpp + ${CMAKE_SOURCE_DIR}/libsrc/mdns/MdnsProvider.cpp ) -target_include_directories(mdns PUBLIC ${QMDNS_INCLUDE_DIR}) +target_link_libraries(mdns + hyperion + qmdnsengine + $<$:bcrypt.lib> +) diff --git a/libsrc/protoserver/CMakeLists.txt b/libsrc/protoserver/CMakeLists.txt index 5fefa7ef..a463f53c 100644 --- a/libsrc/protoserver/CMakeLists.txt +++ b/libsrc/protoserver/CMakeLists.txt @@ -1,37 +1,22 @@ - -# Define the current source locations -set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/protoserver) -set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/protoserver) - -include_directories( - ${CMAKE_CURRENT_BINARY_DIR} - ${PROTOBUF_INCLUDE_DIRS} -) - -set(ProtoServer_PROTOS ${CURRENT_SOURCE_DIR}/message.proto ) - -protobuf_generate_cpp(ProtoServer_PROTO_SRCS ProtoServer_PROTO_HDRS ${ProtoServer_PROTOS} ) -### Split protoclient from protoserver as protoserver relates to HyperionDaemon and standalone capture binarys can't link to it - -add_library(protoclient - ${CURRENT_SOURCE_DIR}/ProtoClientConnection.h - ${CURRENT_SOURCE_DIR}/ProtoClientConnection.cpp - ${ProtoServer_PROTO_SRCS} - ${ProtoServer_PROTO_HDRS} -) - -add_library(protoserver - ${CURRENT_HEADER_DIR}/ProtoServer.h - ${CURRENT_SOURCE_DIR}/ProtoServer.cpp -) +# set and compile proto schema +set(ProtoServer_PROTOS ${CMAKE_SOURCE_DIR}/libsrc/protoserver/message.proto) +protobuf_generate_cpp(ProtoServer_PROTO_SRCS ProtoServer_PROTO_HDRS ${ProtoServer_PROTOS}) # disable warnings for auto generated proto files, we can't change the files .... -if (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX) +if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX) set_source_files_properties(${ProtoServer_PROTO_SRCS} ${ProtoServer_PROTO_HDRS} ${ProtoServer_PROTOS} PROPERTIES COMPILE_FLAGS "-w -Wno-return-local-addr") elseif(MSVC) set_source_files_properties(${ProtoServer_PROTO_SRCS} ${ProtoServer_PROTO_HDRS} ${ProtoServer_PROTOS} PROPERTIES COMPILE_FLAGS "/W0") endif() +### Split protoclient from protoserver as protoserver relates to HyperionDaemon and standalone capture binarys can't link to it +add_library(protoclient + ${CMAKE_SOURCE_DIR}/libsrc/protoserver/ProtoClientConnection.h + ${CMAKE_SOURCE_DIR}/libsrc/protoserver/ProtoClientConnection.cpp + ${ProtoServer_PROTO_SRCS} + ${ProtoServer_PROTO_HDRS} +) + target_link_libraries(protoclient hyperion hyperion-utils @@ -39,6 +24,16 @@ target_link_libraries(protoclient Qt${QT_VERSION_MAJOR}::Gui ) +target_include_directories(protoclient PUBLIC + ${CMAKE_CURRENT_BINARY_DIR} + ${PROTOBUF_INCLUDE_DIRS} +) + +add_library(protoserver + ${CMAKE_SOURCE_DIR}/include/protoserver/ProtoServer.h + ${CMAKE_SOURCE_DIR}/libsrc/protoserver/ProtoServer.cpp +) + target_link_libraries(protoserver hyperion hyperion-utils diff --git a/libsrc/python/CMakeLists.txt b/libsrc/python/CMakeLists.txt index 6b83eb14..d39d34af 100644 --- a/libsrc/python/CMakeLists.txt +++ b/libsrc/python/CMakeLists.txt @@ -1,34 +1,30 @@ -# Include the python directory. Also include the parent (which is for example /usr/include) -# which may be required when it is not includes by the (cross-) compiler by default. -if (NOT CMAKE_VERSION VERSION_LESS "3.12") - find_package(Python3 COMPONENTS Interpreter Development REQUIRED) - include_directories(${Python3_INCLUDE_DIRS} ${Python3_INCLUDE_DIRS}/..) - add_compile_definitions(PYTHON_VERSION_MAJOR=${Python3_VERSION_MAJOR}) - add_compile_definitions(PYTHON_VERSION_MINOR=${Python3_VERSION_MINOR}) -else() - find_package (PythonLibs ${PYTHON_VERSION_STRING} EXACT) # Maps PythonLibs to the PythonInterp version of the main cmake - include_directories(${PYTHON_INCLUDE_DIRS} ${PYTHON_INCLUDE_DIRS}/..) - add_definitions(-DPYTHON_VERSION_MAJOR=${PYTHON_VERSION_MAJOR}) - add_definitions(-DPYTHON_VERSION_MINOR=${PYTHON_VERSION_MINOR}) -endif() - -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/python) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/python) - -FILE ( GLOB PYTHON_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - add_library(python - ${PYTHON_SOURCES} + ${CMAKE_SOURCE_DIR}/include/python/PythonInit.h + ${CMAKE_SOURCE_DIR}/include/python/PythonProgram.h + ${CMAKE_SOURCE_DIR}/include/python/PythonUtils.h + ${CMAKE_SOURCE_DIR}/libsrc/python/PythonInit.cpp + ${CMAKE_SOURCE_DIR}/libsrc/python/PythonProgram.cpp ) +if(NOT CMAKE_VERSION VERSION_LESS "3.12") + find_package(Python3 COMPONENTS Interpreter Development REQUIRED) +else() + find_package (PythonLibs ${PYTHON_VERSION_STRING} EXACT) +endif() + target_link_libraries(python effectengine hyperion-utils + ${Python3_LIBRARIES} + ${PYTHON_LIBRARIES} ) -if (NOT CMAKE_VERSION VERSION_LESS "3.12") - target_link_libraries( python ${Python3_LIBRARIES} ) -else() - target_link_libraries( python ${PYTHON_LIBRARIES} ) -endif() +target_include_directories(python PUBLIC + ${Python3_INCLUDE_DIRS} ${Python3_INCLUDE_DIRS}/.. + ${PYTHON_INCLUDE_DIRS} ${PYTHON_INCLUDE_DIRS}/.. +) + +target_compile_definitions(python PRIVATE + PYTHON_VERSION_MAJOR=$<$:${Python3_VERSION_MAJOR}>$<$:${PYTHON_VERSION_MAJOR}> + PYTHON_VERSION_MINOR=$<$:${Python3_VERSION_MINOR}>$<$:${PYTHON_VERSION_MINOR}> +) diff --git a/libsrc/ssdp/CMakeLists.txt b/libsrc/ssdp/CMakeLists.txt index 2c007c0c..b773b6f9 100644 --- a/libsrc/ssdp/CMakeLists.txt +++ b/libsrc/ssdp/CMakeLists.txt @@ -1,14 +1,13 @@ -# Define the current source locations -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/ssdp) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/ssdp) - -FILE ( GLOB SSDP_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - add_library(ssdp - ${SSDP_SOURCES} + ${CMAKE_SOURCE_DIR}/include/ssdp/SSDPDiscover.h + ${CMAKE_SOURCE_DIR}/include/ssdp/SSDPHandler.h + ${CMAKE_SOURCE_DIR}/include/ssdp/SSDPServer.h + ${CMAKE_SOURCE_DIR}/libsrc/ssdp/SSDPDescription.h + ${CMAKE_SOURCE_DIR}/libsrc/ssdp/SSDPDiscover.cpp + ${CMAKE_SOURCE_DIR}/libsrc/ssdp/SSDPHandler.cpp + ${CMAKE_SOURCE_DIR}/libsrc/ssdp/SSDPServer.cpp ) target_link_libraries(ssdp - Qt${QT_VERSION_MAJOR}::Network webserver ) diff --git a/libsrc/utils/CMakeLists.txt b/libsrc/utils/CMakeLists.txt index e51fcb5d..b7d28eeb 100644 --- a/libsrc/utils/CMakeLists.txt +++ b/libsrc/utils/CMakeLists.txt @@ -1,32 +1,94 @@ -# Define the current source locations - -if(ENABLE_EFFECTENGINE) - # Include the python directory. Also include the parent (which is for example /usr/include) - # which may be required when it is not includes by the (cross-) compiler by default. - if (NOT CMAKE_VERSION VERSION_LESS "3.12") - find_package(Python3 COMPONENTS Interpreter Development REQUIRED) - include_directories(${Python3_INCLUDE_DIRS} ${Python3_INCLUDE_DIRS}/..) - add_compile_definitions(PYTHON_VERSION_MAJOR_MINOR=${Python3_VERSION_MAJOR}${Python3_VERSION_MINOR}) - else() - find_package (PythonLibs ${PYTHON_VERSION_STRING} EXACT) # Maps PythonLibs to the PythonInterp version of the main cmake - include_directories(${PYTHON_INCLUDE_DIRS} ${PYTHON_INCLUDE_DIRS}/..) - add_definitions(-DPYTHON_VERSION_MAJOR_MINOR=${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}) - endif() -endif() - -SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/utils) -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/utils) - -FILE ( GLOB_RECURSE Utils_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) - -list(APPEND Utils_SOURCES "${CMAKE_SOURCE_DIR}/dependencies/include/oklab/ok_color.h") - -if ( NOT ENABLE_PROFILER ) - LIST ( REMOVE_ITEM Utils_SOURCES ${CURRENT_HEADER_DIR}/Profiler.h ${CURRENT_SOURCE_DIR}/Profiler.cpp ) +if(ENABLE_PROFILER) + set(PROFILER ${CURRENT_HEADER_DIR}/Profiler.h ${CURRENT_SOURCE_DIR}/Profiler.cpp) endif() add_library(hyperion-utils - ${Utils_SOURCES} + # Global defines/signal sharing + ${CMAKE_SOURCE_DIR}/include/utils/global_defines.h + ${CMAKE_SOURCE_DIR}/include/utils/GlobalSignals.h + # JSON Schema Checker + ${CMAKE_SOURCE_DIR}/include/utils/jsonschema/QJsonFactory.h + ${CMAKE_SOURCE_DIR}/include/utils/jsonschema/QJsonUtils.h + ${CMAKE_SOURCE_DIR}/include/utils/jsonschema/QJsonSchemaChecker.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp + # Color ARGB/BGR/RGB/RGBA/RGBW etc. structures + ${CMAKE_SOURCE_DIR}/include/utils/ColorArgb.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/ColorArgb.cpp + ${CMAKE_SOURCE_DIR}/include/utils/ColorBgr.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/ColorBgr.cpp + ${CMAKE_SOURCE_DIR}/include/utils/ColorRgb.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/ColorRgb.cpp + ${CMAKE_SOURCE_DIR}/include/utils/ColorRgba.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/ColorRgba.cpp + ${CMAKE_SOURCE_DIR}/include/utils/ColorRgbScalar.h + ${CMAKE_SOURCE_DIR}/include/utils/ColorRgbw.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/ColorRgbw.cpp + # Image declaration + ${CMAKE_SOURCE_DIR}/include/utils/Image.h + ${CMAKE_SOURCE_DIR}/include/utils/ImageData.h + # Image resampler + ${CMAKE_SOURCE_DIR}/include/utils/ImageResampler.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/ImageResampler.cpp + # Color transformation (saturation/luminance) of RGB colors + ${CMAKE_SOURCE_DIR}/include/utils/ColorSys.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/ColorSys.cpp + # Color transformation (saturation/value) of Okhsv colors + ${CMAKE_SOURCE_DIR}/include/utils/OkhsvTransform.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/OkhsvTransform.cpp + # Signal handler + ${CMAKE_SOURCE_DIR}/include/utils/DefaultSignalHandler.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/DefaultSignalHandler.cpp + # File utilities + ${CMAKE_SOURCE_DIR}/include/utils/FileUtils.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/FileUtils.cpp + # JSON utilities + ${CMAKE_SOURCE_DIR}/include/utils/JsonUtils.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/JsonUtils.cpp + # Logger + ${CMAKE_SOURCE_DIR}/include/utils/Logger.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/Logger.cpp + # IP adress/Port checker + ${CMAKE_SOURCE_DIR}/include/utils/NetOrigin.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/NetOrigin.cpp + ${CMAKE_SOURCE_DIR}/include/utils/NetUtils.h + # Process namespace (Hyperion restart) + ${CMAKE_SOURCE_DIR}/include/utils/Process.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/Process.cpp + # Rgb single color adjustment/correction + ${CMAKE_SOURCE_DIR}/include/utils/RgbChannelAdjustment.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/RgbChannelAdjustment.cpp + # Color conversion/transformation + ${CMAKE_SOURCE_DIR}/include/utils/RgbToRgbw.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/RgbToRgbw.cpp + ${CMAKE_SOURCE_DIR}/include/utils/RgbTransform.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/RgbTransform.cpp + # System info class + ${CMAKE_SOURCE_DIR}/include/utils/SysInfo.h + ${CMAKE_SOURCE_DIR}/libsrc/utils/SysInfo.cpp + # Grabber pixel formats enumeration + ${CMAKE_SOURCE_DIR}/include/utils/PixelFormat.h + # Grabber playing modes enumeration + ${CMAKE_SOURCE_DIR}/include/utils/VideoMode.h + # Grabber video standards enumeration + ${CMAKE_SOURCE_DIR}/include/utils/VideoStandard.h + # SettingsManager utilities + ${CMAKE_SOURCE_DIR}/include/utils/settings.h + # Qt string utilities + ${CMAKE_SOURCE_DIR}/include/utils/QStringUtils.h + # QThread sleep class + ${CMAKE_SOURCE_DIR}/include/utils/Sleep.h + # Wait event loop function + ${CMAKE_SOURCE_DIR}/include/utils/WaitTime.h + # Weak connection + ${CMAKE_SOURCE_DIR}/include/utils/WeakConnect.h + # Semver namespace + ${CMAKE_SOURCE_DIR}/include/utils/version.hpp + # Utility methods for Hyperion class + ${CMAKE_SOURCE_DIR}/include/utils/hyperion.h + # Oklab color space + ${CMAKE_SOURCE_DIR}/dependencies/include/oklab/ok_color.h + # Performance tester + ${PROFILER} ) target_link_libraries(hyperion-utils diff --git a/libsrc/utils/Process.cpp b/libsrc/utils/Process.cpp index 9fdd7264..baa2723a 100644 --- a/libsrc/utils/Process.cpp +++ b/libsrc/utils/Process.cpp @@ -1,93 +1,92 @@ #ifdef _WIN32 -#include -#include -#include -#include -#include -namespace Process { + #include + #include + #include + #include + #include -void restartHyperion(int exitCode) -{ - Logger* log = Logger::getInstance("Process"); - Info(log, "Restarting hyperion ..."); + namespace Process + { + void restartHyperion(int exitCode) + { + Logger* log = Logger::getInstance("Process"); + Info(log, "Restarting hyperion ..."); - auto arguments = QCoreApplication::arguments(); - if (!arguments.contains("--wait-hyperion")) - arguments << "--wait-hyperion"; + auto arguments = QCoreApplication::arguments(); + if (!arguments.contains("--wait-hyperion")) + arguments << "--wait-hyperion"; - QProcess::startDetached(QCoreApplication::applicationFilePath(), arguments); + QProcess::startDetached(QCoreApplication::applicationFilePath(), arguments); - //Exit with non-zero code to ensure service deamon restarts hyperion - QCoreApplication::exit(exitCode); -} + //Exit with non-zero code to ensure service deamon restarts hyperion + QCoreApplication::exit(exitCode); + } -QByteArray command_exec(const QString& /*cmd*/, const QByteArray& /*data*/) -{ - return QSTRING_CSTR(QString()); -} -}; + QByteArray command_exec(const QString& /*cmd*/, const QByteArray& /*data*/) + { + return QSTRING_CSTR(QString()); + } + }; #else -#include -#include + #include + #include -#include -#include -#include + #include + #include + #include -#include -#include -#include -#include -#include + #include + #include + #include + #include + #include -#include + #include -#include -#include + #include + #include -namespace Process { - - -void restartHyperion(int exitCode) -{ - Logger* log = Logger::getInstance("Process"); - Info(log, "Restarting hyperion ..."); - - std::cout << std::endl - << " *******************************************" << std::endl - << " * hyperion will restart now *" << std::endl - << " *******************************************" << std::endl << std::endl; - - auto arguments = QCoreApplication::arguments(); - if (!arguments.contains("--wait-hyperion")) - arguments << "--wait-hyperion"; - - QProcess::startDetached(QCoreApplication::applicationFilePath(), arguments); - - //Exit with non-zero code to ensure service deamon restarts hyperion - QCoreApplication::exit(exitCode); -} - -QByteArray command_exec(const QString& cmd, const QByteArray& /*data*/) -{ - char buffer[128]; - QString result; - - std::shared_ptr pipe(popen(cmd.toLocal8Bit().constData(), "r"), pclose); - if (pipe) + namespace Process { - while (!feof(pipe.get())) + void restartHyperion(int exitCode) { - if (fgets(buffer, 128, pipe.get()) != nullptr) - result += buffer; - } - } - return QSTRING_CSTR(result); -} + Logger* log = Logger::getInstance("Process"); + Info(log, "Restarting hyperion ..."); -}; + std::cout << std::endl + << " *******************************************" << std::endl + << " * hyperion will restart now *" << std::endl + << " *******************************************" << std::endl << std::endl; + + auto arguments = QCoreApplication::arguments(); + if (!arguments.contains("--wait-hyperion")) + arguments << "--wait-hyperion"; + + QProcess::startDetached(QCoreApplication::applicationFilePath(), arguments); + + //Exit with non-zero code to ensure service deamon restarts hyperion + QCoreApplication::exit(exitCode); + } + + QByteArray command_exec(const QString& cmd, const QByteArray& /*data*/) + { + char buffer[128]; + QString result; + + std::shared_ptr pipe(popen(cmd.toLocal8Bit().constData(), "r"), pclose); + if (pipe) + { + while (!feof(pipe.get())) + { + if (fgets(buffer, 128, pipe.get()) != nullptr) + result += buffer; + } + } + return QSTRING_CSTR(result); + } + }; #endif diff --git a/libsrc/webserver/CMakeLists.txt b/libsrc/webserver/CMakeLists.txt index 3dcfde15..12e5345f 100644 --- a/libsrc/webserver/CMakeLists.txt +++ b/libsrc/webserver/CMakeLists.txt @@ -1,29 +1,43 @@ -# Define the current source locations -set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/webserver) -set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/webserver) +file(GLOB_RECURSE webFiles RELATIVE ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/assets/webconfig/*) +file(RELATIVE_PATH webConfigPath ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/assets/webconfig) -FILE ( GLOB WebConfig_SOURCES "${CURRENT_HEADER_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) -FILE ( GLOB_RECURSE webFiles RELATIVE ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/assets/webconfig/* ) -FILE ( RELATIVE_PATH webConfigPath ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/assets/webconfig) +foreach(f ${webFiles}) + string(REPLACE "${webConfigPath}/" "" fname ${f}) + set(HYPERION_WEBCONFIG_RES "${HYPERION_WEBCONFIG_RES}\n\t\t${f}") +endforeach() -FOREACH( f ${webFiles} ) - STRING ( REPLACE "${webConfigPath}/" "" fname ${f}) - SET(HYPERION_WEBCONFIG_RES "${HYPERION_WEBCONFIG_RES}\n\t\t${f}") -ENDFOREACH() -CONFIGURE_FILE(${CURRENT_SOURCE_DIR}/WebConfig.qrc.in ${CMAKE_BINARY_DIR}/WebConfig.qrc ) -SET(WebConfig_RESOURCES ${CMAKE_BINARY_DIR}/WebConfig.qrc) +configure_file(${CMAKE_SOURCE_DIR}/libsrc/webserver/WebConfig.qrc.in ${CMAKE_BINARY_DIR}/WebConfig.qrc) add_library(webserver - ${WebConfig_SOURCES} - ${WebConfig_RESOURCES} + ${CMAKE_SOURCE_DIR}/include/webserver/WebServer.h + ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebServer.cpp + ${CMAKE_SOURCE_DIR}/libsrc/webserver/CgiHandler.h + ${CMAKE_SOURCE_DIR}/libsrc/webserver/CgiHandler.cpp + ${CMAKE_SOURCE_DIR}/libsrc/webserver/QtHttpClientWrapper.h + ${CMAKE_SOURCE_DIR}/libsrc/webserver/QtHttpClientWrapper.cpp + ${CMAKE_SOURCE_DIR}/libsrc/webserver/QtHttpHeader.h + ${CMAKE_SOURCE_DIR}/libsrc/webserver/QtHttpHeader.cpp + ${CMAKE_SOURCE_DIR}/libsrc/webserver/QtHttpReply.h + ${CMAKE_SOURCE_DIR}/libsrc/webserver/QtHttpReply.cpp + ${CMAKE_SOURCE_DIR}/libsrc/webserver/QtHttpRequest.h + ${CMAKE_SOURCE_DIR}/libsrc/webserver/QtHttpRequest.cpp + ${CMAKE_SOURCE_DIR}/libsrc/webserver/QtHttpServer.h + ${CMAKE_SOURCE_DIR}/libsrc/webserver/QtHttpServer.cpp + ${CMAKE_SOURCE_DIR}/libsrc/webserver/StaticFileServing.h + ${CMAKE_SOURCE_DIR}/libsrc/webserver/StaticFileServing.cpp + ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebJsonRpc.h + ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebJsonRpc.cpp + ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebSocketClient.h + ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebSocketClient.cpp + ${CMAKE_SOURCE_DIR}/libsrc/webserver/WebSocketUtils.h + ${CMAKE_BINARY_DIR}/WebConfig.qrc ) target_link_libraries(webserver hyperion hyperion-utils hyperion-api - Qt${QT_VERSION_MAJOR}::Network ) if(ENABLE_MDNS) diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt index affe720b..1fba840c 100644 --- a/resources/CMakeLists.txt +++ b/resources/CMakeLists.txt @@ -2,20 +2,20 @@ # All files are available with their file name by calling ":/FILENAME". Probably you need to call Q_INIT_RESOURCE("resources") once # # Define the current source locations -SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/resources) +set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/resources) # catch all files -FILE ( GLOB Hyperion_RESFILES "${CURRENT_SOURCE_DIR}/icons/*" "${CURRENT_SOURCE_DIR}/ssl/*" ) +file(GLOB Hyperion_RESFILES "${CURRENT_SOURCE_DIR}/icons/*" "${CURRENT_SOURCE_DIR}/ssl/*") # fill resources.qrc with RESFILES -FOREACH( f ${Hyperion_RESFILES} ) +foreach(f ${Hyperion_RESFILES}) get_filename_component(fname ${f} NAME) - SET(HYPERION_RES "${HYPERION_RES}\n\t\t${f}") -ENDFOREACH() + set(HYPERION_RES "${HYPERION_RES}\n\t\t${f}") +endforeach() # prep file -CONFIGURE_FILE(${CURRENT_SOURCE_DIR}/resources.qrc.in ${CMAKE_BINARY_DIR}/resources.qrc ) -SET(Hyperion_RES ${CMAKE_BINARY_DIR}/resources.qrc) +configure_file(${CURRENT_SOURCE_DIR}/resources.qrc.in ${CMAKE_BINARY_DIR}/resources.qrc) +set(Hyperion_RES ${CMAKE_BINARY_DIR}/resources.qrc) add_library(resources ${Hyperion_RES} diff --git a/resources/icons/hyperion-128px.png b/resources/icons/hyperion-128px.png new file mode 100644 index 0000000000000000000000000000000000000000..b208c95eaaec03ad60536d23c3312cb5164c9cc4 GIT binary patch literal 10834 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 literal 0 HcmV?d00001 diff --git a/resources/icons/hyperion-192px.png b/resources/icons/hyperion-192px.png new file mode 100644 index 0000000000000000000000000000000000000000..1aae38ac19e8f27f5d87005bcc16a9dc40f55d50 GIT binary patch literal 21900 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 literal 0 HcmV?d00001 diff --git a/resources/icons/hyperion-24px.png b/resources/icons/hyperion-24px.png new file mode 100644 index 0000000000000000000000000000000000000000..be90363c8f939945ff297ef74c29ffe4bc4288cf GIT binary patch literal 1894 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBDAAG{;hE;^%b*2hb1<+n z3NbJPS&Tr)z$nE4G7ZRL@M4sPvx68lplX;H7}_%#SfFa6fHVkr05M1pgl1mAh%j*h z6I`{#0%imoq)m`tVjYm;EbxddW?0+>{umUo3Q%e#RDspr3imfVamB1>jfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuG zfu4bq9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)Btk zRH0j3nOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXKF@SFQHXy^S0x~O7b0S?bb5rw*fljkCG%zx; z0o#Kig>Wg7)(G5Mk)+VII_Kx)7X=q2Ca2mNLTy75LQ)NKxs`uWW-2gHK|u#njv)qB zZ=(+iSEO);L?T!e7>jmXHu~@sVaMgl@HiEiKX^P{978;KuZDQ%%eV^s+f`jvR$g}V z?v2;iXIU_@vmCN%Vd+~WbBIwu$m_(xK$RUzg*^KYeCv%?u_!zx=orBFupy=Jfj~=3 z&yk~ICye+$W+xwiUt5uuzU*g}S@wEWg>3@tUawZJd$sDqh@RxvgvV$HIvIY0lbSO51(^8P7`)^b&M@^vsuasmQka@D2_BcOG9nI@OsQ z|1ERS^LdkPvhI*!^@X&|tEP1#$9p#jc-sq~%rg9C`8m_V*jV`Pt}YLQ9ZPEU+uF`O z`uBI?jdzVq;pg`7^RGXA^z#LYHlb9G`>i*+60R}LIAwR}Dch{>-5ZRJLzD%;t*hVY+ zKuV*UcYb&VPm^26(z!=>zTW(NnN4I**gUz2tqejFwI=s>T)cSQARz96?lK8w-`F#? zPE&YU4>n0oxO`e5Y=_O$NmooN?oY4Y^Xccosoqod|5#s6K6t#3Rba)+yiax+v-S1` zI>o5l8%ic0dr?wn{(S0#-Y*A!tYS^5nxCNm$w^Igb0|Y^aLUBHQ9lirv^?^6Y5n^d1#kIHYk%qZZOHxj;+)vm{fwhwtF5N0gBy>8 zMco^P{5b-#Y0}G^q=L>WwKrawcQS0RMcr|Q?AuSOL>8>?yumN?QbOHf|H@N2x!kM1 z@v!%cT@%ppP55cJ@~-ynjIt%W`6R<^SvGENn`d#Qs+zaxu(_bogu7SSukOpq-^Tsn z=9ZA-qD+r&pM36Z;ncWJDPsAnEurbIiPA>5UL3D`vL^YiMe9Q^)*1J2<*Z4s`eMqq z|JtYRldC)uUldgdX5W?Ceppz1rBv5RxJ)>H-=cY1A`fbRmj0Rj`k+Vu0++Q+qQ9aB$zDo+yj+2_XQ^u9CU^r^{WSNqD?JFd!4 zIQziC;^DLPHUG^ddMCRV`|wDsztcHu`1jx5oAW=~cD;|_7uE*V9-gj#F6*2UngHUE B*Vh05 literal 0 HcmV?d00001 diff --git a/resources/icons/hyperion-256px.png b/resources/icons/hyperion-256px.png new file mode 100644 index 0000000000000000000000000000000000000000..43121db3f7c3ab2ad87d9405fbfbfd5a9a6a5a76 GIT binary patch literal 31694 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 N002ovPDHLkV1hAknq>e0 literal 0 HcmV?d00001 diff --git a/resources/icons/hyperion-36px.png b/resources/icons/hyperion-36px.png new file mode 100644 index 0000000000000000000000000000000000000000..6d50eff434c11a8d9d8c6e6f9e74a207c44a9df5 GIT binary patch literal 2462 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 F`~~nl0*wFw literal 0 HcmV?d00001 diff --git a/resources/icons/hyperion-48px.png b/resources/icons/hyperion-48px.png new file mode 100644 index 0000000000000000000000000000000000000000..851676be2552a87c88b68f0f0a679ed92723aa4a GIT binary patch literal 2556 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 Hu0mjf1MF1f literal 0 HcmV?d00001 diff --git a/resources/icons/hyperion-72px.png b/resources/icons/hyperion-72px.png new file mode 100644 index 0000000000000000000000000000000000000000..5cb47103e986d49a71c78a1f65ee14635e108df7 GIT binary patch literal 4451 zcmV-p5uEOcP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91NT34%1ONa40RR91NB{r;0FW_8?*IT0%}GQ-RCodHn#-@=$63dpnfcw` z%ena2$JnmpCaDpQ9VrSal|YDO0a67iQWi!6FDmsf=mIuue7t1QRVCOV7Lhd5SRk-KV1d8_fdv8!1QrM^5Lh7aStDSc;vamP-+GGElJK*9?6E~{h}f&M zuNisc4I_iW?EQ3VQ8|k;Rx{hC;=e(IcJ5D=UHImRiMqRU} zoiaA|jgRfef40Q!E_7?(D=$g7PrH%r_`asS^LkV6%8J-B6*=Kw>Jpy1jO_$_Cm!$|AT?s0P-_9l13(K{f#<2$jnQmp`$)~v zrjKO?jETTF-fH(20E4%|dU{(MJY}2s5L%uj!8d@@w&L5=wOR%MVFry4zO5^1np&DV zgqnj;wZYU?-BfMqsWSWRCA551f?d_txadESf4Q8`32WUelySZtZm+oaj z?vr~gcbP?LUH84q%)1gSv_YR%a&IhPTAe=oL~qgM=~5%``aP&moDYm(X&|D{tgu1_ zBj!lR0q_8gFY!i)uu4h>v>t%?UYd;99pr1(uarLrNn| zv?a%f=MDXvI+nJsy|3!llvU$PrZ?%VbV^Cg2twR$y=>K1wvDlALr0lpbvc^WmoG~7 zE#?pbx=KZ=3V^!oVFN%O_Sqa~>rSwwb>?~O&d?X5t{l6tnjZ^N6;jwtIwmDM7MO%S zWyf0hiVIz~yU&7^7xjz@%dDmch3K=MHKEHU1CXBs1fLMw2~r#SDleuDZ;(bo;AqwC zQ02j#`naa7ZOCDbWn)6dG0Q3^Q-Dcp2Om;^tOqEL7d*yO)~2azTZhzDRT(DVfA7|2 z=d2m@Pj^?Er0uoACO`vmIq){)&ZL-ZuHRD@Farpr%;owR0Btd~&||t#5G+>OQ|$GL z+ilzX{KX3|%8l>6!kA7RsiXI$k9)T7^-ok)^8Jj`_M3lv{-3t=v5U4$&`s-56w}lf~!yy<=-@!%BS8$;Ys^<<_`PX!ATYOUd-u zB>=jy3lOx>1E7f>F>a=WW7fhYY(FS9RYtG={b@qu!~GAQ;kr_SQoXL>JmBb5^_gkX zv}=;2?~|+#zw5#Bx(!mEse7(`b@>25p~r(E0O_6L0mws1hFlih;yVDrueqT_{uO@u zCH*60{o~JaQbd#$T!7Wwq|`ZUvP~x`Y+iIin)MJ7I>?y?(nxCiT_@i|trOwl6O86Q$EMJGQ$Tjd-e)wyzV z59(04RYB6DX7X0*j5~OTR~=exvZW{QNSHF3K#}=0%RQ19+F*{Poepj8nq)iDL_Cmj z)m^>4GXU!Ir7qf10YKdZ;U*QI@AW&Z5Y0}^{@KHWilj<9X`Ob`3ZUw=OFc^_wY8^{ zS0g}sl;Kwphz&|rq##rW)Q6}>)bvrxI^=1Gi|$)SW08TDhXD zsA&s*_!J;u=fR#ZqZ~MSK>wA>a*H8c9r9E?IsevljJF-AD09dGWaH2UZRI+s4uXko z_lW7>4m7CJw8}XOl9HsM#9A0-B<*vwQj@Upt_$bM3;-FZ2THQG%5rlXc~Mt8r=Uw( z0cq%h(Nixh2PFc_UUENKd0JXciq7Or|If-@!@ zr5v?y0TLv{{pj`G(Mzo<;IwE|E{hr(1{f~yfH|vY&SEg%WXbygQe#z*^93vbso=y@ z20$SKck}=X9_ul>xwVBkoEuq2z5?0(MzMZ z9!@2|nqq?=q}=vLFd2^*777X$WqtJ+las^iq%rVL1(cFKch>VKr5iEQD0a8KzQflg|;*1 z2$cYXjf{+O1Nw**q+tM@8u_`sBlaF|&PfLGVqB@dE%|HrH zs^Fl`;-hpPjpG0X>Dr+%m~w(I+Yv0G4o#`>k=KA$_eII@$R+g_DmW}R1sqEz*rRO$ zspoGWyu}7wZA18uGfCBi05}F$xTzWj6Dpwy?@ua8>haX_;Mv~a?^&p;^yjlo&B>1^ z;w=Ctfg5KP{D%^dSS#Tt5CjK_6nW4<24P8-FtR31=?M|B1kVr?0K+B*z?43s{Mg6& zFihhdbg!O8eh4-vYul@6}djv>pjdTgnNZ>to${jxnJ{@CI z93e^PP0$a;q!gp+)Rik&zPh%yX4o0xb)H=ol_Z35j7Gk9^5mJ>)OUX&pp2N4gpfHm zf}JQx#Av~CrP$s4U4ZVwgC<0gpv4p6V23FF<6zvN#&DmSzPh0`DYSK!GTs9%>ZCY~@ckqhucBpIK^+2K4lX?0$scPTr*z1Wtd&74=^W)1||6Trw zG2Y~fbDW>(@MAE7aPq(==|cn`Knjn54r!-(5}+Hh8Gc<}#_REr5WvZ(Hp!5qc8`zd zOK#m$F2$<44OzuHdixW>{y#5TJP6{Xx>2BoC_eDlTS-~XjQ`P;94{dZqJ z|CN=Kb%|-4^G8UOhSy=#H+XC*Ap>G3Y!t!yJZ)@r@f|eFOa601qS&G7XPNkN5q|;jTq<`Z8qvsPzEuJiq!YCF)Q;N$5o8j zi|+{dqd!D~eO{v_5nryA;lI9f{rU&?^ z-2=&&o#afW1Fi)eq~J!-Iyxqji9YwzN7Bc7iaD#rYBS3{M3XM2EDb&Fpd{pmFjPC0 zK78lI@rnGxr4nz(BSvilpn{{f+(U8+D39|X9%@ixTgs$r+;prlyi>yZ_|qIUPl%ek zmzd?^T=7Z=IkRZ2%UNJfI4)9_MQAVx6AFw;Fqk;*O9jXQh618(z*2c3VU|Q#(k>wt z1*jtMZ4|EicfVs_WE#7X)4z8`?!Niu)7R7W3kBv-0Dyw}P!M6HWFJdy_o(+{dj?P+ zeX5H_)n$Ks(rbLvD84b@NYLa!%L4r|BiS_GofHq_!T$QW- zALXU$?&t^joSUlYCF9EbZNhIe2sT6P%!sp1%aEkga*~)N2NaJ-GILGikpql^l2mi5 zA$}83lzJR4Z6wZE5{4S72ZfLDRW1VDbOaRos(JKKUSD33)zL_r9`Uu`eLwtn>-{@? zV5!asTGhaBWL-Tj^J7KHIyBURiOinklp&IUIz+1d)PlpGBuE&Mr2(EA8U(ngQt&k> z3!xZErsUf-fT}oCYk=mRYuBjG1x3nmO|JRZ^MR8Xya-b+)-#|4qJ-Z2)-j%U7Vr&sr}`fOUx7bbl17zSHN+27dM(0PFGJV8aQ zL-UNw(>Ab%m~P0$$oaLgDeEQ@=ImAcU7u{)Z*BjMVj$1U8%4sQ*S+9IWZd(ykN(`+ zt?jRj=<{Lo(MR8&o<5xu^-Xj$;=_M@@XyTomH*kkb@>6Y0W(&m!=B$i_G9WVEd}|T zZ_WhUjlX2g>$;{{dVOAQZpOvO*3FxHbwIhrLzMU3XH4#APWp}TTW@{&@?ZVMUw?g4 zj@bmvEPA!+i~q|~vhVqLJd`tM&Ob+YSTOcF_)}F)wzjr@bU)7l`(WBz^6pa9RYhby z3?L1oM}rSepL^jC8Fqslr*ZHTpH(|5fN%ZwTfYGy&szEj5Z31a{nUi;gogVGk=613 z{p;`l0Xol1)Rm9u4Y0#h)QdH?_(1OWVeRKfQF0Cx@m;LrpB5KIFANSw1; z)Px@mL6$o5R!T|$=0_O=fC?l4AU{gL#|{9J0seSi{7$a%njd(1=n7uqBb`QP#%njR+CW6l6fXE|Lr005it&j124vYr6| zNIbT0bYMD4ih|}&j%=nDPG**Do{r9cSO8&9!AH^25@t%}>F5A;6Z8}T|Ai2Il>eyN z!Bl@iVD=(l9VJyNNheoJDqc1YHV&{T78Mnhu&afYpt_XIKlH~l5wHym<}Aq0?&0CV z=E2S8yV^R#Y@MJ~fBc%7 zIl03`z~DcD{?-1@)6&!S-;tnh|19fqf$V=Q?3`>I?EgO+%+~6^q5ZM^P5WzIe}@zP z<4jQ1*3;5KSIXAW66*GtnkXj+7mx5?asG?x-VV^RNEYAv;V2bmu%XAOV9PB^!uLlRYkun8^gg%y)cmRP5n zr?fDN(d5+`$cT^{Y)YMkPD^`3ID+kjD-jL~H$z{p2AdCb$@k|}TOaJMdpB%f{+<(P zbh@b{T;Y#N7nn0_OwrK%IDT_ewo&_Fj74 zFvbgAUR@GRh-}{9vDJ-SqOWu^$)2a(N$wdnZQlpp5OheiXjk9pWAS;cY&veSAwD*V z@qb$q#+@j1-J-#3IlI6fKiz(=XWzkH7N)s> z**k=*|GkIY0t4wst;w_jC z%?4xcfN>TW#BfKf4LpehJ|DN7Z6mXlpEOZzJBU;)^xyR3MSQ^h8iT`!kO1x=_L-Dxjmx%4WzpV#lS|q z`p&!B>8_lbu+iiV7P0%#2q{BPvK@ni8me)21(MAVRB@QC0axzMN)R7;0q!PIp7si| z1x5LR8{V08Y*0b^Tm{ohV+MKk#74|yCm*h|%xS*FAmL#2?83LUdaXyg-$h~d>tD7q zh8E{y{nlP(X37Me5FTSo!DBFx@6WmWK4w#bWWV2y+?7(8B*_S~EYAGU?XI0gcnTqU z^^i0_Kbl8MQOqOK z-l^PF#RtSuVxUe3x8R<&uAfNs`0I9CC*fLrKvO)N=*`v8B%V)(K zQXtNc&Wm_Ka|*;pP_#qg!qQ=KaTxO^n>zuH7~nTD8T&0#h;mFerK& z%|+MM`nr@CIFhM%@5#l3lDOP-D^mifu$YqjukZ15pW%DPelQVMR04NO$OPp*7pEt@ zR=mQN-zYq*o=kD3Em#hFUM5iY9hHYkxg>#{`~ZF8s4N`afnAMIT+~(wH`p-ra1fJSug9#nV+p9g~w1S?(#Ht@vSR_{{-rp=qT9T3{WlF(SYt z#nSgWS-1AG^P^3YS~5AX-WyXZ(I?%}uWbp+W`1lNagVMt&S-wlU8gX@Hrj7>25A$CoAS&jzD59CO|@%_da=bLef70%>2es2SRoFMh<=?c&t5GJag3aW z{U9v=zS;<3R5s>L;zB00lX$zN-0YL2jSZ(&&s2|%0JxAlfh){oI|S1wQPVbo*z`^6 z&H@z6nmdwJTxzomD>tkKhQmrVnfqz>qi#_HIh(@#K5lu-B_30EI;jb#$dIev=Tqx*N=j1~p zFM$GdQL!m3??AbY)475=Ie|Y&6sV%CPZ*r?^DP9$?w0q?4TY=snyO;&t^-$7XRI1- z`wzphpgVZeWM^Kry(-p%?3$X*DV4sjmxOm)Gx&Z~o(NxW8;d>MEOfZ;YF`+Sc^tj4 zJ~L1+AeAnRqnAD#YGO(d@!~viu*rVnJpcxJ-pC&ZsSMfzufnpmU+VOTKs-g7olWN) zonpIl5kmrv@9r8`o#mFpG_`l6wVw@9xSJnCd`J+sQ0PDCaAjG;jh9~OMSoQE9u zH5=*eP^rGY@mudi%|WrZDn5uEqfKp1oYNO^Gde3eauj$Abrc9Ixi(7L4dvzF8%mB# ziLYv>@lvt@vKsnnhp^AcDl<=NU4AqhBYI*HOO5Sq{>G0F3-XaTShNewYg78sDxq=< zkiOcQ^&3RZ`IH&^J_O4T2PujQ4ug~6=RcdCw6lG;^n0_9?qTcK!s;iDL;UpD*XXwj zMEYxV&`}ul$!`h24T7~8T0sJzTCd1+MWojp%u}Dv(~~zSTjmV?6Ghjbo&7F9Um{p5 z;nZ^mC9q6bNw)~{f9FegTcVwiz>X8t?~7nW>X_ySAjy$x05YVv;&5a!HVfQ|&gf&D zPw4}_oHTRV$xIUNQ)P-2;-1K5ZqhsNHp$2yl$Vvkz-hVL;>)pwf6RZb>A8R(MOrLp)?_Vav~5caH4jjif)# zF=yRR#guOy1F~EdxV9^YiQqBxtIC5Z`V3W4;C+=c;i{GDFJb$Ts}crsH#8#~V7Glb z`l0NbUhXC?BovG(@vx{WnSp~mLQY2}N??Ob!PLg5q}HzE`BpMVGGJ$qUdJ;6Qn4%O z;NT+lWul8*Hvmcz(#26JnfyvKLrZsyk6kt?3g9gEjxIo}N@H6_2gv?%vM7mLua5)I zFRs8y{}?ZCyhd>cl}sVd&qVHda@6(-sK5!0q5pRMlm>HNZ;N|CV(pNN@@x2nTjxal zZ)BO}QKDOlROJJz&prw5%G}QI6J7|hY>33T*+(5F(*qzmHU)_KE67B|dN}l8#2g1% ziW`-9)?%B|)`SBe@{O1C<&bJ|0gL1kR~Z53CQ$60RFVz`NF-G3=h4`rP*Q*^NjwBe zcjU`%dWHe|3_kBQfd}DDnw{1FAH^F2%1PqoW{WR@(bwL`zXStp()Nda3Y!NgsN<); zkETUD+mv{6rbx)4m@r33mIEbMW(5l7NdXQ3(IL`+W-x#i6*WL&FTEer1HGS;d7veO zOMohXE*^tf{p_6S(kMq7@br1wb-vJRYz&ZOBy!a7TfCq!VwWXH^-d<$G0aE|G@j0p zWKnSzRA5Jt5WHK=9PaKAeOfMeEez$WgFD2%Vd5n(e$}ApYPEGy9>PJ~9nKWq`?<8> zSu{U|Z`IH#U)7;)#_f9$(o&sJXPIVI)3%PiGfhjLFE*fL1*eK=h*4gxE`vG`)#fQb zGOWs+OC6ba86{gmy+2rkrV}C$Gu_KmxO&Dp$npiS%R@c7dIJ3WIth#+jv~S@lwOXyM$XmT%?c#Js8TLOGdp~j0VCW*+9*Ni#N@6V(kXWWMpm)^ z{IU`VPypcVXGkxQ$uU>=wBWgQ_fFAeRM^N_D%@W}8S$;FxXVilfNV;A7}O^Nm88~@ z1$Zl&JNSTfFlDttaXpqyoB9lVLJtomZWT>i>k70pF5(OHu#X2Yb2F%GWo^_W;pdGV zt7C<dfza1eF7`XC4y1;@3M+2AzB z9j-$?pcn7A_L%A@ritZhky!Ut3BG}(ao3G|1{pOVyU*sch5gQZ`KK5XPM1@%L-$u^ z*ps-7ukdgP>LM6X^C<|Vz0&WmBqZ4b0D+`(rtVQgG7h&UX0pG8tM-As(xWrgy~`X* zc`o8vEIKy8*M;t$Dc@8Z3O9{KUc4KN4SZwnHK?_nRKpjF4|;=1F1zW{8{4ejmcY+q z5&;rblbtCZHk+qGc~%4Ix-}fp&5IdrgvsEAQ<)vm+18JQ5nwk9`O^`{kab=Q=vyxe z7gAE(+w&B?FKa-NFQ28;vD0IPCKwPwKJFuP>J+b)EZO{07FwnWEupU_x-m?55d3-4 z%Cc(awqJH|giNf!fJ8Z)FP#%+_zE0X9fj`Wzghc{0MZ%TJ^vVM2bCL?bB#w0gLp2XbFn~Oo*xoolmH%m<1Q0@xD z`E!SKDHASbC+|CVX>T_QB$p4WD4^Kam^*gb<(P!cED5%0MVEr#Bfez;n4Ke%33wR} z9a(u2-uGY;E$SUyuD3BEQ39hzNI8%*J#|^ZF+*^bh@Yr|F3RvSspzFP)O9d99+2V- zw&omEXSUIB+riD$u5}K`{9yqtGU@T26HS8@uSKOjs({s!40@m4!dvE~yB$9u090;v z$Niv0>*txp?f7|Z*@I0VPE1 zo3-}q(n~5Au9lIN(hG$xO*5E7tqJ?o6J}d~bKoZTDwq5bmqpM_aU3|@u)HE_{X1xq z0>#jC0u6(RZPbpO=9!))>lwj!vyEK=Y;qu5>xg3f(>P}2Tl&#O5#VCht@-sdT~m%g zA&P%-bD??o#@rUvMb|AikpE_XryhNQdxPreD=u6$MWi+C$0FhOri=YksYwBz{(5n2 zj&n)*w-KSz7TIUzWXc$>J}KSqhb4RiG;my-XT8f^%t|cYEa*-AtDz%R5snc0fPOQx zKgG{&%5?_#1-roKX^_#U2!1qXj&{!-Lez<~^mj%{-z3)vIH-S@jwp7+V`!R@$Y7Xs z#bkW(ljnyps9^3X%ck3tblPm}q+!oI>1aUeYSEd}&6xAZ_gbqsEp92iPy7L&+^Q&~ z%J-Vd`)t8^p>V=Q8! z=Ld6(@lTR16>Gcl=z2xvfyZSo1ba{byi4qcTl|mOxyw5E1UWYv?U3 z6u8lsrC2*q5js(qJ6G>B1h$DAv%%c<>WO-rFQ6>{HXI!G@wx)}l^_Yk*gV)Tn;SKm z-5w?$-oI(!+M<$cSNgQmDnbOGN}=Ai(Gh4TzYLWPcSe}M%MmMny%xU7?w8tLxohB3 zo88-oisThWz2zLT@H7!~KMVtY>f%1H?*&0w%xuhfht03xm0iuI+5mG0iBWXz%|9JK zN)#mJY7`Ntp!-l&&{0jwIS-8v6YsLxi(l&znk0sA-wWKV@MXAO{W8xzIq~Wkk$J8> z&KG8Z4^X4E93^!+*-Vf8AOL*3Cd(gxu2=F~kY>Xd$}88rMyHtfjkTYIW<1#v0XeN~ zSDhDfA`;pmM8?heq`#~MZa~S-ZHWd%s78~#uo3+f**M%mB4&shqxH?PjXY`|9u-(Dw6@>D`;#+Tu zee+Nu?!2KWoCpKa_!GCnZ}c}i29@h)LSr07RagCdh##^WpCXFQ$esHuu=BKiXHn|$ zV^K$GQudxQG}bn31vifPPOzA$1>d%hKkynR9JvL1-&+~H7n8R3b6*zj2{>atdAg0u z>0c(#RpCB7Cjd#cS_Dezq!ePDOw$oc&Le&Go)!`R=>JjobxJdF&{4>}No=HZ|2|U8 zn%g%y*(VK0c6T4TF1U@CeLs~O-R`M~xtzq$Y4qS6&__%z`QqSd^10WQr%Y99nU*yoO8tTGgB!FqZpyf4H0yT9&V zJ2y5^kJR(&`&^ZwfJyLu*t;@bIZ-lu#~k^xmgwWs~Y& z=h6CY|2oqlzFKBjYm|EOIhmEXz$@8zcEo32M(!wD@axO0cX~+cn)IPs)7W>z_1}@E zdGA8V+0?>q8P<5@Pt-KKl__}zez zanlXz1Ho%IMQC4iH4PV2Og3FsTm@@6oe^*Ad7Bkk$M!vzc4D5Qtj{o@IfvUlh2GnN zj9o0J^}_pIKKaGlsp&=&1(ydlsiyb8(6mh5X9|?zo%2XnIGNB;1?rG3l$&7UAy`U~ zTK<@{(pb@p46@Q%%*@Ci2gqw_j-ltv!49_OX|eR$8IY3ZDjAa4xhx2pGj9kdd9t%w zqV+0#1mQW(ZyA+*&=bKJ&UHqIiEiT_*QIIRYeT@-&PMKpj&gFP*dZ8T(D*>_4UUtbAM$$oHO|M?5z{mXl=z#!H@CPw!?aH4%&fXM>~3XYV|SQ<_wb-77WyI&z3hMZ8}t z3zXN_Ej7TE~V7FhSDKKZcMmX>m*H+Bx{?^{nUa1$lH6QK@5J6V#ElE#y)J zHavaYQr1j>d(3d_^R7&S4`HZns(QB?PAg3D8M_AmCd9$m4ga83U?LX%9}}gZ9rO)Xbkw zU2}2TU%uL8SF$$sf3Xe`E-Gya85Y>7j?&eRF-I%R0qi|f9#*7elbSSifm`a64-H={ zvJU1!BqvXfN_w1?{Chdj$93J1!A!&}*^U+CDI9PL)}ypHi@JIjALa9PiCq$htv2_j zcP$?zbNsC>vntLDhviw1^?Ia8Usb;Q@?#ytL`~}uUVB1Io72!cOKLpKU0QO{YMj!u zg{eMssbR0mZ`mTSA3|NhDj(MrmuP*4qZOp8<5bJWh$ulOLPHwv9@U3~RhmdLk4}IW z6*IXlOHFH(1eLC-&(WjSP|`oEOp7RSA5#rIs|`;)2*>(5tAX>HxM`8vOv?9pMz??< z7DKcJZ)X*ERtfuD5IGCa+F>e-;utkNO)Ax~E59Ay!taNJ5b##sd)gfx9eri0Y4qiz zbZvE4RK7yvM2-uC*eyi6{HTRVQ+wvzhsk=BT+Hf&itO0+ZbV@0eH{NCK17vL3Afzl zZXu)1&oJ(_#V6L}S(p?uN%?ZoC(TNr)P7hJQxvi7+|#cS#QBT(41??*Ssn-E2P2ATDYja=QS_cfbYDP8LE}Bkk9H6Ap1+0gtdi1+!u}NxZvRy+w1voQ_ z0f7sK?T3j>%zNg!6Wmahvxkd0H6|P>91ufH+q!YrgqT)E-F8kl%8n(={!1i+uTwrh z7jPQv8C0-rxcjAVM@4)E@9+_)IilZB?xrFuRH-uVtX)AEuiWvfnYAi)X-?z5Z(ro1 zJv*;Hmfw5WPa1&&b{4EE^eicTKW@D|c$YRk&&zZW$M}ZUIZ1X{NVRn_Uo?pPh+aEy z;H*1iSYqtzV4g*ajucg|<6>y_l*eU{Uz=$LQc#=B#oof|4^(ULbs2GTG<>2Wa&7SA zc87$P&~kH~y)#CoKh99#*Qfaht*q0~Xg6b|_ZvA+LN&gY;zm&L_PT8xdS0CMY_uG% zR-Ui3hh<=gL|#-}DCZy?E-ilVrfB3YuRj-VcW-a;T)$(zHuif#)!5M1>RFBa&Dmw| z$$pOuR=t_^c~?9Zo9NGuJ5s`Zw0JKMj~kX$PXq9MW`xG41jOxFxRXFT*{EHjOyE&k z3Vmr1u^Z2rr#8cmwD)>$kEVrQpbNe8#-P5RY8Km_9VJIu;+Ko zHL6`Pc68HV67Cq}SB|!hi!h-_x{yD7Z*2SN48?U>xrbfMr`)Rr%=R5ZW{nMLGK!hx z)2mmZ7hjH0@?7Wp)TNF6?sg}@iOr^E(um2jjRzo1J8jI$j4|45tSxiX!}7-oJLtW~ zy3vM2k?B2+WKm<>fp12k{nF{o;q}}h{{H{iZ*?*5*$u+z)fl5J3;+Dcm6ukQs*o@V F_&-$)U=9EP literal 0 HcmV?d00001 diff --git a/resources/icons/hyperion-icon-32px.png b/resources/icons/hyperion-icon-32px.png deleted file mode 100644 index ee60b83b46e4ddeec406fbbd4fc126b753455062..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1454 zcmV;f1yTBmP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$^7)eAyR9Fe+R!fUqMHK$3>fY|# z@9CN8jARCh0gZ?zzAzvpLD5CB$im0Qm5_xCLH~h^AOs0w+$e(JUUcC~a3dlKStYZI z5GBD8LS8f7b8mOwXI1&t&2*R;GlopYEOMH!t8Ue)I;YO73V7H_Y+!(m<~CTi;gsEQ z_CZVvs-!4bVh7t|L7SxXJ-mjQBsoxIEoe z_H)E$71HV`vMW~D>qNLL%}TD!V0c|;5zOcs3|X~@PN&Vz6o%+R4W`hKo6x$69_ldt z03NESz$d%UR*_F3pu&PbKya7RRo1{pM%px!x=(~Dihio{FhLe&A{nJ(Jj_ibg6T-t zbzeO$uB)b8@vyo&qk&zS#1j{R`TCTcPb;b;A~bA8I0Y({;1{`2h@tEN(p=&3T0l{u z@W3FUKCzKl&6Hgo`zB13i4s+WV_C$x$cBkc#+e)!(ylH?{lD5zHkJ|(=W^hj2h?Vj z#I8#we&(rfzTSs-kL=*doLjbn^ml)r_~Y0QADx`u(@W{SON@cup&a?^XOTYtk{!QH zsZ@$Csa|9gCu;5l>Hq2qE5Q|5u_q z8Do2|B_G}8#qyjTd#%%`(T};b0DHqr}+)r9^n<=5xNVS@% z&$M$X>P`XXl%{F?fb$cRRoHjRxNny(n<2N4RZ2M*I1))^s%mSUhLNgD1!gJCR*jJC z-&DZ)%n2?Rv)v4pdugr97-F^}4m!SVY^!7tK84N^#r`Zd6o|m%eFMNo=0Uo z{>?}M%ag{pFmBaHK?~?$TBNEgw6wy@SOi&!5Mg<%WZu57?9jhmMx=r_LPxV|>OQ>E zN4Im|-;o;?yiMy*GsdLxh6sv8*nGuGXNdoUENqLiIvGund!cNkBbz1HP_Q(Y)y}1! zi~b2VtCZV^)>e}grvi-(ww*wW%R#t5QRpl@w6;;eIn(|8|IBko5jcKf*l?IkP)XviRhXbfnVF)iQ^GGQ-k z-MCDKrwPHDeT?mvkSn+FYL|?vU!id#bx^(6$-PP~EHcRDuD|n8dx|X163%qaCF{Y@ z8{b;PEG`;-l`oq|<|gjF$vau=TDO;ji?QU**q;4O`e#+Q`@4mIL3~+Toae2ykZHYe zF*`rg$F;dJM5jQl;qiTvqE#Ki_s8)3A$&!bsR3_mQHiHdpLub2pUyjQ=+vUFPlf35 zTA48#J!aG_uk)VG(bXBel@V&i2-|`IcFteHj_p^`R7<=Cm&DJoLD5Au%26F|LolEe ztx#DB;r%rFN?*qLDivhG3RH}$$RQOU3QVC-_FEesccal8NMScXj6fD!2+u+ zYI%X@_NC9hwO^Sz6tay}w2)N-B-|t6qbl`Y;pK#Wd0&I?z4g?3?`97sGh)Rzb+icF@+wYH`{^hd+UvSvH3GRx@ zybYScs|?1NC&DjeP1c2!`ofyza_{w8+>D;}gKK?Sh;L)&%D>D_ zh9+|^$nHdLjuBrz^X|m+;KnzyfYa)~=I>VXNrk;z{)dR~Z-TE>*Wdfr-2eap07*qo IM6N<$f}eP^umAu6 diff --git a/resources/icons/hyperion-icon-512px.png b/resources/icons/hyperion-icon-512px.png deleted file mode 100644 index 53ea8fe8feb1093db2049c3f8a8fe5acb24e27e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69597 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 -#include +#include #include class AmlogicWrapper : public QObject diff --git a/src/hyperion-aml/CMakeLists.txt b/src/hyperion-aml/CMakeLists.txt index cdea66fa..40b04df3 100644 --- a/src/hyperion-aml/CMakeLists.txt +++ b/src/hyperion-aml/CMakeLists.txt @@ -1,25 +1,10 @@ cmake_minimum_required(VERSION 3.5.0) project(hyperion-aml) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Widgets REQUIRED) - -include_directories( - ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/flatbufserver - ${FLATBUFFERS_INCLUDE_DIRS} -) - -set(Hyperion_AML_HEADERS - AmlogicWrapper.h -) - -set(Hyperion_AML_SOURCES - hyperion-aml.cpp - AmlogicWrapper.cpp -) - add_executable(${PROJECT_NAME} - ${Hyperion_AML_HEADERS} - ${Hyperion_AML_SOURCES} + AmlogicWrapper.h + AmlogicWrapper.cpp + hyperion-aml.cpp ) target_link_libraries(${PROJECT_NAME} @@ -29,27 +14,23 @@ target_link_libraries(${PROJECT_NAME} flatbuffers amlogic-grabber framebuffer-grabber - Qt${QT_VERSION_MAJOR}::Core - Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Widgets ) if(ENABLE_MDNS) - target_link_libraries(${PROJECT_NAME} mdns) + target_link_libraries(${PROJECT_NAME} mdns) else() - target_link_libraries(${PROJECT_NAME} ssdp) + target_link_libraries(${PROJECT_NAME} ssdp) endif() -if (ENABLE_AMLOGIC) - target_link_libraries(${PROJECT_NAME} - pcre16 dl z - ) +if(ENABLE_AMLOGIC) + target_link_libraries(${PROJECT_NAME} pcre16 dl z) endif() -install ( TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_aml" ) +install (TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_aml") if(CMAKE_HOST_UNIX) - install(CODE "EXECUTE_PROCESS(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\" )" COMPONENT "hyperion_aml" ) - install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_aml" ) - install(CODE "FILE (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME} )" COMPONENT "hyperion_aml" ) + install(CODE "execute_process(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\")" COMPONENT "hyperion_aml") + install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_aml") + install(CODE "file (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME})" COMPONENT "hyperion_aml") endif(CMAKE_HOST_UNIX) diff --git a/src/hyperion-dispmanx/CMakeLists.txt b/src/hyperion-dispmanx/CMakeLists.txt index 07862bbd..c81d0fdb 100644 --- a/src/hyperion-dispmanx/CMakeLists.txt +++ b/src/hyperion-dispmanx/CMakeLists.txt @@ -1,48 +1,31 @@ cmake_minimum_required(VERSION 3.5.0) project(hyperion-dispmanx) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Widgets REQUIRED) - -include_directories( - ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/flatbufserver - ${FLATBUFFERS_INCLUDE_DIRS} -) - -set(Hyperion_Dispmanx_HEADERS +add_executable(${PROJECT_NAME} DispmanxWrapper.h -) - -set(Hyperion_Dispmanx_SOURCES - hyperion-dispmanx.cpp DispmanxWrapper.cpp + hyperion-dispmanx.cpp ) -add_executable( ${PROJECT_NAME} - ${Hyperion_Dispmanx_HEADERS} - ${Hyperion_Dispmanx_SOURCES} -) - -target_link_libraries( ${PROJECT_NAME} +target_link_libraries(${PROJECT_NAME} commandline hyperion-utils flatbufconnect flatbuffers dispmanx-grabber - Qt${QT_VERSION_MAJOR}::Core - Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Widgets ) if(ENABLE_MDNS) - target_link_libraries(${PROJECT_NAME} mdns) + target_link_libraries(${PROJECT_NAME} mdns) else() - target_link_libraries(${PROJECT_NAME} ssdp) + target_link_libraries(${PROJECT_NAME} ssdp) endif() -install ( TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_dispmanx" ) +install (TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_dispmanx") if(CMAKE_HOST_UNIX) - install(CODE "EXECUTE_PROCESS(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\" )" COMPONENT "hyperion_dispmanx" ) - install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_dispmanx" ) - install(CODE "FILE (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME} )" COMPONENT "hyperion_dispmanx" ) + install(CODE "execute_process(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\")" COMPONENT "hyperion_dispmanx") + install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_dispmanx") + install(CODE "file (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME})" COMPONENT "hyperion_dispmanx") endif(CMAKE_HOST_UNIX) diff --git a/src/hyperion-dispmanx/DispmanxWrapper.h b/src/hyperion-dispmanx/DispmanxWrapper.h index e02e2c5f..ddbb8a77 100644 --- a/src/hyperion-dispmanx/DispmanxWrapper.h +++ b/src/hyperion-dispmanx/DispmanxWrapper.h @@ -2,7 +2,7 @@ // QT includes #include -#include +#include #include class DispmanxWrapper : public QObject @@ -16,7 +16,7 @@ public: ); const Image & getScreenshot(); - + bool open(); /// diff --git a/src/hyperion-framebuffer/CMakeLists.txt b/src/hyperion-framebuffer/CMakeLists.txt index 14ea2ce9..a9470f5c 100644 --- a/src/hyperion-framebuffer/CMakeLists.txt +++ b/src/hyperion-framebuffer/CMakeLists.txt @@ -1,55 +1,35 @@ cmake_minimum_required(VERSION 3.5.0) project(hyperion-framebuffer) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Widgets REQUIRED) - -include_directories( - ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/flatbufserver - ${FLATBUFFERS_INCLUDE_DIRS} -) - -set(Hyperion_FB_HEADERS +add_executable(${PROJECT_NAME} FramebufferWrapper.h -) - -set(Hyperion_FB_SOURCES - hyperion-framebuffer.cpp FramebufferWrapper.cpp + hyperion-framebuffer.cpp ) -add_executable( ${PROJECT_NAME} - ${Hyperion_FB_HEADERS} - ${Hyperion_FB_SOURCES} -) - -target_link_libraries( ${PROJECT_NAME} +target_link_libraries(${PROJECT_NAME} commandline hyperion-utils flatbufconnect flatbuffers framebuffer-grabber - Qt${QT_VERSION_MAJOR}::Core - Qt${QT_VERSION_MAJOR}::Gui - Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Widgets ) if(ENABLE_MDNS) - target_link_libraries(${PROJECT_NAME} mdns) + target_link_libraries(${PROJECT_NAME} mdns) else() - target_link_libraries(${PROJECT_NAME} ssdp) + target_link_libraries(${PROJECT_NAME} ssdp) endif() -if (ENABLE_AMLOGIC) - target_link_libraries( ${PROJECT_NAME} - pcre16 dl z - ) +if(ENABLE_AMLOGIC) + target_link_libraries(${PROJECT_NAME} pcre16 dl z) endif() -install ( TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_framebuffer" ) +install (TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_framebuffer") if(CMAKE_HOST_UNIX) - install(CODE "EXECUTE_PROCESS(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\" )" COMPONENT "hyperion_framebuffer" ) - install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_framebuffer" ) - install(CODE "FILE (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME} )" COMPONENT "hyperion_framebuffer" ) + install(CODE "execute_process(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\")" COMPONENT "hyperion_framebuffer") + install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_framebuffer") + install(CODE "file (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME})" COMPONENT "hyperion_framebuffer") endif(CMAKE_HOST_UNIX) diff --git a/src/hyperion-framebuffer/FramebufferWrapper.h b/src/hyperion-framebuffer/FramebufferWrapper.h index d9bbdfb1..9f1b74f2 100644 --- a/src/hyperion-framebuffer/FramebufferWrapper.h +++ b/src/hyperion-framebuffer/FramebufferWrapper.h @@ -3,7 +3,7 @@ // QT includes #include -#include +#include #include class FramebufferWrapper : public QObject diff --git a/src/hyperion-osx/CMakeLists.txt b/src/hyperion-osx/CMakeLists.txt index efb15cd1..48a127b7 100644 --- a/src/hyperion-osx/CMakeLists.txt +++ b/src/hyperion-osx/CMakeLists.txt @@ -1,43 +1,25 @@ cmake_minimum_required(VERSION 3.5.0) project(hyperion-osx) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui Network Widgets REQUIRED) - -include_directories( - ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/flatbufserver - ${FLATBUFFERS_INCLUDE_DIRS} -) - -set(Hyperion_OSX_HEADERS +add_executable(${PROJECT_NAME} OsxWrapper.h -) - -set(Hyperion_OSX_SOURCES - hyperion-osx.cpp OsxWrapper.cpp + hyperion-osx.cpp ) -add_executable( ${PROJECT_NAME} - ${Hyperion_OSX_HEADERS} - ${Hyperion_OSX_SOURCES} -) - -target_link_libraries( ${PROJECT_NAME} +target_link_libraries(${PROJECT_NAME} commandline hyperion-utils flatbufconnect flatbuffers osx-grabber - Qt${QT_VERSION_MAJOR}::Core - Qt${QT_VERSION_MAJOR}::Gui - Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Widgets ) if(ENABLE_MDNS) - target_link_libraries(${PROJECT_NAME} mdns) + target_link_libraries(${PROJECT_NAME} mdns) else() - target_link_libraries(${PROJECT_NAME} ssdp) + target_link_libraries(${PROJECT_NAME} ssdp) endif() -install ( TARGETS ${PROJECT_NAME} DESTINATION "." COMPONENT "hyperion_osx" ) +install (TARGETS ${PROJECT_NAME} DESTINATION "." COMPONENT "hyperion_osx") diff --git a/src/hyperion-osx/OsxWrapper.h b/src/hyperion-osx/OsxWrapper.h index ef352bd0..335e6519 100644 --- a/src/hyperion-osx/OsxWrapper.h +++ b/src/hyperion-osx/OsxWrapper.h @@ -2,7 +2,7 @@ // QT includes #include -#include +#include #include class OsxWrapper : public QObject diff --git a/src/hyperion-qt/CMakeLists.txt b/src/hyperion-qt/CMakeLists.txt index f35b8534..fe87ef8b 100644 --- a/src/hyperion-qt/CMakeLists.txt +++ b/src/hyperion-qt/CMakeLists.txt @@ -1,32 +1,18 @@ cmake_minimum_required(VERSION 3.5.0) project(hyperion-qt) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui Network Widgets REQUIRED) - -include_directories( - ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/flatbufserver - ${FLATBUFFERS_INCLUDE_DIRS} -) - -set(Hyperion_QT_HEADERS - QtWrapper.h -) - -set(Hyperion_QT_SOURCES - QtWrapper.cpp - hyperion-qt.cpp -) - -# generate windows .rc file for this binary -if (WIN32) - include(${CMAKE_SOURCE_DIR}/cmake/win/win_rc.cmake) - generate_win_rc_file(${PROJECT_NAME}) +if(WIN32) + # generate windows .rc file for this binary + string(REPLACE "/" "\\\\" WIN_RC_ICON_PATH ${CMAKE_SOURCE_DIR}/cmake/nsis/installer.ico) + configure_file(${CMAKE_SOURCE_DIR}/cmake/win/win.rc.in ${CMAKE_BINARY_DIR}/win.rc) + set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/win.rc) endif() add_executable(${PROJECT_NAME} - ${Hyperion_QT_HEADERS} - ${Hyperion_QT_SOURCES} - ${${PROJECT_NAME}_WIN_RC_PATH} + QtWrapper.h + QtWrapper.cpp + hyperion-qt.cpp + ${WIN_RC_FILE} ) target_link_libraries(${PROJECT_NAME} @@ -34,28 +20,25 @@ target_link_libraries(${PROJECT_NAME} qt-grabber flatbufconnect flatbuffers - Qt${QT_VERSION_MAJOR}::Core - Qt${QT_VERSION_MAJOR}::Gui - Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Widgets ) if(ENABLE_MDNS) - target_link_libraries(${PROJECT_NAME} mdns) + target_link_libraries(${PROJECT_NAME} mdns) else() - target_link_libraries(${PROJECT_NAME} ssdp) + target_link_libraries(${PROJECT_NAME} ssdp) endif() if(APPLE) - install ( TARGETS ${PROJECT_NAME} DESTINATION "." COMPONENT "hyperion_qt" ) + install (TARGETS ${PROJECT_NAME} DESTINATION "." COMPONENT "hyperion_qt") elseif(NOT WIN32) - install ( TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_qt" ) + install (TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_qt") else() - install ( TARGETS ${PROJECT_NAME} DESTINATION "bin" COMPONENT "hyperion_qt" ) + install (TARGETS ${PROJECT_NAME} DESTINATION "bin" COMPONENT "hyperion_qt") endif() if(CMAKE_HOST_UNIX AND NOT APPLE) - install(CODE "EXECUTE_PROCESS(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\" )" COMPONENT "hyperion_qt" ) - install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_qt" ) - install(CODE "FILE (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME} )" COMPONENT "hyperion_qt" ) + install(CODE "execute_process(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\")" COMPONENT "hyperion_qt") + install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_qt") + install(CODE "file (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME})" COMPONENT "hyperion_qt") endif() diff --git a/src/hyperion-qt/QtWrapper.h b/src/hyperion-qt/QtWrapper.h index e4e689a3..979b27aa 100644 --- a/src/hyperion-qt/QtWrapper.h +++ b/src/hyperion-qt/QtWrapper.h @@ -4,7 +4,7 @@ #include // Hyperion-Qt includes -#include +#include #include class QtWrapper : public QObject diff --git a/src/hyperion-remote/CMakeLists.txt b/src/hyperion-remote/CMakeLists.txt index a2805cad..ef60a7dc 100644 --- a/src/hyperion-remote/CMakeLists.txt +++ b/src/hyperion-remote/CMakeLists.txt @@ -1,68 +1,50 @@ cmake_minimum_required(VERSION 3.5.0) project(hyperion-remote) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Widgets REQUIRED) - -# The following I do not understand completely... -# libQtCore.so uses some hardcoded library path inside which are incorrect after copying the file RPi file system -# Therefore, an extra path is needed on which to find the required libraries -IF ( EXISTS ${CMAKE_FIND_ROOT_PATH}/lib/arm-linux-gnueabihf ) - LINK_DIRECTORIES(${LINK_DIRECTORIES} ${CMAKE_FIND_ROOT_PATH}/lib/arm-linux-gnueabihf) -ENDIF() - -set(hyperion-remote_HEADERS - JsonConnection.h) - -set(hyperion-remote_SOURCES - hyperion-remote.cpp - JsonConnection.cpp) - -# generate windows .rc file for this binary -if (WIN32) - include(${CMAKE_SOURCE_DIR}/cmake/win/win_rc.cmake) - generate_win_rc_file(${PROJECT_NAME}) +if(WIN32) + # generate windows .rc file for this binary + string(REPLACE "/" "\\\\" WIN_RC_ICON_PATH ${CMAKE_SOURCE_DIR}/cmake/nsis/installer.ico) + configure_file(${CMAKE_SOURCE_DIR}/cmake/win/win.rc.in ${CMAKE_BINARY_DIR}/win.rc) + set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/win.rc) endif() add_executable(${PROJECT_NAME} - ${hyperion-remote_HEADERS} - ${hyperion-remote_SOURCES} - ${${PROJECT_NAME}_WIN_RC_PATH} + JsonConnection.h + JsonConnection.cpp + hyperion-remote.cpp + ${WIN_RC_FILE} ) target_link_libraries(${PROJECT_NAME} commandline hyperion-utils - Qt${QT_VERSION_MAJOR}::Core - Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Widgets ) -if (ENABLE_AMLOGIC) - target_link_libraries(${PROJECT_NAME} - pcre16 dl z - ) +if(ENABLE_AMLOGIC) + target_link_libraries(${PROJECT_NAME} pcre16 dl z) endif() if(ENABLE_MDNS) - target_link_libraries(${PROJECT_NAME} mdns) + target_link_libraries(${PROJECT_NAME} mdns) else() - target_link_libraries(${PROJECT_NAME} ssdp) + target_link_libraries(${PROJECT_NAME} ssdp) endif() if(ENABLE_EFFECTENGINE) - target_link_libraries(${PROJECT_NAME} effectengine) + target_link_libraries(${PROJECT_NAME} effectengine) endif() if(APPLE) - install ( TARGETS ${PROJECT_NAME} DESTINATION "." COMPONENT "hyperion_remote" ) + install (TARGETS ${PROJECT_NAME} DESTINATION "." COMPONENT "hyperion_remote") elseif(NOT WIN32) - install ( TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_remote" ) + install (TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_remote") else() - install ( TARGETS ${PROJECT_NAME} DESTINATION "bin" COMPONENT "hyperion_remote" ) + install (TARGETS ${PROJECT_NAME} DESTINATION "bin" COMPONENT "hyperion_remote") endif() if(CMAKE_HOST_UNIX AND NOT APPLE) - install(CODE "EXECUTE_PROCESS(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\" )" COMPONENT "hyperion_remote" ) - install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_remote" ) - install(CODE "FILE (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME} )" COMPONENT "hyperion_remote" ) + install(CODE "execute_process(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\")" COMPONENT "hyperion_remote") + install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_remote") + install(CODE "file (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME})" COMPONENT "hyperion_remote") endif() diff --git a/src/hyperion-v4l2/CMakeLists.txt b/src/hyperion-v4l2/CMakeLists.txt index 1404d2ad..37189cdc 100644 --- a/src/hyperion-v4l2/CMakeLists.txt +++ b/src/hyperion-v4l2/CMakeLists.txt @@ -1,25 +1,10 @@ cmake_minimum_required(VERSION 3.5.0) project(hyperion-v4l2) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Widgets REQUIRED) - -include_directories( - ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/flatbufserver - ${FLATBUFFERS_INCLUDE_DIRS} -) - -set(Hyperion_V4L2_HEADERS - ScreenshotHandler.h -) - -set(Hyperion_V4L2_SOURCES - hyperion-v4l2.cpp - ScreenshotHandler.cpp -) - add_executable(${PROJECT_NAME} - ${Hyperion_V4L2_HEADERS} - ${Hyperion_V4L2_SOURCES} + ScreenshotHandler.h + ScreenshotHandler.cpp + hyperion-v4l2.cpp ) target_link_libraries(${PROJECT_NAME} @@ -28,27 +13,23 @@ target_link_libraries(${PROJECT_NAME} hyperion-utils flatbufconnect flatbuffers - Qt${QT_VERSION_MAJOR}::Core - Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Widgets ) if(ENABLE_MDNS) - target_link_libraries(${PROJECT_NAME} mdns) + target_link_libraries(${PROJECT_NAME} mdns) else() - target_link_libraries(${PROJECT_NAME} ssdp) + target_link_libraries(${PROJECT_NAME} ssdp) endif() -if (ENABLE_AMLOGIC) - target_link_libraries(${PROJECT_NAME} - pcre16 dl z - ) +if(ENABLE_AMLOGIC) + target_link_libraries(${PROJECT_NAME} pcre16 dl z) endif() -install ( TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_v4l2" ) +install (TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_v4l2") if(CMAKE_HOST_UNIX) - install(CODE "EXECUTE_PROCESS(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\" )" COMPONENT "hyperion_v4l2" ) - install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_v4l2" ) - install(CODE "FILE (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME} )" COMPONENT "hyperion_v4l2" ) + install(CODE "execute_process(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\")" COMPONENT "hyperion_v4l2") + install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_v4l2") + install(CODE "file (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME})" COMPONENT "hyperion_v4l2") endif(CMAKE_HOST_UNIX) diff --git a/src/hyperion-v4l2/hyperion-v4l2.cpp b/src/hyperion-v4l2/hyperion-v4l2.cpp index 73195177..daef9567 100644 --- a/src/hyperion-v4l2/hyperion-v4l2.cpp +++ b/src/hyperion-v4l2/hyperion-v4l2.cpp @@ -10,7 +10,7 @@ #include // grabber includes -#include "grabber/V4L2Grabber.h" +#include "grabber/video/v4l2/V4L2Grabber.h" // flatbuf includes #include @@ -255,7 +255,7 @@ int main(int argc, char** argv) SSDPDiscover discover; host = discover.getFirstService(searchType::STY_FLATBUFSERVER); #endif - + QHostAddress address; if (!NetUtils::resolveHostToAddress(log, host, address, port)) { diff --git a/src/hyperion-x11/CMakeLists.txt b/src/hyperion-x11/CMakeLists.txt index 88095ae3..20ca0f7a 100644 --- a/src/hyperion-x11/CMakeLists.txt +++ b/src/hyperion-x11/CMakeLists.txt @@ -1,32 +1,14 @@ cmake_minimum_required(VERSION 3.5.0) project(hyperion-x11) -find_package(X11 REQUIRED) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui Network Widgets REQUIRED) - - -include_directories( - ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/flatbufserver - ${X11_INCLUDES} - ${FLATBUFFERS_INCLUDE_DIRS} -) - if(APPLE) include_directories("/opt/X11/include") endif(APPLE) -set(Hyperion_X11_HEADERS - X11Wrapper.h -) - -set(Hyperion_X11_SOURCES - hyperion-x11.cpp - X11Wrapper.cpp -) - add_executable(${PROJECT_NAME} - ${Hyperion_X11_HEADERS} - ${Hyperion_X11_SOURCES} + X11Wrapper.h + X11Wrapper.cpp + hyperion-x11.cpp ) target_link_libraries(${PROJECT_NAME} @@ -35,25 +17,19 @@ target_link_libraries(${PROJECT_NAME} flatbufconnect flatbuffers x11-grabber - ${X11_LIBRARIES} - ${X11_Xrandr_LIB} - ${X11_Xrender_LIB} - Qt${QT_VERSION_MAJOR}::Core - Qt${QT_VERSION_MAJOR}::Gui - Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Widgets ) if(ENABLE_MDNS) - target_link_libraries(${PROJECT_NAME} mdns) + target_link_libraries(${PROJECT_NAME} mdns) else() - target_link_libraries(${PROJECT_NAME} ssdp) + target_link_libraries(${PROJECT_NAME} ssdp) endif() -install ( TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_x11" ) +install (TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_x11") if(CMAKE_HOST_UNIX) - install(CODE "EXECUTE_PROCESS(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\" )" COMPONENT "hyperion_x11" ) - install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_x11" ) - install(CODE "FILE (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME} )" COMPONENT "hyperion_x11" ) + install(CODE "execute_process(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\")" COMPONENT "hyperion_x11") + install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_x11") + install(CODE "file (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME})" COMPONENT "hyperion_x11") endif(CMAKE_HOST_UNIX) diff --git a/src/hyperion-x11/X11Wrapper.h b/src/hyperion-x11/X11Wrapper.h index d0f9f1f2..13dbfdff 100644 --- a/src/hyperion-x11/X11Wrapper.h +++ b/src/hyperion-x11/X11Wrapper.h @@ -4,7 +4,7 @@ #include // Hyperion-X11 includes -#include +#include #include class X11Wrapper : public QObject diff --git a/src/hyperion-xcb/CMakeLists.txt b/src/hyperion-xcb/CMakeLists.txt index f378aef2..98b2ba32 100644 --- a/src/hyperion-xcb/CMakeLists.txt +++ b/src/hyperion-xcb/CMakeLists.txt @@ -1,48 +1,31 @@ cmake_minimum_required(VERSION 3.5.0) project(hyperion-xcb) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Widgets REQUIRED) - -include_directories( - ${CMAKE_CURRENT_BINARY_DIR}/../../libsrc/flatbufserver - ${FLATBUFFERS_INCLUDE_DIRS} -) - -set(Hyperion_XCB_HEADERS +add_executable(${PROJECT_NAME} XcbWrapper.h -) - -set(Hyperion_XCB_SOURCES hyperion-xcb.cpp XcbWrapper.cpp ) -add_executable(${PROJECT_NAME} - ${Hyperion_XCB_HEADERS} - ${Hyperion_XCB_SOURCES} -) - target_link_libraries(${PROJECT_NAME} commandline hyperion-utils flatbufconnect flatbuffers xcb-grabber - Qt${QT_VERSION_MAJOR}::Core - Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Widgets ) if(ENABLE_MDNS) - target_link_libraries(${PROJECT_NAME} mdns) + target_link_libraries(${PROJECT_NAME} mdns) else() - target_link_libraries(${PROJECT_NAME} ssdp) + target_link_libraries(${PROJECT_NAME} ssdp) endif() install (TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_xcb") if(CMAKE_HOST_UNIX) - install(CODE "EXECUTE_PROCESS(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\" )" COMPONENT "hyperion_xcb" ) - install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_xcb" ) - install(CODE "FILE (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME} )" COMPONENT "hyperion_xcb" ) + install(CODE "execute_process(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\")" COMPONENT "hyperion_xcb") + install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME "${PROJECT_NAME}" COMPONENT "hyperion_xcb") + install(CODE "file (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME})" COMPONENT "hyperion_xcb") endif(CMAKE_HOST_UNIX) diff --git a/src/hyperion-xcb/XcbWrapper.h b/src/hyperion-xcb/XcbWrapper.h index d75aa63f..e3806ade 100644 --- a/src/hyperion-xcb/XcbWrapper.h +++ b/src/hyperion-xcb/XcbWrapper.h @@ -4,7 +4,7 @@ #include // Hyperion-Xcb includes -#include +#include #include //Utils includes diff --git a/src/hyperiond/CMakeLists.txt b/src/hyperiond/CMakeLists.txt index 35326c22..4d227d81 100644 --- a/src/hyperiond/CMakeLists.txt +++ b/src/hyperiond/CMakeLists.txt @@ -1,41 +1,32 @@ -if (APPLE) +if(APPLE) project(Hyperion) else() project(hyperiond) endif() -if(ENABLE_EFFECTENGINE) - if (NOT CMAKE_VERSION VERSION_LESS "3.12") - find_package(Python3 COMPONENTS Interpreter Development REQUIRED) - include_directories(${Python3_INCLUDE_DIRS} ${Python3_INCLUDE_DIRS}/..) - else() - find_package (PythonLibs ${PYTHON_VERSION_STRING} EXACT) # Maps PythonLibs to the PythonInterp version of the main cmake - include_directories(${PYTHON_INCLUDE_DIRS} ${PYTHON_INCLUDE_DIRS}/..) - endif() -endif () +##################################### +############ Preparation ############ +##################################### -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Gui Widgets REQUIRED) +if(WIN32) + # generate windows .rc file for this binary + string(REPLACE "/" "\\\\" WIN_RC_ICON_PATH ${CMAKE_SOURCE_DIR}/cmake/nsis/installer.ico) + configure_file(${CMAKE_SOURCE_DIR}/cmake/win/win.rc.in ${CMAKE_BINARY_DIR}/win.rc) + set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/win.rc) -# generate windows .rc file for this binary -if (WIN32) - include(${CMAKE_SOURCE_DIR}/cmake/win/win_rc.cmake) - generate_win_rc_file(${PROJECT_NAME}) -endif(WIN32) - -# include resource files for macos bundle (copy LICENSE file and correct line breaks) -if (APPLE) + # promote hyperiond as GUI app + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup") +elseif(APPLE) + # include resource files for macos bundle (copy LICENSE file and correct line breaks) configure_file(${CMAKE_SOURCE_DIR}/LICENSE ${CMAKE_BINARY_DIR}/LICENSE COPYONLY) execute_process(COMMAND bash -c "perl -pi -e 's/\n/\r/g' ${CMAKE_BINARY_DIR}/LICENSE") - set(BUNDLE_RESOURCE_FILES ${CMAKE_SOURCE_DIR}/cmake/osxbundle/Hyperion.icns ${CMAKE_BINARY_DIR}/LICENSE) - set_source_files_properties(${BUNDLE_RESOURCE_FILES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) -endif(APPLE) + set(MACOS_BUNDLE_RESOURCE_FILES ${CMAKE_SOURCE_DIR}/cmake/osxbundle/Hyperion.icns ${CMAKE_BINARY_DIR}/LICENSE) + set_source_files_properties(${MACOS_BUNDLE_RESOURCE_FILES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) +endif() -if (UNIX) - find_package(Qt${QT_VERSION_MAJOR} COMPONENTS DBus QUIET ) - if (Qt${QT_VERSION_MAJOR}DBus_FOUND) - set(hyperiond_POWER_MNG_DBUS "Qt${QT_VERSION_MAJOR}::DBus") - endif() -endif(UNIX) +##################################### +########### General steps ########### +##################################### add_executable(${PROJECT_NAME} console.h @@ -45,19 +36,10 @@ add_executable(${PROJECT_NAME} systray.cpp SuspendHandler.cpp main.cpp - ${hyperiond_WIN_RC_PATH} - ${BUNDLE_RESOURCE_FILES} + ${WIN_RC_FILE} + ${MACOS_BUNDLE_RESOURCE_FILES} ) -if (UNIX AND NOT APPLE AND Qt${QT_VERSION_MAJOR}DBus_FOUND) - target_compile_definitions(${PROJECT_NAME} PUBLIC HYPERION_HAS_DBUS) -endif() - -# promote hyperiond as GUI app -if (WIN32) - target_link_options(${PROJECT_NAME} PUBLIC /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup) -endif(WIN32) - target_link_libraries(${PROJECT_NAME} commandline hyperion @@ -70,18 +52,16 @@ target_link_libraries(${PROJECT_NAME} Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Widgets - ${hyperiond_POWER_MNG_DBUS} ) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS DBus QUIET) +if(Qt${QT_VERSION_MAJOR}DBus_FOUND) + target_link_libraries(${PROJECT_NAME} "Qt${QT_VERSION_MAJOR}::DBus") +endif() + if(ENABLE_EFFECTENGINE) target_link_libraries(${PROJECT_NAME} effectengine python) - - if (NOT CMAKE_VERSION VERSION_LESS "3.12") - target_link_libraries( ${PROJECT_NAME} ${Python3_LIBRARIES} ) - else() - target_link_libraries( ${PROJECT_NAME} ${PYTHON_LIBRARIES} ) - endif() -endif () +endif() if(ENABLE_FLATBUF_SERVER) target_link_libraries(${PROJECT_NAME} flatbufserver) @@ -91,71 +71,71 @@ if(ENABLE_PROTOBUF_SERVER) target_link_libraries(${PROJECT_NAME} protoserver) endif() -if (ENABLE_AMLOGIC) - target_link_libraries(${PROJECT_NAME} - #Qt${QT_VERSION_MAJOR}::Core - pcre16 dl z - ) +if(ENABLE_AMLOGIC) + target_link_libraries(${PROJECT_NAME} pcre16 dl z) endif(ENABLE_AMLOGIC) -if (ENABLE_DISPMANX) +if(ENABLE_DISPMANX) target_link_libraries(${PROJECT_NAME} dispmanx-grabber) endif (ENABLE_DISPMANX) -if (ENABLE_FB) +if(ENABLE_FB) target_link_libraries(${PROJECT_NAME} framebuffer-grabber) endif (ENABLE_FB) -if (ENABLE_OSX) +if(ENABLE_OSX) target_link_libraries(${PROJECT_NAME} osx-grabber) endif (ENABLE_OSX) -if (ENABLE_V4L2) +if(ENABLE_V4L2) target_link_libraries(${PROJECT_NAME} v4l2-grabber) -endif () +endif() -if (ENABLE_MF) +if(ENABLE_MF) target_link_libraries(${PROJECT_NAME} mf-grabber) endif (ENABLE_MF) -if (ENABLE_AUDIO) +if(ENABLE_AUDIO) target_link_libraries(hyperiond audio-grabber) endif() -if (ENABLE_AMLOGIC) +if(ENABLE_AMLOGIC) target_link_libraries(${PROJECT_NAME} amlogic-grabber) endif (ENABLE_AMLOGIC) -if (ENABLE_X11) +if(ENABLE_X11) if(APPLE) include_directories("/opt/X11/include") endif(APPLE) target_link_libraries(${PROJECT_NAME} x11-grabber) endif (ENABLE_X11) -if (ENABLE_XCB) +if(ENABLE_XCB) target_link_libraries(${PROJECT_NAME} xcb-grabber) endif (ENABLE_XCB) -if (ENABLE_QT) +if(ENABLE_QT) target_link_libraries(${PROJECT_NAME} qt-grabber) endif (ENABLE_QT) -if (ENABLE_DX) - include_directories(${DIRECTX9_INCLUDE_DIRS}) +if(ENABLE_DX) target_link_libraries(${PROJECT_NAME} directx-grabber) endif (ENABLE_DX) -if (ENABLE_CEC) - target_link_libraries(${PROJECT_NAME} cechandler) +if(ENABLE_CEC) + target_link_libraries(${PROJECT_NAME} cechandler) endif (ENABLE_CEC) -if (ENABLE_MDNS) +if(ENABLE_MDNS) target_link_libraries(${PROJECT_NAME} mdns) endif() -if (APPLE) - set_target_properties( ${PROJECT_NAME} PROPERTIES +##################################### +########### Install steps ########### +##################################### + +if(APPLE) + set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE TRUE MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/cmake/osxbundle/Info.plist.in MACOSX_BUNDLE_BUNDLE_NAME "Hyperion" @@ -168,69 +148,83 @@ if (APPLE) MACOSX_BUNDLE_LONG_VERSION_STRING ${HYPERION_VERSION} ) - install ( TARGETS ${PROJECT_NAME} DESTINATION . COMPONENT "Hyperion") + install (TARGETS ${PROJECT_NAME} DESTINATION . COMPONENT "Hyperion") elseif(NOT WIN32) - install ( TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "Hyperion" ) - install ( DIRECTORY ${CMAKE_SOURCE_DIR}/bin/service DESTINATION "share/hyperion/" COMPONENT "Hyperion" ) - install ( FILES ${CMAKE_SOURCE_DIR}/effects/readme.txt DESTINATION "share/hyperion/effects" COMPONENT "Hyperion" ) - install ( FILES ${CMAKE_SOURCE_DIR}/resources/icons/hyperion-icon-32px.png DESTINATION "share/hyperion/icons" COMPONENT "Hyperion" ) + # install Hyperion/service files/effect folder + install (TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "Hyperion") + install (DIRECTORY ${CMAKE_SOURCE_DIR}/bin/service DESTINATION "share/hyperion" COMPONENT "Hyperion") + install (FILES ${CMAKE_SOURCE_DIR}/effects/readme.txt DESTINATION "share/hyperion/effects" COMPONENT "Hyperion") - # Desktop file for Hyperion - install ( FILES ${CMAKE_SOURCE_DIR}/cmake/desktop/hyperiond_128.png DESTINATION "share/hyperion/desktop" COMPONENT "Hyperion" ) - install ( FILES ${CMAKE_SOURCE_DIR}/cmake/desktop/hyperiond.desktop DESTINATION "share/hyperion/desktop" COMPONENT "Hyperion" ) + # install Hyperion icons + set(ICON_SIZES 16 22 24 32 36 48 64 72 96 128 192 256 512) + foreach(size ${ICON_SIZES}) + set(ICONS_FROM "${CMAKE_SOURCE_DIR}/resources/icons/hyperion-${size}px.png") + set(ICONS_TO "share/hyperion/icons/${size}x${size}/apps/") + install(FILES ${ICONS_FROM} DESTINATION ${ICONS_TO} RENAME "hyperion.png" COMPONENT "Hyperion") + endforeach(size) + + # install desktop/appstream file + install (FILES ${CMAKE_SOURCE_DIR}/cmake/desktop/hyperion.metainfo.xml DESTINATION "share/hyperion/desktop" COMPONENT "Hyperion") + install (FILES ${CMAKE_SOURCE_DIR}/cmake/desktop/hyperion.desktop DESTINATION "share/hyperion/desktop" COMPONENT "Hyperion") else() - install ( TARGETS ${PROJECT_NAME} DESTINATION "bin" COMPONENT "Hyperion" ) - install ( FILES ${CMAKE_SOURCE_DIR}/effects/readme.txt DESTINATION "effects" COMPONENT "Hyperion" ) - - #set( CMAKE_INSTALL_UCRT_LIBRARIES TRUE ) - #set( CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE ) - #include( InstallRequiredSystemLibraries ) + install (TARGETS ${PROJECT_NAME} DESTINATION "bin" COMPONENT "Hyperion") + install (FILES ${CMAKE_SOURCE_DIR}/effects/readme.txt DESTINATION "effects" COMPONENT "Hyperion") endif() -if (CMAKE_HOST_UNIX AND NOT APPLE) - install( CODE "EXECUTE_PROCESS(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\" )" COMPONENT "Hyperion" ) - install( FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME ${PROJECT_NAME} COMPONENT "Hyperion" ) - install( CODE "FILE (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME})" COMPONENT "Hyperion" ) - install( FILES ${CMAKE_SOURCE_DIR}/bin/scripts/updateHyperionUser.sh DESTINATION "share/hyperion/scripts" COMPONENT "Hyperion" ) +if(CMAKE_HOST_UNIX AND NOT APPLE) + install(CODE "execute_process(COMMAND ln -sf \"../share/hyperion/bin/${PROJECT_NAME}\" \"${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}\")" COMPONENT "Hyperion") + install(FILES "${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME}" DESTINATION "bin" RENAME ${PROJECT_NAME} COMPONENT "Hyperion") + install(CODE "file (REMOVE ${CMAKE_BINARY_DIR}/symlink_${PROJECT_NAME})" COMPONENT "Hyperion") + install(FILES ${CMAKE_SOURCE_DIR}/bin/scripts/updateHyperionUser.sh DESTINATION "share/hyperion/scripts" COMPONENT "Hyperion") endif() +###################################### +########## Additional steps ########## +###################################### + # Deploy Qt DLLs into the binary folder. # This is necessary for starting the application from within the IDE -if (WIN32) +if(WIN32) get_target_property(QT_QMAKE_EXECUTABLE Qt${QT_VERSION_MAJOR}::qmake IMPORTED_LOCATION) get_filename_component(QT_BIN_DIR "${QT_QMAKE_EXECUTABLE}" DIRECTORY) find_program(WINDEPLOYQT_EXECUTABLE windeployqt HINTS "${QT_BIN_DIR}") - if (NOT WINDEPLOYQT_EXECUTABLE) + if(NOT WINDEPLOYQT_EXECUTABLE) find_program(WINDEPLOYQT_EXECUTABLE windeployqt) endif() - if (WINDEPLOYQT_EXECUTABLE AND NOT CMAKE_GITHUB_ACTION) + if(WINDEPLOYQT_EXECUTABLE AND NOT CMAKE_GITHUB_ACTION) set(WINDEPLOYQT_PARAMS_RUNTIME --verbose 0 --no-compiler-runtime --no-opengl-sw --no-system-d3d-compiler) message(STATUS "Found windeployqt: ${WINDEPLOYQT_EXECUTABLE} PATH_HINT:${QT_BIN_DIR}") add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${WINDEPLOYQT_EXECUTABLE} ${WINDEPLOYQT_PARAMS_RUNTIME} "$") endif() find_package(OpenSSL REQUIRED) - if (OPENSSL_FOUND) + if(OPENSSL_FOUND) string(REGEX MATCHALL "[0-9]+" openssl_versions "${OPENSSL_VERSION}") list(GET openssl_versions 0 openssl_version_major) list(GET openssl_versions 1 openssl_version_minor) - set(library_suffix "-${openssl_version_major}_${openssl_version_minor}") - if (CMAKE_SIZEOF_VOID_P EQUAL 8) - string(APPEND library_suffix "-x64") + set(open_ssl_version_suffix) + if(openssl_version_major VERSION_EQUAL 1 AND openssl_version_minor VERSION_EQUAL 1) + set(open_ssl_version_suffix "-1_1") + else() + set(open_ssl_version_suffix "-3") + endif() + + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + string(APPEND open_ssl_version_suffix "-x64") endif() find_file(OPENSSL_SSL - NAMES "libssl${library_suffix}.dll" + NAMES "libssl${open_ssl_version_suffix}.dll" PATHS ${OPENSSL_INCLUDE_DIR}/.. ${OPENSSL_INCLUDE_DIR}/../bin NO_DEFAULT_PATH ) find_file(OPENSSL_CRYPTO - NAMES "libcrypto${library_suffix}.dll" + NAMES "libcrypto${open_ssl_version_suffix}.dll" PATHS ${OPENSSL_INCLUDE_DIR}/.. ${OPENSSL_INCLUDE_DIR}/../bin NO_DEFAULT_PATH ) @@ -248,11 +242,11 @@ if(ENABLE_DEPLOY_DEPENDENCIES) # Deploy all dependencies for package creation include(${CMAKE_SOURCE_DIR}/cmake/Dependencies.cmake) - if (APPLE) #macOS + if(APPLE) #macOS DeployMacOS(${PROJECT_NAME}) elseif (NOT WIN32) # Linux DeployLinux(${PROJECT_NAME}) elseif(WIN32) # Windows DeployWindows(${PROJECT_NAME}) - endif () + endif() endif(ENABLE_DEPLOY_DEPENDENCIES) diff --git a/src/hyperiond/hyperiond.h b/src/hyperiond/hyperiond.h index 2a292746..c8b63113 100644 --- a/src/hyperiond/hyperiond.h +++ b/src/hyperiond/hyperiond.h @@ -7,62 +7,62 @@ #include #ifdef ENABLE_DISPMANX - #include + #include #else typedef QObject DispmanxWrapper; #endif #if defined(ENABLE_V4L2) || defined(ENABLE_MF) - #include + #include #else typedef QObject VideoWrapper; #endif #ifdef ENABLE_FB - #include + #include #else typedef QObject FramebufferWrapper; #endif #ifdef ENABLE_AMLOGIC - #include + #include #else typedef QObject AmlogicWrapper; #endif #ifdef ENABLE_OSX - #include + #include #else typedef QObject OsxWrapper; #endif #ifdef ENABLE_X11 - #include + #include #else typedef QObject X11Wrapper; #endif #ifdef ENABLE_XCB - #include + #include #else typedef QObject XcbWrapper; #endif #ifdef ENABLE_QT - #include + #include #else typedef QObject QtWrapper; #endif #ifdef ENABLE_DX - #include + #include #else typedef QObject DirectXWrapper; #endif #include #ifdef ENABLE_AUDIO - #include + #include #else typedef QObject AudioWrapper; #endif @@ -211,8 +211,8 @@ private: OsxWrapper* _osxGrabber; QtWrapper* _qtGrabber; DirectXWrapper* _dxGrabber; - AudioWrapper* _audioGrabber; SSDPHandler* _ssdp; + AudioWrapper* _audioGrabber; #ifdef ENABLE_CEC CECHandler* _cecHandler; #endif diff --git a/src/hyperiond/main.cpp b/src/hyperiond/main.cpp index fa9352b9..9bdda136 100644 --- a/src/hyperiond/main.cpp +++ b/src/hyperiond/main.cpp @@ -115,7 +115,7 @@ QCoreApplication* createApplication(int &argc, char *argv[]) app->addLibraryPath(QApplication::applicationDirPath() + "/../lib"); app->setApplicationDisplayName("Hyperion"); #ifndef __APPLE__ - app->setWindowIcon(QIcon(":/hyperion-icon-32px.png")); + app->setWindowIcon(QIcon(":/hyperion-32px.png")); #endif return app; } diff --git a/src/hyperiond/systray.cpp b/src/hyperiond/systray.cpp index ab7b6c8a..34f8de0a 100644 --- a/src/hyperiond/systray.cpp +++ b/src/hyperiond/systray.cpp @@ -33,8 +33,8 @@ SysTray::SysTray(HyperionDaemon *hyperiond) , _hyperiond(hyperiond) , _hyperion(nullptr) , _instanceManager(HyperionIManager::getInstance()) - , _suspendHandler (hyperiond->getSuspendHandlerInstance()) , _webPort(8090) + , _suspendHandler (hyperiond->getSuspendHandlerInstance()) { Q_INIT_RESOURCE(resources); @@ -281,7 +281,7 @@ void SysTray::handleInstanceStateChange(InstanceState state, quint8 instance, co connect(quitAction, &QAction::triggered, _trayIcon, &QSystemTrayIcon::hide, Qt::DirectConnection); connect(&_colorDlg, &QColorDialog::currentColorChanged, this, &SysTray::setColor); - QIcon icon(":/hyperion-icon-32px.png"); + QIcon icon(":/hyperion-32px.png"); _trayIcon->setIcon(icon); _trayIcon->show(); setWindowIcon(icon); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0e447f57..28378df6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,17 +3,17 @@ include_directories(../libsrc) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED) -MACRO (link_to_hyperion TARGET) - target_link_libraries( ${TARGET} blackborder leddevice jsonserver hyperion-utils hyperion ) +macro (link_to_hyperion TARGET) + target_link_libraries(${TARGET} blackborder leddevice jsonserver hyperion-utils hyperion) if(ENABLE_EFFECTENGINE) - target_link_libraries( ${TARGET} effectengine ) + target_link_libraries(${TARGET} effectengine) endif() -ENDMACRO() +endmacro() if(ENABLE_DEV_SPI) # Add the simple test executable 'TestSpi' add_executable(test_spi TestSpi.cpp) - target_link_libraries( test_spi leddevice hyperion-utils hyperion ) + target_link_libraries(test_spi leddevice hyperion-utils hyperion) add_executable(spidev_test spidev_test.c) add_executable(gpio2spi switchPinCtrl.c) endif(ENABLE_DEV_SPI) @@ -42,12 +42,12 @@ endif(ENABLE_X11) add_executable(test_versions TestVersions.cpp) target_link_libraries(test_versions Qt${QT_VERSION_MAJOR}::Core) -add_executable(test_image2ledsmap TestImage2LedsMap.cpp "${CMAKE_BINARY_DIR}/resources.qrc" ) +add_executable(test_image2ledsmap TestImage2LedsMap.cpp "${CMAKE_BINARY_DIR}/resources.qrc") link_to_hyperion(test_image2ledsmap) ######### These tests are broken. May they fix someone ########## -#if (ENABLE_DISPMANX) +#if(ENABLE_DISPMANX) # add_subdirectory(dispmanx2png) #endif (ENABLE_DISPMANX) diff --git a/test/dispmanx2png/CMakeLists.txt b/test/dispmanx2png/CMakeLists.txt index 7dfdd759..80037b9b 100644 --- a/test/dispmanx2png/CMakeLists.txt +++ b/test/dispmanx2png/CMakeLists.txt @@ -1,5 +1,5 @@ # this is only available on real pi -IF ( "${PLATFORM}" MATCHES rpi) +if("${PLATFORM}" MATCHES rpi) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Gui REQUIRED) @@ -16,4 +16,4 @@ IF ( "${PLATFORM}" MATCHES rpi) target_link_libraries(dispmanx2png dispmanx-grabber Qt${QT_VERSION_MAJOR::Gui) -ENDIF() +endif() diff --git a/test/dispmanx2png/dispmanx2png.cpp b/test/dispmanx2png/dispmanx2png.cpp index 2739f4fd..2c2a1748 100644 --- a/test/dispmanx2png/dispmanx2png.cpp +++ b/test/dispmanx2png/dispmanx2png.cpp @@ -7,7 +7,7 @@ #include // Dispmanx grabber includes -#include +#include static bool running = true; From cd1046ac1f8dc564343d14dcab29df16283dbae7 Mon Sep 17 00:00:00 2001 From: Paulchen-Panther <16664240+Paulchen-Panther@users.noreply.github.com> Date: Sat, 18 Nov 2023 06:36:04 +0000 Subject: [PATCH 059/117] Cleanup & README correction --- Installation.md | 53 ------------------------------------- README.md | 10 +++---- dependencies/CMakeLists.txt | 3 --- 3 files changed, 5 insertions(+), 61 deletions(-) delete mode 100644 Installation.md diff --git a/Installation.md b/Installation.md deleted file mode 100644 index f55c1cdf..00000000 --- a/Installation.md +++ /dev/null @@ -1,53 +0,0 @@ - -# Installation -This page contains general installation steps for Hyperion. - -## Windows & macOS -For Windows and macOS is an installation file available on our [Release page](https://github.com/hyperion-project/hyperion.ng/releases). - -## Linux: -On the following operating systems, Hyperion can currently be installed/updated using the method listed below: -- Raspbian Buster/Raspberry Pi OS and later (armhf/arm64) -- Debian Buster(10) and later (armhf/arm64/x86_64) -- Ubuntu 20.04 and later (armhf/arm64/x86_64) - -*** - -### Install Hyperion: -1. Add necessary packages for the installation: -```bash -sudo apt-get update && sudo apt-get install wget gpg apt-transport-https lsb-release -``` - -2. Add Hyperion’s official GPG key: -```bash -wget -qO- https://apt.hyperion-project.org/hyperion.pub.key | sudo gpg --dearmor -o /usr/share/keyrings/hyperion.pub.gpg -``` - -3. Add Hyperion-Project to your APT sources: -```bash -echo "deb [signed-by=/usr/share/keyrings/hyperion.pub.gpg] https://apt.hyperion-project.org/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hyperion.list -``` - -4. Update your local package index and install Hyperion: -```bash -sudo apt-get update && sudo apt-get install hyperion -``` -*** - -### Update Hyperion: -```bash -sudo apt-get install hyperion -``` -*** - -### If you want to uninstall Hyperion, use the following commands: -1. Remove Hyperion: -```bash -sudo apt-get --purge autoremove hyperion -``` - -2. Remove the Hyperion-Project APT source from your system: -```bash -sudo rm /usr/share/keyrings/hyperion.pub.gpg /etc/apt/sources.list.d/hyperion.list -``` diff --git a/README.md b/README.md index 028922b1..3259a81d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Latest-Release](https://img.shields.io/github/v/release/hyperion-project/hyperion.ng?include_prereleases&label=Latest%20Release&logo=github&logoColor=white&color=0f83e7)](https://github.com/hyperion-project/hyperion.ng/releases) -[![GitHub Actions](https://github.com/hyperion-project/hyperion.ng/workflows/Hyperion%20CI%20Build/badge.svg?branch=master)](https://github.com/hyperion-project/hyperion.ng/actions) +[![GitHub Actions](https://github.com/hyperion-project/hyperion.ng/actions/workflows/push_pull.yml/badge.svg)](https://github.com/hyperion-project/hyperion.ng/actions) [![CodeQL Analysis](https://github.com/hyperion-project/hyperion.ng/actions/workflows/codeql.yml/badge.svg)](https://github.com/hyperion-project/hyperion.ng/actions/workflows/codeql.yml) [![Forum](https://img.shields.io/website/https/hyperion-project.org.svg?label=Forum&down_color=red&down_message=offline&up_color=4bc51d&up_message=online&logo=homeadvisor&logoColor=white)](https://www.hyperion-project.org) [![Documentation](https://img.shields.io/website/https/docs.hyperion-project.org.svg?label=Documentation&down_color=red&down_message=offline&up_color=4bc51d&up_message=online&logo=read-the-docs)](https://docs.hyperion-project.org) @@ -45,7 +45,7 @@ For an example, you can participate in the translation.
## Supported Platforms -Find here more details on [supported platforms and configuration sets](doc/development/SupportedPlatforms.md) +Find here more details on [supported platforms and configuration sets](doc/development/SupportedPlatforms.md). ## Documentation Covers these topics: @@ -57,16 +57,16 @@ Covers these topics: [![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) ## Changelog -Released and unreleased changes at [CHANGELOG.md](CHANGELOG.md) +Released and unreleased changes at [CHANGELOG.md](CHANGELOG.md). ## Building See [CompileHowto.md](doc/development/CompileHowto.md). ## Installation -See [Documentation](https://docs.hyperion-project.org/en/user/Installation.html) or at [Installation.md](Installation.md). +See [Documentation](https://docs.hyperion-project.org/en/user/Installation.html) or on the [Release Repository](https://releases.hyperion-project.org). ## Download -Releases available from the [Hyperion release page](https://github.com/hyperion-project/hyperion.ng/releases) +Releases available from the [Hyperion release page](https://github.com/hyperion-project/hyperion.ng/releases). ## Privacy Policy See [PRIVACY.md](PRIVACY.md). diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index 28ba5d04..2669448d 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -48,10 +48,7 @@ if (ENABLE_MDNS) BUILD_ALWAYS OFF DOWNLOAD_COMMAND "" SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/qmdnsengine - BINARY_DIR ${CMAKE_BINARY_DIR}/dependencies/external/qmdnsengine/build CMAKE_ARGS ${QMDNS_CMAKE_ARGS} - CONFIGURE_COMMAND ${CMAKE_COMMAND} -S -B ${QMDNS_CMAKE_ARGS} -G ${CMAKE_GENERATOR} - BUILD_COMMAND ${CMAKE_COMMAND} --build --config $ INSTALL_DIR ${CMAKE_BINARY_DIR} BUILD_BYPRODUCTS ${QMDNS_LIBRARIES} ) From 2e0cc9cfa807c0080c849120f85df758329a4cd3 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Thu, 23 Nov 2023 20:51:49 +0100 Subject: [PATCH 060/117] Improvements (#1658) * Restart Servial Device, if write error occurred. * Fix typos * Fix FOUND_USER command * Use SUDO_USER in postinst * Align install_pr with new package names * Package named arm64 instead of aarch64 * Update Change log * Fix to have the architecture resolved * All tls plugin directory for Qt6 --------- Co-authored-by: Paulchen-Panther <16664240+Paulchen-Panther@users.noreply.github.com> --- .github/workflows/qt5_6.yml | 16 +-- CHANGELOG.md | 1 + assets/webconfig/content/update.html | 118 +++++++++--------- bin/scripts/docker-compile.sh | 4 +- bin/scripts/install_pr.sh | 60 ++++++--- cmake/Dependencies.cmake | 4 +- cmake/package-scripts/postinst | 2 +- libsrc/leddevice/LedDevice.cpp | 2 +- .../leddevice/dev_net/LedDevicePhilipsHue.cpp | 2 +- libsrc/leddevice/dev_serial/ProviderRs232.cpp | 14 ++- 10 files changed, 129 insertions(+), 94 deletions(-) diff --git a/.github/workflows/qt5_6.yml b/.github/workflows/qt5_6.yml index ac4622b3..5b08ef41 100644 --- a/.github/workflows/qt5_6.yml +++ b/.github/workflows/qt5_6.yml @@ -41,13 +41,13 @@ jobs: fail-fast: false matrix: os: [ - { distribution: debian, codename: buster, description: Debian Buster (x86_64), architecture: [ amd64, linux/amd64 ] }, - { distribution: debian, codename: bullseye, description: Debian Bullseye (x86_64), architecture: [ amd64, linux/amd64 ] }, - { distribution: debian, codename: buster, description: Debian Buster (Raspberry Pi 1/ZERO), architecture: [ armv6, linux/arm/v5 ] }, - { distribution: debian, codename: buster, description: Debian Buster (Raspberry Pi 2/3/4), architecture: [ armv7, linux/arm/v7 ] }, - { distribution: debian, codename: bullseye, description: Debian Bullseye (Raspberry Pi 2/3/4), architecture: [ armv7, linux/arm/v7 ] }, - { distribution: debian, codename: buster, description: Debian Buster (Generic AARCH64), architecture: [ aarch64, linux/arm64 ] }, - { distribution: debian, codename: bullseye, description: Debian Bullseye (Generic AARCH64), architecture: [ aarch64, linux/arm64 ] } + { distribution: debian, codename: buster, description: Debian Buster (x86_64), architecture: [ amd64, linux/amd64 ] }, + { distribution: debian, codename: bullseye, description: Debian Bullseye (x86_64), architecture: [ amd64, linux/amd64 ] }, + { distribution: debian, codename: buster, description: Debian Buster (Raspberry Pi 1/ZERO), architecture: [ armv6, linux/arm/v5 ] }, + { distribution: debian, codename: buster, description: Debian Buster (Raspberry Pi 2/3/4), architecture: [ armv7, linux/arm/v7 ] }, + { distribution: debian, codename: bullseye, description: Debian Bullseye (Raspberry Pi 2/3/4), architecture: [ armv7, linux/arm/v7 ] }, + { distribution: debian, codename: buster, description: Debian Buster (Generic AARCH64), architecture: [ arm64, linux/arm64 ] }, + { distribution: debian, codename: bullseye, description: Debian Bullseye (Generic AARCH64), architecture: [ arm64, linux/arm64 ] } ] isQt5: - ${{ inputs.qt_version == '5' }} @@ -58,7 +58,7 @@ jobs: platform: rpi - os.architecture[0]: armv7 platform: rpi - - os.architecture[0]: aarch64 + - os.architecture[0]: arm64 platform: amlogic exclude: - isQt5: true diff --git a/CHANGELOG.md b/CHANGELOG.md index e4024253..bb8bbeb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Note: The wizard will configure an APIv2 capable bridge always with Entertainmen ### Changed - Updated misleading error messages in case Hyperion is not able to support the suspend/lock feature (#1622) +- Restart Serial Device, if write error occurred - ws281x - Update logic to identify is user is admin and disable device configuration if not (#1621) - Hide Hyperion from the Dock on macOS, as all features can be accessed from the menubar - Thanks @Rastafabisch diff --git a/assets/webconfig/content/update.html b/assets/webconfig/content/update.html index f4e8aa28..3a638bd7 100644 --- a/assets/webconfig/content/update.html +++ b/assets/webconfig/content/update.html @@ -1,68 +1,70 @@
-
-
- -
-

Overview about all available Hyperion versions. On top you could update or downgrade your version of Hyperion whenever you want. Sorted from newest to oldest

-

At the moment the respective install button is disabled. Development is still ongoing here.

-
-
-

-
-
-
-
-
+
+
+ +
+

Overview about all available Hyperion versions. On top you could update or downgrade your version of Hyperion whenever you want. Sorted from newest to oldest

+

At the moment the respective install button is disabled. Development is still ongoing here.

+
+
+

+
+
+
+
+
diff --git a/bin/scripts/docker-compile.sh b/bin/scripts/docker-compile.sh index bafde87f..82a098b7 100755 --- a/bin/scripts/docker-compile.sh +++ b/bin/scripts/docker-compile.sh @@ -126,7 +126,7 @@ do shift BUILD_PLATFORM=$1 ;; - --qt5) + --qt5) BUILD_WITH_QT5=true ;; -l|--local) @@ -214,7 +214,7 @@ if [ $ARCHITECTURE != $CURRENT_ARCHITECTURE ]; then ENTRYPOINT_OPTION= if [ $CURRENT_ARCHITECTURE != "amd64" ]; then - echo "---> Emulation builds can only be executed on linux/amd64, linux/x86_64 platforms, current architecture is ${CURRENT_ARCHITECTURE}" + echo "---> Emulation builds can only be executed on linux/amd64, linux/x86_64 platforms, current architecture is ${CURRENT_ARCHITECTURE}" exit 1 fi else diff --git a/bin/scripts/install_pr.sh b/bin/scripts/install_pr.sh index cd023504..0080d6d8 100755 --- a/bin/scripts/install_pr.sh +++ b/bin/scripts/install_pr.sh @@ -12,6 +12,11 @@ hasPython3=$? type python > /dev/null 2> /dev/null hasPython2=$? +DISTRIBUTION="debian" +CODENAME="bullseye" +ARCHITECTURE="" +WITH_QT5=false + BASE_PATH='.'; if [[ "${hasWget}" -ne 0 ]] && [[ "${hasCurl}" -ne 0 ]]; then @@ -38,19 +43,20 @@ function request_call() { fi } -while getopts ":c:t:m:r:" opt; do +while getopts ":a:c:r:t:5" opt; do case "$opt" in - t) PR_TOKEN=$OPTARG ;; - r) run_id=$OPTARG ;; - m) ARCHITECTURE=$OPTARG ;; + a) ARCHITECTURE=$OPTARG ;; c) CONFIGDIR=$OPTARG ;; + r) run_id=$OPTARG ;; + t) PR_TOKEN=$OPTARG ;; + 5) WITH_QT5=true ;; esac done shift $(( OPTIND - 1 )) # Check for a command line argument (PR number) if [ "$1" == "" ] || [ $# -gt 1 ] || [ -z ${PR_TOKEN} ]; then - echo "Usage: $0 -t -m -r -c " >&2 + echo "Usage: $0 -t -a -r -c " >&2 exit 1 else pr_number="$1" @@ -68,30 +74,39 @@ if [[ -z ${ARCHITECTURE} ]]; then fi #Test if multiarchitecture setup, i.e. user-space is 32bit -if [ ${ARCHITECTURE} == "aarch64" ]; then +if [[ "${ARCHITECTURE}" == "aarch64" || "${ARCHITECTURE}" == "arm64" ]]; then + ARCHITECTURE="arm64" USER_ARCHITECTURE=$ARCHITECTURE IS_V7L=`cat /proc/$$/maps |grep -m1 -c v7l` if [ $IS_V7L -ne 0 ]; then - USER_ARCHITECTURE="armv7l" + USER_ARCHITECTURE="armv7" else IS_V6L=`cat /proc/$$/maps |grep -m1 -c v6l` if [ $IS_V6L -ne 0 ]; then - USER_ARCHITECTURE="armv6l" + USER_ARCHITECTURE="armv6" fi fi if [ $ARCHITECTURE != $USER_ARCHITECTURE ]; then - echo "---> Identified kernel target architecture: $ARCHITECTURE" echo "---> Identified user space target architecture: $USER_ARCHITECTURE" ARCHITECTURE=$USER_ARCHITECTURE fi +else + ARCHITECTURE=${ARCHITECTURE//x86_/amd} fi -echo 'armv6l armv7l aarch64 x86_64' | grep -qw ${ARCHITECTURE} +echo 'armv6l armv7l arm64 amd64' | grep -qw ${ARCHITECTURE} if [ $? -ne 0 ]; then echo "---> Critical Error: Target architecture $ARCHITECTURE is unknown -> abort" exit 1 else - echo "---> Download Pull Request for identified runtime architecture: $ARCHITECTURE" + PACKAGE="${ARCHITECTURE}" + QTVERSION="5" + if [ ${WITH_QT5} == false ]; then + QTVERSION="6" + PACKAGE="${PACKAGE}_qt6" + fi + + echo "---> Download package for identified runtime architecture: $ARCHITECTURE and Qt$QTVERSION" fi # Determine if PR number exists @@ -153,14 +168,25 @@ fi # Get archive_download_url from workflow artifacts=$(request_call "$api_url/actions/runs/$run_id/artifacts") -archive_download_url=$(echo "$artifacts" | tr '\r\n' ' ' | ${pythonCmd} -c """ -import json,sys + +PACKAGE_NAME=$(echo "$artifacts" | tr '\r\n' ' ' | ${pythonCmd} -c """ +import json,sys, re data = json.load(sys.stdin) for i in data['artifacts']: - if i['name'] == '"$ARCHITECTURE"': - print(i['archive_download_url']) - break + if re.match('.*{}$'.format(re.escape('$PACKAGE')), i['name']): + print(i['name']) + break +""" 2>/dev/null) + +archive_download_url=$(echo "$artifacts" | tr '\r\n' ' ' | ${pythonCmd} -c """ +import json,sys, re +data = json.load(sys.stdin) + +for i in data['artifacts']: + if re.match('.*{}$'.format(re.escape('$PACKAGE')), i['name']): + print(i['archive_download_url']) + break """ 2>/dev/null) if [ -z "$archive_download_url" ]; then @@ -171,7 +197,7 @@ if [ -z "$archive_download_url" ]; then fi # Download packed PR artifact -echo "---> Downloading the Pull Request #$pr_number" +echo "---> Downloading Pull Request #$pr_number, package: $PACKAGE_NAME" if [ $hasCurl -eq 0 ]; then curl -skH "Authorization: token ${PR_TOKEN}" -o $BASE_PATH/temp.zip -L --get $archive_download_url elif [ $hasWget -eq 0 ]; then diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index 847c4c40..4715bb61 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -46,7 +46,7 @@ macro(DeployMacOS TARGET) MESSAGE("The following unresolved dependencies were discovered: ${unresolved_deps}") endif() - foreach(PLUGIN "platforms" "sqldrivers" "imageformats") + foreach(PLUGIN "platforms" "sqldrivers" "imageformats" "tls") if(EXISTS ${PLUGIN_DIR}/${PLUGIN}) file(GLOB files "${PLUGIN_DIR}/${PLUGIN}/*") foreach(file ${files}) @@ -230,7 +230,7 @@ macro(DeployLinux TARGET) # Copy Qt plugins to 'share/hyperion/lib' if (QT_PLUGINS_DIR) - foreach(PLUGIN "platforms" "sqldrivers" "imageformats") + foreach(PLUGIN "platforms" "sqldrivers" "imageformats" "tls") if (EXISTS ${QT_PLUGINS_DIR}/${PLUGIN}) file(GLOB files "${QT_PLUGINS_DIR}/${PLUGIN}/*.so") foreach(file ${files}) diff --git a/cmake/package-scripts/postinst b/cmake/package-scripts/postinst index 54cd3901..88d2e12a 100644 --- a/cmake/package-scripts/postinst +++ b/cmake/package-scripts/postinst @@ -47,7 +47,7 @@ if [ "$IS_UPGRADE" = false ]; then then # systemd echo "---> init deamon: systemd" - FOUND_USR=`who | grep -o -m1 '^\w*\b'` || "root" + FOUND_USR=${SUDO_USER:-root} install_file /usr/share/hyperion/service/hyperion.systemd /etc/systemd/system/hyperion@.service systemctl enable hyperion"@${FOUND_USR}".service START_MSG="--> systemctl start hyperion for user ${FOUND_USR}" diff --git a/libsrc/leddevice/LedDevice.cpp b/libsrc/leddevice/LedDevice.cpp index f49878c4..92ceb1e6 100644 --- a/libsrc/leddevice/LedDevice.cpp +++ b/libsrc/leddevice/LedDevice.cpp @@ -360,7 +360,7 @@ int LedDevice::rewriteLEDs() int LedDevice::writeBlack(int numberOfWrites) { - Debug(_log, "Set LED strip to black to switch of LEDs"); + Debug(_log, "Set LED strip to black to switch LEDs off"); return writeColor(ColorRgb::BLACK, numberOfWrites); } diff --git a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp index 77712594..ab6d0a17 100644 --- a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp @@ -1301,7 +1301,7 @@ QJsonObject LedDevicePhilipsHueBridge::discover(const QJsonObject& /*params*/) ); #else QString discoveryMethod("ssdp"); - deviceList = discover(); + deviceList = discoverSsdp(); #endif devicesDiscovered.insert("discoveryMethod", discoveryMethod); diff --git a/libsrc/leddevice/dev_serial/ProviderRs232.cpp b/libsrc/leddevice/dev_serial/ProviderRs232.cpp index ea4f99b9..aa0511fc 100644 --- a/libsrc/leddevice/dev_serial/ProviderRs232.cpp +++ b/libsrc/leddevice/dev_serial/ProviderRs232.cpp @@ -185,7 +185,7 @@ bool ProviderRs232::tryOpen(int delayAfterConnect_ms) } else { - QString errortext = QString("Invalid serial device name: %1 %2!").arg(_deviceName, _location); + QString errortext = QString("Invalid serial device: %1 %2!").arg(_deviceName, _location); this->setInError( errortext ); return false; } @@ -237,9 +237,9 @@ int ProviderRs232::writeBytes(const qint64 size, const uint8_t *data) { if (!_rs232Port.waitForBytesWritten(WRITE_TIMEOUT.count())) { - if ( _rs232Port.error() == QSerialPort::TimeoutError ) + if (_rs232Port.error() == QSerialPort::TimeoutError) { - Debug(_log, "Timeout after %dms: %d frames already dropped", WRITE_TIMEOUT.count(), _frameDropCounter); + Debug(_log, "Timeout after %dms: %d frames already dropped, Rs232 SerialPortError [%d]: %s", WRITE_TIMEOUT.count(), _frameDropCounter, _rs232Port.error(), QSTRING_CSTR(_rs232Port.errorString())); ++_frameDropCounter; @@ -258,10 +258,16 @@ int ProviderRs232::writeBytes(const qint64 size, const uint8_t *data) } else { - this->setInError( QString ("Rs232 SerialPortError: %1").arg(_rs232Port.errorString()) ); + this->setInError( QString ("Error writing data to %1, Error: %2").arg(_deviceName).arg(_rs232Port.error())); rc = -1; } } + + if (rc == -1) + { + Info(_log, "Try restarting the device %s after error occured...", QSTRING_CSTR(_activeDeviceType)); + emit enable(); + } } return rc; } From a1f0821f3308671b573a09cd158db5e9ebe99fb4 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Mon, 27 Nov 2023 09:06:43 +0100 Subject: [PATCH 061/117] Introduce Event Services (#1653) * Allow to enable/disable suspend & lock event handling * Fix Windows * Refactor event handling incl.CEC * Revert "Auxiliary commit to revert individual files from 0d9a8b8a3a4a09609a339f54c7d8a9384c561282" This reverts commit 80737d926ad151a07b2493dd1685ed502975cb2e. * Support Events for Grabbers generically * Have CECEvent to actions configurable, further clean-ups * Remove handleEvent from V4L2grabber, as grabber will be stopped on suspend * Validate that one CEC Event can only trigger one action * MacOS lock/unlock added * fast windows fix * Corrections * Fix CodeQL findings * add macos lock/unlock handler * Migration of CEC-config and have default actions * Correct target_link_libraries * Include Foundation * macOS include AppKit * Support Scheduled Events, cleanups. * Fix destructing * Fix coredump during free * Consider additional error sceanrio * Fix missing code * install desktop icons * correct bash logic --------- Co-authored-by: Paulchen-Panther <16664240+Paulchen-Panther@users.noreply.github.com> --- CHANGELOG.md | 7 + assets/webconfig/content/conf_events.html | 11 + assets/webconfig/i18n/en.json | 88 ++- assets/webconfig/index.html | 1 + assets/webconfig/js/content_events.js | 157 +++++ assets/webconfig/js/content_grabber.js | 6 - bin/scripts/install_pr.sh | 2 +- cmake/package-scripts/postinst | 18 +- cmake/package-scripts/prerm | 2 +- config/hyperion.config.json.default | 29 +- include/api/JsonAPI.h | 20 +- include/cec/CECEvent.h | 8 - include/cec/CECHandler.h | 33 +- include/db/SettingsTable.h | 2 + include/events/EventEnum.h | 50 ++ include/events/EventHandler.h | 49 ++ include/events/EventScheduler.h | 55 ++ include/events/OsEventHandler.h | 132 ++++ include/grabber/video/VideoWrapper.h | 8 - include/grabber/video/v4l2/V4L2Grabber.h | 13 +- include/hyperion/Grabber.h | 6 + include/hyperion/GrabberWrapper.h | 4 + include/hyperion/HyperionIManager.h | 33 +- include/utils/settings.h | 55 +- libsrc/CMakeLists.txt | 10 +- libsrc/api/JsonAPI.cpp | 33 +- libsrc/cec/CECHandler.cpp | 343 ++++++---- libsrc/cec/CMakeLists.txt | 1 - libsrc/events/CMakeLists.txt | 27 + libsrc/events/EventHandler.cpp | 186 ++++++ libsrc/events/EventScheduler.cpp | 153 +++++ libsrc/events/OsEventHandler.cpp | 626 ++++++++++++++++++ libsrc/grabber/video/VideoWrapper.cpp | 17 +- libsrc/grabber/video/v4l2/V4L2Grabber.cpp | 35 +- libsrc/hyperion/CMakeLists.txt | 1 + libsrc/hyperion/GrabberWrapper.cpp | 8 + libsrc/hyperion/HyperionIManager.cpp | 33 +- libsrc/hyperion/SettingsManager.cpp | 38 ++ libsrc/hyperion/hyperion.schema.json | 12 + libsrc/hyperion/resource.qrc | 4 + libsrc/hyperion/schema/schema-cecEvents.json | 95 +++ .../hyperion/schema/schema-eventActions.json | 25 + .../hyperion/schema/schema-grabberV4L2.json | 26 +- libsrc/hyperion/schema/schema-osEvents.json | 34 + .../hyperion/schema/schema-schedEvents.json | 40 ++ src/hyperiond/CMakeLists.txt | 2 +- src/hyperiond/SuspendHandler.cpp | 312 --------- src/hyperiond/SuspendHandler.h | 79 --- src/hyperiond/hyperiond.cpp | 119 ++-- src/hyperiond/hyperiond.h | 26 +- src/hyperiond/main.cpp | 36 +- src/hyperiond/systray.cpp | 12 +- src/hyperiond/systray.h | 13 +- 53 files changed, 2306 insertions(+), 829 deletions(-) create mode 100644 assets/webconfig/content/conf_events.html create mode 100644 assets/webconfig/js/content_events.js delete mode 100644 include/cec/CECEvent.h create mode 100644 include/events/EventEnum.h create mode 100644 include/events/EventHandler.h create mode 100644 include/events/EventScheduler.h create mode 100644 include/events/OsEventHandler.h create mode 100644 libsrc/events/CMakeLists.txt create mode 100644 libsrc/events/EventHandler.cpp create mode 100644 libsrc/events/EventScheduler.cpp create mode 100644 libsrc/events/OsEventHandler.cpp create mode 100644 libsrc/hyperion/schema/schema-cecEvents.json create mode 100644 libsrc/hyperion/schema/schema-eventActions.json create mode 100644 libsrc/hyperion/schema/schema-osEvents.json create mode 100644 libsrc/hyperion/schema/schema-schedEvents.json delete mode 100644 src/hyperiond/SuspendHandler.cpp delete mode 100644 src/hyperiond/SuspendHandler.h diff --git a/CHANGELOG.md b/CHANGELOG.md index bb8bbeb8..f4cde2a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New language: Hebrew +**Event Services** +Newly introduced Event Service configuration and consistent handling across all components +- Suspend/Resume & Screen Locking support for MaxOS +- Allow to enable/disable suspend & lock on operating system events (#1633, #1632) +- Scheduled events allowing to suspend,resume, etc. (#1088) +- Configurable CEC event handling + ##### LED-Devices **Philips Hue** diff --git a/assets/webconfig/content/conf_events.html b/assets/webconfig/content/conf_events.html new file mode 100644 index 00000000..df98b737 --- /dev/null +++ b/assets/webconfig/content/conf_events.html @@ -0,0 +1,11 @@ +
+
+
+ +
+
+
+
+
+ + diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 0b238935..4e8742a9 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -192,6 +192,12 @@ "conf_network_tok_intro": "Here you can create and delete tokens for API authentication. Created tokens will only be displayed once.", "conf_network_tok_lastuse": "Last use", "conf_network_tok_title": "Token Management", + "conf_cec_events_heading_title": "CEC Events", + "conf_cec_events_intro": "Settings related to diffent CEC (Consumer Electronics Control) protocol events Hyperion can handle", + "conf_os_events_heading_title": "Operating System Events", + "conf_os_events_intro": "Settings related to diffent Operating System events Hyperion can handle", + "conf_sched_events_heading_title": "Scheduled Events", + "conf_sched_events_intro": "Settings related to scheduled, i.e. time based events, which Hyperion will handle", "conf_webconfig_label_intro": "Webconfiguration settings. Edit wisely.", "dashboard_active_instance": "Selected instance", "dashboard_alert_message_confedit": "Your Hyperion configuration has been modified. To apply it, restart Hyperion.", @@ -243,6 +249,30 @@ "edt_append_pixel": "Pixel", "edt_append_s": "s", "edt_append_sdegree": "s/degree", + "edt_conf_action_title": "Action", + "edt_conf_action_expl": "Action to be applied", + "edt_conf_action_record_validation_error": "The same event can trigger only one action. Clean up Actions $1", + "edt_conf_audio_device_expl": "Selected audio input device", + "edt_conf_audio_device_title": "Audio Device", + "edt_conf_audio_effects_expl": "Select an effect on how the audio signal is transformed to", + "edt_conf_audio_effects_title": "Audio Effects", + "edt_conf_audio_effect_enum_vumeter": "VU-Meter", + "edt_conf_audio_effect_hotcolor_expl": "Hot Color", + "edt_conf_audio_effect_hotcolor_title": "Hot Color", + "edt_conf_audio_effect_multiplier_expl": "Audio Signal Value multiplier", + "edt_conf_audio_effect_multiplier_title": "Multiplier", + "edt_conf_audio_effect_safecolor_expl": "Safe Color", + "edt_conf_audio_effect_safecolor_title": "Safe Color", + "edt_conf_audio_effect_safevalue_expl": "Safe Threshold", + "edt_conf_audio_effect_safevalue_title": "Safe Threshold", + "edt_conf_audio_effect_set_defaults": "Reset to default values", + "edt_conf_audio_effect_tolerance_expl": "Tolerance used when auto calculating a signal multipler from 0-100", + "edt_conf_audio_effect_tolerance_title": "Tolerance", + "edt_conf_audio_effect_warncolor_expl": "Warning Color", + "edt_conf_audio_effect_warncolor_title": "Warning Color", + "edt_conf_audio_effect_warnvalue_expl": "Warning Threshold", + "edt_conf_audio_effect_warnvalue_title": "Warning Threshold", + "edt_conf_audio_heading_title": "Audio Capture", "edt_conf_bb_blurRemoveCnt_expl": "Number of pixels that get removed from the detected border to cut away blur.", "edt_conf_bb_blurRemoveCnt_title": "Blur pixel", "edt_conf_bb_borderFrameCnt_expl": "Number of frames before a consistent detected border is set.", @@ -258,6 +288,17 @@ "edt_conf_bb_unknownFrameCnt_title": "Unknown frames", "edt_conf_bge_heading_title": "Background Effect/Color", "edt_conf_bobls_heading_title": "Boblight Server", + "edt_conf_cec_actions_header_title": "Actions", + "edt_conf_cec_actions_header_expl": "Define which action should take place on a recognised CEC event", + "edt_conf_cec_actions_header_item_title": "Action", + "edt_conf_cec_button_release_delay_ms_title": "Button release time", + "edt_conf_cec_button_release_delay_ms_expl": "Remote button press release time", + "edt_conf_cec_button_repeat_rate_ms_title": "Button repeat rate", + "edt_conf_cec_button_repeat_rate_ms_expl": "Remote button press repeat rate", + "edt_conf_cec_double_tap_timeout_ms_title": "Button delay before repeating", + "edt_conf_cec_double_tap_timeout_ms_expl": "Remote button press delay before repeating", + "edt_conf_cec_event_title": "CEC Event", + "edt_conf_cec_event_expl": "CEC event that will trigger an action", "edt_conf_color_accuracyLevel_expl": "Level how accurate dominat colors are evaluated. A higher level creates more accurate results, but also requries more processing power. Should to be combined with reduced pixel processing.", "edt_conf_color_accuracyLevel_title": "Accuracy level", "edt_conf_color_backlightColored_expl": "Add some color to your backlight.", @@ -320,6 +361,13 @@ "edt_conf_enum_HORIZONTAL": "Horizontal", "edt_conf_enum_VERTICAL": "Vertical", "edt_conf_enum_BOTH": "Horizontal & Vertical", + "edt_conf_enum_action_idle": "Idle", + "edt_conf_enum_action_restart": "Restart", + "edt_conf_enum_action_resume": "Resume", + "edt_conf_enum_action_resumeIdle": "ResumeIdle", + "edt_conf_enum_action_suspend": "Suspend", + "edt_conf_enum_action_toggleIdle": "ToggleIdle", + "edt_conf_enum_action_toggleSuspend": "ToggleSuspend", "edt_conf_enum_automatic": "Automatic", "edt_conf_enum_bbclassic": "Classic", "edt_conf_enum_bbdefault": "Default", @@ -328,6 +376,12 @@ "edt_conf_enum_bgr": "BGR", "edt_conf_enum_bottom_up": "Bottom up", "edt_conf_enum_brg": "BRG", + "edt_conf_enum_cec_key_f1_blue": "Blue button pressed", + "edt_conf_enum_cec_key_f2_red": "Red button pressed", + "edt_conf_enum_cec_key_f3_green": "Green button pressed", + "edt_conf_enum_cec_key_f4_yellow": "Yellow button pressed", + "edt_conf_enum_cec_opcode_set stream path": "TV on", + "edt_conf_enum_cec_opcode_standby": "TV off", "edt_conf_enum_color": "Color", "edt_conf_enum_custom": "Custom", "edt_conf_enum_decay": "Decay", @@ -455,9 +509,18 @@ "edt_conf_net_localApiAuth_title": "Local API Authentication", "edt_conf_net_restirctedInternetAccessAPI_expl": "You can restrict the access to the API through the internet to certain IP's.", "edt_conf_net_restirctedInternetAccessAPI_title": "Restrict to IP's", + "edt_conf_os_events_lockEnable_title": "Listen to lock events", + "edt_conf_os_events_lockEnable_expl": "Listen to screen lock/unlock events", + "edt_conf_os_events_suspendEnable_title": "Listen to suspend events", + "edt_conf_os_events_suspendEnable_expl": "Listen to operating system suspend/resume events", + "edt_conf_os_events_suspendOnLockEnable_title": "Suspend when locked", + "edt_conf_os_events_suspendOnLockEnable_expl": "Suspend when the screen is locked, otherwise go into idle mode", "edt_conf_pbs_heading_title": "Protocol Buffers Server", "edt_conf_pbs_timeout_expl": "If no data are received for the given period, the component will be (soft) disabled.", "edt_conf_pbs_timeout_title": "Timeout", + "edt_conf_sched_actions_header_title": "Actions", + "edt_conf_sched_actions_header_expl": "Define which action should take place on a point in time. The action will be scheduled daily.", + "edt_conf_sched_actions_header_item_title": "Action", "edt_conf_smooth_continuousOutput_expl": "Update the LEDs even there is no changed picture.", "edt_conf_smooth_continuousOutput_title": "Continuous output", "edt_conf_smooth_decay_expl": "The speed of decay. 1 is linear, greater values are have stronger effect.", @@ -475,6 +538,8 @@ "edt_conf_smooth_updateDelay_title": "Output delay", "edt_conf_smooth_updateFrequency_expl": "The output speed to your LED controller.", "edt_conf_smooth_updateFrequency_title": "Update frequency", + "edt_conf_time_event_title": "Time", + "edt_conf_time_event_expl": "Point in time that will trigger an action", "edt_conf_v4l2_blueSignalThreshold_expl": "Darkens low blue values (recognized as black)", "edt_conf_v4l2_blueSignalThreshold_title": "Blue signal threshold", "edt_conf_v4l2_cecDetection_expl": "If enabled, USB capture will be temporarily disabled when CEC standby event received from HDMI bus.", @@ -534,27 +599,6 @@ "edt_conf_v4l2_hardware_set_defaults_tip": "Set device's default values for brightness, contrast, hue and saturation", "edt_conf_v4l2_noSignalCounterThreshold_title": "Signal Counter Threshold", "edt_conf_v4l2_noSignalCounterThreshold_expl": "Count of frames (check that with grabber's current FPS mode) after which the no signal is triggered", - "edt_conf_audio_device_expl": "Selected audio input device", - "edt_conf_audio_device_title": "Audio Device", - "edt_conf_audio_effects_expl": "Select an effect on how the audio signal is transformed to", - "edt_conf_audio_effects_title": "Audio Effects", - "edt_conf_audio_effect_enum_vumeter": "VU-Meter", - "edt_conf_audio_effect_hotcolor_expl": "Hot Color", - "edt_conf_audio_effect_hotcolor_title": "Hot Color", - "edt_conf_audio_effect_multiplier_expl": "Audio Signal Value multiplier", - "edt_conf_audio_effect_multiplier_title": "Multiplier", - "edt_conf_audio_effect_safecolor_expl": "Safe Color", - "edt_conf_audio_effect_safecolor_title": "Safe Color", - "edt_conf_audio_effect_safevalue_expl": "Safe Threshold", - "edt_conf_audio_effect_safevalue_title": "Safe Threshold", - "edt_conf_audio_effect_set_defaults": "Reset to default values", - "edt_conf_audio_effect_tolerance_expl": "Tolerance used when auto calculating a signal multipler from 0-100", - "edt_conf_audio_effect_tolerance_title": "Tolerance", - "edt_conf_audio_effect_warncolor_expl": "Warning Color", - "edt_conf_audio_effect_warncolor_title": "Warning Color", - "edt_conf_audio_effect_warnvalue_expl": "Warning Threshold", - "edt_conf_audio_effect_warnvalue_title": "Warning Threshold", - "edt_conf_audio_heading_title": "Audio Capture", "edt_conf_webc_crtPath_expl": "Path to the certification file (format should be PEM)", "edt_conf_webc_crtPath_title": "Certificate path", "edt_conf_webc_docroot_expl": "Local webinterface root path (just for webui developer)", @@ -983,6 +1027,8 @@ "main_menu_dashboard_token": "Dashboard", "main_menu_effect_conf_token": "Effects", "main_menu_effectsconfigurator_token": "Effects Configurator", + "main_menu_events": "Event Services", + "main_menu_event_services_token": "Event Services", "main_menu_general_conf_token": "General", "main_menu_grabber_conf_token": "Capturing Hardware", "main_menu_input_selection_token": "Input Selection", diff --git a/assets/webconfig/index.html b/assets/webconfig/index.html index 9249eb39..88bf52b3 100644 --- a/assets/webconfig/index.html +++ b/assets/webconfig/index.html @@ -280,6 +280,7 @@
  • System
  • +
    + diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 01a9f6f8..68344d73 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -117,6 +117,10 @@ "conf_leds_layout_cl_topright": "Top Right (Corner)", "conf_leds_layout_cl_vleddepth": "Vertical LED depth", "conf_leds_layout_frame": "Classic Layout (LED Frame)", + "conf_leds_layout_gapleft": "Left gap", + "conf_leds_layout_gapright": "Right gap", + "conf_leds_layout_gaptop": "Top gap", + "conf_leds_layout_gapbottom": "Bottom gap", "conf_leds_layout_generatedconf": "Generated/Current LED Configuration", "conf_leds_layout_generation_success": "LED Layout generated sucessfully", "conf_leds_layout_generation_error": "LED Layout was not generated", diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index c457262b..0432524a 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -468,17 +468,18 @@ function createClassicLeds() { aceEdt.set(finalLedArray); } -function createMatrixLayout(ledshoriz, ledsvert, cabling, start, direction) { +function createMatrixLayout(ledshoriz, ledsvert, cabling, start, direction, gap) { // Big thank you to RanzQ (Juha Rantanen) from Github for this script // https://raw.githubusercontent.com/RanzQ/hyperion-audio-effects/master/matrix-config.js - var parallel = false - var leds = [] - var hblock = 1.0 / ledshoriz - var vblock = 1.0 / ledsvert + let parallel = false; + const leds = []; + + const hblock = (1.0 - gap.left - gap.right) / ledshoriz; + const vblock = (1.0 - gap.top - gap.bottom) / ledsvert; if (cabling == "parallel") { - parallel = true + parallel = true; } /** @@ -487,10 +488,10 @@ function createMatrixLayout(ledshoriz, ledsvert, cabling, start, direction) { * @param {Number} y Vertical position in matrix */ function addLed(x, y) { - var hscanMin = x * hblock - var hscanMax = (x + 1) * hblock - var vscanMin = y * vblock - var vscanMax = (y + 1) * vblock + let hscanMin = gap.left + (x * hblock); + let hscanMax = gap.left + (x + 1) * hblock; + let vscanMin = gap.top + y * vblock; + let vscanMax = gap.top + (y + 1) * vblock; hscanMin = round(hscanMin); hscanMax = round(hscanMax); @@ -502,43 +503,41 @@ function createMatrixLayout(ledshoriz, ledsvert, cabling, start, direction) { hmax: hscanMax, vmin: vscanMin, vmax: vscanMax - }) + }); } - var startYX = start.split('-') - var startX = startYX[1] === 'right' ? ledshoriz - 1 : 0 - var startY = startYX[0] === 'bottom' ? ledsvert - 1 : 0 - var endX = startX === 0 ? ledshoriz - 1 : 0 - var endY = startY === 0 ? ledsvert - 1 : 0 - var forward = startX < endX + const startYX = start.split('-'); + let startX = startYX[1] === 'right' ? ledshoriz - 1 : 0; + let startY = startYX[0] === 'bottom' ? ledsvert - 1 : 0; + let endX = startX === 0 ? ledshoriz - 1 : 0; + let endY = startY === 0 ? ledsvert - 1 : 0; + let forward = startX < endX; + let downward = startY < endY; - var downward = startY < endY - - var x, y + let x, y; if (direction === 'vertical') { for (x = startX; forward && x <= endX || !forward && x >= endX; x += forward ? 1 : -1) { for (y = startY; downward && y <= endY || !downward && y >= endY; y += downward ? 1 : -1) { - - addLed(x, y) + addLed(x, y); } if (!parallel) { - downward = !downward - var tmp = startY - startY = endY - endY = tmp + downward = !downward; + const tmp = startY; + startY = endY; + endY = tmp; } } } else { for (y = startY; downward && y <= endY || !downward && y >= endY; y += downward ? 1 : -1) { for (x = startX; forward && x <= endX || !forward && x >= endX; x += forward ? 1 : -1) { - addLed(x, y) + addLed(x, y); } if (!parallel) { - forward = !forward - var tmp = startX - startX = endX - endX = tmp + forward = !forward; + const tmp = startX; + startX = endX; + endX = tmp; } } } @@ -551,13 +550,20 @@ function createMatrixLeds() { // https://raw.githubusercontent.com/RanzQ/hyperion-audio-effects/master/matrix-config.js //get values - var ledshoriz = parseInt($("#ip_ma_ledshoriz").val()); - var ledsvert = parseInt($("#ip_ma_ledsvert").val()); - var cabling = $("#ip_ma_cabling").val(); - var direction = $("#ip_ma_direction").val(); - var start = $("#ip_ma_start").val(); + const ledshoriz = parseInt($("#ip_ma_ledshoriz").val()); + const ledsvert = parseInt($("#ip_ma_ledsvert").val()); + const cabling = $("#ip_ma_cabling").val(); + const direction = $("#ip_ma_direction").val(); + const start = $("#ip_ma_start").val(); + const gap = { + //gap values % -> float + left: parseInt($("#ip_ma_gapleft").val()) / 100, + right: parseInt($("#ip_ma_gapright").val()) / 100, + top: parseInt($("#ip_ma_gaptop").val()) / 100, + bottom: parseInt($("#ip_ma_gapbottom").val()) / 100, + }; - nonBlacklistLedArray = createMatrixLayout(ledshoriz, ledsvert, cabling, start, direction); + nonBlacklistLedArray = createMatrixLayout(ledshoriz, ledsvert, cabling, start, direction, gap); finalLedArray = blackListLeds(nonBlacklistLedArray, ledBlacklist); createLedPreview(finalLedArray); @@ -797,6 +803,35 @@ $(document).ready(function () { $('.ledMAconstr').on("change", function () { valValue(this.id, this.value, this.min, this.max); + + // top/bottom and left/right must not overlap + switch (this.id) { + case "ip_ma_gapleft": + let left = 100 - parseInt($("#ip_ma_gapright").val()); + if (this.value > left) { + $(this).val(left); + } + break; + case "ip_ma_gapright": + let right = 100 - parseInt($("#ip_ma_gapleft").val()); + if (this.value > right) { + $(this).val(right); + } + break; + case "ip_ma_gaptop": + let top = 100 - parseInt($("#ip_ma_gapbottom").val()); + if (this.value > top) { + $(this).val(top); + } + break; + case "ip_ma_gapbottom": + let bottom = 100 - parseInt($("#ip_ma_gaptop").val()); + if (this.value > bottom) { + $(this).val(bottom); + } + break; + default: + } createMatrixLeds(); }); diff --git a/cmake/package-scripts/postinst b/cmake/package-scripts/postinst index d1832220..f24e6ac6 100644 --- a/cmake/package-scripts/postinst +++ b/cmake/package-scripts/postinst @@ -26,13 +26,6 @@ install_file() echo "--- Hyperion ambient light postinstall ---" -#check system -CPU_RPI=`grep -m1 -c 'BCM2708\|BCM2709\|BCM2710\|BCM2835\|BCM2836\|BCM2837\|BCM2711' /proc/cpuinfo` -CPU_X32X64=`uname -m | grep 'x86_32\|i686\|x86_64' | wc -l` - -#Check for a bootloader as Berryboot -BOOT_BERRYBOOT=$(grep -m1 -c '\(/var/media\|/media/pi\)/berryboot' /etc/mtab) - #get current system ip NET_IP=`hostname -I | cut -d " " -f1` @@ -128,25 +121,6 @@ fi rm -r /usr/share/hyperion/desktop 2>/dev/null rm -r /usr/share/hyperion/icons 2>/dev/null -#Check, if dtparam=spi=on is in place -if [ $CPU_RPI -eq 1 ]; then - BOOT_DIR="/boot" - if [ $BOOT_BERRYBOOT -eq 1 ]; then - BOOT_DIR=$(sed -ne "s#/dev/mmcblk0p1 \([^ ]*\) vfat rw,.*#\1#p" /etc/mtab) - fi - if [ -z "$BOOT_DIR" -o ! -f "$BOOT_DIR/config.txt" ]; then - echo '---> Warning: RPi using BERRYBOOT found but can not locate where config.txt is to enable SPI. (BOOT_DIR='"$BOOT_DIR)" - SPIOK=1 # Not sure if OK, but don't ask to reboot - else - SPIOK=`grep '^\dtparam=spi=on' "$BOOT_DIR/config.txt" | wc -l` - if [ $SPIOK -ne 1 ]; then - echo '---> Raspberry Pi found, but SPI is not set, we write "dtparam=spi=on" to '"$BOOT_DIR/config.txt" - sed -i '$a dtparam=spi=on' "$BOOT_DIR/config.txt" - REBOOTMESSAGE="echo Please reboot your Raspberry Pi, we inserted dtparam=spi=on to $BOOT_DIR/config.txt" - fi - fi -fi - echo ${START_MSG} echo "-----------------------------------------------------------------------------" @@ -171,7 +145,7 @@ if [ -e /opt/hyperion/ ] then echo echo "---------------------------------------------------------------------------------" - echo "- It seemd that you have an older version of hyperion installed in /opt/hyperion -" + echo "- It seems that you have an older version of hyperion installed in /opt/hyperion -" echo "- please remove it to avoid problems -" echo "---------------------------------------------------------------------------------" fi diff --git a/include/blackborder/BlackBorderDetector.h b/include/blackborder/BlackBorderDetector.h index 77438f4c..66aaa32c 100644 --- a/include/blackborder/BlackBorderDetector.h +++ b/include/blackborder/BlackBorderDetector.h @@ -110,7 +110,7 @@ namespace hyperion } // Construct result - BlackBorder detectedBorder; + BlackBorder detectedBorder{}; detectedBorder.unknown = firstNonBlackXPixelIndex == -1 || firstNonBlackYPixelIndex == -1; detectedBorder.horizontalSize = firstNonBlackYPixelIndex; detectedBorder.verticalSize = firstNonBlackXPixelIndex; @@ -167,7 +167,7 @@ namespace hyperion } // Construct result - BlackBorder detectedBorder; + BlackBorder detectedBorder{}; detectedBorder.unknown = firstNonBlackXPixelIndex == -1 || firstNonBlackYPixelIndex == -1; detectedBorder.horizontalSize = firstNonBlackYPixelIndex; detectedBorder.verticalSize = firstNonBlackXPixelIndex; @@ -224,7 +224,7 @@ namespace hyperion } // Construct result - BlackBorder detectedBorder; + BlackBorder detectedBorder{}; detectedBorder.unknown = firstNonBlackXPixelIndex == -1 || firstNonBlackYPixelIndex == -1; detectedBorder.horizontalSize = firstNonBlackYPixelIndex; detectedBorder.verticalSize = firstNonBlackXPixelIndex; @@ -267,7 +267,7 @@ namespace hyperion } // Construct result - BlackBorder detectedBorder; + BlackBorder detectedBorder{}; detectedBorder.unknown = firstNonBlackYPixelIndex == -1; detectedBorder.horizontalSize = firstNonBlackYPixelIndex; detectedBorder.verticalSize = 0; diff --git a/libsrc/hyperion/schema/schema-ledConfig.json b/libsrc/hyperion/schema/schema-ledConfig.json index 3232063c..3a3c6809 100644 --- a/libsrc/hyperion/schema/schema-ledConfig.json +++ b/libsrc/hyperion/schema/schema-ledConfig.json @@ -144,6 +144,30 @@ "start": { "type": "string", "enum": [ "top-left", "top-right", "bottom-left", "bottom-right" ] + }, + "gapleft": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 0 + }, + "gapright": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 0 + }, + "gaptop": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 0 + }, + "gapbottom": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 0 } }, "additionalProperties": false From 806206ec012deb803c89e6ad8a9976c6bfb933dd Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Fri, 15 Mar 2024 20:23:53 +0100 Subject: [PATCH 093/117] Fix #1714 (#1717) --- include/grabber/amlogic/AmlogicWrapper.h | 9 ++++----- libsrc/grabber/amlogic/AmlogicWrapper.cpp | 7 ++++--- libsrc/grabber/directx/DirectXWrapper.cpp | 7 +++++-- libsrc/grabber/dispmanx/DispmanxWrapper.cpp | 3 ++- libsrc/grabber/framebuffer/FramebufferWrapper.cpp | 6 ++++-- libsrc/grabber/osx/OsxWrapper.cpp | 6 ++++-- libsrc/grabber/qt/QtWrapper.cpp | 7 +++++-- libsrc/grabber/x11/X11Wrapper.cpp | 8 +++++--- libsrc/grabber/xcb/XcbWrapper.cpp | 4 +++- 9 files changed, 36 insertions(+), 21 deletions(-) diff --git a/include/grabber/amlogic/AmlogicWrapper.h b/include/grabber/amlogic/AmlogicWrapper.h index 3f763f5c..5e355dff 100644 --- a/include/grabber/amlogic/AmlogicWrapper.h +++ b/include/grabber/amlogic/AmlogicWrapper.h @@ -18,12 +18,11 @@ public: /// /// Constructs the Amlogic frame grabber /// - /// @param[in] grabWidth The width of the grabbed image [pixels] - /// @param[in] grabHeight The height of the grabbed images [pixels] - /// @param[in] pixelDecimation Decimation factor for image [pixels] + /// @param[in] updateRate_Hz The image grab rate [Hz] + /// @param[in] pixelDecimation Decimation factor for image [pixels]/// /// - AmlogicWrapper(int pixelDecimation=GrabberWrapper::DEFAULT_PIXELDECIMATION, - int updateRate_Hz=GrabberWrapper::DEFAULT_RATE_HZ); + AmlogicWrapper(int updateRate_Hz=GrabberWrapper::DEFAULT_RATE_HZ, + int pixelDecimation=GrabberWrapper::DEFAULT_PIXELDECIMATION); /// /// Constructs the Amlogic frame grabber from configuration settings diff --git a/libsrc/grabber/amlogic/AmlogicWrapper.cpp b/libsrc/grabber/amlogic/AmlogicWrapper.cpp index d568e999..6c3b302c 100644 --- a/libsrc/grabber/amlogic/AmlogicWrapper.cpp +++ b/libsrc/grabber/amlogic/AmlogicWrapper.cpp @@ -1,14 +1,15 @@ #include -AmlogicWrapper::AmlogicWrapper(int pixelDecimation, int updateRate_Hz) +AmlogicWrapper::AmlogicWrapper(int updateRate_Hz, int pixelDecimation) : GrabberWrapper(GRABBERTYPE, &_grabber, updateRate_Hz) - , _grabber() + , _grabber() { _grabber.setPixelDecimation(pixelDecimation); } AmlogicWrapper::AmlogicWrapper(const QJsonDocument& grabberConfig) - : GrabberWrapper(GRABBERTYPE, &_grabber) + : AmlogicWrapper(GrabberWrapper::DEFAULT_RATE_HZ, + GrabberWrapper::DEFAULT_PIXELDECIMATION) { this->handleSettingsUpdate(settings::SYSTEMCAPTURE, grabberConfig); } diff --git a/libsrc/grabber/directx/DirectXWrapper.cpp b/libsrc/grabber/directx/DirectXWrapper.cpp index c0ba4ecc..1d5d905e 100644 --- a/libsrc/grabber/directx/DirectXWrapper.cpp +++ b/libsrc/grabber/directx/DirectXWrapper.cpp @@ -6,14 +6,17 @@ DirectXWrapper::DirectXWrapper( int updateRate_Hz, int cropLeft, int cropRight, int cropTop, int cropBottom ) : GrabberWrapper(GRABBERTYPE, &_grabber, updateRate_Hz) - , _grabber(display, cropLeft, cropRight, cropTop, cropBottom) + , _grabber(display, cropLeft, cropRight, cropTop, cropBottom) { _grabber.setPixelDecimation(pixelDecimation); } DirectXWrapper::DirectXWrapper(const QJsonDocument& grabberConfig) - : GrabberWrapper(GRABBERTYPE, &_grabber) + : DirectXWrapper(GrabberWrapper::DEFAULT_RATE_HZ, + 0, + GrabberWrapper::DEFAULT_PIXELDECIMATION, + 0,0,0,0) { this->handleSettingsUpdate(settings::SYSTEMCAPTURE, grabberConfig); } diff --git a/libsrc/grabber/dispmanx/DispmanxWrapper.cpp b/libsrc/grabber/dispmanx/DispmanxWrapper.cpp index 32824762..0194a4f1 100644 --- a/libsrc/grabber/dispmanx/DispmanxWrapper.cpp +++ b/libsrc/grabber/dispmanx/DispmanxWrapper.cpp @@ -12,7 +12,8 @@ DispmanxWrapper::DispmanxWrapper( int updateRate_Hz, } DispmanxWrapper::DispmanxWrapper(const QJsonDocument& grabberConfig) - : GrabberWrapper(GRABBERTYPE, &_grabber) + : DispmanxWrapper(GrabberWrapper::DEFAULT_RATE_HZ, + GrabberWrapper::DEFAULT_PIXELDECIMATION) { this->handleSettingsUpdate(settings::SYSTEMCAPTURE, grabberConfig); } diff --git a/libsrc/grabber/framebuffer/FramebufferWrapper.cpp b/libsrc/grabber/framebuffer/FramebufferWrapper.cpp index 2588afed..1797e513 100644 --- a/libsrc/grabber/framebuffer/FramebufferWrapper.cpp +++ b/libsrc/grabber/framebuffer/FramebufferWrapper.cpp @@ -4,13 +4,15 @@ FramebufferWrapper::FramebufferWrapper( int updateRate_Hz, int deviceIdx, int pixelDecimation) : GrabberWrapper(GRABBERTYPE, &_grabber, updateRate_Hz) - , _grabber(deviceIdx) + , _grabber(deviceIdx) { _grabber.setPixelDecimation(pixelDecimation); } FramebufferWrapper::FramebufferWrapper(const QJsonDocument& grabberConfig) - : GrabberWrapper(GRABBERTYPE, &_grabber) + : FramebufferWrapper(GrabberWrapper::DEFAULT_RATE_HZ, + 0, + GrabberWrapper::DEFAULT_PIXELDECIMATION) { this->handleSettingsUpdate(settings::SYSTEMCAPTURE, grabberConfig); } diff --git a/libsrc/grabber/osx/OsxWrapper.cpp b/libsrc/grabber/osx/OsxWrapper.cpp index 86058d4f..d9bb51b8 100644 --- a/libsrc/grabber/osx/OsxWrapper.cpp +++ b/libsrc/grabber/osx/OsxWrapper.cpp @@ -5,13 +5,15 @@ OsxWrapper::OsxWrapper( int updateRate_Hz, int pixelDecimation ) : GrabberWrapper(GRABBERTYPE, &_grabber, updateRate_Hz) - , _grabber(display) + , _grabber(display) { _grabber.setPixelDecimation(pixelDecimation); } OsxWrapper::OsxWrapper(const QJsonDocument& grabberConfig) - : GrabberWrapper(GRABBERTYPE, &_grabber) + : OsxWrapper(GrabberWrapper::DEFAULT_RATE_HZ, + kCGDirectMainDisplay, + GrabberWrapper::DEFAULT_PIXELDECIMATION) { this->handleSettingsUpdate(settings::SYSTEMCAPTURE, grabberConfig); } diff --git a/libsrc/grabber/qt/QtWrapper.cpp b/libsrc/grabber/qt/QtWrapper.cpp index d4560676..1cb8287d 100644 --- a/libsrc/grabber/qt/QtWrapper.cpp +++ b/libsrc/grabber/qt/QtWrapper.cpp @@ -6,13 +6,16 @@ QtWrapper::QtWrapper( int updateRate_Hz, int cropLeft, int cropRight, int cropTop, int cropBottom ) : GrabberWrapper(GRABBERTYPE, &_grabber, updateRate_Hz) - , _grabber(display, cropLeft, cropRight, cropTop, cropBottom) + , _grabber(display, cropLeft, cropRight, cropTop, cropBottom) { _grabber.setPixelDecimation(pixelDecimation); } QtWrapper::QtWrapper(const QJsonDocument& grabberConfig) - : GrabberWrapper(GRABBERTYPE, &_grabber) + : QtWrapper(GrabberWrapper::DEFAULT_RATE_HZ, + 0, + GrabberWrapper::DEFAULT_PIXELDECIMATION, + 0,0,0,0) { this->handleSettingsUpdate(settings::SYSTEMCAPTURE, grabberConfig); } diff --git a/libsrc/grabber/x11/X11Wrapper.cpp b/libsrc/grabber/x11/X11Wrapper.cpp index d8df3cee..df2c5c51 100644 --- a/libsrc/grabber/x11/X11Wrapper.cpp +++ b/libsrc/grabber/x11/X11Wrapper.cpp @@ -4,14 +4,16 @@ X11Wrapper::X11Wrapper( int updateRate_Hz, int pixelDecimation, int cropLeft, int cropRight, int cropTop, int cropBottom) : GrabberWrapper(GRABBERTYPE, &_grabber, updateRate_Hz) - , _grabber(cropLeft, cropRight, cropTop, cropBottom) - , _init(false) + , _grabber(cropLeft, cropRight, cropTop, cropBottom) + , _init(false) { _grabber.setPixelDecimation(pixelDecimation); } X11Wrapper::X11Wrapper(const QJsonDocument& grabberConfig) - : GrabberWrapper(GRABBERTYPE, &_grabber) + : X11Wrapper(GrabberWrapper::DEFAULT_RATE_HZ, + GrabberWrapper::DEFAULT_PIXELDECIMATION, + 0,0,0,0) { this->handleSettingsUpdate(settings::SYSTEMCAPTURE, grabberConfig); } diff --git a/libsrc/grabber/xcb/XcbWrapper.cpp b/libsrc/grabber/xcb/XcbWrapper.cpp index aa3db000..c9d4abd4 100644 --- a/libsrc/grabber/xcb/XcbWrapper.cpp +++ b/libsrc/grabber/xcb/XcbWrapper.cpp @@ -11,7 +11,9 @@ XcbWrapper::XcbWrapper( int updateRate_Hz, } XcbWrapper::XcbWrapper(const QJsonDocument& grabberConfig) - : GrabberWrapper(GRABBERTYPE, &_grabber) + : XcbWrapper(GrabberWrapper::DEFAULT_RATE_HZ, + GrabberWrapper::DEFAULT_PIXELDECIMATION, + 0,0,0,0) { this->handleSettingsUpdate(settings::SYSTEMCAPTURE, grabberConfig); } From 8123a112876c54201b26155d5e402f9756a9d13a Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Sat, 23 Mar 2024 14:08:52 +0100 Subject: [PATCH 094/117] Update fr.json (POEditor.com) --- assets/webconfig/i18n/fr.json | 47 ++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/assets/webconfig/i18n/fr.json b/assets/webconfig/i18n/fr.json index 55504413..7965ed38 100644 --- a/assets/webconfig/i18n/fr.json +++ b/assets/webconfig/i18n/fr.json @@ -52,7 +52,7 @@ "conf_helptable_option": "Option", "conf_leds_config_error": "Erreur dans la configuration de la disposition", "conf_leds_config_warning": "Verifiez la configuration de la disposition des LEDs", - "conf_leds_contr_label_contrtype": "Type de contrôleur : ", + "conf_leds_contr_label_contrtype": "Type de contrôleur: ", "conf_leds_device_info_log": "Si vos LED ne fonctionnent pas, vérifiez ici s'il y a des erreurs :", "conf_leds_device_intro": "Hyperion prend en charge de nombreux contrôleurs pour transmettre des données à votre appareil cible. Sélectionnez un contrôleur LED dans la liste triée et configurez-le. Nous avons choisi les meilleurs paramètres par défaut pour chaque appareil.", "conf_leds_error_get_properties_text": "Erreur lors de l'obtention des propriétés de l'appareil. Veuillez vérifier les éléments de configuration.", @@ -82,6 +82,7 @@ "conf_leds_layout_cl_bottomright": "Bas droit (Coin)", "conf_leds_layout_cl_cornergap": "Écart aux coins", "conf_leds_layout_cl_edgegap": "Écart aux bords", + "conf_leds_layout_cl_entertainment": "Espace de divertissement", "conf_leds_layout_cl_gaglength": "Taille de l'écart", "conf_leds_layout_cl_gappos": "Position de l'écart", "conf_leds_layout_cl_hleddepth": "Profondeur LED horizontale", @@ -193,34 +194,34 @@ "dashboard_alert_message_disabled": "Cette instance est actuellement désactivée ! Pour l'utiliser à nouveau, activez-la dans le tableau de bord.", "dashboard_alert_message_disabled_t": "Instance matérielle LED désactivée", "dashboard_componentbox_label_comp": "Composant", - "dashboard_componentbox_label_status": "Etat", + "dashboard_componentbox_label_status": "État", "dashboard_componentbox_label_title": "Etat des composants", - "dashboard_infobox_label_currenthyp": "Votre version d'Hyperion : ", + "dashboard_infobox_label_currenthyp": "Votre version Hyperion : ", "dashboard_infobox_label_disableh": "Désactiver l'instance : $1", "dashboard_infobox_label_enableh": "Activer l'instance : $1", - "dashboard_infobox_label_instance": "Instance", + "dashboard_infobox_label_instance": "Instance:", "dashboard_infobox_label_latesthyp": "Dernière version d'Hyperion : ", - "dashboard_infobox_label_platform": "Plateforme", + "dashboard_infobox_label_platform": "Plateforme:", "dashboard_infobox_label_port_boblight": "Serveur Boblight :", "dashboard_infobox_label_port_flat": "Flatbuffer:", "dashboard_infobox_label_port_json": "Server-JSON:", "dashboard_infobox_label_port_proto": "Protobuffer:", "dashboard_infobox_label_ports": "Ports", "dashboard_infobox_label_ports_websocket": "WebSocket (ws|wss):", - "dashboard_infobox_label_smartacc": "Accès simplifié", - "dashboard_infobox_label_statush": "Statut d'Hyperion : ", + "dashboard_infobox_label_smartacc": "Accès Intelligent", + "dashboard_infobox_label_statush": "Statut Hyperion : ", "dashboard_infobox_label_title": "Information", "dashboard_infobox_label_watchedversionbranch": "Branche surveillée :", "dashboard_infobox_message_updatesuccess": "Vous utilisez la dernière version d'Hyperion.", "dashboard_infobox_message_updatewarning": "Une nouvelle version d'Hyperion est disponible ! ($1)", - "dashboard_label_intro": "Ce dashboard vous donne une vue rapide sur l'état d'Hyperion.", + "dashboard_label_intro": "Ce dashboard vous donne une vue rapide sur l'état Hyperion", "dashboard_message_default_password": "La WebUI utilise le mot de passe par défaut. Nous recommandons de le modifier.", "dashboard_message_default_password_t": "Utilisation du mot de passe par défaut pour la WebUI", "dashboard_message_do_not_show_again": "Ne plus afficher ce message", "dashboard_message_global_setting": "Les réglages sur cette page sont indépendantes des instances. Tout changement sera global a toutes les instances.", "dashboard_message_global_setting_t": "Réglage global", "dashboard_newsbox_label_title": "Hyperion - Blog", - "dashboard_newsbox_noconn": "Impossible d'accéder au blog d'Hyperion. Votre connexion internet fonctionne-t-elle ?", + "dashboard_newsbox_noconn": "Impossible d'accéder au blog Hyperion. Votre connexion internet est fonctionnel ?", "dashboard_newsbox_readmore": "Lire plus", "dashboard_newsbox_visitblog": "Visiter le Blog d'Hyperion", "edt_append_degree": "°", @@ -271,6 +272,7 @@ "edt_conf_bb_unknownFrameCnt_title": "image inconnu", "edt_conf_bge_heading_title": "Effet/Couleur d'arrière-plan", "edt_conf_bobls_heading_title": "Serveur Boblight", + "edt_conf_cec_event_title": "Évenement CEC", "edt_conf_color_accuracyLevel_expl": "Niveau de précision de l'évaluation des couleurs dominantes. Un niveau plus élevé permet d'obtenir des résultats plus précis, mais nécessite également une plus grande puissance de traitement. Doit être combiné avec un traitement réduit des pixels.", "edt_conf_color_accuracyLevel_title": "Niveau d'exactitude", "edt_conf_color_backlightColored_expl": "Ajouter des couleurs à votre rétroéclairage", @@ -333,6 +335,10 @@ "edt_conf_enum_PAL": "PAL", "edt_conf_enum_SECAM": "SECAM", "edt_conf_enum_VERTICAL": "Verticale", + "edt_conf_enum_action_idle": "Inactif", + "edt_conf_enum_action_restart": "Redémarrer", + "edt_conf_enum_action_resume": "Continuer", + "edt_conf_enum_action_suspend": "Suspendre", "edt_conf_enum_automatic": "Automatique", "edt_conf_enum_bbclassic": "Classique", "edt_conf_enum_bbdefault": "Defaut", @@ -341,6 +347,8 @@ "edt_conf_enum_bgr": "BGR", "edt_conf_enum_bottom_up": "De bas en haut", "edt_conf_enum_brg": "BRG", + "edt_conf_enum_cec_key_f1_blue": "Bouton bleu enfoncé", + "edt_conf_enum_cec_key_f2_red": "Bouton rouge enfoncé", "edt_conf_enum_color": "Couleur", "edt_conf_enum_custom": "Personnalisation", "edt_conf_enum_decay": "Décroissance", @@ -468,6 +476,8 @@ "edt_conf_net_localApiAuth_title": "Authentification de l'API locale", "edt_conf_net_restirctedInternetAccessAPI_expl": "Vous pouvez limiter l'accès à l'API via l'internet à certaines IP.", "edt_conf_net_restirctedInternetAccessAPI_title": "Limité aux IP", + "edt_conf_os_events_suspendEnable_expl": "Écoute les événements de suspension/reprise du système d'exploitation", + "edt_conf_os_events_suspendOnLockEnable_title": "Suspendre lorsqu'il est verrouillé", "edt_conf_pbs_heading_title": "Serveur Protocol Buffers", "edt_conf_pbs_timeout_expl": "Si aucune donnée n'est reçue dans la période de temps donnée, le composant sera désactivé.", "edt_conf_pbs_timeout_title": "Temps écoulé", @@ -488,6 +498,7 @@ "edt_conf_smooth_updateDelay_title": "Changer le retard", "edt_conf_smooth_updateFrequency_expl": "Le vitesse de sortie pour le contrôleur led.", "edt_conf_smooth_updateFrequency_title": "Charger la fréquence", + "edt_conf_time_event_title": "Temps", "edt_conf_v4l2_blueSignalThreshold_expl": "Assombrie les valeurs bleues faibles (reconnues comme noires)", "edt_conf_v4l2_blueSignalThreshold_title": "Seuil de signal bleu", "edt_conf_v4l2_cecDetection_expl": "Si cette option est activée, la capture USB sera temporairement désactivée lorsque l'événement CEC standby sera reçu du bus HDMI.", @@ -556,8 +567,11 @@ "edt_conf_webc_keyPassPhrase_title": "Mot de passe de clé", "edt_conf_webc_keyPath_expl": "Chemin vers la clé (doit être au format PEM, chiffrée avec RSA)", "edt_conf_webc_keyPath_title": "Chemin de clé privée", + "edt_conf_webc_port_title": "Port HTTP", "edt_conf_webc_sslport_expl": "Port HTTPS du serveur web", "edt_conf_webc_sslport_title": "Port HTTPS", + "edt_dev_auth_key_title": "Jeton Autorisation", + "edt_dev_auth_key_title_info": "Jeton d'autorisation requis pour avoir accès au matériel", "edt_dev_enum_sub_min_cool_adjust": "Soustraire le blanc froid", "edt_dev_enum_sub_min_warm_adjust": "Soustraire le blanc chaud", "edt_dev_enum_subtract_minimum": "Soustraire le minimum", @@ -675,6 +689,7 @@ "edt_dev_spec_transistionTime_title": "Temps de transition", "edt_dev_spec_uid_title": "UID", "edt_dev_spec_universe_title": "Univers", + "edt_dev_spec_useAPIv2_title": "Utliser API v2", "edt_dev_spec_useEntertainmentAPI_title": "Utiliser l'API Hue Entertainment", "edt_dev_spec_useOrbSmoothing_title": "Utiliser le lissage orb", "edt_dev_spec_useRgbwProtocol_title": "Utiliser le protocole RGBW", @@ -745,6 +760,7 @@ "edt_eff_ledlist": "Liste de LED", "edt_eff_ledtest_header": "Test LED", "edt_eff_ledtest_header_desc": "sortie tournante: rouge, verte, blanche, noire", + "edt_eff_ledtest_seq_header": "Séquence d'essais LED", "edt_eff_length": "Longueur", "edt_eff_lightclock_header": "Horloge lumineuse", "edt_eff_lightclock_header_desc": "Une véritable horloge lumineuse ! Ajustez les couleurs des heures, des minutes et des secondes. Un marqueur 3/6/9/12 heures est également disponible. Si l'horloge est erronée, vous devez vérifier l'horloge de votre système.", @@ -866,9 +882,9 @@ "general_btn_grantAccess": "Donner l'accès", "general_btn_iswitch": "Basculer", "general_btn_next": "Suivant", - "general_btn_off": "Off", + "general_btn_off": "Désactivation", "general_btn_ok": "OK", - "general_btn_on": "On", + "general_btn_on": "Activation", "general_btn_overwrite": "Remplacer", "general_btn_rename": "Renommer", "general_btn_restarthyperion": "Redémarrer Hyperion", @@ -887,7 +903,7 @@ "general_comp_BLACKBORDER": "Détection des bandes noires", "general_comp_BOBLIGHTSERVER": "Serveur Boblight", "general_comp_FLATBUFSERVER": "Serveur Flatbuffers", - "general_comp_FORWARDER": "Transfert", + "general_comp_FORWARDER": "Transition", "general_comp_GRABBER": "Capture d'écran", "general_comp_LEDDEVICE": "Périphérique LED", "general_comp_PROTOSERVER": "Serveur Protocol Buffers", @@ -912,6 +928,7 @@ "general_speech_en": "Anglais", "general_speech_es": "Espagnol", "general_speech_fr": "Français", + "general_speech_he": "Hébreu", "general_speech_hu": "Hongrois", "general_speech_it": "Italien", "general_speech_ja": "Japonais", @@ -970,6 +987,8 @@ "main_menu_dashboard_token": "Tableau de bord", "main_menu_effect_conf_token": "Effets", "main_menu_effectsconfigurator_token": "Configurateur d'effets", + "main_menu_event_services_token": "Services Évènements", + "main_menu_events": "Services Évènements", "main_menu_general_conf_token": "Général", "main_menu_grabber_conf_token": "Capture matérielle", "main_menu_input_selection_token": "Sélection de l'entrée", @@ -1082,6 +1101,7 @@ "wiz_cololight_noprops": "Récupération des propriétés du périphérique impossible. Définissez manuellement le nombre de LED physiques", "wiz_cololight_title": "Assistant Cololight", "wiz_guideyou": "Le $1 vous guidera à travers les réglages. Cliquez sur le bouton !", + "wiz_hue_blinkblue": "Laisse-le s'éclairer", "wiz_hue_clientkey": "Clé Client :", "wiz_hue_create_user": "Créer un nouvel utilisateur", "wiz_hue_desc1": "Il recherche automatiquement un Hue Bridge, au cas où il n'en trouverait pas, vous devez fournir l'adresse IP et appuyer sur le bouton actualiser à droite. Maintenant, vous avez besoin d'un ID utilisateur, si vous n'en avez pas, créez-en un nouveau.", @@ -1114,6 +1134,9 @@ "wiz_identify_tip": "Identifier le dispositif configuré en l'allumant", "wiz_ids_disabled": "Désactivé", "wiz_ids_entire": "Image entière", + "wiz_layout": "Générer une mise en page", + "wiz_nanoleaf_press_onoff_button": "Veuillez appuyer sur le bouton Marche/Arrêt de votre appareil Nanoleaf pendant 5 à 7 secondes.", + "wiz_nanoleaf_user_auth_intro": "L'assistant vous aide à générer un jeton d'autorisation utilisateur requis pour permettre à Hyperion d'accéder à l'appareil.", "wiz_noLights": "Pas de $1 trouvé! Veuillez connecter les lumières au réseau ou configurez les manuellement.", "wiz_pos": "Position/État", "wiz_rgb_expl": "Le point coloré change de couleur (rouge, vert) toutes les x secondes, en même temps que vos LEDs passent à cette couleur. Répondez aux questions en bas pour vérifier/corriger votre ordre d'octets.", From efda0714b3560368d1f24acc8c6e8eccfa75cf4f Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Sat, 23 Mar 2024 14:08:53 +0100 Subject: [PATCH 095/117] Update de.json (POEditor.com) --- assets/webconfig/i18n/de.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/assets/webconfig/i18n/de.json b/assets/webconfig/i18n/de.json index aa64787d..56848b0a 100644 --- a/assets/webconfig/i18n/de.json +++ b/assets/webconfig/i18n/de.json @@ -115,6 +115,10 @@ "conf_leds_layout_cl_topright": "Oben Rechts (Ecke)", "conf_leds_layout_cl_vleddepth": "Vertikale LED-Tiefe", "conf_leds_layout_frame": "Klassisches Layout (Rahmen)", + "conf_leds_layout_gapbottom": "Abstand von unten", + "conf_leds_layout_gapleft": "Abstand von links", + "conf_leds_layout_gapright": "Abstand von rechts", + "conf_leds_layout_gaptop": "Abstand von oben", "conf_leds_layout_generatedconf": "Generierte/Aktuelle LED-Konfiguration", "conf_leds_layout_generation_error": "LED Layout wurde nicht erzeugt", "conf_leds_layout_generation_success": "LED-Layout erfolgreich erstellt", @@ -969,7 +973,9 @@ "general_speech_en": "Englisch", "general_speech_es": "Spanisch", "general_speech_fr": "Französisch", + "general_speech_he": "Hebräisch", "general_speech_hu": "Ungarisch", + "general_speech_id": "Indonesisch", "general_speech_it": "Italienisch", "general_speech_ja": "Japanisch", "general_speech_nb": "Norwegisch (Bokmål)", @@ -980,6 +986,7 @@ "general_speech_ru": "Russisch", "general_speech_sv": "Schwedisch", "general_speech_tr": "Türkisch", + "general_speech_uk": "Ukrainisch", "general_speech_vi": "Vietnamesisch", "general_speech_zh-CN": "Chinesisch (vereinfacht)", "general_webui_title": "Hyperion - Web Konfiguration", From 35d8992bcc68eec8ba47a33220197817e036b635 Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Sat, 23 Mar 2024 14:08:54 +0100 Subject: [PATCH 096/117] Update sv.json (POEditor.com) --- assets/webconfig/i18n/sv.json | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/assets/webconfig/i18n/sv.json b/assets/webconfig/i18n/sv.json index b768e74b..2bf66b95 100644 --- a/assets/webconfig/i18n/sv.json +++ b/assets/webconfig/i18n/sv.json @@ -78,10 +78,10 @@ "conf_leds_layout_checkp1": "Den svartfärgade lysdioden är den första lysdioden. Detta är den punkt där data matas.", "conf_leds_layout_checkp2": "Layouten är utsikten från att stå framför TV:n, inte bakom den.", "conf_leds_layout_checkp3": "Se till att riktningen är korrekt inställd, den andra och tredje lysdioden är markerade med grått för att visa dataflödet.", - "conf_leds_layout_checkp4": "Processgap: Om du behöver ett gap, ignorera det när du anger LED topp/botten/höger/vänster och skriv sedan in under gap length hur många lysdioder du vill ta bort. Ändra nu mellanrummet för att placera mellanrummet på rätt plats.", - "conf_leds_layout_cl_bottom": "Under", - "conf_leds_layout_cl_bottomleft": "Nedre vänstra hörnet", - "conf_leds_layout_cl_bottomright": "Nedre högra hörnet", + "conf_leds_layout_checkp4": "Fallmellanrum: För att skapa ett mellanrum, ignorera det först när du definierar Övre/Nedre/Vänster/Höger och ställ sedan in din mellanrums längd för att ta bort ett antal LED-lampor. Modifiera mellanrumspositionen tills den matchar.", + "conf_leds_layout_cl_bottom": "Nedre", + "conf_leds_layout_cl_bottomleft": "Nedre vänster (hörn)", + "conf_leds_layout_cl_bottomright": "Nedre höger (hörn)", "conf_leds_layout_cl_cornergap": "Hörnavstånd", "conf_leds_layout_cl_edgegap": "Ramavstånd", "conf_leds_layout_cl_entertainment": "Underhållningsområde", @@ -115,6 +115,10 @@ "conf_leds_layout_cl_topright": "Övre högra hörnet", "conf_leds_layout_cl_vleddepth": "Vertikalt LED-djup", "conf_leds_layout_frame": "Klassisk layout (ram)", + "conf_leds_layout_gapbottom": "Nedre mellanrum", + "conf_leds_layout_gapleft": "Vänster mellanrum", + "conf_leds_layout_gapright": "Höger mellanrum", + "conf_leds_layout_gaptop": "Övre mellanrum", "conf_leds_layout_generatedconf": "Genererad/aktuell LED-konfiguration", "conf_leds_layout_generation_error": "LED-layout genererades inte", "conf_leds_layout_generation_success": "LED-layout genererad framgångsrikt", @@ -541,7 +545,7 @@ "edt_conf_v4l2_cecDetection_expl": "USB-insamling är tillfälligt inaktiverad när en CEC-standbysignal tas emot från HDMI-bussen.", "edt_conf_v4l2_cecDetection_title": "CEC-detektering", "edt_conf_v4l2_cropBottom_expl": "Antal pixlar på under som ska tas bort från bilden.", - "edt_conf_v4l2_cropBottom_title": "Beskär under", + "edt_conf_v4l2_cropBottom_title": "Beskär nedre", "edt_conf_v4l2_cropHeightValidation_error": "Beskärningstopp + Beskärningsbotten kan inte vara större än höjd ($1)", "edt_conf_v4l2_cropLeft_expl": "Antal pixlar till vänster som ska tas bort från bilden.", "edt_conf_v4l2_cropLeft_title": "Beskär vänster", @@ -850,7 +854,7 @@ "edt_eff_traces_header": "Färgspår", "edt_eff_traces_header_desc": "Kräver en ny design", "edt_eff_trails_header": "Stjärnfall", - "edt_eff_trails_header_desc": "I olika färger, gör en önskan!", + "edt_eff_trails_header_desc": "Färgade stjärnor som faller från toppen till botten.", "edt_eff_url": "Bildadress", "edt_eff_waves_header": "Vågor", "edt_eff_waves_header_desc": "Skapa vågor av färg! Blanda dina favoritfärger och välj en mittpunkt.", @@ -969,7 +973,9 @@ "general_speech_en": "Engelska", "general_speech_es": "Spanska", "general_speech_fr": "Franska", + "general_speech_he": "Hebreiska", "general_speech_hu": "Ungerska", + "general_speech_id": "Indonesiska", "general_speech_it": "Italienska", "general_speech_ja": "Japanska", "general_speech_nb": "Norska (Bokmål)", @@ -980,6 +986,7 @@ "general_speech_ru": "Ryska", "general_speech_sv": "Svenska", "general_speech_tr": "Turkiska", + "general_speech_uk": "Ukrainska", "general_speech_vi": "Vietnamesiska", "general_speech_zh-CN": "Kinesiska (förenklad)", "general_webui_title": "Hyperion - Webbkonfiguration", @@ -1183,7 +1190,7 @@ "wiz_nanoleaf_user_auth_title": "Guiden för generering av auktorisationsnyckel", "wiz_noLights": "Inga $1s hittades! Anslut $1s till nätverket eller konfigurera dem manuellt.", "wiz_pos": "Position/status", - "wiz_rgb_expl": "Färgpunkten ändrar färg (röd, grön) var x sekund, samtidigt ändrar dina lysdioder färg. Svara på frågorna nedan för att kontrollera/korrigera din RGB-byteordning.", + "wiz_rgb_expl": "Färgpricken byter färg (röd, grön) varje x sekunder, samtidigt som dina LED-lampor också byter färg. Besvara frågorna längst ner för att kontrollera/korrigera byteordningen.", "wiz_rgb_intro1": "Den här guiden hjälper dig att hitta rätt byteordning för dina lysdioder. Klicka på Fortsätt för att börja.", "wiz_rgb_intro2": "När behöver du denna assistent? För initial konfiguration eller om dina lysdioder ska vara röda, till exempel, men de är blå eller gröna.", "wiz_rgb_q": "Vilken färg visar dina lysdioder när den färgade pricken överst...", From ac57fea09a219efd89550c7409e2404163db649e Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sat, 30 Mar 2024 15:43:50 +0100 Subject: [PATCH 097/117] Fix Kodi Color Calibration, Refactor Wizards (#1718) * Fix #1674 and refactor wizards * Have own code file per LED-Device Wizard * Include SonarLint feedback * Cleanups * Apply module pattern * Address CodeQL findings * Address CodeQL findings --- assets/webconfig/js/content_huebridge.js | 52 - assets/webconfig/js/content_leds.js | 35 +- assets/webconfig/js/ui_utils.js | 29 + assets/webconfig/js/wizard.js | 2301 +---------------- .../webconfig/js/wizards/LedDevice_atmoorb.js | 283 ++ .../js/wizards/LedDevice_nanoleaf.js | 94 + .../js/wizards/LedDevice_philipshue.js | 988 +++++++ .../webconfig/js/wizards/LedDevice_utils.js | 60 + .../js/wizards/LedDevice_yeelight.js | 300 +++ .../js/wizards/colorCalibrationKodiWizard.js | 485 ++++ .../js/wizards/rgbByteOrderWizard.js | 143 + 11 files changed, 2432 insertions(+), 2338 deletions(-) delete mode 100644 assets/webconfig/js/content_huebridge.js create mode 100644 assets/webconfig/js/wizards/LedDevice_atmoorb.js create mode 100644 assets/webconfig/js/wizards/LedDevice_nanoleaf.js create mode 100644 assets/webconfig/js/wizards/LedDevice_philipshue.js create mode 100644 assets/webconfig/js/wizards/LedDevice_utils.js create mode 100644 assets/webconfig/js/wizards/LedDevice_yeelight.js create mode 100644 assets/webconfig/js/wizards/colorCalibrationKodiWizard.js create mode 100644 assets/webconfig/js/wizards/rgbByteOrderWizard.js diff --git a/assets/webconfig/js/content_huebridge.js b/assets/webconfig/js/content_huebridge.js deleted file mode 100644 index 99da3c76..00000000 --- a/assets/webconfig/js/content_huebridge.js +++ /dev/null @@ -1,52 +0,0 @@ -$(document).ready( function() { - - $("#create_user").on("click", function() { - var connectionRetries = 15; - var data = {"devicetype":"hyperion#"+Date.now()}; - var UserInterval = setInterval(function(){ - $.ajax({ - type: "POST", - url: 'http://'+$("#ip").val()+'/api', - processData: false, - timeout: 1000, - contentType: 'application/json', - data: JSON.stringify(data), - success: function(r) { - connectionRetries--; - $("#connectionTime").html(connectionRetries); - if(connectionRetries == 0) { - abortConnection(UserInterval); - } - else - { - $("#abortConnection").hide(); - $('#pairmodal').modal('show'); - $("#ip_alert").hide(); - if (typeof r[0].error != 'undefined') { - console.log("link not pressed"); - } - if (typeof r[0].success != 'undefined') { - $('#pairmodal').modal('hide'); - $('#user').val(r[0].success.username); - - $( "#hue_lights" ).empty(); - get_hue_lights(); - clearInterval(UserInterval); - } - } - }, - error: function(XMLHttpRequest, textStatus, errorThrown) { - $("#ip_alert").show(); - clearInterval(UserInterval); - } - }); - },1000); -}); - -function abortConnection(UserInterval){ - clearInterval(UserInterval); - $("#abortConnection").show(); - $('#pairmodal').modal('hide'); -} - -}); diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 0432524a..938bf8ee 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -1086,40 +1086,7 @@ $(document).ready(function () { conf_editor.validate().length || window.readOnlyMode ? $('#btn_submit_controller').prop('disabled', true) : $('#btn_submit_controller').prop('disabled', false); // LED controller specific wizards - $('#btn_wiz_holder').html(""); - $('#btn_led_device_wiz').off(); - - if (ledType == "philipshue") { - var ledWizardType = ledType; - var data = { type: ledWizardType }; - var hue_title = 'wiz_hue_title'; - changeWizard(data, hue_title, startWizardPhilipsHue); - } - else if (ledType == "nanoleaf") { - var ledWizardType = ledType; - var data = { type: ledWizardType }; - var nanoleaf_user_auth_title = 'wiz_nanoleaf_user_auth_title'; - changeWizard(data, nanoleaf_user_auth_title, startWizardNanoleafUserAuth); - $('#btn_wiz_holder').hide(); - } - else if (ledType == "atmoorb") { - var ledWizardType = (this.checked) ? "atmoorb" : ledType; - var data = { type: ledWizardType }; - var atmoorb_title = 'wiz_atmoorb_title'; - changeWizard(data, atmoorb_title, startWizardAtmoOrb); - } - else if (ledType == "yeelight") { - var ledWizardType = (this.checked) ? "yeelight" : ledType; - var data = { type: ledWizardType }; - var yeelight_title = 'wiz_yeelight_title'; - changeWizard(data, yeelight_title, startWizardYeelight); - } - - function changeWizard(data, hint, fn) { - $('#btn_wiz_holder').html("") - createHint("wizard", $.i18n(hint), "btn_wiz_holder", "btn_led_device_wiz"); - $('#btn_led_device_wiz').off().on('click', data, fn); - } + createLedDeviceWizards(ledType); conf_editor.on('ready', function () { var hwLedCountDefault = 1; diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index 0061dde9..4cbb7967 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -1393,3 +1393,32 @@ function isValidHostnameOrIP(value) { return (isValidHostnameOrIP4(value) || isValidIPv6(value) || isValidServicename(value)); } +const loadedScripts = []; + +function isScriptLoaded(src) { + return loadedScripts.indexOf(src) > -1; +} + +function loadScript(src, callback, ...params) { + if (isScriptLoaded(src)) { + debugMessage('Script ' + src + ' already loaded'); + if (callback && typeof callback === 'function') { + callback( ...params); + } + return; + } + + const script = document.createElement('script'); + script.src = src; + + script.onload = function () { + debugMessage('Script ' + src + ' loaded successfully'); + loadedScripts.push(src); + + if (callback && typeof callback === 'function') { + callback(...params); + } + }; + + document.head.appendChild(script); +} diff --git a/assets/webconfig/js/wizard.js b/assets/webconfig/js/wizard.js index 70584ddb..2524924f 100755 --- a/assets/webconfig/js/wizard.js +++ b/assets/webconfig/js/wizard.js @@ -3,2274 +3,71 @@ $(window.hyperion).one("ready", function (event) { if (getStorage("wizardactive") === 'true') { requestPriorityClear(); setStorage("wizardactive", false); - if (getStorage("kodiAddress") != null) { - kodiAddress = getStorage("kodiAddress"); - - if (getStorage("kodiPort") != null) { - kodiPort = getStorage("kodiPort"); - } - sendToKodi("stop"); - } } }); +$("#btn_wizard_colorcalibration").click(async function () { + const { colorCalibrationKodiWizard } = await import('./wizards/colorCalibrationKodiWizard.js'); + colorCalibrationKodiWizard.start(); +}); + +$('#btn_wizard_byteorder').on('click', async () => { + const { rgbByteOrderWizard } = await import('./wizards/rgbByteOrderWizard.js'); + rgbByteOrderWizard.start(); +}); + function resetWizard(reload) { $("#wizard_modal").modal('hide'); - clearInterval(wIntveralId); requestPriorityClear(); setStorage("wizardactive", false); $('#wizp1').toggle(true); $('#wizp2').toggle(false); $('#wizp3').toggle(false); - //cc - if (withKodi) - sendToKodi("stop"); - step = 0; - if (!reload) location.reload(); -} - -//rgb byte order wizard -var wIntveralId; -var new_rgb_order; - -function changeColor() { - var color = $("#wiz_canv_color").css('background-color'); - - if (color == 'rgb(255, 0, 0)') { - $("#wiz_canv_color").css('background-color', 'rgb(0, 255, 0)'); - requestSetColor('0', '255', '0'); - } - else { - $("#wiz_canv_color").css('background-color', 'rgb(255, 0, 0)'); - requestSetColor('255', '0', '0'); + if (!reload) { + location.reload(); } } -function startWizardRGB() { - //create html - $('#wiz_header').html('' + $.i18n('wiz_rgb_title')); - $('#wizp1_body').html('

    ' + $.i18n('wiz_rgb_title') + '

    ' + $.i18n('wiz_rgb_intro1') + '

    ' + $.i18n('wiz_rgb_intro2') + '

    '); - $('#wizp1_footer').html(''); - $('#wizp2_body').html('

    ' + $.i18n('wiz_rgb_expl') + '

    '); - $('#wizp2_body').append('
    ' + $.i18n('edt_append_s') + '
    '); - $('#wizp2_body').append(''); - $('#wizp2_body').append('
    '); - $('#wizp2_footer').html(''); +function createLedDeviceWizards(ledType) { - if (getStorage("darkMode") == "on") - $('#wizard_logo').attr("src", 'img/hyperion/logo_negativ.png'); + let data = {}; + let title; - //open modal - $("#wizard_modal").modal({ - backdrop: "static", - keyboard: false, - show: true - }); + $('#btn_wiz_holder').html(""); + $('#btn_led_device_wiz').off(); + if (ledType == "philipshue") { + $('#btn_wiz_holder').show(); + data = { ledType }; + title = 'wiz_hue_title'; + } + else if (ledType == "nanoleaf") { + $('#btn_wiz_holder').hide(); + data = { ledType }; + title = 'wiz_nanoleaf_user_auth_title'; + } + else if (ledType == "atmoorb") { + $('#btn_wiz_holder').show(); + data = { ledType }; + title = 'wiz_atmoorb_title'; + } + else if (ledType == "yeelight") { + $('#btn_wiz_holder').show(); + data = { ledType }; + title = 'wiz_yeelight_title'; + } - //listen for continue - $('#btn_wiz_cont').off().on('click', function () { - beginWizardRGB(); - $('#wizp1').toggle(false); - $('#wizp2').toggle(true); + if (Object.keys(data).length !== 0) { + startLedDeviceWizard(data, title, ledType + "Wizard"); + } +} + +function startLedDeviceWizard(data, hint, wizardName) { + $('#btn_wiz_holder').html("") + 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); }); } -function beginWizardRGB() { - $("#wiz_switchtime_select").off().on('change', function () { - clearInterval(wIntveralId); - var time = $("#wiz_switchtime_select").val(); - wIntveralId = setInterval(function () { changeColor(); }, time * 1000); - }); - - $('.wselect').on("change", function () { - var rgb_order = window.serverConfig.device.colorOrder.split(""); - var redS = $("#wiz_r_select").val(); - var greenS = $("#wiz_g_select").val(); - var blueS = rgb_order.toString().replace(/,/g, "").replace(redS, "").replace(greenS, ""); - - for (var i = 0; i < rgb_order.length; i++) { - if (redS == rgb_order[i]) - $('#wiz_g_select option[value=' + rgb_order[i] + ']').prop('disabled', true); - else - $('#wiz_g_select option[value=' + rgb_order[i] + ']').prop('disabled', false); - if (greenS == rgb_order[i]) - $('#wiz_r_select option[value=' + rgb_order[i] + ']').prop('disabled', true); - else - $('#wiz_r_select option[value=' + rgb_order[i] + ']').prop('disabled', false); - } - - if (redS != 'null' && greenS != 'null') { - $('#btn_wiz_save').prop('disabled', false); - - for (var i = 0; i < rgb_order.length; i++) { - if (rgb_order[i] == "r") - rgb_order[i] = redS; - else if (rgb_order[i] == "g") - rgb_order[i] = greenS; - else - rgb_order[i] = blueS; - } - - rgb_order = rgb_order.toString().replace(/,/g, ""); - - if (redS == "r" && greenS == "g") { - $('#btn_wiz_save').toggle(false); - $('#btn_wiz_checkok').toggle(true); - - window.readOnlyMode ? $('#btn_wiz_checkok').prop('disabled', true) : $('#btn_wiz_checkok').prop('disabled', false); - } - else { - $('#btn_wiz_save').toggle(true); - window.readOnlyMode ? $('#btn_wiz_save').prop('disabled', true) : $('#btn_wiz_save').prop('disabled', false); - - $('#btn_wiz_checkok').toggle(false); - } - new_rgb_order = rgb_order; - } - else - $('#btn_wiz_save').prop('disabled', true); - }); - - $("#wiz_switchtime_select").append(createSelOpt('5', '5'), createSelOpt('10', '10'), createSelOpt('15', '15'), createSelOpt('30', '30')); - $("#wiz_switchtime_select").trigger('change'); - - $("#wiz_r_select").append(createSelOpt("null", ""), createSelOpt('r', $.i18n('general_col_red')), createSelOpt('g', $.i18n('general_col_green')), createSelOpt('b', $.i18n('general_col_blue'))); - $("#wiz_g_select").html($("#wiz_r_select").html()); - $("#wiz_r_select").trigger('change'); - - requestSetColor('255', '0', '0'); - setTimeout(requestSetSource, 100, 'auto'); - setStorage("wizardactive", true); - - $('#btn_wiz_abort').off().on('click', function () { resetWizard(true); }); - - $('#btn_wiz_checkok').off().on('click', function () { - showInfoDialog('success', "", $.i18n('infoDialog_wizrgb_text')); - resetWizard(); - }); - - $('#btn_wiz_save').off().on('click', function () { - resetWizard(); - window.serverConfig.device.colorOrder = new_rgb_order; - requestWriteConfig({ "device": window.serverConfig.device }); - }); -} - -$('#btn_wizard_byteorder').off().on('click', startWizardRGB); - -//color calibration wizard - -const defaultKodiPort = 9090; - -var kodiAddress = document.location.hostname; -var kodiPort = defaultKodiPort; - -var kodiUrl = new URL("ws://" + kodiAddress); -kodiUrl.port = kodiPort; -kodiUrl.pathname = "/jsonrpc/websocket"; - -var wiz_editor; -var colorLength; -var cobj; -var step = 0; -var withKodi = false; -var profile = 0; -var websAddress; -var imgAddress; -var vidAddress = "https://sourceforge.net/projects/hyperion-project/files/resources/vid/"; -var picnr = 0; -var availVideos = ["Sweet_Cocoon", "Caminandes_2_GranDillama", "Caminandes_3_Llamigos"]; - -if (getStorage("kodiAddress") != null) { - - kodiAddress = getStorage("kodiAddress"); - kodiUrl.host = kodiAddress; -} - -if (getStorage("kodiPort") != null) { - kodiPort = getStorage("kodiPort"); - kodiUrl.port = kodiPort; -} - -function switchPicture(pictures) { - if (typeof pictures[picnr] === 'undefined') - picnr = 0; - - sendToKodi('playP', pictures[picnr]); - picnr++; -} - -function sendToKodi(type, content, cb) { - var command; - - switch (type) { - case "msg": - command = { "jsonrpc": "2.0", "method": "GUI.ShowNotification", "params": { "title": $.i18n('wiz_cc_title'), "message": content, "image": "info", "displaytime": 5000 }, "id": "1" }; - break; - case "stop": - command = { "jsonrpc": "2.0", "method": "Player.Stop", "params": { "playerid": 2 }, "id": "1" }; - break; - case "playP": - content = imgAddress + content + '.png'; - command = { "jsonrpc": "2.0", "method": "Player.Open", "params": { "item": { "file": content } }, "id": "1" }; - break; - case "playV": - content = vidAddress + content; - command = { "jsonrpc": "2.0", "method": "Player.Open", "params": { "item": { "file": content } }, "id": "1" }; - break; - case "rotate": - command = { "jsonrpc": "2.0", "method": "Player.Rotate", "params": { "playerid": 2 }, "id": "1" }; - break; - default: - if (cb != undefined) { - cb("error"); - } - } - - if ("WebSocket" in window) { - - if (kodiUrl.port === '') { - kodiUrl.port = defaultKodiPort; - } - var ws = new WebSocket(kodiUrl); - - ws.onopen = function () { - ws.send(JSON.stringify(command)); - }; - - ws.onmessage = function (evt) { - var response = JSON.parse(evt.data); - if (response.method === "System.OnQuit") { - ws.close(); - } else { - if (cb != undefined) { - if (response.result != undefined) { - if (response.result === "OK") { - cb("success"); - ws.close(); - } else { - cb("error"); - ws.close(); - } - } - } - } - }; - - ws.onerror = function (evt) { - if (cb != undefined) { - cb("error"); - ws.close(); - } - }; - - ws.onclose = function (evt) { - }; - - } - else { - console.log("Kodi Access: WebSocket NOT supported by this browser"); - cb("error"); - } -} - -function performAction() { - var h; - - if (step == 1) { - $('#wiz_cc_desc').html($.i18n('wiz_cc_chooseid')); - updateWEditor(["id"]); - $('#btn_wiz_back').prop("disabled", true); - } - else - $('#btn_wiz_back').prop("disabled", false); - - if (step == 2) { - updateWEditor(["white"]); - h = $.i18n('wiz_cc_adjustit', $.i18n('edt_conf_color_white_title')); - if (withKodi) { - h += '
    ' + $.i18n('wiz_cc_kodishould', $.i18n('edt_conf_color_white_title')); - sendToKodi('playP', "white"); - } - else - h += '
    ' + $.i18n('wiz_cc_lettvshow', $.i18n('edt_conf_color_white_title')); - $('#wiz_cc_desc').html(h); - } - if (step == 3) { - updateWEditor(["gammaRed", "gammaGreen", "gammaBlue"]); - h = '

    ' + $.i18n('wiz_cc_adjustgamma') + '

    '; - if (withKodi) { - sendToKodi('playP', "HGradient"); - h += ''; - } - else - h += '

    ' + $.i18n('wiz_cc_lettvshowm', "grey_1, grey_2, grey_3, HGradient, VGradient") + '

    '; - $('#wiz_cc_desc').html(h); - $('#wiz_cc_btn_sp').off().on('click', function () { - switchPicture(["VGradient", "grey_1", "grey_2", "grey_3", "HGradient"]); - }); - } - if (step == 4) { - updateWEditor(["red"]); - h = $.i18n('wiz_cc_adjustit', $.i18n('edt_conf_color_red_title')); - if (withKodi) { - h += '
    ' + $.i18n('wiz_cc_kodishould', $.i18n('edt_conf_color_red_title')); - sendToKodi('playP', "red"); - } - else - h += '
    ' + $.i18n('wiz_cc_lettvshow', $.i18n('edt_conf_color_red_title')); - $('#wiz_cc_desc').html(h); - } - if (step == 5) { - updateWEditor(["green"]); - h = $.i18n('wiz_cc_adjustit', $.i18n('edt_conf_color_green_title')); - if (withKodi) { - h += '
    ' + $.i18n('wiz_cc_kodishould', $.i18n('edt_conf_color_green_title')); - sendToKodi('playP', "green"); - } - else - h += '
    ' + $.i18n('wiz_cc_lettvshow', $.i18n('edt_conf_color_green_title')); - $('#wiz_cc_desc').html(h); - } - if (step == 6) { - updateWEditor(["blue"]); - h = $.i18n('wiz_cc_adjustit', $.i18n('edt_conf_color_blue_title')); - if (withKodi) { - h += '
    ' + $.i18n('wiz_cc_kodishould', $.i18n('edt_conf_color_blue_title')); - sendToKodi('playP', "blue"); - } - else - h += '
    ' + $.i18n('wiz_cc_lettvshow', $.i18n('edt_conf_color_blue_title')); - $('#wiz_cc_desc').html(h); - } - if (step == 7) { - updateWEditor(["cyan"]); - h = $.i18n('wiz_cc_adjustit', $.i18n('edt_conf_color_cyan_title')); - if (withKodi) { - h += '
    ' + $.i18n('wiz_cc_kodishould', $.i18n('edt_conf_color_cyan_title')); - sendToKodi('playP', "cyan"); - } - else - h += '
    ' + $.i18n('wiz_cc_lettvshow', $.i18n('edt_conf_color_cyan_title')); - $('#wiz_cc_desc').html(h); - } - if (step == 8) { - updateWEditor(["magenta"]); - h = $.i18n('wiz_cc_adjustit', $.i18n('edt_conf_color_magenta_title')); - if (withKodi) { - h += '
    ' + $.i18n('wiz_cc_kodishould', $.i18n('edt_conf_color_magenta_title')); - sendToKodi('playP', "magenta"); - } - else - h += '
    ' + $.i18n('wiz_cc_lettvshow', $.i18n('edt_conf_color_magenta_title')); - $('#wiz_cc_desc').html(h); - } - if (step == 9) { - updateWEditor(["yellow"]); - h = $.i18n('wiz_cc_adjustit', $.i18n('edt_conf_color_yellow_title')); - if (withKodi) { - h += '
    ' + $.i18n('wiz_cc_kodishould', $.i18n('edt_conf_color_yellow_title')); - sendToKodi('playP', "yellow"); - } - else - h += '
    ' + $.i18n('wiz_cc_lettvshow', $.i18n('edt_conf_color_yellow_title')); - $('#wiz_cc_desc').html(h); - } - if (step == 10) { - updateWEditor(["backlightThreshold", "backlightColored"]); - h = $.i18n('wiz_cc_backlight'); - if (withKodi) { - h += '
    ' + $.i18n('wiz_cc_kodishould', $.i18n('edt_conf_color_black_title')); - sendToKodi('playP', "black"); - } - else - h += '
    ' + $.i18n('wiz_cc_lettvshow', $.i18n('edt_conf_color_black_title')); - $('#wiz_cc_desc').html(h); - } - if (step == 11) { - updateWEditor([""], true); - h = '

    ' + $.i18n('wiz_cc_testintro') + '

    '; - if (withKodi) { - h += '

    ' + $.i18n('wiz_cc_testintrok') + '

    '; - sendToKodi('stop'); - for (var i = 0; i < availVideos.length; i++) { - var txt = availVideos[i].replace(/_/g, " "); - h += '
    '; - } - h += '
    '; - } - else - h += '

    ' + $.i18n('wiz_cc_testintrowok') + ' ' + $.i18n('wiz_cc_link') + '

    '; - h += '

    ' + $.i18n('wiz_cc_summary') + '

    '; - $('#wiz_cc_desc').html(h); - - $('.videobtn').off().on('click', function (e) { - if (e.target.id == "stop") - sendToKodi("stop"); - else - sendToKodi("playV", e.target.id + '.mp4'); - - $(this).prop("disabled", true); - setTimeout(function () { $('.videobtn').prop("disabled", false) }, 10000); - }); - - $('#btn_wiz_next').prop("disabled", true); - $('#btn_wiz_save').toggle(true); - window.readOnlyMode ? $('#btn_wiz_save').prop('disabled', true) : $('#btn_wiz_save').prop('disabled', false); - } - else { - $('#btn_wiz_next').prop("disabled", false); - $('#btn_wiz_save').toggle(false); - } -} - -function updateWEditor(el, all) { - for (var key in cobj) { - if (all === true || el[0] == key || el[1] == key || el[2] == key) - $('#editor_container_wiz [data-schemapath*=".' + profile + '.' + key + '"]').toggle(true); - else - $('#editor_container_wiz [data-schemapath*=".' + profile + '.' + key + '"]').toggle(false); - } -} - -function startWizardCC() { - - //create html - $('#wiz_header').html('' + $.i18n('wiz_cc_title')); - $('#wizp1_body').html('

    ' + $.i18n('wiz_cc_title') + '

    ' + - '

    ' + $.i18n('wiz_cc_intro1') + '

    ' + - '' + - '' + - '' - ); - $('#wizp1_footer').html('' + - '' - ); - $('#wizp2_body').html('
    ' - ); - $('#wizp2_footer').html('' + - '' + - '' + - '' - ); - - if (getStorage("darkMode") == "on") - $('#wizard_logo').prop("src", 'img/hyperion/logo_negativ.png'); - - //open modal - $("#wizard_modal").modal({ - backdrop: "static", - keyboard: false, - show: true - }); - - $('#wiz_cc_kodiip').off().on('change', function () { - - kodiAddress = encodeURIComponent($(this).val().trim()); - - $('#kodi_status').html(''); - if (kodiAddress !== "") { - - if (!isValidHostnameOrIP(kodiAddress)) { - - $('#kodi_status').html('

    ' + $.i18n('edt_msgcust_error_hostname_ip') + '

    '); - withKodi = false; - - } else { - - if (isValidIPv6(kodiAddress)) { - kodiUrl.hostname = "[" + kodiAddress + "]"; - } else { - kodiUrl.hostname = kodiAddress; - } - - $('#kodi_status').html('

    ' + $.i18n('wiz_cc_try_connect') + '

    '); - $('#btn_wiz_cont').prop('disabled', true); - - sendToKodi("msg", $.i18n('wiz_cc_kodimsg_start'), function (cb) { - if (cb == "error") { - $('#kodi_status').html('

    ' + $.i18n('wiz_cc_kodidiscon') + '

    ' + $.i18n('wiz_cc_kodidisconlink') + ' ' + $.i18n('wiz_cc_link') + '

    '); - withKodi = false; - } - else { - setStorage("kodiAddress", kodiAddress); - setStorage("kodiPort", defaultKodiPort); - - $('#kodi_status').html('

    ' + $.i18n('wiz_cc_kodicon') + '

    '); - withKodi = true; - } - - $('#btn_wiz_cont').prop('disabled', false); - }); - } - } - }); - - //listen for continue - $('#btn_wiz_cont').off().on('click', function () { - beginWizardCC(); - $('#wizp1').toggle(false); - $('#wizp2').toggle(true); - }); - - $('#wiz_cc_kodiip').trigger("change"); - colorLength = window.serverConfig.color.channelAdjustment; - cobj = window.schema.color.properties.channelAdjustment.items.properties; - websAddress = document.location.hostname + ':' + window.serverConfig.webConfig.port; - imgAddress = 'http://' + websAddress + '/img/cc/'; - setStorage("wizardactive", true); - - //check profile count - if (colorLength.length > 1) { - $('#multi_cali').html('

    ' + $.i18n('wiz_cc_morethanone') + '

    '); - for (var i = 0; i < colorLength.length; i++) - $('#wiz_select').append(createSelOpt(i, i + 1 + ' (' + colorLength[i].id + ')')); - - $('#wiz_select').off().on('change', function () { - profile = $(this).val(); - }); - } - - //prepare editor - wiz_editor = createJsonEditor('editor_container_wiz', { - color: window.schema.color - }, true, true); - - $('#editor_container_wiz h4').toggle(false); - $('#editor_container_wiz .btn-group').toggle(false); - $('#editor_container_wiz [data-schemapath="root.color.imageToLedMappingType"]').toggle(false); - for (var i = 0; i < colorLength.length; i++) - $('#editor_container_wiz [data-schemapath*="root.color.channelAdjustment.' + i + '."]').toggle(false); -} - -function beginWizardCC() { - $('#btn_wiz_next').off().on('click', function () { - step++; - performAction(); - }); - - $('#btn_wiz_back').off().on('click', function () { - step--; - performAction(); - }); - - $('#btn_wiz_abort').off().on('click', resetWizard); - - $('#btn_wiz_save').off().on('click', function () { - requestWriteConfig(wiz_editor.getValue()); - resetWizard(); - }); - - wiz_editor.on("change", function (e) { - var val = wiz_editor.getEditor('root.color.channelAdjustment.' + profile + '').getValue(); - var temp = JSON.parse(JSON.stringify(val)); - delete temp.leds - requestAdjustment(JSON.stringify(temp), "", true); - }); - - step++ - performAction(); -} - -$('#btn_wizard_colorcalibration').off().on('click', startWizardCC); - -// Layout positions -var lightPosTop = { hmin: 0.15, hmax: 0.85, vmin: 0, vmax: 0.2 }; -var lightPosTopLeft = { hmin: 0, hmax: 0.15, vmin: 0, vmax: 0.15 }; -var lightPosTopRight = { hmin: 0.85, hmax: 1.0, vmin: 0, vmax: 0.15 }; -var lightPosBottom = { hmin: 0.15, hmax: 0.85, vmin: 0.8, vmax: 1.0 }; -var lightPosBottomLeft = { hmin: 0, hmax: 0.15, vmin: 0.85, vmax: 1.0 }; -var lightPosBottomRight = { hmin: 0.85, hmax: 1.0, vmin: 0.85, vmax: 1.0 }; -var lightPosLeft = { hmin: 0, hmax: 0.15, vmin: 0.15, vmax: 0.85 }; -var lightPosLeftTop = { hmin: 0, hmax: 0.15, vmin: 0, vmax: 0.5 }; -var lightPosLeftMiddle = { hmin: 0, hmax: 0.15, vmin: 0.25, vmax: 0.75 }; -var lightPosLeftBottom = { hmin: 0, hmax: 0.15, vmin: 0.5, vmax: 1.0 }; -var lightPosRight = { hmin: 0.85, hmax: 1.0, vmin: 0.15, vmax: 0.85 }; -var lightPosRightTop = { hmin: 0.85, hmax: 1.0, vmin: 0, vmax: 0.5 }; -var lightPosRightMiddle = { hmin: 0.85, hmax: 1.0, vmin: 0.25, vmax: 0.75 }; -var lightPosRightBottom = { hmin: 0.85, hmax: 1.0, vmin: 0.5, vmax: 1.0 }; -var lightPosEntire = { hmin: 0.0, hmax: 1.0, vmin: 0.0, vmax: 1.0 }; - -var lightPosBottomLeft14 = { hmin: 0, hmax: 0.25, vmin: 0.85, vmax: 1.0 }; -var lightPosBottomLeft12 = { hmin: 0.25, hmax: 0.5, vmin: 0.85, vmax: 1.0 }; -var lightPosBottomLeft34 = { hmin: 0.5, hmax: 0.75, vmin: 0.85, vmax: 1.0 }; -var lightPosBottomLeft11 = { hmin: 0.75, hmax: 1, vmin: 0.85, vmax: 1.0 }; - -var lightPosBottomLeft112 = { hmin: 0, hmax: 0.5, vmin: 0.85, vmax: 1.0 }; -var lightPosBottomLeft121 = { hmin: 0.5, hmax: 1, vmin: 0.85, vmax: 1.0 }; -var lightPosBottomLeftNewMid = { hmin: 0.25, hmax: 0.75, vmin: 0.85, vmax: 1.0 }; - -var lightPosTopLeft112 = { hmin: 0, hmax: 0.5, vmin: 0, vmax: 0.15 }; -var lightPosTopLeft121 = { hmin: 0.5, hmax: 1, vmin: 0, vmax: 0.15 }; -var lightPosTopLeftNewMid = { hmin: 0.25, hmax: 0.75, vmin: 0, vmax: 0.15 }; - -function assignLightPos(pos, name) { - var i = null; - - if (pos === "top") - i = lightPosTop; - else if (pos === "topleft") - i = lightPosTopLeft; - else if (pos === "topright") - i = lightPosTopRight; - else if (pos === "bottom") - i = lightPosBottom; - else if (pos === "bottomleft") - i = lightPosBottomLeft; - else if (pos === "bottomright") - i = lightPosBottomRight; - else if (pos === "left") - i = lightPosLeft; - else if (pos === "lefttop") - i = lightPosLeftTop; - else if (pos === "leftmiddle") - i = lightPosLeftMiddle; - else if (pos === "leftbottom") - i = lightPosLeftBottom; - else if (pos === "right") - i = lightPosRight; - else if (pos === "righttop") - i = lightPosRightTop; - else if (pos === "rightmiddle") - i = lightPosRightMiddle; - else if (pos === "rightbottom") - i = lightPosRightBottom; - else if (pos === "lightPosBottomLeft14") - i = lightPosBottomLeft14; - else if (pos === "lightPosBottomLeft12") - i = lightPosBottomLeft12; - else if (pos === "lightPosBottomLeft34") - i = lightPosBottomLeft34; - else if (pos === "lightPosBottomLeft11") - i = lightPosBottomLeft11; - else if (pos === "lightPosBottomLeft112") - i = lightPosBottomLeft112; - else if (pos === "lightPosBottomLeft121") - i = lightPosBottomLeft121; - else if (pos === "lightPosBottomLeftNewMid") - i = lightPosBottomLeftNewMid; - else if (pos === "lightPosTopLeft112") - i = lightPosTopLeft112; - else if (pos === "lightPosTopLeft121") - i = lightPosTopLeft121; - else if (pos === "lightPosTopLeftNewMid") - i = lightPosTopLeftNewMid; - else - i = lightPosEntire; - - i.name = name; - return i; -} - -function getHostInLights(hostname) { - return lights.filter( - function (lights) { - return lights.host === hostname - } - ); -} - -function getIpInLights(ip) { - return lights.filter( - function (lights) { - return lights.ip === ip - } - ); -} - -function getIdInLights(id) { - return lights.filter( - function (lights) { - return lights.id === id - } - ); -} - -// External properties properties, 2-dimensional arry of [ledType][key] -devicesProperties = {}; - -//**************************** -// Wizard Philips Hue -//**************************** - -var hueIPs = []; -var hueIPsinc = 0; -var hueLights = []; -var hueEntertainmentConfigs = []; -var hueEntertainmentServices = []; -var lightLocation = []; -var groupLights = []; -var groupChannels = []; -var groupLightsLocations = []; -var isAPIv2Ready = true; -var isEntertainmentReady = true; - -function startWizardPhilipsHue(e) { - //create html - - var hue_title = 'wiz_hue_title'; - var hue_intro1 = 'wiz_hue_e_intro1'; - var hue_desc1 = 'wiz_hue_desc1'; - var hue_create_user = 'wiz_hue_create_user'; - - $('#wiz_header').html('' + $.i18n(hue_title)); - $('#wizp1_body').html('

    ' + $.i18n(hue_title) + '

    ' + $.i18n(hue_intro1) + '

    '); - $('#wizp1_footer').html(''); - $('#wizp2_body').html('
    '); - - var topContainer_html = '

    ' + $.i18n(hue_desc1) + '

    ' + - '
    ' + - '
    ' + - '

    ' + $.i18n('wiz_hue_ip') + '

    ' + - '
    ' + - ' ' + - ' ' + '
    ' + - '
    ' + - ' ' + - '
    '; - - if (storedAccess === 'expert') { - topContainer_html += '
    ' + - ':' + - '
    '; - } - - topContainer_html += '

    '; - topContainer_html += ''; - - $('#wh_topcontainer').append(topContainer_html); - - $('#usrcont').append('

    ' + $.i18n('wiz_hue_username') + '

    ' + - '
    ' + - '
    ' + - ' ' + - ' ' + - '

    ' + - '' - ); - - $('#usrcont').append('

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


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

    <\p>' + - ''); - - $('#wizp2_body').append('

    '); - createTable("gidsh", "gidsb", "hue_grp_ids_t"); - $('.gidsh').append(createTableRow([$.i18n('edt_dev_spec_groupId_title'), ""], true)); - - $('#wizp2_body').append(''); - - createTable("lidsh", "lidsb", "hue_ids_t"); - $('.lidsh').append(createTableRow([$.i18n('edt_dev_spec_lightid_title'), $.i18n('wiz_pos'), $.i18n('wiz_identify')], true)); - $('#wizp2_footer').html(''); - $('#wizp3_body').html('' + $.i18n('wiz_hue_press_link') + '


    '); - - if (getStorage("darkMode") == "on") - $('#wizard_logo').attr("src", 'img/hyperion/logo_negativ.png'); - - //open modal - $("#wizard_modal").modal({ - backdrop: "static", - keyboard: false, - show: true - }); - - //listen for continue - $('#btn_wiz_cont').off().on('click', function () { - beginWizardHue(); - $('#wizp1').toggle(false); - $('#wizp2').toggle(true); - }); -} - -function checkHueBridge(cb, hueUser) { - var usr = (typeof hueUser != "undefined") ? hueUser : 'config'; - if (usr === 'config') { - $('#wiz_hue_discovered').html(""); - } - - if (hueIPs[hueIPsinc]) { - var host = hueIPs[hueIPsinc].host; - var port = hueIPs[hueIPsinc].port; - - if (usr != '') - { - getProperties_hue_bridge(cb, decodeURIComponent(host), port, usr); - } - else - { - cb(false, usr); - } - - if (isAPIv2Ready) { - $('#port').val(443); - } - } -} - -function checkBridgeResult(reply, usr) { - if (reply) { - //abort checking, first reachable result is used - $('#wiz_hue_ipstate').html(""); - $('#host').val(hueIPs[hueIPsinc].host) - $('#port').val(hueIPs[hueIPsinc].port) - - $('#usrcont').toggle(true); - - checkHueBridge(checkUserResult, $('#user').val()); - } - else { - $('#usrcont').toggle(false); - $('#wiz_hue_ipstate').html($.i18n('wiz_hue_failure_ip')); - } -}; - -function checkUserResult(reply, username) { - $('#usrcont').toggle(true); - - var hue_create_user = 'wiz_hue_e_create_user'; - if (!isEntertainmentReady) { - hue_create_user = 'wiz_hue_create_user'; - $('#hue_client_key_r').toggle(false); - } else { - $('#hue_client_key_r').toggle(true); - } - - $('#wiz_hue_create_user').text($.i18n(hue_create_user)); - $('#wiz_hue_create_user').toggle(true); - - if (reply) { - $('#user').val(username); - - if (isEntertainmentReady && $('#clientkey').val() == "") { - $('#wiz_hue_usrstate').html($.i18n('wiz_hue_e_clientkey_needed')); - $('#wiz_hue_create_user').toggle(true); - } else { - $('#wiz_hue_usrstate').html(""); - $('#wiz_hue_create_user').toggle(false); - - if (isEntertainmentReady) { - $('#hue_id_headline').text($.i18n('wiz_hue_e_desc3')); - $('#hue_grp_ids_t').toggle(true); - - get_hue_groups(username); - - } else { - $('#hue_id_headline').text($.i18n('wiz_hue_desc2')); - $('#hue_grp_ids_t').toggle(false); - - get_hue_lights(username); - - } - } - } - else { - //abort checking, first reachable result is used - $('#wiz_hue_usrstate').html($.i18n('wiz_hue_failure_user')); - $('#wiz_hue_create_user').toggle(true); - } -}; - -function useGroupId(id, username) { - $('#groupId').val(hueEntertainmentConfigs[id].id); - if (isAPIv2Ready) { - var group = hueEntertainmentConfigs[id]; - - groupLights = []; - for (const light of group.light_services) { - groupLights.push(light.rid); - } - - groupChannels = []; - for (const channel of group.channels) { - groupChannels.push(channel); - } - - groupLightsLocations = []; - for (const location of group.locations.service_locations) { - groupLightsLocations.push(location); - } - } else { - //Ensure ligthIDs are strings - groupLights = hueEntertainmentConfigs[id].lights.map(num => { - return String(num); - }); - - var lightLocations = hueEntertainmentConfigs[id].locations; - for (var locationID in lightLocations) { - var lightLocation = {}; - - let position = { - x: lightLocations[locationID][0], - y: lightLocations[locationID][1], - z: lightLocations[locationID][2] - }; - lightLocation.position = position; - - groupLightsLocations.push(lightLocation); - } - } - - get_hue_lights(username); -} - -function updateBridgeDetails(properties) { - var ledDeviceProperties = properties.config; - - if (!jQuery.isEmptyObject(ledDeviceProperties)) { - isEntertainmentReady = properties.isEntertainmentReady; - isAPIv2Ready = properties.isAPIv2Ready; - - if (ledDeviceProperties.name && ledDeviceProperties.bridgeid && ledDeviceProperties.modelid) { - $('#wiz_hue_discovered').html( - "Bridge: " + ledDeviceProperties.name + - ", Modelid: " + ledDeviceProperties.modelid + - ", Firmware: " + ledDeviceProperties.swversion + "
    " + - "API-Version: " + ledDeviceProperties.apiversion + - ", Entertainment: " + (isEntertainmentReady ? "✓" : "-") + - ", APIv2: " + (isAPIv2Ready ? "✓" : "-") - ); - } - } -} - -async function discover_hue_bridges() { - $('#wiz_hue_ipstate').html($.i18n('edt_dev_spec_devices_discovery_inprogress')); - - // $('#wiz_hue_discovered').html("") - const res = await requestLedDeviceDiscovery('philipshue'); - if (res && !res.error) { - const r = res.info; - - // Process devices returned by discovery - if (r.devices.length == 0) { - $('#wiz_hue_ipstate').html($.i18n('wiz_hue_failure_ip')); - $('#wiz_hue_discovered').html("") - } - else { - hueIPs = []; - hueIPsinc = 0; - - var discoveryMethod = "ssdp"; - if (res.info.discoveryMethod) { - discoveryMethod = res.info.discoveryMethod; - } - - for (const device of r.devices) { - if (device) { - var host; - var port; - if (discoveryMethod === "ssdp") { - if (device.hostname && device.domain) { - host = device.hostname + "." + device.domain; - port = device.port; - } else { - host = device.ip; - port = device.port; - } - } else { - host = device.service; - port = device.port; - } - - if (host) { - - if (!hueIPs.some(item => item.host === host)) { - hueIPs.push({ host: host, port: port }); - } - } - } - } - - $('#wiz_hue_ipstate').html(""); - $('#host').val(hueIPs[hueIPsinc].host) - $('#port').val(hueIPs[hueIPsinc].port) - - $('#hue_bridge_select').html(""); - - for (var key in hueIPs) { - $('#hue_bridge_select').append(createSelOpt(key, hueIPs[key].host)); - } - - $('.hue_bridge_sel_watch').on("click", function () { - hueIPsinc = $(this).val(); - - var name = $("#hue_bridge_select option:selected").text(); - $('#host').val(name); - $('#port').val(hueIPs[hueIPsinc].port) - - var usr = $('#user').val(); - if (usr != "") { - checkHueBridge(checkUserResult, usr); - } else { - checkHueBridge(checkBridgeResult); - } - }); - - $('.hue_bridge_sel_watch').click(); - } - } -} - -async function getProperties_hue_bridge(cb, hostAddress, port, username, resourceFilter) { - let params = { host: hostAddress, username: username, filter: resourceFilter }; - if (port !== 'undefined') { - params.port = parseInt(port); - } - - var ledType = 'philipshue'; - var key = hostAddress; - - //Create ledType cache entry - if (!devicesProperties[ledType]) { - devicesProperties[ledType] = {}; - } - - // Use device's properties, if properties in chache - if (devicesProperties[ledType][key] && devicesProperties[ledType][key][username]) { - updateBridgeDetails(devicesProperties[ledType][key]); - cb(true, username); - } else { - const res = await requestLedDeviceProperties(ledType, params); - if (res && !res.error) { - var ledDeviceProperties = res.info.properties; - if (!jQuery.isEmptyObject(ledDeviceProperties)) { - - devicesProperties[ledType][key] = {}; - devicesProperties[ledType][key][username] = ledDeviceProperties; - - isAPIv2Ready = res.info.isAPIv2Ready; - devicesProperties[ledType][key].isAPIv2Ready = isAPIv2Ready; - isEntertainmentReady = res.info.isEntertainmentReady; - devicesProperties[ledType][key].isEntertainmentReady = isEntertainmentReady; - - updateBridgeDetails(devicesProperties[ledType][key]); - if (username === "config") { - cb(true); - } else { - cb(true, username); - } - } else { - cb(false, username); - } - } else { - cb(false, username); - } - } -} - -async function identify_hue_device(hostAddress, port, username, name, id, id_v1) { - var disabled = $('#btn_wiz_save').is(':disabled'); - // Take care that new record cannot be save during background process - $('#btn_wiz_save').prop('disabled', true); - - let params = { host: decodeURIComponent(hostAddress), username: username, lightName: decodeURIComponent(name), lightId: id, lightId_v1: id_v1 }; - - if (port !== 'undefined') { - params.port = parseInt(port); - } - - await requestLedDeviceIdentification('philipshue', params); - - if (!window.readOnlyMode) { - $('#btn_wiz_save').prop('disabled', disabled); - } -} - -//return editor Value -function eV(vn, defaultVal = "") { - var editor = (vn) ? conf_editor.getEditor("root.specificOptions." + vn) : null; - return (editor == null) ? defaultVal : ((defaultVal != "" && !isNaN(defaultVal) && isNaN(editor.getValue())) ? defaultVal : editor.getValue()); -} - -function beginWizardHue() { - var usr = eV("username"); - if (usr != "") { - $('#user').val(usr); - } - - var clkey = eV("clientkey"); - if (clkey != "") { - $('#clientkey').val(clkey); - } - - //check if host is empty/reachable/search for bridge - if (eV("host") == "") { - hueIPs = []; - hueIPsinc = 0; - - discover_hue_bridges(); - } - else { - var host = eV("host"); - $('#host').val(host); - - var port = eV("port"); - if (port > 0) { - $('#port').val(port); - } - else { - $('#port').val(''); - } - hueIPs.push({ host: host, port: port }); - - if (usr != "") { - checkHueBridge(checkUserResult, usr); - } else { - checkHueBridge(checkBridgeResult); - } - } - - $('#retry_bridge').off().on('click', function () { - var host = $('#host').val(); - var port = parseInt($('#port').val()); - - if (host != "") { - - var idx = hueIPs.findIndex(item => item.host === host && item.port === port); - if (idx === -1) { - hueIPs.push({ host: host, port: port }); - hueIPsinc = hueIPs.length - 1; - } else { - hueIPsinc = idx; - } - } - else { - discover_hue_bridges(); - } - - var usr = $('#user').val(); - if (usr != "") { - checkHueBridge(checkUserResult, usr); - } else { - checkHueBridge(checkBridgeResult); - } - }); - - $('#retry_usr').off().on('click', function () { - checkHueBridge(checkUserResult, $('#user').val()); - }); - - $('#wiz_hue_create_user').off().on('click', function () { - createHueUser(); - }); - - function assignLightEntertainmentPos(isFocusCenter, position, name, id) { - - var x = position.x; - var z = position.z; - - if (isFocusCenter) { - // Map lights as in centered range -0.5 to 0.5 - if (x < -0.5) { - x = -0.5; - } else if (x > 0.5) { - x = 0.5; - } - if (z < -0.5) { - z = -0.5; - } else if (z > 0.5) { - z = 0.5; - } - } else { - // Map lights as in full range -1 to 1 - x /= 2; - z /= 2; - } - - var h = x + 0.5; - var v = -z + 0.5; - - var hmin = h - 0.05; - var hmax = h + 0.05; - var vmin = v - 0.05; - var vmax = v + 0.05; - - let layoutObject = { - hmin: hmin < 0 ? 0 : hmin, - hmax: hmax > 1 ? 1 : hmax, - vmin: vmin < 0 ? 0 : vmin, - vmax: vmax > 1 ? 1 : vmax, - name: name - }; - - if (id) { - layoutObject.name += "_" + id; - } - return layoutObject; - } - - function assignSegmentedLightPos(segment, position, name) { - var layoutObjects = []; - - var segTotalLength = 0; - for (var key in segment) { - - segTotalLength += segment[key].length; - } - - var min; - var max; - var horizontal = true; - - var layoutObject = assignLightPos(position, name); - if (position === "left" || position === "right") { - // vertical distribution - min = layoutObject.vmin; - max = layoutObject.vmax; - horizontal = false; - - } else { - // horizontal distribution - min = layoutObject.hmin; - max = layoutObject.hmax; - } - - var step = (max - min) / segTotalLength; - var start = min; - - for (var key in segment) { - min = start; - max = round(start + segment[key].length * step); - - if (horizontal) { - layoutObject.hmin = min; - layoutObject.hmax = max; - } else { - layoutObject.vmin = min; - layoutObject.vmax = max; - } - layoutObject.name = name + "_" + key; - layoutObjects.push(JSON.parse(JSON.stringify(layoutObject))); - - start = max; - } - - return layoutObjects; - } - - $('#btn_wiz_save').off().on("click", function () { - var hueLedConfig = []; - var finalLightIds = []; - var channelNumber = 0; - - //create hue led config - for (var key in groupLights) { - var lightId = groupLights[key]; - - if ($('#hue_' + lightId).val() != "disabled") { - finalLightIds.push(lightId); - - var lightName; - if (isAPIv2Ready) { - var light = hueLights.find(light => light.id === lightId); - lightName = light.metadata.name; - } else { - lightName = hueLights[lightId].name; - } - - var position = $('#hue_' + lightId).val(); - var lightIdx = groupLights.indexOf(lightId); - var lightLocation = groupLightsLocations[lightIdx]; - - var serviceID; - if (isAPIv2Ready) { - serviceID = lightLocation.service.rid; - } - - if (position.startsWith("entertainment")) { - - // Layout per entertainment area definition at bridge - var isFocusCenter = false; - if (position === "entertainment_center") { - isFocusCenter = true; - } - - if (isAPIv2Ready) { - - groupChannels.forEach((channel) => { - if (channel.members[0].service.rid === serviceID) { - var layoutObject = assignLightEntertainmentPos(isFocusCenter, channel.position, lightName, channel.channel_id); - hueLedConfig.push(JSON.parse(JSON.stringify(layoutObject))); - ++channelNumber; - } - }); - } else { - var layoutObject = assignLightEntertainmentPos(isFocusCenter, lightLocation.position, lightName); - hueLedConfig.push(JSON.parse(JSON.stringify(layoutObject))); - } - } - else { - // Layout per manual settings - var maxSegments = 1; - - if (isAPIv2Ready) { - var service = hueEntertainmentServices.find(service => service.id === serviceID); - maxSegments = service.segments.max_segments; - } - - if (maxSegments > 1) { - var segment = service.segments.segments; - var layoutObjects = assignSegmentedLightPos(segment, position, lightName); - hueLedConfig.push(...layoutObjects); - } else { - var layoutObject = assignLightPos(position, lightName); - hueLedConfig.push(JSON.parse(JSON.stringify(layoutObject))); - } - channelNumber += maxSegments; - } - } - } - - var sc = window.serverConfig; - sc.leds = hueLedConfig; - - //Adjust gamma, brightness and compensation - var c = sc.color.channelAdjustment[0]; - c.gammaBlue = 1.0; - c.gammaRed = 1.0; - c.gammaGreen = 1.0; - c.brightness = 100; - c.brightnessCompensation = 0; - - //device config - - //Start with a clean configuration - var d = {}; - d.host = $('#host').val(); - d.port = parseInt($('#port').val()); - d.username = $('#user').val(); - d.type = 'philipshue'; - d.colorOrder = 'rgb'; - d.lightIds = finalLightIds; - d.transitiontime = parseInt(eV("transitiontime", 1)); - d.restoreOriginalState = (eV("restoreOriginalState", false) == true); - d.switchOffOnBlack = (eV("switchOffOnBlack", false) == true); - - d.blackLevel = parseFloat(eV("blackLevel", 0.009)); - d.onBlackTimeToPowerOff = parseInt(eV("onBlackTimeToPowerOff", 600)); - d.onBlackTimeToPowerOn = parseInt(eV("onBlackTimeToPowerOn", 300)); - d.brightnessFactor = parseFloat(eV("brightnessFactor", 1)); - - d.clientkey = $('#clientkey').val(); - d.groupId = $('#groupId').val(); - d.blackLightsTimeout = parseInt(eV("blackLightsTimeout", 5000)); - d.brightnessMin = parseFloat(eV("brightnessMin", 0)); - d.brightnessMax = parseFloat(eV("brightnessMax", 1)); - d.brightnessThreshold = parseFloat(eV("brightnessThreshold", 0.0001)); - d.handshakeTimeoutMin = parseInt(eV("handshakeTimeoutMin", 300)); - d.handshakeTimeoutMax = parseInt(eV("handshakeTimeoutMax", 1000)); - d.verbose = (eV("verbose") == true); - - d.autoStart = conf_editor.getEditor("root.generalOptions.autoStart").getValue(); - d.enableAttempts = parseInt(conf_editor.getEditor("root.generalOptions.enableAttempts").getValue()); - d.enableAttemptsInterval = parseInt(conf_editor.getEditor("root.generalOptions.enableAttemptsInterval").getValue()); - - d.useEntertainmentAPI = isEntertainmentReady; - d.useAPIv2 = isAPIv2Ready; - - if (isEntertainmentReady) { - d.hardwareLedCount = channelNumber; - if (window.serverConfig.device.type !== d.type) { - //smoothing on, if new device - sc.smoothing = { enable: true }; - } - } else { - d.hardwareLedCount = finalLightIds.length; - d.verbose = false; - if (window.serverConfig.device.type !== d.type) { - //smoothing off, if new device - sc.smoothing = { enable: false }; - } - } - - window.serverConfig.device = d; - - requestWriteConfig(sc, true); - resetWizard(); - }); - - $('#btn_wiz_abort').off().on('click', resetWizard); -} - -function createHueUser() { - var host = hueIPs[hueIPsinc].host; - var port = hueIPs[hueIPsinc].port; - - let params = { host: host }; - if (port !== 'undefined') { - params.port = parseInt(port); - } - - var retryTime = 30; - var retryInterval = 2; - - var UserInterval = setInterval(function () { - - $('#wizp1').toggle(false); - $('#wizp2').toggle(false); - $('#wizp3').toggle(true); - - (async () => { - - retryTime -= retryInterval; - $("#connectionTime").html(retryTime); - if (retryTime <= 0) { - abortConnection(UserInterval); - clearInterval(UserInterval); - } - else { - const res = await requestLedDeviceAddAuthorization('philipshue', params); - if (res && !res.error) { - var response = res.info; - - if (jQuery.isEmptyObject(response)) { - debugMessage(retryTime + ": link button not pressed or device not reachable"); - } else { - $('#wizp1').toggle(false); - $('#wizp2').toggle(true); - $('#wizp3').toggle(false); - - var username = response.username; - if (username != 'undefined') { - $('#user').val(username); - conf_editor.getEditor("root.specificOptions.username").setValue(username); - conf_editor.getEditor("root.specificOptions.host").setValue(host); - conf_editor.getEditor("root.specificOptions.port").setValue(port); - } - - if (isEntertainmentReady) { - var clientkey = response.clientkey; - if (clientkey != 'undefined') { - $('#clientkey').val(clientkey); - conf_editor.getEditor("root.specificOptions.clientkey").setValue(clientkey); - } - } - checkHueBridge(checkUserResult, username); - clearInterval(UserInterval); - } - } else { - $('#wizp1').toggle(false); - $('#wizp2').toggle(true); - $('#wizp3').toggle(false); - clearInterval(UserInterval); - } - } - })(); - - }, retryInterval * 1000); -} - -function get_hue_groups(username) { - var host = hueIPs[hueIPsinc].host; - - if (devicesProperties['philipshue'][host] && devicesProperties['philipshue'][host][username]) { - var ledProperties = devicesProperties['philipshue'][host][username]; - - if (isAPIv2Ready) { - if (!jQuery.isEmptyObject(ledProperties.data)) { - if (Object.keys(ledProperties.data).length > 0) { - hueEntertainmentConfigs = ledProperties.data.filter(config => { - return config.type === "entertainment_configuration"; - }); - hueEntertainmentServices = ledProperties.data.filter(config => { - return (config.type === "entertainment" && config.renderer === true); - }); - } - } - } else { - if (!jQuery.isEmptyObject(ledProperties.groups)) { - hueEntertainmentConfigs = []; - var hueGroups = ledProperties.groups; - for (var groupid in hueGroups) { - if (hueGroups[groupid].type == 'Entertainment') { - hueGroups[groupid].id = groupid; - hueEntertainmentConfigs.push(hueGroups[groupid]); - } - } - } - } - - if (Object.keys(hueEntertainmentConfigs).length > 0) { - - $('.lidsb').html(""); - $('#wh_topcontainer').toggle(false); - $('#hue_grp_ids_t').toggle(true); - - for (var groupid in hueEntertainmentConfigs) { - $('.gidsb').append(createTableRow([groupid + ' (' + hueEntertainmentConfigs[groupid].name + ')', ''])); - } - } else { - noAPISupport('wiz_hue_e_noegrpids', username); - } - } -} - -function noAPISupport(txt, username) { - showNotification('danger', $.i18n('wiz_hue_e_title'), $.i18n('wiz_hue_e_noapisupport_hint')); - conf_editor.getEditor("root.specificOptions.useEntertainmentAPI").setValue(false); - $("#root_specificOptions_useEntertainmentAPI").trigger("change"); - $('#btn_wiz_holder').append('
    ' + $.i18n('wiz_hue_e_noapisupport_hint') + '
    '); - $('#hue_grp_ids_t').toggle(false); - var txt = (txt) ? $.i18n(txt) : $.i18n('wiz_hue_e_nogrpids'); - $('

    ' + txt + '
    ' + $.i18n('wiz_hue_e_noapisupport') + '

    ').insertBefore('#wizp2_body #hue_ids_t'); - $('#hue_id_headline').html($.i18n('wiz_hue_desc2')); - - get_hue_lights(username); -} - -function get_hue_lights(username) { - var host = hueIPs[hueIPsinc].host; - - if (devicesProperties['philipshue'][host] && devicesProperties['philipshue'][host][username]) { - var ledProperties = devicesProperties['philipshue'][host][username]; - - if (isAPIv2Ready) { - if (!jQuery.isEmptyObject(ledProperties.data)) { - if (Object.keys(ledProperties.data).length > 0) { - hueLights = ledProperties.data.filter(config => { - return config.type === "light"; - }); - } - } - } else { - if (!jQuery.isEmptyObject(ledProperties.lights)) { - hueLights = ledProperties.lights; - } - } - - if (Object.keys(hueLights).length > 0) { - if (!isEntertainmentReady) { - $('#wh_topcontainer').toggle(false); - } - $('#hue_ids_t, #btn_wiz_save').toggle(true); - - var 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" - ]; - - if (isEntertainmentReady) { - lightOptions.unshift("entertainment_center"); - lightOptions.unshift("entertainment"); - } else { - lightOptions.unshift("disabled"); - groupLights = Object.keys(hueLights); - } - - $('.lidsb').html(""); - - var pos = ""; - for (var id in groupLights) { - var lightId = groupLights[id]; - var lightId_v1 = "/lights/" + lightId; - - var lightName; - if (isAPIv2Ready) { - var light = hueLights.find(light => light.id === lightId); - lightName = light.metadata.name; - lightId_v1 = light.id_v1; - } else { - lightName = hueLights[lightId].name; - } - - if (isEntertainmentReady) { - var lightLocation = {}; - lightLocation = groupLightsLocations[id]; - if (lightLocation) { - if (isAPIv2Ready) { - pos = 0; - } else { - var x = lightLocation.position.x; - var y = lightLocation.position.y; - var z = lightLocation.position.z; - - var xval = (x < 0) ? "left" : "right"; - if (z != 1 && x >= -0.25 && x <= 0.25) xval = ""; - switch (z) { - case 1: // top / Ceiling height - pos = "top" + xval; - break; - case 0: // middle / TV height - pos = (xval == "" && y >= 0.75) ? "bottom" : xval + "middle"; - break; - case -1: // bottom / Ground height - pos = xval + "bottom"; - break; - } - } - } - } - - var options = ""; - for (var opt in lightOptions) { - var val = lightOptions[opt]; - var txt = (val != 'entire' && val != 'disabled') ? 'conf_leds_layout_cl_' : 'wiz_ids_'; - options += '

    ' + $.i18n('wiz_cc_testintrowok') + ' ' + $.i18n('wiz_cc_link') + '

    '; + h += '

    ' + $.i18n('wiz_cc_summary') + '

    '; + $('#wiz_cc_desc').html(h); + + $('.videobtn').off().on('click', function (e) { + if (e.target.id == "stop") + sendToKodi("stop"); + else + sendToKodi("playV", e.target.id + '.mp4'); + + $(this).prop("disabled", true); + setTimeout(function () { $('.videobtn').prop("disabled", false) }, 10000); + }); + + $('#btn_wiz_next').prop("disabled", true); + $('#btn_wiz_save').toggle(true); + window.readOnlyMode ? $('#btn_wiz_save').prop('disabled', true) : $('#btn_wiz_save').prop('disabled', false); + } + else { + $('#btn_wiz_next').prop("disabled", false); + $('#btn_wiz_save').toggle(false); + } + } + + + + function switchPicture(pictures) { + if (typeof pictures[picnr] === 'undefined') + picnr = 0; + + sendToKodi('playP', pictures[picnr]); + picnr++; + } + + + function initializeWebSocket(cb) { + if ("WebSocket" in window) { + + if (kodiUrl.port === '') { + kodiUrl.port = defaultKodiPort; + } + + if (!ws || ws.readyState !== WebSocket.OPEN) { + + // Establish WebSocket connection + ws = new WebSocket(kodiUrl); + + // WebSocket onopen event + ws.onopen = function (event) { + withKodi = true; + cb("opened"); + }; + + // WebSocket onmessage event (handle incoming messages) + ws.onmessage = function (event) { + const response = JSON.parse(event.data); + if (response.method === "System.OnQuit") { + closeWebSocket(); + } else if (response.result != undefined) { + if (response.result !== "OK") { + cb("error"); + } + } + }; + + // WebSocket onerror event + ws.onerror = function (error) { + cb("error"); + }; + + // WebSocket onclose event + ws.onclose = function (event) { + withKodi = false; + if (event.code === 1006) { + // Ignore error 1006 due to Kodi issue + console.log("WebSocket closed with error code 1006. Ignoring due to Kodi bug."); + } + else { + console.error("WebSocket closed with code:", event.code); + } + }; + } else { + console.log("WebSocket connection is already open."); + } + } + else { + console.log("Kodi Access: WebSocket NOT supported by this browser"); + cb("error"); + } + } + + function setupEventListeners() { + $('#btn_wiz_cancel').off().on('click', function () { + stop(true); + }); + $('#wiz_cc_kodiip').off().on('change', function () { + + kodiAddress = encodeURIComponent($(this).val().trim()); + + $('#kodi_status').html(''); + if (kodiAddress !== "") { + + if (!isValidHostnameOrIP(kodiAddress)) { + + $('#kodi_status').html('

    ' + $.i18n('edt_msgcust_error_hostname_ip') + '

    '); + withKodi = false; + + } else { + + if (isValidIPv6(kodiAddress)) { + kodiUrl.hostname = "[" + kodiAddress + "]"; + } else { + kodiUrl.hostname = kodiAddress; + } + + $('#kodi_status').html('

    ' + $.i18n('wiz_cc_try_connect') + '

    '); + $('#btn_wiz_cont').prop('disabled', true); + + closeWebSocket(); + initializeWebSocket(function (cb) { + + if (cb == "opened") { + setStorage("kodiAddress", kodiAddress); + setStorage("kodiPort", defaultKodiPort); + + $('#kodi_status').html('

    ' + $.i18n('wiz_cc_kodicon') + '

    '); + $('#btn_wiz_cont').prop('disabled', false); + + if (withKodi) { + sendToKodi("msg", $.i18n('wiz_cc_kodimsg_start')); + } + } + else { + $('#kodi_status').html('

    ' + $.i18n('wiz_cc_kodidiscon') + '

    ' + $.i18n('wiz_cc_kodidisconlink') + ' ' + $.i18n('wiz_cc_link') + '

    '); + withKodi = false; + } + + $('#btn_wiz_cont').prop('disabled', false); + }); + } + } + }); + + //listen for continue + $('#btn_wiz_cont').off().on('click', function () { + begin(); + $('#wizp1').toggle(false); + $('#wizp2').toggle(true); + }); + } + + function init() { + colorLength = window.serverConfig.color.channelAdjustment; + cobj = window.schema.color.properties.channelAdjustment.items.properties; + websAddress = document.location.hostname + ':' + window.serverConfig.webConfig.port; + imgAddress = 'http://' + websAddress + '/img/cc/'; + setStorage("wizardactive", true); + } + + function initProfiles() { + //check profile count + if (colorLength.length > 1) { + $('#multi_cali').html('

    ' + $.i18n('wiz_cc_morethanone') + '

    '); + for (let i = 0; i < colorLength.length; i++) + $('#wiz_select').append(createSelOpt(i, i + 1 + ' (' + colorLength[i].id + ')')); + + $('#wiz_select').off().on('change', function () { + profile = $(this).val(); + }); + } + } + + function createEditor() { + wiz_editor = createJsonEditor('editor_container_wiz', { + color: window.schema.color + }, true, true); + + $('#editor_container_wiz h4').toggle(false); + $('#editor_container_wiz .btn-group').toggle(false); + $('#editor_container_wiz [data-schemapath="root.color.imageToLedMappingType"]').toggle(false); + $('#editor_container_wiz [data-schemapath="root.color.reducedPixelSetFactorFactor"]').toggle(false); + for (let i = 0; i < colorLength.length; i++) + $('#editor_container_wiz [data-schemapath*="root.color.channelAdjustment.' + i + '."]').toggle(false); + } + function updateEditor(el, all) { + for (let key in cobj) { + if (all === true || el[0] == key || el[1] == key || el[2] == key) + $('#editor_container_wiz [data-schemapath*=".' + profile + '.' + key + '"]').toggle(true); + else + $('#editor_container_wiz [data-schemapath*=".' + profile + '.' + key + '"]').toggle(false); + } + } + + function stop(reload) { + if (withKodi) { + sendToKodi("stop"); + } + closeWebSocket(); + resetWizard(reload); + } + + function begin() { + step = 0; + + $('#btn_wiz_next').off().on('click', function () { + step++; + performAction(); + }); + + $('#btn_wiz_back').off().on('click', function () { + step--; + performAction(); + }); + + $('#btn_wiz_abort').off().on('click', function () { + stop(true); + }); + + $('#btn_wiz_save').off().on('click', function () { + requestWriteConfig(wiz_editor.getValue()); + stop(true); + }); + + wiz_editor.on("change", function (e) { + const val = wiz_editor.getEditor('root.color.channelAdjustment.' + profile + '').getValue(); + const temp = JSON.parse(JSON.stringify(val)); + delete temp.leds + requestAdjustment(JSON.stringify(temp), "", true); + }); + + step++ + performAction(); + } + + return { + start: function () { + //create html + $('#wiz_header').html('' + $.i18n('wiz_cc_title')); + $('#wizp1_body').html('

    ' + $.i18n('wiz_cc_title') + '

    ' + + '

    ' + $.i18n('wiz_cc_intro1') + '

    ' + + '' + + '' + + '' + ); + $('#wizp1_footer').html('' + + '' + ); + $('#wizp2_body').html('
    ' + ); + $('#wizp2_footer').html('' + + '' + + '' + + '' + ); + + if (getStorage("darkMode") == "on") + $('#wizard_logo').prop("src", 'img/hyperion/logo_negativ.png'); + + //open modal + $("#wizard_modal").modal({ + backdrop: "static", + keyboard: false, + show: true + }); + + setupEventListeners(); + $('#wiz_cc_kodiip').trigger("change"); + init(); + initProfiles(); + createEditor(); + } + }; +})(); + +export { colorCalibrationKodiWizard }; diff --git a/assets/webconfig/js/wizards/rgbByteOrderWizard.js b/assets/webconfig/js/wizards/rgbByteOrderWizard.js new file mode 100644 index 00000000..5d612174 --- /dev/null +++ b/assets/webconfig/js/wizards/rgbByteOrderWizard.js @@ -0,0 +1,143 @@ +//**************************** +// Wizard RGB byte order +//**************************** + +const rgbByteOrderWizard = (() => { + + let wIntveralId; + let new_rgb_order; + + function changeColor() { + let color = $("#wiz_canv_color").css('background-color'); + + if (color == 'rgb(255, 0, 0)') { + $("#wiz_canv_color").css('background-color', 'rgb(0, 255, 0)'); + requestSetColor('0', '255', '0'); + } + else { + $("#wiz_canv_color").css('background-color', 'rgb(255, 0, 0)'); + requestSetColor('255', '0', '0'); + } + } + + + function stopWizardRGB(reload) { + console.log("stopWizardRGB - reload: ", reload); + clearInterval(wIntveralId); + resetWizard(reload); + } + + function beginWizardRGB() { + $("#wiz_switchtime_select").off().on('change', function () { + clearInterval(wIntveralId); + const time = $("#wiz_switchtime_select").val(); + wIntveralId = setInterval(function () { changeColor(); }, time * 1000); + }); + + $('.wselect').on("change", function () { + let rgb_order = window.serverConfig.device.colorOrder.split(""); + const redS = $("#wiz_r_select").val(); + const greenS = $("#wiz_g_select").val(); + const blueS = rgb_order.toString().replace(/,/g, "").replace(redS, "").replace(greenS, ""); + + for (const color of rgb_order) { + if (redS == color) + $('#wiz_g_select option[value=' + color + ']').prop('disabled', true); + else + $('#wiz_g_select option[value=' + color + ']').prop('disabled', false); + if (greenS == color) + $('#wiz_r_select option[value=' + color + ']').prop('disabled', true); + else + $('#wiz_r_select option[value=' + color + ']').prop('disabled', false); + } + + if (redS != 'null' && greenS != 'null') { + $('#btn_wiz_save').prop('disabled', false); + + for (let i = 0; i < rgb_order.length; i++) { + if (rgb_order[i] == "r") + rgb_order[i] = redS; + else if (rgb_order[i] == "g") + rgb_order[i] = greenS; + else + rgb_order[i] = blueS; + } + + rgb_order = rgb_order.toString().replace(/,/g, ""); + + if (redS == "r" && greenS == "g") { + $('#btn_wiz_save').toggle(false); + $('#btn_wiz_checkok').toggle(true); + + window.readOnlyMode ? $('#btn_wiz_checkok').prop('disabled', true) : $('#btn_wiz_checkok').prop('disabled', false); + } + else { + $('#btn_wiz_save').toggle(true); + window.readOnlyMode ? $('#btn_wiz_save').prop('disabled', true) : $('#btn_wiz_save').prop('disabled', false); + + $('#btn_wiz_checkok').toggle(false); + } + new_rgb_order = rgb_order; + } + else + $('#btn_wiz_save').prop('disabled', true); + }); + + $("#wiz_switchtime_select").append(createSelOpt('5', '5'), createSelOpt('10', '10'), createSelOpt('15', '15'), createSelOpt('30', '30')); + $("#wiz_switchtime_select").trigger('change'); + + $("#wiz_r_select").append(createSelOpt("null", ""), createSelOpt('r', $.i18n('general_col_red')), createSelOpt('g', $.i18n('general_col_green')), createSelOpt('b', $.i18n('general_col_blue'))); + $("#wiz_g_select").html($("#wiz_r_select").html()); + $("#wiz_r_select").trigger('change'); + + requestSetColor('255', '0', '0'); + setTimeout(requestSetSource, 100, 'auto'); + setStorage("wizardactive", true); + + $('#btn_wiz_abort').off().on('click', function () { stopWizardRGB(true); }); + + $('#btn_wiz_checkok').off().on('click', function () { + showInfoDialog('success', "", $.i18n('infoDialog_wizrgb_text')); + stopWizardRGB(); + }); + + $('#btn_wiz_save').off().on('click', function () { + stopWizardRGB(); + window.serverConfig.device.colorOrder = new_rgb_order; + requestWriteConfig({ "device": window.serverConfig.device }); + }); + } + + return { + start: function () { + //create html + $('#wiz_header').html('' + $.i18n('wiz_rgb_title')); + $('#wizp1_body').html('

    ' + $.i18n('wiz_rgb_title') + '

    ' + $.i18n('wiz_rgb_intro1') + '

    ' + $.i18n('wiz_rgb_intro2') + '

    '); + $('#wizp1_footer').html(''); + $('#wizp2_body').html('

    ' + $.i18n('wiz_rgb_expl') + '

    '); + $('#wizp2_body').append('
    ' + $.i18n('edt_append_s') + '
    '); + $('#wizp2_body').append(''); + $('#wizp2_body').append('
    '); + $('#wizp2_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 + }); + + //listen for continue + $('#btn_wiz_cont').off().on('click', function () { + beginWizardRGB(); + $('#wizp1').toggle(false); + $('#wizp2').toggle(true); + }); + } + }; +})(); + +export { rgbByteOrderWizard }; From 86d08823a81740f8ab893e4059193b3400e59c13 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sat, 30 Mar 2024 15:44:18 +0100 Subject: [PATCH 098/117] Add workaround for issue #1692 (#1695) --- libsrc/webserver/StaticFileServing.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/libsrc/webserver/StaticFileServing.cpp b/libsrc/webserver/StaticFileServing.cpp index 4c550280..4133ce66 100644 --- a/libsrc/webserver/StaticFileServing.cpp +++ b/libsrc/webserver/StaticFileServing.cpp @@ -132,7 +132,16 @@ void StaticFileServing::onRequestNeedsReply (QtHttpRequest * request, QtHttpRepl QMimeType mime = _mimeDb->mimeTypeForFile (file.fileName ()); if (file.open (QFile::ReadOnly)) { QByteArray data = file.readAll (); - reply->addHeader ("Content-Type", mime.name ().toLocal8Bit ()); + + // Workaround https://bugreports.qt.io/browse/QTBUG-97392 + if (mime.name() == QStringLiteral("application/x-extension-html")) + { + reply->addHeader ("Content-Type", "text/html"); + } + else + { + reply->addHeader ("Content-Type", mime.name().toLocal8Bit()); + } reply->addHeader(QtHttpHeader::AccessControlAllow, "*" ); reply->appendRawData (data); file.close (); From d5438acbf4e27de61f5f7fe20c2acba793652233 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Tue, 2 Apr 2024 21:44:46 +0200 Subject: [PATCH 099/117] Fix Cross Site Scripting Vulnerability 1 (#1720) --- libsrc/webserver/StaticFileServing.cpp | 14 +++++++++----- libsrc/webserver/StaticFileServing.h | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/libsrc/webserver/StaticFileServing.cpp b/libsrc/webserver/StaticFileServing.cpp index 4133ce66..3f3bc5a7 100644 --- a/libsrc/webserver/StaticFileServing.cpp +++ b/libsrc/webserver/StaticFileServing.cpp @@ -39,12 +39,15 @@ void StaticFileServing::setBaseUrl(const QString& url) void StaticFileServing::setSSDPDescription(const QString& desc) { if(desc.isEmpty()) + { _ssdpDescription.clear(); - else + } else + { _ssdpDescription = desc.toLocal8Bit(); + } } -void StaticFileServing::printErrorToReply (QtHttpReply * reply, QtHttpReply::StatusCode code, QString errorMessage) +void StaticFileServing::printErrorToReply (QtHttpReply * reply, QtHttpReply::StatusCode code, const QString& errorMessage) { reply->setStatusCode(code); reply->addHeader ("Content-Type", QByteArrayLiteral ("text/html")); @@ -62,13 +65,13 @@ void StaticFileServing::printErrorToReply (QtHttpReply * reply, QtHttpReply::Sta if (errorPage.open (QFile::ReadOnly)) { QByteArray data = errorPage.readAll(); - data = data.replace("{MESSAGE}", errorMessage.toLocal8Bit() ); + data = data.replace("{MESSAGE}", QString(errorMessage.toLocal8Bit()).toHtmlEscaped().toLocal8Bit() ); reply->appendRawData (data); errorPage.close (); } else { - reply->appendRawData (QString(QString::number(code) + " - " +errorMessage).toLocal8Bit()); + reply->appendRawData (QString(QString::number(code) + " - " +errorMessage.toLocal8Bit()).toHtmlEscaped().toLocal8Bit()); } if (errorPageFooter.open (QFile::ReadOnly)) @@ -103,7 +106,8 @@ void StaticFileServing::onRequestNeedsReply (QtHttpRequest * request, QtHttpRepl } return; } - else if(uri_parts.at(0) == "description.xml" && !_ssdpDescription.isNull()) + + if(uri_parts.at(0) == "description.xml" && !_ssdpDescription.isNull()) { reply->addHeader ("Content-Type", "text/xml"); reply->appendRawData (_ssdpDescription); diff --git a/libsrc/webserver/StaticFileServing.h b/libsrc/webserver/StaticFileServing.h index b328bf16..8a058ea6 100644 --- a/libsrc/webserver/StaticFileServing.h +++ b/libsrc/webserver/StaticFileServing.h @@ -37,7 +37,7 @@ private: Logger * _log; QByteArray _ssdpDescription; - void printErrorToReply (QtHttpReply * reply, QtHttpReply::StatusCode code, QString errorMessage); + void printErrorToReply (QtHttpReply * reply, QtHttpReply::StatusCode code, const QString& errorMessage); }; From c2fe42a7314cce3289444204951badfe2d6dd42b Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sat, 13 Apr 2024 22:54:17 +0200 Subject: [PATCH 100/117] Fix #1722 (#1723) --- src/hyperion-v4l2/hyperion-v4l2.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hyperion-v4l2/hyperion-v4l2.cpp b/src/hyperion-v4l2/hyperion-v4l2.cpp index b166948b..ecc6ed58 100644 --- a/src/hyperion-v4l2/hyperion-v4l2.cpp +++ b/src/hyperion-v4l2/hyperion-v4l2.cpp @@ -231,6 +231,7 @@ int main(int argc, char** argv) ScreenshotHandler handler("screenshot.png", signalDetectionOffset); QObject::connect(&grabber, SIGNAL(newFrame(Image)), &handler, SLOT(receiveImage(Image))); + grabber.prepare(); grabber.start(); QCoreApplication::exec(); grabber.stop(); From aca757138ef50931454b68f92a5ba27a0f8f5d46 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Tue, 16 Apr 2024 21:57:51 +0200 Subject: [PATCH 101/117] Nanoleaf Updates (#1724) * Add new devices * Do not restore ExtControl state --- assets/webconfig/js/content_leds.js | 4 ++++ libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp | 4 ++-- libsrc/leddevice/dev_net/LedDeviceNanoleaf.h | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 938bf8ee..f058450a 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -2510,6 +2510,10 @@ function nanoleafGeneratelayout(panelLayout, panelOrderTopDown, panelOrderLeftRi 18: { name: "LightLinesSingleZone", led: true, sideLengthX: 77, sideLengthY: 77 }, 19: { name: "ControllerCap", led: false, sideLengthX: 11, sideLengthY: 11 }, 20: { name: "PowerConnector", led: false, sideLengthX: 11, sideLengthY: 11 }, + 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 }, 999: { name: "Unknown", led: true, sideLengthX: 100, sideLengthY: 100 } }; diff --git a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp index e2b8b3fb..d7711a02 100644 --- a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp @@ -708,7 +708,7 @@ bool LedDeviceNanoleaf::storeState() QJsonObject effects = responseEffects.getBody().object(); DebugIf(verbose, _log, "effects: [%s]", QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData()); _originalEffect = effects[API_EFFECT_SELECT].toString(); - _originalIsDynEffect = _originalEffect == "*Dynamic*" || _originalEffect == "*Solid*"; + _originalIsDynEffect = _originalEffect != "*Dynamic*" || _originalEffect == "*Solid*" || _originalEffect == "*ExtControl*"; } break; } @@ -759,7 +759,7 @@ bool LedDeviceNanoleaf::restoreState() } } else { - Warning(_log, "%s restoring effect failed with error: Cannot restore dynamic or solid effect. Device is switched off", QSTRING_CSTR(_activeDeviceType)); + Info(_log, "%s cannot restore dynamic or solid effects. Device is switched off instead", QSTRING_CSTR(_activeDeviceType)); _originalIsOn = false; } break; diff --git a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h index 8aae03ec..dd9353b6 100644 --- a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h +++ b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h @@ -179,7 +179,11 @@ private: LIGHT_LINES = 17, LIGHT_LINES_SINGLZONE = 18, CONTROLLER_CAP = 19, - POWER_CONNECTOR = 20 + POWER_CONNECTOR = 20, + NL_4D_LIGHTSTRIP = 29, + SKYLIGHT_PANEL = 30, + SKYLIGHT_CONTROLLER_PRIMARY = 31, + SKYLIGHT_CONTROLLER_PASSIV = 32 }; /// From 7645ebb526c9cb4618015201fbcef23fdf151152 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 21:58:33 +0200 Subject: [PATCH 102/117] Bump softprops/action-gh-release from 1 to 2 (#1719) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/qt5_6.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/qt5_6.yml b/.github/workflows/qt5_6.yml index 2af9e875..b8096150 100644 --- a/.github/workflows/qt5_6.yml +++ b/.github/workflows/qt5_6.yml @@ -232,7 +232,7 @@ jobs: path: all-artifacts - name: 📦 Upload - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: name: Hyperion ${{ env.VERSION }} tag_name: ${{ env.TAG }} From accf27a09c092ae76d62b639151a1af4c857075d Mon Sep 17 00:00:00 2001 From: Portisch Date: Tue, 30 Apr 2024 07:28:19 +0200 Subject: [PATCH 103/117] Remove unused libs for Amlogic platform (#1725) --- src/hyperion-aml/CMakeLists.txt | 4 ---- src/hyperion-framebuffer/CMakeLists.txt | 4 ---- src/hyperion-remote/CMakeLists.txt | 4 ---- src/hyperion-v4l2/CMakeLists.txt | 4 ---- src/hyperiond/CMakeLists.txt | 4 ---- 5 files changed, 20 deletions(-) diff --git a/src/hyperion-aml/CMakeLists.txt b/src/hyperion-aml/CMakeLists.txt index 40b04df3..fea62f34 100644 --- a/src/hyperion-aml/CMakeLists.txt +++ b/src/hyperion-aml/CMakeLists.txt @@ -23,10 +23,6 @@ else() target_link_libraries(${PROJECT_NAME} ssdp) endif() -if(ENABLE_AMLOGIC) - target_link_libraries(${PROJECT_NAME} pcre16 dl z) -endif() - install (TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_aml") if(CMAKE_HOST_UNIX) diff --git a/src/hyperion-framebuffer/CMakeLists.txt b/src/hyperion-framebuffer/CMakeLists.txt index a9470f5c..8ac51da4 100644 --- a/src/hyperion-framebuffer/CMakeLists.txt +++ b/src/hyperion-framebuffer/CMakeLists.txt @@ -22,10 +22,6 @@ else() target_link_libraries(${PROJECT_NAME} ssdp) endif() -if(ENABLE_AMLOGIC) - target_link_libraries(${PROJECT_NAME} pcre16 dl z) -endif() - install (TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_framebuffer") if(CMAKE_HOST_UNIX) diff --git a/src/hyperion-remote/CMakeLists.txt b/src/hyperion-remote/CMakeLists.txt index ef60a7dc..8046fc15 100644 --- a/src/hyperion-remote/CMakeLists.txt +++ b/src/hyperion-remote/CMakeLists.txt @@ -21,10 +21,6 @@ target_link_libraries(${PROJECT_NAME} Qt${QT_VERSION_MAJOR}::Widgets ) -if(ENABLE_AMLOGIC) - target_link_libraries(${PROJECT_NAME} pcre16 dl z) -endif() - if(ENABLE_MDNS) target_link_libraries(${PROJECT_NAME} mdns) else() diff --git a/src/hyperion-v4l2/CMakeLists.txt b/src/hyperion-v4l2/CMakeLists.txt index 37189cdc..92aadb28 100644 --- a/src/hyperion-v4l2/CMakeLists.txt +++ b/src/hyperion-v4l2/CMakeLists.txt @@ -22,10 +22,6 @@ else() target_link_libraries(${PROJECT_NAME} ssdp) endif() -if(ENABLE_AMLOGIC) - target_link_libraries(${PROJECT_NAME} pcre16 dl z) -endif() - install (TARGETS ${PROJECT_NAME} DESTINATION "share/hyperion/bin" COMPONENT "hyperion_v4l2") if(CMAKE_HOST_UNIX) diff --git a/src/hyperiond/CMakeLists.txt b/src/hyperiond/CMakeLists.txt index 79a0b221..8ecbce4c 100644 --- a/src/hyperiond/CMakeLists.txt +++ b/src/hyperiond/CMakeLists.txt @@ -71,10 +71,6 @@ if(ENABLE_PROTOBUF_SERVER) target_link_libraries(${PROJECT_NAME} protoserver) endif() -if(ENABLE_AMLOGIC) - target_link_libraries(${PROJECT_NAME} pcre16 dl z) -endif(ENABLE_AMLOGIC) - if(ENABLE_DISPMANX) target_link_libraries(${PROJECT_NAME} dispmanx-grabber) endif (ENABLE_DISPMANX) From 94850d890a152ce999289bb5b5ec3d44899abbd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 May 2024 16:37:45 +0200 Subject: [PATCH 104/117] Bump actions/download-artifact from 4.1.4 to 4.1.7 (#1730) --- .github/workflows/qt5_6.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/qt5_6.yml b/.github/workflows/qt5_6.yml index b8096150..9cd59df8 100644 --- a/.github/workflows/qt5_6.yml +++ b/.github/workflows/qt5_6.yml @@ -226,7 +226,7 @@ jobs: echo '::endgroup::' - name: 💾 Artifact download - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.7 with: pattern: artifact-* path: all-artifacts From cf287f5adb3ff5ef2a7fc23e1ab94064ed6b24fb Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Wed, 8 May 2024 22:06:32 +0200 Subject: [PATCH 105/117] Refactor Hyperion JSON-API (#1727) --- CHANGELOG.md | 27 + assets/webconfig/i18n/en.json | 16 +- assets/webconfig/js/content_index.js | 55 +- assets/webconfig/js/content_logging.js | 9 +- assets/webconfig/js/content_network.js | 4 +- assets/webconfig/js/hyperion.js | 15 +- assets/webconfig/js/ledsim.js | 4 +- assets/webconfig/js/ui_utils.js | 6 +- config/hyperion.config.json.default | 4 +- .../JSON-API _Commands_Overview.md | 122 + include/api/API.h | 683 +++-- include/api/JsonAPI.h | 222 +- include/api/JsonApiCommand.h | 332 +++ include/api/JsonApiSubscription.h | 135 + include/api/{JsonCB.h => JsonCallbacks.h} | 102 +- include/api/JsonInfo.h | 43 + include/api/apiStructs.h | 3 + include/db/AuthTable.h | 9 +- include/events/EventEnum.h | 5 +- include/events/OsEventHandler.h | 2 + include/grabber/GrabberConfig.h | 58 + include/hyperion/AuthManager.h | 22 +- include/hyperion/Hyperion.h | 28 +- include/hyperion/HyperionIManager.h | 8 +- include/hyperion/PriorityMuxer.h | 7 + include/utils/JsonUtils.h | 16 +- include/utils/NetOrigin.h | 4 +- include/utils/jsonschema/QJsonFactory.h | 7 +- libsrc/api/API.cpp | 680 +++-- libsrc/api/CMakeLists.txt | 8 +- .../api/JSONRPC_schema/schema-adjustment.json | 6 + libsrc/api/JSONRPC_schema/schema-clear.json | 6 + .../api/JSONRPC_schema/schema-clearall.json | 6 + libsrc/api/JSONRPC_schema/schema-color.json | 6 + .../JSONRPC_schema/schema-componentstate.json | 6 + libsrc/api/JSONRPC_schema/schema-config.json | 5 + .../JSONRPC_schema/schema-create-effect.json | 5 + .../JSONRPC_schema/schema-delete-effect.json | 5 + libsrc/api/JSONRPC_schema/schema-effect.json | 6 + libsrc/api/JSONRPC_schema/schema-image.json | 6 + .../api/JSONRPC_schema/schema-ledcolors.json | 15 +- .../api/JSONRPC_schema/schema-leddevice.json | 3 + .../api/JSONRPC_schema/schema-processing.json | 6 + .../api/JSONRPC_schema/schema-serverinfo.json | 19 + .../JSONRPC_schema/schema-sourceselect.json | 6 + libsrc/api/JsonAPI.cpp | 2610 +++++++---------- libsrc/api/JsonCB.cpp | 416 --- libsrc/api/JsonCallbacks.cpp | 459 +++ libsrc/api/JsonInfo.cpp | 620 ++++ libsrc/effectengine/EffectFileHandler.cpp | 8 +- libsrc/events/EventHandler.cpp | 8 +- libsrc/events/OsEventHandler.cpp | 8 +- libsrc/hyperion/AuthManager.cpp | 29 +- libsrc/hyperion/Hyperion.cpp | 5 + libsrc/hyperion/HyperionIManager.cpp | 5 + libsrc/hyperion/PriorityMuxer.cpp | 5 + libsrc/hyperion/SettingsManager.cpp | 2 +- libsrc/hyperion/schema/schema-network.json | 29 +- libsrc/leddevice/LedDeviceWrapper.cpp | 10 +- libsrc/utils/JsonUtils.cpp | 98 +- libsrc/utils/NetOrigin.cpp | 97 +- src/hyperion-remote/JsonConnection.cpp | 6 +- src/hyperion-v4l2/hyperion-v4l2.cpp | 2 +- src/hyperiond/systray.cpp | 6 +- 64 files changed, 4203 insertions(+), 2962 deletions(-) create mode 100644 doc/development/JSON-API _Commands_Overview.md create mode 100644 include/api/JsonApiCommand.h create mode 100644 include/api/JsonApiSubscription.h rename include/api/{JsonCB.h => JsonCallbacks.h} (53%) create mode 100644 include/api/JsonInfo.h create mode 100644 include/grabber/GrabberConfig.h delete mode 100644 libsrc/api/JsonCB.cpp create mode 100644 libsrc/api/JsonCallbacks.cpp create mode 100644 libsrc/api/JsonInfo.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ca3c166..75577551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,14 +8,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking +**JSON-API** +- Align JSON subscription update elements. `ledcolors-imagestream-update, ledcolors-ledstream-update, logmsg-update` now return data via `data` and not `result + ### Added - Support gaps on Matrix Layout (#1696) +**JSON-API** +- New subscription support for event updates, i.e. `Suspend, Resume, Idle, idleResume, Restart, Quit`. +- Support direct or multiple instance addressing via single requests (#809) +- Support of `serverinfo` subcommands: `getInfo, subscribe, unsubscribe, getSubscriptions, getSubscriptionCommands` +- [Overview](https://github.com/hyperion-project/hyperion.ng/blob/API_Auth/doc/development/JSON-API%20_Commands_Overview.md) of API commands and subscription updates + ### Changed +- Fixed: Cross Site Scripting Vulnerability (CVE-2024-4174, CVE-2024-4175) +- Fixed: hyperion-v4l2 taking screenshot failed (#1722) +- Nanoleaf: Support new devices and do not restore ExtControl state +- Workaround to address Web UI keeps forcing browser to download the html instead (#1692) +- Fixed: Kodi Color Calibration, Refactor Wizards (#1674) +- Fixed: Token Dialog not closing + +**JSON-API** +- Refactored JSON-API to ensure consistent authorization behaviour across sessions and single requests with token authorization. +- Provide additional error details with API responses, esp. on JSON parsing, validation or token errors. +- Generate random TANs for every API request from the Hyperion UI +- Fixed: Handling of IP4 addresses wrapped in IPv6 for external network connections- + ### Removed +**JSON-API** +- Removed ability to enable/disable local admin authorization. All admin commands require authorization, i.e. `authorize-adminRequired` will always be `true`. +- Removed `session-updates` subscription +- `serverinfo/subscribe` element will be deprecated and replaced by corresponding subcommand + ## [2.0.16](https://github.com/hyperion-project/hyperion.ng/releases/tag/2.0.16) - 2024-01 ### Added diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 68344d73..0d638c53 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -186,6 +186,7 @@ "conf_network_json_intro": "The JSON-RPC-Port of all Hyperion instances, used for remote control.", "conf_network_net_intro": "Network related settings which are applied to all network services.", "conf_network_proto_intro": "The PROTO-Port of all Hyperion instances, used for picture streams (HyperionScreenCap, Kodi Addon, Android Hyperion Grabber, ...)", + "conf_network_tok_idhead": "ID", "conf_network_tok_cidhead": "Description", "conf_network_tok_comment_title": "Token description", "conf_network_tok_desc": "Tokens grant other applications access to the Hyperion API, an application can request a token where you need to accept it or you create them on your own below. These tokens are just required when \"API Authorization\" is enabled in network settings.", @@ -500,19 +501,16 @@ "edt_conf_log_level_expl": "Depending on loglevel you see less or more messages in your log.", "edt_conf_log_level_title": "Log-Level", "edt_conf_net_apiAuth_expl": "Enforce all applications that use the Hyperion API to authenticate themself against Hyperion (Exception: see \"Local API Authentication\"). Higher security, as you control the access and revoke it at any time.", - "edt_conf_net_apiAuth_title": "API Authentication", "edt_conf_net_heading_title": "Network", - "edt_conf_net_internetAccessAPI_expl": "Allow access to the Hyperion API/Webinterface from the internet. Disable for higher security.", + "edt_conf_net_internetAccessAPI_expl": "Allow access to the Hyperion API/Web Interface from the Internet. Disable for increased security.", "edt_conf_net_internetAccessAPI_title": "Internet API Access", - "edt_conf_net_ipWhitelist_expl": "You can whitelist IP addresses instead allowing all connections from internet to connect to the Hyperion API/Webinterface.", - "edt_conf_net_ipWhitelist_title": "Whitelisted IP's", + "edt_conf_net_ipWhitelist_expl": "Define whitelisted IP addresses from which API requests from the Internet are allowed. All other external connections will be denied.", + "edt_conf_net_ipWhitelist_title": "Whitelisted IP addresses", "edt_conf_net_ip_itemtitle": "IP", - "edt_conf_net_localAdminAuth_expl": "When enabled, administration access from your local network needs a password.", - "edt_conf_net_localAdminAuth_title": "Local Admin API Authentication", - "edt_conf_net_localApiAuth_expl": "When enabled, connections from your home network needs to authenticate themselves against Hyperion with a token.", + "edt_conf_net_localApiAuth_expl": "When disabled, API authorisation via password or token is not required for local connections. The exception is administrative commands.", "edt_conf_net_localApiAuth_title": "Local API Authentication", - "edt_conf_net_restirctedInternetAccessAPI_expl": "You can restrict the access to the API through the internet to certain IP's.", - "edt_conf_net_restirctedInternetAccessAPI_title": "Restrict to IP's", + "edt_conf_net_restirctedInternetAccessAPI_expl": "You can restrict API requests over the Internet to only those IP addresses on the whitelist.", + "edt_conf_net_restirctedInternetAccessAPI_title": "Restrict to IP addresses", "edt_conf_os_events_lockEnable_title": "Listen to lock events", "edt_conf_os_events_lockEnable_expl": "Listen to screen lock/unlock events", "edt_conf_os_events_suspendEnable_title": "Listen to suspend events", diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index e8ec28a8..6609b3bd 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -73,26 +73,30 @@ $(document).ready(function () { //End language selection $(window.hyperion).on("cmd-authorize-tokenRequest cmd-authorize-getPendingTokenRequests", function (event) { - var val = event.response.info; - if (Array.isArray(event.response.info)) { - if (event.response.info.length == 0) { - return - } - val = event.response.info[0] - if (val.comment == '') - $('#modal_dialog').modal('hide'); - } - showInfoDialog("grantToken", $.i18n('conf_network_tok_grantT'), $.i18n('conf_network_tok_grantMsg') + '
    App: ' + val.comment + '
    Code: ' + val.id + '') - $("#tok_grant_acc").off().on('click', function () { - tokenList.push(val) - // forward event, in case we need to rebuild the list now - $(window.hyperion).trigger({ type: "build-token-list" }); - requestHandleTokenRequest(val.id, true) - }); - $("#tok_deny_acc").off().on('click', function () { - requestHandleTokenRequest(val.id, false) - }); + if (event.response && event.response.info !== undefined) { + var val = event.response.info; + + if (Array.isArray(event.response.info)) { + if (event.response.info.length == 0) { + return + } + val = event.response.info[0] + if (val.comment == '') + $('#modal_dialog').modal('hide'); + } + + showInfoDialog("grantToken", $.i18n('conf_network_tok_grantT'), $.i18n('conf_network_tok_grantMsg') + '
    App: ' + val.comment + '
    Code: ' + val.id + '') + $("#tok_grant_acc").off().on('click', function () { + tokenList.push(val) + // forward event, in case we need to rebuild the list now + $(window.hyperion).trigger({ type: "build-token-list" }); + requestHandleTokenRequest(val.id, true) + }); + $("#tok_deny_acc").off().on('click', function () { + requestHandleTokenRequest(val.id, false) + }); + } }); $(window.hyperion).one("cmd-authorize-getTokenList", function (event) { @@ -186,21 +190,12 @@ $(document).ready(function () { } }); - $(window.hyperion).on("cmd-authorize-adminRequired", function (event) { - //Check if a admin login is required. - //If yes: check if default pw is set. If no: go ahead to get server config and render page - if (event.response.info.adminRequired === true) - requestRequiresDefaultPasswortChange(); - else - requestServerConfigSchema(); - }); - $(window.hyperion).on("error", function (event) { //If we are getting an error "No Authorization" back with a set loginToken we will forward to new Login (Token is expired. //e.g.: hyperiond was started new in the meantime) if (event.reason == "No Authorization" && getStorage("loginToken")) { removeStorage("loginToken"); - requestRequiresAdminAuth(); + requestRequiresDefaultPasswortChange(); } else if (event.reason == "Selected Hyperion instance isn't running") { //Switch to default instance @@ -211,7 +206,7 @@ $(document).ready(function () { }); $(window.hyperion).on("open", function (event) { - requestRequiresAdminAuth(); + requestRequiresDefaultPasswortChange(); }); $(window.hyperion).on("ready", function (event) { diff --git a/assets/webconfig/js/content_logging.js b/assets/webconfig/js/content_logging.js index 7a9c791d..a8d5fa36 100644 --- a/assets/webconfig/js/content_logging.js +++ b/assets/webconfig/js/content_logging.js @@ -3,10 +3,13 @@ var createdCont = false; var isScroll = true; performTranslation(); -requestLoggingStop(); $(document).ready(function () { + window.addEventListener('hashchange', function(event) { + requestLoggingStop(); + }); + requestLoggingStart(); $('#conf_cont').append(createOptPanel('fa-reorder', $.i18n("edt_conf_log_heading_title"), 'editor_container', 'btn_submit')); @@ -178,9 +181,9 @@ $(document).ready(function () { if (!window.loggingHandlerInstalled) { window.loggingHandlerInstalled = true; - $(window.hyperion).on("cmd-logging-update", function (event) { + $(window.hyperion).on("cmd-logmsg-update", function (event) { - var messages = (event.response.result.messages); + var messages = (event.response.data.messages); if (messages.length != 0) { if (!createdCont) { diff --git a/assets/webconfig/js/content_network.js b/assets/webconfig/js/content_network.js index 26fec2c8..9f9f68c3 100644 --- a/assets/webconfig/js/content_network.js +++ b/assets/webconfig/js/content_network.js @@ -213,13 +213,13 @@ $(document).ready(function () { for (var key in tokenList) { var lastUse = (tokenList[key].last_use) ? tokenList[key].last_use : "-"; var btn = ''; - $('.tktbody').append(createTableRow([tokenList[key].comment, lastUse, btn], false, true)); + $('.tktbody').append(createTableRow([tokenList[key].id, tokenList[key].comment, lastUse, btn], false, true)); $('#tok' + tokenList[key].id).off().on('click', handleDeleteToken); } } createTable('tkthead', 'tktbody', 'tktable'); - $('.tkthead').html(createTableRow([$.i18n('conf_network_tok_cidhead'), $.i18n('conf_network_tok_lastuse'), $.i18n('general_btn_delete')], true, true)); + $('.tkthead').html(createTableRow([$.i18n('conf_network_tok_idhead'), $.i18n('conf_network_tok_cidhead'), $.i18n('conf_network_tok_lastuse'), $.i18n('general_btn_delete')], true, true)); buildTokenList(); function handleDeleteToken(e) { diff --git a/assets/webconfig/js/hyperion.js b/assets/webconfig/js/hyperion.js index 7bd59c65..8153e827 100644 --- a/assets/webconfig/js/hyperion.js +++ b/assets/webconfig/js/hyperion.js @@ -177,6 +177,7 @@ function sendToHyperion(command, subcommand, msg) else msg = ""; + window.wsTan = Math.floor(Math.random() * 1000) window.websocket.send('{"command":"'+command+'", "tan":'+window.wsTan+subcommand+msg+'}'); } @@ -187,7 +188,7 @@ function sendToHyperion(command, subcommand, msg) // data: The json data as Object // tan: The optional tan, default 1. If the tan is -1, we skip global response error handling // Returns data of response or false if timeout -async function sendAsyncToHyperion (command, subcommand, data, tan = 1) { +async function sendAsyncToHyperion (command, subcommand, data, tan = Math.floor(Math.random() * 1000) ) { let obj = { command, tan } if (subcommand) {Object.assign(obj, {subcommand})} if (data) { Object.assign(obj, data) } @@ -486,38 +487,38 @@ async function requestLedDeviceDiscovery(type, params) { let data = { ledDeviceType: type, params: params }; - return sendAsyncToHyperion("leddevice", "discover", data, Math.floor(Math.random() * 1000) ); + return sendAsyncToHyperion("leddevice", "discover", data); } async function requestLedDeviceProperties(type, params) { let data = { ledDeviceType: type, params: params }; - return sendAsyncToHyperion("leddevice", "getProperties", data, Math.floor(Math.random() * 1000)); + return sendAsyncToHyperion("leddevice", "getProperties", data); } function requestLedDeviceIdentification(type, params) { let data = { ledDeviceType: type, params: params }; - return sendAsyncToHyperion("leddevice", "identify", data, Math.floor(Math.random() * 1000)); + return sendAsyncToHyperion("leddevice", "identify", data); } async function requestLedDeviceAddAuthorization(type, params) { let data = { ledDeviceType: type, params: params }; - return sendAsyncToHyperion("leddevice", "addAuthorization", data, Math.floor(Math.random() * 1000)); + return sendAsyncToHyperion("leddevice", "addAuthorization", data); } async function requestInputSourcesDiscovery(type, params) { let data = { sourceType: type, params: params }; - return sendAsyncToHyperion("inputsource", "discover", data, Math.floor(Math.random() * 1000)); + return sendAsyncToHyperion("inputsource", "discover", data); } async function requestServiceDiscovery(type, params) { let data = { serviceType: type, params: params }; - return sendAsyncToHyperion("service", "discover", data, Math.floor(Math.random() * 1000)); + return sendAsyncToHyperion("service", "discover", data); } diff --git a/assets/webconfig/js/ledsim.js b/assets/webconfig/js/ledsim.js index 32a8898b..af633ba7 100644 --- a/assets/webconfig/js/ledsim.js +++ b/assets/webconfig/js/ledsim.js @@ -261,7 +261,7 @@ $(document).ready(function () { $("body").get(0).style.setProperty("--background-var", "none"); } else { - printLedsToCanvas(event.response.result.leds) + printLedsToCanvas(event.response.data.leds) $("body").get(0).style.setProperty("--background-var", "url(" + ($('#leds_preview_canv')[0]).toDataURL("image/jpg") + ") no-repeat top left"); } }); @@ -275,7 +275,7 @@ $(document).ready(function () { } } else { - var imageData = (event.response.result.image); + var imageData = (event.response.data.image); var image = new Image(); image.onload = function () { diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index 4cbb7967..4bfeb85e 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -319,9 +319,9 @@ function showInfoDialog(type, header, message) { }); $(document).on('click', '[data-dismiss-modal]', function () { - var target = $(this).attr('data-dismiss-modal'); - $.find(target).modal('hide'); - }); + var target = $(this).data('dismiss-modal'); + $($.find(target)).modal('hide'); +}); } function createHintH(type, text, container) { diff --git a/config/hyperion.config.json.default b/config/hyperion.config.json.default index 18fc9dc9..f126b638 100644 --- a/config/hyperion.config.json.default +++ b/config/hyperion.config.json.default @@ -220,9 +220,7 @@ "internetAccessAPI": false, "restirctedInternetAccessAPI": false, "ipWhitelist": [], - "apiAuth": true, - "localApiAuth": false, - "localAdminAuth": true + "localApiAuth": false }, "ledConfig": { diff --git a/doc/development/JSON-API _Commands_Overview.md b/doc/development/JSON-API _Commands_Overview.md new file mode 100644 index 00000000..549c5e62 --- /dev/null +++ b/doc/development/JSON-API _Commands_Overview.md @@ -0,0 +1,122 @@ +# JSON-API Commands Overview + +## Commands & Sub-Commands + +List of commands and related sub-commands which can be used via JSON-API requests. + +_Authorization (via password or bearer token)_ + +**No** - No authorization required
    +**Yes** - Authorization required, but can be disabled for local network calls
    +**Admin**: Authorization is always required + +_Instance specific_ + +**Yes** - A specific instance can be addressed
    +**Multi** - Multiple instances can be addressed via one request
    +**No** - The command is not instance related + +_http/s Support_ + +**Yes** - Command can be used by individual http/s requests
    +**No** - Applies only to WebSocket or http/s sessions + +| Command | Sub-Command | Authorization | Instance specific | http/s Support | +|:---------------|:------------------------|:--------------|:------------------|:---------------| +| adjustment | - | Yes | Multi | Yes | +| authorize | adminRequired | No | No | Yes | +| authorize | answerRequest | Admin | No | No | +| authorize | createToken | Admin | No | No | +| authorize | deleteToken | Admin | No | Yes | +| authorize | getPendingTokenRequests | Admin | No | No | +| authorize | getTokenList | Admin | No | Yes | +| authorize | login | No | No | No | +| authorize | logout | No | No | No | +| authorize | newPassword | Admin | No | Yes | +| authorize | newPasswordRequired | No | No | Yes | +| authorize | renameToken | Admin | No | Yes | +| authorize | requestToken | No | No | Yes | +| authorize | tokenRequired | No | No | Yes | +| clear | - | Yes | Multi | Yes | +| clearall | - | Yes | Multi | Yes | +| color | - | Yes | Multi | Yes | +| componentstate | - | Yes | Multi | Yes | +| config | getconfig | Admin | Yes | Yes | +| config | getschema | Admin | Yes | Yes | +| config | reload | Admin | Yes | Yes | +| config | restoreconfig | Admin | Yes | Yes | +| config | setconfig | Admin | Yes | Yes | +| correction | - | Yes | Yes | Yes | +| create-effect | - | Yes | Yes | Yes | +| delete-effect | - | Yes | Yes | Yes | +| effect | - | Yes | Multi | Yes | +| image | - | Yes | Multi | Yes | +| inputsource | discover | Yes | No | Yes | +| inputsource | getProperties | Yes | No | Yes | +| instance | createInstance | Admin | No | Yes | +| instance | deleteInstance | Admin | No | Yes | +| instance | saveName | Admin | No | Yes | +| instance | startInstance | Yes | No | Yes | +| instance | stopInstance | Yes | No | Yes | +| instance | switchTo | Yes | No | Yes | +| ledcolors | imagestream-start | Yes | Yes | Yes | +| ledcolors | imagestream-stop | Yes | Yes | Yes | +| ledcolors | ledstream-start | Yes | Yes | Yes | +| ledcolors | ledstream-stop | Yes | Yes | Yes | +| leddevice | addAuthorization | Yes | Yes | Yes | +| leddevice | discover | Yes | Yes | Yes | +| leddevice | getProperties | Yes | Yes | Yes | +| leddevice | identify | Yes | Yes | Yes | +| logging | start | Yes | No | Yes | +| logging | stop | Yes | No | Yes | +| processing | - | Yes | Multi | Yes | +| serverinfo | - | Yes | Yes | Yes | +| serverinfo | getInfo | Yes | Yes | Yes | +| serverinfo | subscribe | Yes | Yes | No | +| serverinfo | unsubscribe | Yes | Yes | No | +| serverinfo | getSubscriptions | Yes | Yes | No | +| serverinfo | getSubscriptionCommands | No | No | No | +| service | discover | Yes | No | Yes | +| sourceselect | - | Yes | Multi | Yes | +| sysinfo | - | Yes | No | Yes | +| system | restart | Yes | No | Yes | +| system | resume | Yes | No | Yes | +| system | suspend | Yes | No | Yes | +| system | toggleSuspend | Yes | No | Yes | +| system | idle | Yes | No | Yes | +| system | toggleIdle | Yes | No | Yes | +| temperature | - | Yes | Yes | Yes | +| transform | - | Yes | Yes | Yes | +| videomode | - | Yes | No | Yes | + +## Subscription updates + +List of updates which can be subscribed to via the `serverinfo/subscribe`request. + +_Instance specific_ + +**Yes** - A specific instance can be addressed
    +**No** - The command is not instance related + +_in "all"_ + +**Yes** - Updates are subscribed using "all" as the command
    +**No** - Subscription is only triggered via JSON-API request + +| Subscription Command | Instance specific | in "all" | +|:-----------------------------|:------------------|:---------| +| adjustment-update | Yes | Yes | +| components-update | Yes | Yes | +| effects-update | Yes | Yes | +| event-update | No | Yes | +| imageToLedMapping-update | Yes | Yes | +| instance-update | Yes | Yes | +| ledcolors-imagestream-update | Yes | No | +| ledcolors-ledstream-update | Yes | No | +| leds-update | Yes | Yes | +| logmsg-update | No | No | +| priorities-update | Yes | Yes | +| settings-update | Yes | Yes | +| token-update | No | Yes | +| videomode-update | No | Yes | + diff --git a/include/api/API.h b/include/api/API.h index 31a60f97..96e1f9b8 100644 --- a/include/api/API.h +++ b/include/api/API.h @@ -14,11 +14,15 @@ // qt includes #include -class QTimer; -class JsonCB; +class JsonCallbacks; class HyperionIManager; -const QString NO_AUTH = "No Authorization"; +// Constants +namespace { + +const char NO_AUTHORIZATION[] = "No Authorization";; + +} /// /// @brief API for Hyperion to be inherted from a child class with specific protocol implementations @@ -31,205 +35,214 @@ const QString NO_AUTH = "No Authorization"; class API : public QObject { - Q_OBJECT + Q_OBJECT public: #include - // workaround Q_ARG std::map template issues - typedef std::map MapRegister; - typedef QMap MapAuthDefs; - /// - /// Constructor - /// - ///@ param The parent Logger - /// @param localConnection Is this a local network connection? Use utils/NetOrigin to check that - /// @param parent Parent QObject - /// - API(Logger *log, bool localConnection, QObject *parent); - -protected: - /// - /// @brief Initialize the API - /// This call is REQUIRED! - /// - void init(); - - /// - /// @brief Set a single color - /// @param[in] priority The priority of the written color - /// @param[in] ledColor The color to write to the leds - /// @param[in] timeout_ms The time the leds are set to the given color [ms] - /// @param[in] origin The setter - /// - void setColor(int priority, const std::vector &ledColors, int timeout_ms = -1, const QString &origin = "API", hyperion::Components callerComp = hyperion::COMP_INVALID); - - /// - /// @brief Set a image - /// @param[in] data The command data - /// @param[in] comp The component that should be used - /// @param[out] replyMsg The replyMsg on failure - /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF - /// @return True on success - /// - bool setImage(ImageCmdData &data, hyperion::Components comp, QString &replyMsg, hyperion::Components callerComp = hyperion::COMP_INVALID); - - /// - /// @brief Clear a priority in the Muxer, if -1 all priorities are cleared - /// @param priority The priority to clear - /// @param replyMsg the message on failure - /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF - /// @return True on success - /// - bool clearPriority(int priority, QString &replyMsg, hyperion::Components callerComp = hyperion::COMP_INVALID); - - /// - /// @brief Set a new component state - /// @param comp The component name - /// @param compState The new state of the comp - /// @param replyMsg The reply on failure - /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF - /// @ return True on success - /// - bool setComponentState(const QString &comp, bool &compState, QString &replyMsg, hyperion::Components callerComp = hyperion::COMP_INVALID); - - /// - /// @brief Set a ledToImageMapping type - /// @param type mapping type string - /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF - /// - void setLedMappingType(int type, hyperion::Components callerComp = hyperion::COMP_INVALID); - - /// - /// @brief Set the 2D/3D modes type - /// @param mode The VideoMode - /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF - /// - void setVideoMode(VideoMode mode, hyperion::Components callerComp = hyperion::COMP_INVALID); - -#if defined(ENABLE_EFFECTENGINE) - /// - /// @brief Set an effect - /// @param dat The effect data - /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF - /// REQUIRED dat fields: effectName, priority, duration, origin - /// @return True on success else false - /// - bool setEffect(const EffectCmdData &dat, hyperion::Components callerComp = hyperion::COMP_INVALID); -#endif - - /// - /// @brief Set source auto select enabled or disabled - /// @param sate The new state - /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF - /// - void setSourceAutoSelect(bool state, hyperion::Components callerComp = hyperion::COMP_INVALID); - - /// - /// @brief Set the visible priority to given priority - /// @param priority The priority to set - /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF - /// - void setVisiblePriority(int priority, hyperion::Components callerComp = hyperion::COMP_INVALID); - - /// - /// @brief Register a input or update the meta data of a previous register call - /// ATTENTION: Check unregisterInput() method description !!! - /// @param[in] priority The priority of the channel - /// @param[in] component The component of the channel - /// @param[in] origin Who set the channel (CustomString@IP) - /// @param[in] owner Specific owner string, might be empty - /// @param[in] callerComp The component that call this (e.g. PROTO/FLAT) - /// - void registerInput(int priority, hyperion::Components component, const QString &origin, const QString &owner, hyperion::Components callerComp); - - /// - /// @brief Revoke a registerInput() call by priority. We maintain all registered priorities in this scope - /// ATTENTION: This is MANDATORY if you change (priority change) or stop(clear/timeout) DURING lifetime. If this class destructs it's not needed - /// @param priority The priority to unregister - /// - void unregisterInput(int priority); - - /// - /// @brief Handle the instance switching - /// @param inst The requested instance - /// @return True on success else false - /// - bool setHyperionInstance(quint8 inst); + // workaround Q_ARG std::map template issues + typedef std::map MapRegister; + typedef QMap MapAuthDefs; /// - /// @brief Check if Hyperion ist enabled - /// @return True when enabled else false - /// - bool isHyperionEnabled(); + /// Constructor + /// + ///@ param The parent Logger + /// @param localConnection Is this a local network connection? Use utils/NetOrigin to check that + /// @param parent Parent QObject + /// + API(Logger *log, bool localConnection, QObject *parent); - /// - /// @brief Get all instances data - /// @return The instance data - /// - QVector getAllInstanceData(); +protected: + /// + /// @brief Initialize the API + /// This call is REQUIRED! + /// + void init(); - /// - /// @brief Start instance - /// @param index The instance index - /// @param tan The tan - /// @return True on success else false - /// - bool startInstance(quint8 index, int tan = 0); + virtual void stopDataConnections() = 0; - /// - /// @brief Stop instance - /// @param index The instance index - /// - void stopInstance(quint8 index); + /// + /// @brief Set a single color + /// @param[in] priority The priority of the written color + /// @param[in] ledColor The color to write to the leds + /// @param[in] timeout_ms The time the leds are set to the given color [ms] + /// @param[in] origin The setter + /// + void setColor(int priority, const std::vector &ledColors, int timeout_ms = -1, const QString &origin = "API", hyperion::Components callerComp = hyperion::COMP_INVALID); - ////////////////////////////////// - /// AUTH / ADMINISTRATION METHODS - ////////////////////////////////// + /// + /// @brief Set a image + /// @param[in] data The command data + /// @param[in] comp The component that should be used + /// @param[out] replyMsg The replyMsg on failure + /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF + /// @return True on success + /// + bool setImage(ImageCmdData &data, hyperion::Components comp, QString &replyMsg, hyperion::Components callerComp = hyperion::COMP_INVALID); - /// - /// @brief Delete instance. Requires ADMIN ACCESS - /// @param index The instance index - /// @param replyMsg The reply Msg - /// @return False with reply - /// - bool deleteInstance(quint8 index, QString &replyMsg); + /// + /// @brief Clear a priority in the Muxer, if -1 all priorities are cleared + /// @param priority The priority to clear + /// @param replyMsg the message on failure + /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF + /// @return True on success + /// + bool clearPriority(int priority, QString &replyMsg, hyperion::Components callerComp = hyperion::COMP_INVALID); - /// - /// @brief Create instance. Requires ADMIN ACCESS - /// @param name With given name - /// @return False with reply - /// - QString createInstance(const QString &name); + /// + /// @brief Set a new component state + /// @param comp The component name + /// @param compState The new state of the comp + /// @param replyMsg The reply on failure + /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF + /// @ return True on success + /// + bool setComponentState(const QString &comp, bool &compState, QString &replyMsg, hyperion::Components callerComp = hyperion::COMP_INVALID); - /// - /// @brief Rename an instance. Requires ADMIN ACCESS - /// @param index The instance index - /// @param name With given name - /// @return False with reply - /// - QString setInstanceName(quint8 index, const QString &name); + /// + /// @brief Set a ledToImageMapping type + /// @param type mapping type string + /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF + /// + void setLedMappingType(int type, hyperion::Components callerComp = hyperion::COMP_INVALID); + + /// + /// @brief Set the 2D/3D modes type + /// @param mode The VideoMode + /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF + /// + void setVideoMode(VideoMode mode, hyperion::Components callerComp = hyperion::COMP_INVALID); #if defined(ENABLE_EFFECTENGINE) - /// - /// @brief Delete an effect. Requires ADMIN ACCESS - /// @param name The effect name - /// @return True on success else false - /// - QString deleteEffect(const QString &name); - - /// - /// @brief Delete an effect. Requires ADMIN ACCESS - /// @param name The effect name - /// @return True on success else false - /// - QString saveEffect(const QJsonObject &data); + /// + /// @brief Set an effect + /// @param dat The effect data + /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF + /// REQUIRED dat fields: effectName, priority, duration, origin + /// @return True on success else false + /// + bool setEffect(const EffectCmdData &dat, hyperion::Components callerComp = hyperion::COMP_INVALID); #endif - /// - /// @brief Save settings object. Requires ADMIN ACCESS - /// @param data The data object - /// + /// + /// @brief Set source auto select enabled or disabled + /// @param sate The new state + /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF + /// + void setSourceAutoSelect(bool state, hyperion::Components callerComp = hyperion::COMP_INVALID); + + /// + /// @brief Set the visible priority to given priority + /// @param priority The priority to set + /// @param callerComp The HYPERION COMPONENT that calls this function! e.g. PROT/FLATBUF + /// + void setVisiblePriority(int priority, hyperion::Components callerComp = hyperion::COMP_INVALID); + + /// + /// @brief Register a input or update the meta data of a previous register call + /// ATTENTION: Check unregisterInput() method description !!! + /// @param[in] priority The priority of the channel + /// @param[in] component The component of the channel + /// @param[in] origin Who set the channel (CustomString@IP) + /// @param[in] owner Specific owner string, might be empty + /// @param[in] callerComp The component that call this (e.g. PROTO/FLAT) + /// + void registerInput(int priority, hyperion::Components component, const QString &origin, const QString &owner, hyperion::Components callerComp); + + /// + /// @brief Revoke a registerInput() call by priority. We maintain all registered priorities in this scope + /// ATTENTION: This is MANDATORY if you change (priority change) or stop(clear/timeout) DURING lifetime. If this class destructs it's not needed + /// @param priority The priority to unregister + /// + void unregisterInput(int priority); + + /// + /// @brief Handle the instance switching + /// @param inst The requested instance + /// @return True on success else false + /// + bool setHyperionInstance(quint8 inst); + + /// + /// @brief Check if Hyperion ist enabled + /// @return True when enabled else false + /// + bool isHyperionEnabled(); + + /// + /// @brief Get all instances data + /// @return The instance data + /// + QVector getAllInstanceData() const; + + /// + /// @brief Get the current instances index + /// @return The instance index set + /// + quint8 getCurrentInstanceIndex() const { return _currInstanceIndex; } + + /// + /// @brief Start instance + /// @param index The instance index + /// @param tan The tan + /// @return True on success else false + /// + bool startInstance(quint8 index, int tan = 0); + + /// + /// @brief Stop instance + /// @param index The instance index + /// + void stopInstance(quint8 index); + + ////////////////////////////////// + /// AUTH / ADMINISTRATION METHODS + ////////////////////////////////// + + /// + /// @brief Delete instance. Requires ADMIN ACCESS + /// @param index The instance index + /// @param replyMsg The reply Msg + /// @return False with reply + /// + bool deleteInstance(quint8 index, QString &replyMsg); + + /// + /// @brief Create instance. Requires ADMIN ACCESS + /// @param name With given name + /// @return False with reply + /// + QString createInstance(const QString &name); + + /// + /// @brief Rename an instance. Requires ADMIN ACCESS + /// @param index The instance index + /// @param name With given name + /// @return False with reply + /// + QString setInstanceName(quint8 index, const QString &name); + +#if defined(ENABLE_EFFECTENGINE) + /// + /// @brief Delete an effect. Requires ADMIN ACCESS + /// @param name The effect name + /// @return True on success else false + /// + QString deleteEffect(const QString &name); + + /// + /// @brief Delete an effect. Requires ADMIN ACCESS + /// @param name The effect name + /// @return True on success else false + /// + QString saveEffect(const QJsonObject &data); +#endif + + /// + /// @brief Save settings object. Requires ADMIN ACCESS + /// @param data The data object + /// bool saveSettings(const QJsonObject &data); /// @@ -238,171 +251,189 @@ protected: /// bool restoreSettings(const QJsonObject &data); - /// - /// @brief Test if we are authorized to use the interface - /// @return The result - /// - bool isAuthorized() { return _authorized; }; + /// + /// @brief Set the authorizationn state + /// @param authorized True, if authorized + /// + void setAuthorization(bool authorized) { _authorized = authorized; } - /// - /// @brief Test if we are authorized to use the admin interface - /// @return The result - /// - bool isAdminAuthorized() { return _adminAuthorized; }; + /// + /// @brief Test if we are authorized to use the interface + /// @return The result + /// + bool isAuthorized() const { return _authorized; } - /// - /// @brief Update the Password of Hyperion. Requires ADMIN ACCESS - /// @param password Old password - /// @param newPassword New password - /// @return True on success else false - /// - bool updateHyperionPassword(const QString &password, const QString &newPassword); + /// + /// @brief Set the authorizationn state for admin activities + /// @param authorized True, if authorized + /// + void setAdminAuthorization(bool adminAuthorized) { _adminAuthorized = adminAuthorized; } - /// - /// @brief Get a new token from AuthManager. Requires ADMIN ACCESS - /// @param comment The comment of the request - /// @param def The final definition - /// @return Empty string on success else error message - /// - QString createToken(const QString &comment, AuthManager::AuthDefinition &def); + /// + /// @brief Test if we are authorized to use admin activites + /// @return The result + /// + bool isAdminAuthorized() const { return _adminAuthorized; } - /// - /// @brief Rename a token by given id. Requires ADMIN ACCESS - /// @param id The id of the token - /// @param comment The new comment - /// @return Empty string on success else error message - /// - QString renameToken(const QString &id, const QString &comment); + /// + /// @brief Return, if connection is from local network segment + /// @return The result + /// + bool islocalConnection() const { return _localConnection; } - /// - /// @brief Delete a token by given id. Requires ADMIN ACCESS - /// @param id The id of the token - /// @return Empty string on success else error message - /// - QString deleteToken(const QString &id); + /// + /// @brief Update the Password of Hyperion. Requires ADMIN ACCESS + /// @param password Old password + /// @param newPassword New password + /// @return True on success else false + /// + bool updateHyperionPassword(const QString &password, const QString &newPassword); - /// - /// @brief Set a new token request - /// @param comment The comment - /// @param id The id + /// + /// @brief Get a new token from AuthManager. Requires ADMIN ACCESS + /// @param comment The comment of the request + /// @param def The final definition + /// @return Empty string on success else error message + /// + QString createToken(const QString &comment, AuthManager::AuthDefinition &def); + + /// + /// @brief Rename a token by given id. Requires ADMIN ACCESS + /// @param tokenId The id of the token + /// @param comment The new comment + /// @return Empty string on success else error message + /// + QString renameToken(const QString &tokenId, const QString &comment); + + /// + /// @brief Delete a token by given id. Requires ADMIN ACCESS + /// @param tokenId The id of the token + /// @return Empty string on success else error message + /// + QString deleteToken(const QString &tokenId); + + /// + /// @brief Set a new token request + /// @param comment The comment + /// @param tokenId The id of the token /// @param tan The tan - /// - void setNewTokenRequest(const QString &comment, const QString &id, const int &tan); + /// + void setNewTokenRequest(const QString &comment, const QString &tokenId, const int &tan); - /// - /// @brief Cancel new token request - /// @param comment The comment - /// @param id The id - /// - void cancelNewTokenRequest(const QString &comment, const QString &id); + /// + /// @brief Cancel new token request + /// @param comment The comment + /// @param tokenId The id of the token + /// + void cancelNewTokenRequest(const QString &comment, const QString &tokenId); - /// - /// @brief Handle a pending token request. Requires ADMIN ACCESS - /// @param id The id fo the request - /// @param accept True when it should be accepted, else false - /// @return True on success - bool handlePendingTokenRequest(const QString &id, bool accept); - - /// - /// @brief Get the current List of Tokens. Requires ADMIN ACCESS - /// @param def returns the defintions - /// @return True on success - /// - bool getTokenList(QVector &def); - - /// - /// @brief Get all current pending token requests. Requires ADMIN ACCESS - /// @return True on success - /// - bool getPendingTokenRequests(QVector &map); - - /// - /// @brief Is User Token Authorized. On success this will grant acces to API and ADMIN API - /// @param userToken The user Token - /// @return True on succes - /// - bool isUserTokenAuthorized(const QString &userToken); - - /// - /// @brief Get the current User Token (session token). Requires ADMIN ACCESS - /// @param userToken The user Token - /// @return True on success - /// - bool getUserToken(QString &userToken); - - /// - /// @brief Is a token authorized. On success this will grant acces to the API (NOT ADMIN API) - /// @param token The user Token + /// + /// @brief Handle a pending token request. Requires ADMIN ACCESS + /// @param tokenId The id fo the request + /// @param accept True when it should be accepted, else false /// @return True on success - /// - bool isTokenAuthorized(const QString &token); + bool handlePendingTokenRequest(const QString &tokenId, bool accept); - /// - /// @brief Is User authorized. On success this will grant acces to the API and ADMIN API - /// @param password The password of the User - /// @return True if authorized - /// - bool isUserAuthorized(const QString &password); + /// + /// @brief Get the current List of Tokens. Requires ADMIN ACCESS + /// @param def returns the defintions + /// @return True on success + /// + bool getTokenList(QVector &def); - /// - /// @brief Test if Hyperion has the default PW - /// @return The result - /// - bool hasHyperionDefaultPw(); + /// + /// @brief Get all current pending token requests. Requires ADMIN ACCESS + /// @return True on success + /// + bool getPendingTokenRequests(QVector &map); - /// - /// @brief Logout revokes all authorizations - /// - void logout(); + /// + /// @brief Is User Token Authorized. On success this will grant acces to API and ADMIN API + /// @param userToken The user Token + /// @return True on succes + /// + bool isUserTokenAuthorized(const QString &userToken); - /// Reflect auth status of this client - bool _authorized; - bool _adminAuthorized; + /// + /// @brief Get the current User Token (session token). Requires ADMIN ACCESS + /// @param userToken The user Token + /// @return True on success + /// + bool getUserToken(QString &userToken); - /// Is this a local connection - bool _localConnection; + /// + /// @brief Is a token authorized. On success this will grant acces to the API (NOT ADMIN API) + /// @param token The user Token + /// @return True on success + /// + bool isTokenAuthorized(const QString &token); - AuthManager *_authManager; - HyperionIManager *_instanceManager; + /// + /// @brief Is User authorized. On success this will grant acces to the API and ADMIN API + /// @param password The password of the User + /// @return True if authorized + /// + bool isUserAuthorized(const QString &password); - Logger *_log; - Hyperion *_hyperion; + /// + /// @brief Test if Hyperion has the default PW + /// @return The result + /// + bool hasHyperionDefaultPw(); + + /// + /// @brief Logout revokes all authorizations + /// + void logout(); + + + AuthManager *_authManager; + HyperionIManager *_instanceManager; + + Logger *_log; + Hyperion *_hyperion; signals: - /// - /// @brief The API might decide to block connections for security reasons, this emitter should close the socket - /// - void forceClose(); + /// + /// @brief The API might decide to block connections for security reasons, this emitter should close the socket + /// + void forceClose(); - /// - /// @brief Emits whenever a new Token request is pending. This signal is just active when ADMIN ACCESS has been granted - /// @param id The id of the request - /// @param comment The comment of the request; If the commen is EMPTY the request has been revoked by the caller. So remove it from the pending list - /// - void onPendingTokenRequest(const QString &id, const QString &comment); + /// + /// @brief Emits whenever a new Token request is pending. This signal is just active when ADMIN ACCESS has been granted + /// @param tokenId The id of the request + /// @param comment The comment of the request; If the commen is EMPTY the request has been revoked by the caller. So remove it from the pending list + /// + void onPendingTokenRequest(const QString &tokenId, const QString &comment); - /// - /// @brief Handle emits from AuthManager of accepted/denied/timeouts token request, just if QObject matches with this instance it will emit. - /// @param success If true the request was accepted else false and no token was created - /// @param token The new token that is now valid - /// @param comment The comment that was part of the request - /// @param id The id that was part of the request - /// @param tan The tan that was part of the request - /// - void onTokenResponse(bool success, const QString &token, const QString &comment, const QString &id, const int &tan); + /// + /// @brief Handle emits from AuthManager of accepted/denied/timeouts token request, just if QObject matches with this instance it will emit. + /// @param success If true the request was accepted else false and no token was created + /// @param token The new token that is now valid + /// @param comment The comment that was part of the request + /// @param tokenId The id that was part of the request + /// @param tan The tan that was part of the request + /// + void onTokenResponse(bool success, const QString &token, const QString &comment, const QString &tokenId, const int &tan); - /// - /// @brief Handle emits from HyperionIManager of startInstance request, just if QObject matches with this instance it will emit. - /// @param tan The tan that was part of the request - /// - void onStartInstanceResponse(const int &tan); + /// + /// @brief Handle emits from HyperionIManager of startInstance request, just if QObject matches with this instance it will emit. + /// @param tan The tan that was part of the request + /// + void onStartInstanceResponse(const int &tan); private: - void stopDataConnectionss(); - // Contains all active register call data - std::map _activeRegisters; + /// Reflect authorization status of this client + bool _authorized; + bool _adminAuthorized; - // current instance index - quint8 _currInstanceIndex; + /// Is this a local connection + bool _localConnection; + + // Contains all active register call data + std::map _activeRegisters; + + // current instance index + quint8 _currInstanceIndex; }; diff --git a/include/api/JsonAPI.h b/include/api/JsonAPI.h index 8e36b158..35d7e204 100644 --- a/include/api/JsonAPI.h +++ b/include/api/JsonAPI.h @@ -2,19 +2,24 @@ // parent class #include +#include + #include // hyperion includes #include #include #include +#include // qt includes #include #include +#include +#include class QTimer; -class JsonCB; +class JsonCallbacks; class AuthManager; class JsonAPI : public API @@ -46,40 +51,24 @@ public: void initialize(); public slots: - /// - /// @brief Is called whenever the current Hyperion instance pushes new led raw values (if enabled) - /// @param ledColors The current led colors - /// - void streamLedcolorsUpdate(const std::vector &ledColors); - - /// - /// @brief Push images whenever hyperion emits (if enabled) - /// @param image The current image - /// - void setImage(const Image &image); - - /// - /// @brief Process and push new log messages from logger (if enabled) - /// - void incommingLogMessage(const Logger::T_LOG_MESSAGE &); private slots: /// /// @brief Handle emits from API of a new Token request. - /// @param id The id of the request + /// @param identifier The identifier of the request /// @param comment The comment which needs to be accepted /// - void newPendingTokenRequest(const QString &id, const QString &comment); + void issueNewPendingTokenRequest(const QString &identifier, const QString &comment); /// /// @brief Handle emits from AuthManager of accepted/denied/timeouts token request, just if QObject matches with this instance we are allowed to send response. /// @param success If true the request was accepted else false and no token was created /// @param token The new token that is now valid /// @param comment The comment that was part of the request - /// @param id The id that was part of the request + /// @param identifier The identifier that was part of the request /// @param tan The tan that was part of the request /// - void handleTokenResponse(bool success, const QString &token, const QString &comment, const QString &id, const int &tan); + void handleTokenResponse(bool success, const QString &token, const QString &comment, const QString &identifier, const int &tan); /// /// @brief Handle whenever the state of a instance (HyperionIManager) changes according to enum instanceState @@ -89,11 +78,6 @@ private slots: /// void handleInstanceStateChange(InstanceState state, quint8 instance, const QString &name = QString()); - /// - /// @brief Stream a new LED Colors update - /// - void streamLedColorsUpdate(); - signals: /// /// Signal emits with the reply message provided with handleMessage() @@ -111,31 +95,9 @@ signals: void signalEvent(Event event); private: - // true if further callbacks are forbidden (http) - bool _noListener; - /// The peer address of the client - QString _peerAddress; - - // The JsonCB instance which handles data subscription/notifications - JsonCB *_jsonCB; - - // streaming buffers - QJsonObject _streaming_leds_reply; - QJsonObject _streaming_image_reply; - QJsonObject _streaming_logging_reply; - - /// flag to determine state of log streaming - bool _streaming_logging_activated; - - /// timer for led color refresh - QTimer *_ledStreamTimer; - - /// led stream connection handle - QMetaObject::Connection _ledStreamConnection; - - /// the current streaming led values - std::vector _currentLedValues; + void handleCommand(const JsonApiCommand& cmd, const QJsonObject &message); + void handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject &message); /// /// @brief Handle the switches of Hyperion instances @@ -150,14 +112,14 @@ private: /// /// @param message the incoming message /// - void handleColorCommand(const QJsonObject &message, const QString &command, int tan); + void handleColorCommand(const QJsonObject& message, const JsonApiCommand& cmd); /// /// Handle an incoming JSON Image message /// /// @param message the incoming message /// - void handleImageCommand(const QJsonObject &message, const QString &command, int tan); + void handleImageCommand(const QJsonObject &message, const JsonApiCommand& cmd); #if defined(ENABLE_EFFECTENGINE) /// @@ -165,21 +127,21 @@ private: /// /// @param message the incoming message /// - void handleEffectCommand(const QJsonObject &message, const QString &command, int tan); + void handleEffectCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// /// Handle an incoming JSON Effect message (Write JSON Effect) /// /// @param message the incoming message /// - void handleCreateEffectCommand(const QJsonObject &message, const QString &command, int tan); + void handleCreateEffectCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// /// Handle an incoming JSON Effect message (Delete JSON Effect) /// /// @param message the incoming message /// - void handleDeleteEffectCommand(const QJsonObject &message, const QString &command, int tan); + void handleDeleteEffectCommand(const QJsonObject &message, const JsonApiCommand& cmd); #endif /// @@ -187,158 +149,250 @@ private: /// /// @param message the incoming message /// - void handleSysInfoCommand(const QJsonObject &message, const QString &command, int tan); + void handleSysInfoCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// /// Handle an incoming JSON Server info message /// /// @param message the incoming message /// - void handleServerInfoCommand(const QJsonObject &message, const QString &command, int tan); + void handleServerInfoCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// /// Handle an incoming JSON Clear message /// /// @param message the incoming message /// - void handleClearCommand(const QJsonObject &message, const QString &command, int tan); + void handleClearCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// /// Handle an incoming JSON Clearall message /// /// @param message the incoming message /// - void handleClearallCommand(const QJsonObject &message, const QString &command, int tan); + void handleClearallCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// /// Handle an incoming JSON Adjustment message /// /// @param message the incoming message /// - void handleAdjustmentCommand(const QJsonObject &message, const QString &command, int tan); + void handleAdjustmentCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// /// Handle an incoming JSON SourceSelect message /// /// @param message the incoming message /// - void handleSourceSelectCommand(const QJsonObject &message, const QString &command, int tan); + void handleSourceSelectCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// Handle an incoming JSON GetConfig message and check subcommand /// /// @param message the incoming message /// - void handleConfigCommand(const QJsonObject &message, const QString &command, int tan); + void handleConfigCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// Handle an incoming JSON GetSchema message from handleConfigCommand() /// /// @param message the incoming message /// - void handleSchemaGetCommand(const QJsonObject &message, const QString &command, int tan); + void handleSchemaGetCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// Handle an incoming JSON SetConfig message from handleConfigCommand() /// /// @param message the incoming message /// - void handleConfigSetCommand(const QJsonObject &message, const QString &command, int tan); + void handleConfigSetCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// Handle an incoming JSON RestoreConfig message from handleConfigCommand() /// /// @param message the incoming message /// - void handleConfigRestoreCommand(const QJsonObject &message, const QString &command, int tan); + void handleConfigRestoreCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// /// Handle an incoming JSON Component State message /// /// @param message the incoming message /// - void handleComponentStateCommand(const QJsonObject &message, const QString &command, int tan); + void handleComponentStateCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// Handle an incoming JSON Led Colors message /// /// @param message the incoming message /// - void handleLedColorsCommand(const QJsonObject &message, const QString &command, int tan); + void handleLedColorsCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// Handle an incoming JSON Logging message /// /// @param message the incoming message /// - void handleLoggingCommand(const QJsonObject &message, const QString &command, int tan); + void handleLoggingCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// Handle an incoming JSON Processing message /// /// @param message the incoming message /// - void handleProcessingCommand(const QJsonObject &message, const QString &command, int tan); + void handleProcessingCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// Handle an incoming JSON VideoMode message /// /// @param message the incoming message /// - void handleVideoModeCommand(const QJsonObject &message, const QString &command, int tan); + void handleVideoModeCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// Handle an incoming JSON plugin message /// /// @param message the incoming message /// - void handleAuthorizeCommand(const QJsonObject &message, const QString &command, int tan); + void handleAuthorizeCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// Handle an incoming JSON instance message /// /// @param message the incoming message /// - void handleInstanceCommand(const QJsonObject &message, const QString &command, int tan); + void handleInstanceCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// Handle an incoming JSON Led Device message /// /// @param message the incoming message /// - void handleLedDeviceCommand(const QJsonObject &message, const QString &command, int tan); + void handleLedDeviceCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// Handle an incoming JSON message regarding Input Sources (Grabbers) /// /// @param message the incoming message /// - void handleInputSourceCommand(const QJsonObject& message, const QString& command, int tan); + void handleInputSourceCommand(const QJsonObject& message, const JsonApiCommand& cmd); /// Handle an incoming JSON message to request remote hyperion servers providing a given hyperion service /// /// @param message the incoming message /// - void handleServiceCommand(const QJsonObject &message, const QString &command, int tan); + void handleServiceCommand(const QJsonObject &message, const JsonApiCommand& cmd); /// Handle an incoming JSON message for actions related to the overall Hyperion system /// /// @param message the incoming message /// - void handleSystemCommand(const QJsonObject &message, const QString &command, int tan); + void handleSystemCommand(const QJsonObject &message, const JsonApiCommand& cmd); - /// - /// Handle an incoming JSON message of unknown type - /// - void handleNotImplemented(const QString &command, int tan); + + void applyColorAdjustments(const QJsonObject &adjustment, ColorAdjustment *colorAdjustment); + void applyColorAdjustment(const QString &colorName, const QJsonObject &adjustment, RgbChannelAdjustment &rgbAdjustment); + void applyGammaTransform(const QString &transformName, const QJsonObject &adjustment, RgbTransform &rgbTransform, char channel); + + void applyTransforms(const QJsonObject &adjustment, ColorAdjustment *colorAdjustment); + template + void applyTransform(const QString &transformName, const QJsonObject &adjustment, T &transform, void (T::*setFunction)(bool)); + template + void applyTransform(const QString &transformName, const QJsonObject &adjustment, T &transform, void (T::*setFunction)(double)); + template + void applyTransform(const QString &transformName, const QJsonObject &adjustment, T &transform, void (T::*setFunction)(uint8_t)); + + void handleTokenRequired(const JsonApiCommand& cmd); + void handleAdminRequired(const JsonApiCommand& cmd); + void handleNewPasswordRequired(const JsonApiCommand& cmd); + void handleLogout(const JsonApiCommand& cmd); + void handleNewPassword(const QJsonObject &message, const JsonApiCommand& cmd); + void handleCreateToken(const QJsonObject &message, const JsonApiCommand& cmd); + void handleRenameToken(const QJsonObject &message, const JsonApiCommand& cmd); + void handleDeleteToken(const QJsonObject &message, const JsonApiCommand& cmd); + void handleRequestToken(const QJsonObject &message, const JsonApiCommand& cmd); + void handleGetPendingTokenRequests(const JsonApiCommand& cmd); + void handleAnswerRequest(const QJsonObject &message, const JsonApiCommand& cmd); + void handleGetTokenList(const JsonApiCommand& cmd); + void handleLogin(const QJsonObject &message, const JsonApiCommand& cmd); + + void handleLedDeviceDiscover(LedDevice& ledDevice, const QJsonObject& message, const JsonApiCommand& cmd); + void handleLedDeviceGetProperties(LedDevice& ledDevice, const QJsonObject& message, const JsonApiCommand& cmd); + void handleLedDeviceIdentify(LedDevice& ledDevice, const QJsonObject& message, const JsonApiCommand& cmd); + void handleLedDeviceAddAuthorization(LedDevice& ledDevice, const QJsonObject& message, const JsonApiCommand& cmd); + + QJsonObject getBasicCommandReply(bool success, const QString &command, int tan, InstanceCmd::Type isInstanceCmd) const; /// /// Send a standard reply indicating success /// - void sendSuccessReply(const QString &command = "", int tan = 0); + void sendSuccessReply(const JsonApiCommand& cmd); + + /// + /// Send a standard reply indicating success + /// + void sendSuccessReply(const QString &command = "", int tan = 0, InstanceCmd::Type isInstanceCmd = InstanceCmd::No); /// /// Send a standard reply indicating success with data /// - void sendSuccessDataReply(const QJsonDocument &doc, const QString &command = "", int tan = 0); + void sendSuccessDataReply(const QJsonValue &infoData, const JsonApiCommand& cmd); + + /// + /// Send a standard reply indicating success with data + /// + void sendSuccessDataReply(const QJsonValue &infoData, const QString &command = "", int tan = 0, InstanceCmd::Type isInstanceCmd = InstanceCmd::No); + + /// + /// Send a standard reply indicating success with data and error details + /// + void sendSuccessDataReplyWithError(const QJsonValue &infoData, const JsonApiCommand& cmd, const QStringList& errorDetails = {}); + + /// + /// Send a standard reply indicating success with data and error details + /// + void sendSuccessDataReplyWithError(const QJsonValue &infoData, const QString &command = "", int tan = 0, const QStringList& errorDetails = {}, InstanceCmd::Type isInstanceCmd = InstanceCmd::No); + + /// + /// Send a message with data. + /// Note: To be used as a new message and not as a response to a previous request. + /// + void sendNewRequest(const QJsonValue &infoData, const JsonApiCommand& cmd); + + /// + /// Send a message with data + /// Note: To be used as a new message and not as a response to a previous request. + /// + void sendNewRequest(const QJsonValue &infoData, const QString &command, InstanceCmd::Type isInstanceCmd = InstanceCmd::No); /// /// Send an error message back to the client /// /// @param error String describing the error /// - void sendErrorReply(const QString &error, const QString &command = "", int tan = 0); + void sendErrorReply(const QString &error, const JsonApiCommand& cmd); + + /// + /// Send an error message back to the client + /// + /// @param error String describing the error + /// @param errorDetails additional information detailing the error scenario + /// + void sendErrorReply(const QString &error, const QStringList& errorDetails, const JsonApiCommand& cmd); + + /// + /// Send an error message back to the client + /// + /// @param error String describing the error + /// @param errorDetails additional information detailing the error scenario + /// + void sendErrorReply(const QString &error, const QStringList& errorDetails = {}, const QString &command = "", int tan = 0, InstanceCmd::Type isInstanceCmd = InstanceCmd::No); + + void sendNoAuthorization(const JsonApiCommand& cmd); /// /// @brief Kill all signal/slot connections to stop possible data emitter /// - void stopDataConnections(); + void stopDataConnections() override; + + static QString findCommand (const QString& jsonS); + static int findTan (const QString& jsonString); + + // true if further callbacks are forbidden (http) + bool _noListener; + + /// The peer address of the client + QString _peerAddress; + + // The JsonCallbacks instance which handles data subscription/notifications + QSharedPointer _jsonCB; + }; diff --git a/include/api/JsonApiCommand.h b/include/api/JsonApiCommand.h new file mode 100644 index 00000000..c63d968b --- /dev/null +++ b/include/api/JsonApiCommand.h @@ -0,0 +1,332 @@ +#ifndef JSONAPICOMMAND_H +#define JSONAPICOMMAND_H + +#include +#include +#include + +class Command { +public: + enum Type { + Unknown, + Adjustment, + Authorize, + Clear, + ClearAll, + Color, + ComponentState, + Config, + Correction, + CreateEffect, + DeleteEffect, + Effect, + Image, + InputSource, + Instance, + LedColors, + LedDevice, + Logging, + Processing, + ServerInfo, + Service, + SourceSelect, + SysInfo, + System, + Temperature, + Transform, + VideoMode + }; + + static QString toString(Type type) { + switch (type) { + case Adjustment: return "adjustment"; + case Authorize: return "authorize"; + case Clear: return "clear"; + case ClearAll: return "clearall"; + case Color: return "color"; + case ComponentState: return "componentstate"; + case Config: return "config"; + case Correction: return "correction"; + case CreateEffect: return "create-effect"; + case DeleteEffect: return "delete-effect"; + case Effect: return "effect"; + case Image: return "image"; + case InputSource: return "inputsource"; + case Instance: return "instance"; + case LedColors: return "ledcolors"; + case LedDevice: return "leddevice"; + case Logging: return "logging"; + case Processing: return "processing"; + case ServerInfo: return "serverinfo"; + case SourceSelect: return "sourceselect"; + case SysInfo: return "sysinfo"; + case System: return "system"; + case Temperature: return "temperature"; + case Transform: return "transform"; + case VideoMode: return "videomode"; + case Service: return "service"; + default: return "unknown"; + } + } +}; + +class SubCommand { +public: + enum Type { + Unknown, + Empty, + AdminRequired, + AddAuthorization, + AnswerRequest, + CreateInstance, + CreateToken, + DeleteInstance, + DeleteToken, + Discover, + GetConfig, + GetInfo, + GetPendingTokenRequests, + GetProperties, + GetSchema, + GetSubscriptionCommands, + GetSubscriptions, + GetTokenList, + Identify, + Idle, + ImageStreamStart, + ImageStreamStop, + LedStreamStart, + LedStreamStop, + Login, + Logout, + NewPassword, + NewPasswordRequired, + Reload, + RenameToken, + RequestToken, + Restart, + RestoreConfig, + Resume, + SaveName, + SetConfig, + Start, + StartInstance, + Stop, + StopInstance, + Subscribe, + Suspend, + SwitchTo, + ToggleIdle, + ToggleSuspend, + TokenRequired, + Unsubscribe + }; + + static QString toString(Type type) { + switch (type) { + case Empty: return ""; + case AdminRequired: return "adminRequired"; + case AddAuthorization: return "addAuthorization"; + case AnswerRequest: return "answerRequest"; + case CreateInstance: return "createInstance"; + case CreateToken: return "createToken"; + case DeleteInstance: return "deleteInstance"; + case DeleteToken: return "deleteToken"; + case Discover: return "discover"; + case GetConfig: return "getconfig"; + case GetInfo: return "getInfo"; + case GetPendingTokenRequests: return "getPendingTokenRequests"; + case GetProperties: return "getProperties"; + case GetSchema: return "getschema"; + case GetSubscriptionCommands: return "getSubscriptionCommands"; + case GetSubscriptions: return "getSubscriptions"; + case GetTokenList: return "getTokenList"; + case Identify: return "identify"; + case Idle: return "idle"; + case ImageStreamStart: return "imagestream-start"; + case ImageStreamStop: return "imagestream-stop"; + case LedStreamStart: return "ledstream-start"; + case LedStreamStop: return "ledstream-stop"; + case Login: return "login"; + case Logout: return "logout"; + case NewPassword: return "newPassword"; + case NewPasswordRequired: return "newPasswordRequired"; + case Reload: return "reload"; + case RenameToken: return "renameToken"; + case RequestToken: return "requestToken"; + case Restart: return "restart"; + case RestoreConfig: return "restoreconfig"; + case Resume: return "resume"; + case SaveName: return "saveName"; + case SetConfig: return "setconfig"; + case Start: return "start"; + case StartInstance: return "startInstance"; + case Stop: return "stop"; + case StopInstance: return "stopInstance"; + case Subscribe: return "subscribe"; + case Suspend: return "suspend"; + case SwitchTo: return "switchTo"; + case ToggleIdle: return "toggleIdle"; + case ToggleSuspend: return "toggleSuspend"; + case TokenRequired: return "tokenRequired"; + case Unsubscribe: return "unsubscribe"; + default: return "unknown"; + } + } +}; + +class Authorization { +public: + enum Type { + Admin, + Yes, + No + }; +}; + +class NoListenerCmd { +public: + enum Type { + No, + Yes + }; +}; + +class InstanceCmd { +public: + enum Type { + No, + Yes, + Multi + }; +}; + +class JsonApiCommand { +public: + + JsonApiCommand() + : command(Command::Unknown), + subCommand(SubCommand::Unknown), + tan(0), + authorization(Authorization::Admin), + isInstanceCmd(InstanceCmd::No), + isNolistenerCmd(NoListenerCmd::Yes) + {} + + JsonApiCommand(Command::Type command, SubCommand::Type subCommand, + Authorization::Type authorization, + InstanceCmd::Type isInstanceCmd, + NoListenerCmd::Type isNolistenerCmd, + int tan = 0) + : command(command), + subCommand(subCommand), + tan(tan), + authorization(authorization), + isInstanceCmd(isInstanceCmd), + isNolistenerCmd(isNolistenerCmd) + {} + + Command::Type getCommand() const { return command; } + SubCommand::Type getSubCommand() const { return subCommand; } + InstanceCmd::Type instanceCmd() const { return isInstanceCmd; } + int getTan() const { return tan; } + + QString toString() const { + QString cmd = Command::toString(command); + if (subCommand > SubCommand::Empty) { + cmd += QString("-%2").arg(SubCommand::toString(subCommand)); + } + return cmd; + } + + Command::Type command; + SubCommand::Type subCommand; + int tan; + + Authorization::Type authorization; + InstanceCmd::Type isInstanceCmd; + NoListenerCmd::Type isNolistenerCmd; +}; + +typedef QMap, JsonApiCommand> CommandLookupMap; + +class ApiCommandRegister { +public: + + static const CommandLookupMap& getCommandLookup() { + static const CommandLookupMap commandLookup { + { {"adjustment", ""}, { Command::Adjustment, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, + { {"authorize", "adminRequired"}, { Command::Authorize, SubCommand::AdminRequired, Authorization::No, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"authorize", "answerRequest"}, { Command::Authorize, SubCommand::AnswerRequest, Authorization::Admin, InstanceCmd::No, NoListenerCmd::No} }, + { {"authorize", "createToken"}, { Command::Authorize, SubCommand::CreateToken, Authorization::Admin, InstanceCmd::No, NoListenerCmd::No} }, + { {"authorize", "deleteToken"}, { Command::Authorize, SubCommand::DeleteToken, Authorization::Admin, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"authorize", "getPendingTokenRequests"}, { Command::Authorize, SubCommand::GetPendingTokenRequests, Authorization::Admin, InstanceCmd::No, NoListenerCmd::No} }, + { {"authorize", "getTokenList"}, { Command::Authorize, SubCommand::GetTokenList, Authorization::Admin, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"authorize", "login"}, { Command::Authorize, SubCommand::Login, Authorization::No, InstanceCmd::No, NoListenerCmd::No} }, + { {"authorize", "logout"}, { Command::Authorize, SubCommand::Logout, Authorization::No, InstanceCmd::No, NoListenerCmd::No} }, + { {"authorize", "newPassword"}, { Command::Authorize, SubCommand::NewPassword, Authorization::Admin, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"authorize", "newPasswordRequired"}, { Command::Authorize, SubCommand::NewPasswordRequired, Authorization::No, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"authorize", "renameToken"}, { Command::Authorize, SubCommand::RenameToken, Authorization::Admin, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"authorize", "requestToken"}, { Command::Authorize, SubCommand::RequestToken, Authorization::No, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"authorize", "tokenRequired"}, { Command::Authorize, SubCommand::TokenRequired, Authorization::No, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"clear", ""}, { Command::Clear, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, + { {"clearall", ""}, { Command::ClearAll, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, + { {"color", ""}, { Command::Color, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, + { {"componentstate", ""}, { Command::ComponentState, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, + { {"config", "getconfig"}, { Command::Config, SubCommand::GetConfig, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"config", "getschema"}, { Command::Config, SubCommand::GetSchema, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"config", "reload"}, { Command::Config, SubCommand::Reload, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"config", "restoreconfig"}, { Command::Config, SubCommand::RestoreConfig, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"config", "setconfig"}, { Command::Config, SubCommand::SetConfig, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"correction", ""}, { Command::Correction, SubCommand::Empty, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"create-effect", ""}, { Command::CreateEffect, SubCommand::Empty, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"delete-effect", ""}, { Command::DeleteEffect, SubCommand::Empty, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"effect", ""}, { Command::Effect, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, + { {"image", ""}, { Command::Image, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, + { {"inputsource", "discover"}, { Command::InputSource, SubCommand::Discover, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"inputsource", "getProperties"}, { Command::InputSource, SubCommand::GetProperties, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"instance", "createInstance"}, { Command::Instance, SubCommand::CreateInstance, Authorization::Admin, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"instance", "deleteInstance"}, { Command::Instance, SubCommand::DeleteInstance, Authorization::Admin, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"instance", "saveName"}, { Command::Instance, SubCommand::SaveName, Authorization::Admin, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"instance", "startInstance"}, { Command::Instance, SubCommand::StartInstance, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"instance", "stopInstance"}, { Command::Instance, SubCommand::StopInstance, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"instance", "switchTo"}, { Command::Instance, SubCommand::SwitchTo, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"ledcolors", "imagestream-start"}, { Command::LedColors, SubCommand::ImageStreamStart, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"ledcolors", "imagestream-stop"}, { Command::LedColors, SubCommand::ImageStreamStop, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"ledcolors", "ledstream-start"}, { Command::LedColors, SubCommand::LedStreamStart, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"ledcolors", "ledstream-stop"}, { Command::LedColors, SubCommand::LedStreamStop, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"leddevice", "addAuthorization"}, { Command::LedDevice, SubCommand::AddAuthorization, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"leddevice", "discover"}, { Command::LedDevice, SubCommand::Discover, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"leddevice", "getProperties"}, { Command::LedDevice, SubCommand::GetProperties, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"leddevice", "identify"}, { Command::LedDevice, SubCommand::Identify, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"logging", "start"}, { Command::Logging, SubCommand::Start, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"logging", "stop"}, { Command::Logging, SubCommand::Stop, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"processing", ""}, { Command::Processing, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, + { {"serverinfo", ""}, { Command::ServerInfo, SubCommand::Empty, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"serverinfo", "getInfo"}, { Command::ServerInfo, SubCommand::GetInfo, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"serverinfo", "subscribe"}, { Command::ServerInfo, SubCommand::Subscribe, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::No} }, + { {"serverinfo", "unsubscribe"}, { Command::ServerInfo, SubCommand::Unsubscribe, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::No} }, + { {"serverinfo", "getSubscriptions"}, { Command::ServerInfo, SubCommand::GetSubscriptions, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::No} }, + { {"serverinfo", "getSubscriptionCommands"}, { Command::ServerInfo, SubCommand::GetSubscriptionCommands, Authorization::No, InstanceCmd::No, NoListenerCmd::No} }, + { {"service", "discover"}, { Command::Service, SubCommand::Discover, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"sourceselect", ""}, { Command::SourceSelect, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, + { {"sysinfo", ""}, { Command::SysInfo, SubCommand::Empty, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"system", "restart"}, { Command::System, SubCommand::Restart, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"system", "resume"}, { Command::System, SubCommand::Resume, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"system", "suspend"}, { Command::System, SubCommand::Suspend, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"system", "toggleSuspend"}, { Command::System, SubCommand::ToggleSuspend, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"system", "idle"}, { Command::System, SubCommand::Idle, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"system", "toggleIdle"}, { Command::System, SubCommand::ToggleIdle, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} }, + { {"temperature", ""}, { Command::Temperature, SubCommand::Empty, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"transform", ""}, { Command::Transform, SubCommand::Empty, Authorization::Yes, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"videomode", ""}, { Command::VideoMode, SubCommand::Empty, Authorization::Yes, InstanceCmd::No, NoListenerCmd::Yes} } + }; + return commandLookup; + } + + static JsonApiCommand getCommandInfo(const QString& command, const QString& subCommand) { + return getCommandLookup().value({command, subCommand}); + } +}; + +#endif // JSONAPICOMMAND_H diff --git a/include/api/JsonApiSubscription.h b/include/api/JsonApiSubscription.h new file mode 100644 index 00000000..3d011bbd --- /dev/null +++ b/include/api/JsonApiSubscription.h @@ -0,0 +1,135 @@ +#ifndef JSONAPISUBSCRIPTION_H +#define JSONAPISUBSCRIPTION_H + +#include // Required to determine the cmake options + +#include +#include + + +class Subscription { +public: + enum Type { + Unknown, + AdjustmentUpdate, + ComponentsUpdate, +#if defined(ENABLE_EFFECTENGINE) + EffectsUpdate, +#endif + EventUpdate, + ImageToLedMappingUpdate, + ImageUpdate, + InstanceUpdate, + LedColorsUpdate, + LedsUpdate, + LogMsgUpdate, + PrioritiesUpdate, + SettingsUpdate, + TokenUpdate, + VideomodeUpdate + }; + + static QString toString(Type type) { + switch (type) { + case AdjustmentUpdate: return "adjustment-update"; + case ComponentsUpdate: return "components-update"; +#if defined(ENABLE_EFFECTENGINE) + case EffectsUpdate: return "effects-update"; +#endif + case EventUpdate: return "event-update"; + case ImageToLedMappingUpdate: return "imageToLedMapping-update"; + case ImageUpdate: return "ledcolors-imagestream-update"; + case InstanceUpdate: return "instance-update"; + case LedColorsUpdate: return "ledcolors-ledstream-update"; + case LedsUpdate: return "leds-update"; + case LogMsgUpdate: return "logmsg-update"; + case PrioritiesUpdate: return "priorities-update"; + case SettingsUpdate: return "settings-update"; + case TokenUpdate: return "token-update"; + case VideomodeUpdate: return "videomode-update"; + default: return "unknown"; + } + } + + static bool isInstanceSpecific(Type type) { + switch (type) { + case AdjustmentUpdate: + case ComponentsUpdate: +#if defined(ENABLE_EFFECTENGINE) + case EffectsUpdate: +#endif + case ImageToLedMappingUpdate: + case ImageUpdate: + case LedColorsUpdate: + case LedsUpdate: + case PrioritiesUpdate: + case SettingsUpdate: + return true; + case EventUpdate: + case InstanceUpdate: + case LogMsgUpdate: + case TokenUpdate: + case VideomodeUpdate: + default: + return false; + } + } +}; + +class JsonApiSubscription { +public: + + JsonApiSubscription() + : cmd(Subscription::Unknown), + isAll(false) + {} + + JsonApiSubscription(Subscription::Type cmd, bool isAll) + : cmd(cmd), + isAll(isAll) + {} + + Subscription::Type getSubscription() const { return cmd; } + bool isPartOfAll() const { return isAll; } + + QString toString() const { + return Subscription::toString(cmd); + } + + Subscription::Type cmd; + bool isAll; +}; + +typedef QMap SubscriptionLookupMap; + +class ApiSubscriptionRegister { +public: + + static const SubscriptionLookupMap& getSubscriptionLookup() { + static const SubscriptionLookupMap subscriptionLookup { + { {"adjustment-update"}, { Subscription::AdjustmentUpdate, true} }, + { {"components-update"}, { Subscription::ComponentsUpdate, true} }, +#if defined(ENABLE_EFFECTENGINE) + { {"effects-update"}, { Subscription::EffectsUpdate, true} }, +#endif + { {"event-update"}, { Subscription::EventUpdate, true} }, + { {"imageToLedMapping-update"}, { Subscription::ImageToLedMappingUpdate, true} }, + { {"ledcolors-imagestream-update"}, { Subscription::ImageUpdate, false} }, + { {"ledcolors-ledstream-update"}, { Subscription::LedColorsUpdate, false} }, + { {"instance-update"}, { Subscription::InstanceUpdate, true} }, + { {"leds-update"}, { Subscription::LedsUpdate, true} }, + { {"logmsg-update"}, { Subscription::LogMsgUpdate, false} }, + { {"priorities-update"}, { Subscription::PrioritiesUpdate, true} }, + { {"settings-update"}, { Subscription::SettingsUpdate, true} }, + { {"token-update"}, { Subscription::TokenUpdate, true} }, + { {"videomode-update"}, { Subscription::VideomodeUpdate, true} } + }; + return subscriptionLookup; + } + + static JsonApiSubscription getSubscriptionInfo(const QString& subscription) { + return getSubscriptionLookup().value({subscription}); + } +}; + +#endif // JSONAPISUBSCRIPTION_H diff --git a/include/api/JsonCB.h b/include/api/JsonCallbacks.h similarity index 53% rename from include/api/JsonCB.h rename to include/api/JsonCallbacks.h index 2d59b3eb..a5c14ce4 100644 --- a/include/api/JsonCB.h +++ b/include/api/JsonCallbacks.h @@ -1,51 +1,82 @@ #pragma once +#include "api/JsonApiSubscription.h" +#include +#include + // qt incl #include #include +#include -// components def #include - -// videModes #include -// settings #include -// AuthManager #include - #include class Hyperion; class ComponentRegister; class PriorityMuxer; -class JsonCB : public QObject +class JsonCallbacks : public QObject { Q_OBJECT public: - JsonCB(QObject* parent); + JsonCallbacks(Logger* log, const QString& peerAddress, QObject* parent); /// /// @brief Subscribe to future data updates given by cmd - /// @param cmd The cmd which will be subscribed for - /// @param unsubscribe Revert subscription + /// @param cmd The cmd which will be subscribed for /// @return True on success, false if not found /// - bool subscribeFor(const QString& cmd, bool unsubscribe = false); + bool subscribe(const QString& cmd); + + /// + /// @brief Subscribe to future data updates given by subscription list + /// @param type Array of subscriptionsm + /// + QStringList subscribe(const QJsonArray& subscriptions); + + /// + /// @brief Subscribe to future data updates given by cmd + /// @param cmd The cmd which will be subscribed to + /// @return True on success, false if not found + /// + bool subscribe(Subscription::Type subscription); + + /// + /// @brief Unsubscribe to future data updates given by cmd + /// @param cmd The cmd which will be unsubscribed + /// @return True on success, false if not found + /// + bool unsubscribe(const QString& cmd); + + /// + /// @brief Unsubscribe to future data updates given by subscription list + /// @param type Array of subscriptions + /// + QStringList unsubscribe(const QJsonArray& subscriptions); + + /// + /// @brief Unsubscribe to future data updates given by cmd + /// @param cmd The cmd which will be subscribed to + /// @return True on success, false if not found + /// + bool unsubscribe(Subscription::Type cmd); /// /// @brief Get all possible commands to subscribe for + /// @param fullList Return all possible commands or those not triggered by API requests (subscriptions="ALL") /// @return The list of commands /// - QStringList getCommands() { return _availableCommands; }; - + QStringList getCommands(bool fullList = true) const; /// /// @brief Get all subscribed commands /// @return The list of commands /// - QStringList getSubscribedCommands() { return _subscribedCommands; }; + QStringList getSubscribedCommands() const; /// /// @brief Reset subscriptions, disconnect all signals @@ -124,18 +155,49 @@ private slots: /// void handleTokenChange(const QVector &def); + /// + /// @brief Is called whenever the current Hyperion instance pushes new led raw values (if enabled) + /// @param ledColors The current led colors + /// + void handleLedColorUpdate(const std::vector &ledColors); + + /// + /// @brief Is called whenever the current Hyperion instance pushes new image update (if enabled) + /// @param image The current image + /// + void handleImageUpdate(const Image &image); + + /// + /// @brief Process and push new log messages from logger (if enabled) + /// + void handleLogMessageUpdate(const Logger::T_LOG_MESSAGE &); + + /// + /// @brief Is called whenever an event is triggert + /// @param image The current event + /// + void handleEventUpdate(const Event &event); + private: - /// pointer of Hyperion instance + + /// construct callback msg + void doCallback(Subscription::Type cmd, const QVariant& data); + + Logger *_log; Hyperion* _hyperion; + + /// The peer address of the client + QString _peerAddress; + /// pointer of comp register ComponentRegister* _componentRegister; /// priority muxer instance PriorityMuxer* _prioMuxer; - /// contains all available commands - QStringList _availableCommands; + /// contains active subscriptions - QStringList _subscribedCommands; - /// construct callback msg - void doCallback(const QString& cmd, const QVariant& data); + QSet _subscribedCommands; + + /// flag to determine state of log streaming + bool _islogMsgStreamingActive; }; diff --git a/include/api/JsonInfo.h b/include/api/JsonInfo.h new file mode 100644 index 00000000..1346f97e --- /dev/null +++ b/include/api/JsonInfo.h @@ -0,0 +1,43 @@ +#ifndef JSONINFO_H +#define JSONINFO_H + +#include +#include +#include + +#include +#include + +class JsonInfo +{ + +public: + static QJsonArray getAdjustmentInfo(const Hyperion* hyperion, Logger* log); + static QJsonArray getPrioritiestInfo(const Hyperion* hyperion); + static QJsonArray getPrioritiestInfo(int currentPriority, const PriorityMuxer::InputsMap& activeInputs); + static QJsonArray getEffects(const Hyperion* hyperion); + static QJsonArray getAvailableScreenGrabbers(); + static QJsonArray getAvailableVideoGrabbers(); + static QJsonArray getAvailableAudioGrabbers(); + static QJsonObject getGrabbers(const Hyperion* hyperion); + static QJsonObject getAvailableLedDevices(); + static QJsonObject getCecInfo(); + static QJsonArray getServices(); + static QJsonArray getComponents(const Hyperion* hyperion); + static QJsonArray getInstanceInfo(); + static QJsonArray getActiveEffects(const Hyperion* hyperion); + static QJsonArray getActiveColors(const Hyperion* hyperion); + static QJsonArray getTransformationInfo(const Hyperion* hyperion); + static QJsonObject getSystemInfo(const Hyperion* hyperion); + QJsonObject discoverSources (const QString& sourceType, const QJsonObject& params); + +private: + + template + void discoverGrabber(QJsonArray& inputs, const QJsonObject& params) const; + QJsonArray discoverScreenInputs(const QJsonObject& params) const; + QJsonArray discoverVideoInputs(const QJsonObject& params) const; + QJsonArray discoverAudioInputs(const QJsonObject& params) const; +}; + +#endif // JSONINFO_H diff --git a/include/api/apiStructs.h b/include/api/apiStructs.h index 112132fc..de1d6e67 100644 --- a/include/api/apiStructs.h +++ b/include/api/apiStructs.h @@ -2,6 +2,9 @@ #include #include +#include + +#include struct ImageCmdData { diff --git a/include/db/AuthTable.h b/include/db/AuthTable.h index c6495ea5..161c3856 100644 --- a/include/db/AuthTable.h +++ b/include/db/AuthTable.h @@ -8,6 +8,11 @@ #include #include +namespace hyperion { +const char DEFAULT_USER[] = "Hyperion"; +const char DEFAULT_PASSWORD[] = "hyperion"; +} + /// /// @brief Authentication table interface /// @@ -149,10 +154,10 @@ public: inline bool resetHyperionUser() { QVariantMap map; - map["password"] = calcPasswordHashOfUser("Hyperion", "hyperion"); + map["password"] = calcPasswordHashOfUser(hyperion::DEFAULT_USER, hyperion::DEFAULT_PASSWORD); VectorPair cond; - cond.append(CPair("user", "Hyperion")); + cond.append(CPair("user", hyperion::DEFAULT_USER)); return updateRecord(cond, map); } diff --git a/include/events/EventEnum.h b/include/events/EventEnum.h index 54f9c2c8..8d7934a8 100644 --- a/include/events/EventEnum.h +++ b/include/events/EventEnum.h @@ -13,7 +13,8 @@ enum class Event ResumeIdle, ToggleIdle, Reload, - Restart + Restart, + Quit }; inline const char* eventToString(Event event) @@ -24,6 +25,7 @@ inline const char* eventToString(Event event) case Event::Resume: return "Resume"; case Event::ToggleSuspend: return "ToggleSuspend"; case Event::Idle: return "Idle"; + case Event::Quit: return "Quit"; case Event::ResumeIdle: return "ResumeIdle"; case Event::ToggleIdle: return "ToggleIdle"; case Event::Reload: return "Reload"; @@ -39,6 +41,7 @@ inline Event stringToEvent(const QString& event) if (event.compare("Resume")==0) return Event::Resume; if (event.compare("ToggleSuspend")==0) return Event::ToggleSuspend; if (event.compare("Idle")==0) return Event::Idle; + if (event.compare("Quit")==0) return Event::Quit; if (event.compare("ResumeIdle")==0) return Event::ResumeIdle; if (event.compare("ToggleIdle")==0) return Event::ToggleIdle; if (event.compare("Reload")==0) return Event::Reload; diff --git a/include/events/OsEventHandler.h b/include/events/OsEventHandler.h index e65865f5..6bed4f28 100644 --- a/include/events/OsEventHandler.h +++ b/include/events/OsEventHandler.h @@ -29,6 +29,7 @@ public: public slots: void suspend(bool sleep); void lock(bool isLocked); + void quit(); virtual void handleSettingsUpdate(settings::type type, const QJsonDocument& config); @@ -101,6 +102,7 @@ public: void handleSignal(int signum); + private: static OsEventHandlerLinux* getInstance(); diff --git a/include/grabber/GrabberConfig.h b/include/grabber/GrabberConfig.h new file mode 100644 index 00000000..f3a575c2 --- /dev/null +++ b/include/grabber/GrabberConfig.h @@ -0,0 +1,58 @@ +#ifndef GRABBERCONFIG_H +#define GRABBERCONFIG_H + +#if defined(ENABLE_MF) +#include +#elif defined(ENABLE_V4L2) +#include +#endif + +#if defined(ENABLE_AUDIO) +#include + +#ifdef WIN32 +#include +#endif + +#ifdef __linux__ +#include +#endif +#endif + +#ifdef ENABLE_QT +#include +#endif + +#ifdef ENABLE_DX +#include +#endif + +#if defined(ENABLE_X11) +#include +#endif + +#if defined(ENABLE_XCB) +#include +#endif + +#if defined(ENABLE_DX) +#include +#endif + +#if defined(ENABLE_FB) +#include +#endif + +#if defined(ENABLE_DISPMANX) +#include +#endif + +#if defined(ENABLE_AMLOGIC) +#include +#endif + +#if defined(ENABLE_OSX) +#include +#endif + +#endif // GRABBERCONFIG_H diff --git a/include/hyperion/AuthManager.h b/include/hyperion/AuthManager.h index 3ee594c5..790f70b6 100644 --- a/include/hyperion/AuthManager.h +++ b/include/hyperion/AuthManager.h @@ -3,6 +3,8 @@ #include #include +#include + //qt #include #include @@ -41,24 +43,12 @@ public: /// QString getID() const { return _uuid; } - /// - /// @brief Check authorization is required according to the user setting - /// @return True if authorization required else false - /// - bool isAuthRequired() const { return _authRequired; } - /// /// @brief Check if authorization is required for local network connections /// @return True if authorization required else false /// bool isLocalAuthRequired() const { return _localAuthRequired; } - /// - /// @brief Check if authorization is required for local network connections for admin access - /// @return True if authorization required else false - /// - bool isLocalAdminAuthRequired() const { return _localAdminAuthRequired; } - /// /// @brief Reset Hyperion user /// @return True on success else false @@ -172,7 +162,7 @@ public slots: /// @param usr the defined user /// @return The token /// - QString getUserToken(const QString &usr = "Hyperion") const; + QString getUserToken(const QString &usr = hyperion::DEFAULT_USER) const; /// /// @brief Get all available token entries @@ -230,15 +220,9 @@ private: /// All pending requests QMap _pendingRequests; - /// Reflect state of global auth - bool _authRequired; - /// Reflect state of local auth bool _localAuthRequired; - /// Reflect state of local admin auth - bool _localAdminAuthRequired; - /// Timer for counting against pendingRequest timeouts QTimer *_timer; diff --git a/include/hyperion/Hyperion.h b/include/hyperion/Hyperion.h index 3a442f40..a15eda8e 100644 --- a/include/hyperion/Hyperion.h +++ b/include/hyperion/Hyperion.h @@ -67,6 +67,7 @@ class Hyperion : public QObject Q_OBJECT public: /// Type definition of the info structure used by the priority muxer + using InputsMap = PriorityMuxer::InputsMap; using InputInfo = PriorityMuxer::InputInfo; /// @@ -107,7 +108,7 @@ public: /// QString getActiveDeviceType() const; - bool getReadOnlyMode() {return _readOnlyMode; } + bool getReadOnlyMode() const {return _readOnlyMode; } public slots: @@ -235,13 +236,13 @@ public slots: /// @param priority The priority channel of the effect /// @param timeout The timeout of the effect (after the timout, the effect will be cleared) int setEffect(const QString &effectName - , const QJsonObject &args - , int priority - , int timeout = PriorityMuxer::ENDLESS - , const QString &pythonScript = "" - , const QString &origin="System" - , const QString &imageData = "" - ); + , const QJsonObject &args + , int priority + , int timeout = PriorityMuxer::ENDLESS + , const QString &pythonScript = "" + , const QString &origin="System" + , const QString &imageData = "" + ); /// Get the list of available effects /// @return The list of available effects @@ -303,7 +304,14 @@ public slots: QList getActivePriorities() const; /// - /// Returns the information of a specific priorrity channel + /// Returns the information of all priority channels. + /// + /// @return The information fo all priority channels + /// + PriorityMuxer::InputsMap getPriorityInfo() const; + + /// + /// Returns the information of a specific priority channel /// /// @param[in] priority The priority channel /// @@ -346,7 +354,7 @@ public slots: /// @brief Get the component Register /// return Component register pointer /// - ComponentRegister* getComponentRegister() { return _componentRegister; } + ComponentRegister* getComponentRegister() const { return _componentRegister; } /// /// @brief Called from components to update their current state. DO NOT CALL FROM USERS diff --git a/include/hyperion/HyperionIManager.h b/include/hyperion/HyperionIManager.h index 03e71690..d7610d5b 100644 --- a/include/hyperion/HyperionIManager.h +++ b/include/hyperion/HyperionIManager.h @@ -55,10 +55,16 @@ public slots: Hyperion* getHyperionInstance(quint8 instance = 0); /// - /// @brief Get instance data of all instaces in db + running state + /// @brief Get instance data of all instances in db + running state /// QVector getInstanceData() const; + + /// + /// @brief Get all instance indicies of running instances + /// + QList getRunningInstanceIdx() const; + /// /// @brief Start a Hyperion instance /// @param instance Instance index diff --git a/include/hyperion/PriorityMuxer.h b/include/hyperion/PriorityMuxer.h index ed8d4fb5..d307843b 100644 --- a/include/hyperion/PriorityMuxer.h +++ b/include/hyperion/PriorityMuxer.h @@ -141,6 +141,13 @@ public: /// QList getPriorities() const; + /// + /// Returns the information of all priority channels. + /// + /// @return The information fo all priority channels + /// + InputsMap getInputInfo() const; + /// /// Returns the information of a specified priority channel. /// If a priority is no longer available the _lowestPriorityInfo (255) is returned diff --git a/include/utils/JsonUtils.h b/include/utils/JsonUtils.h index 694e279f..10b89d90 100644 --- a/include/utils/JsonUtils.h +++ b/include/utils/JsonUtils.h @@ -3,6 +3,8 @@ #include #include +#include +#include #include namespace JsonUtils { @@ -14,7 +16,7 @@ namespace JsonUtils { /// @param[in] ignError Ignore errors during file read (no log output) /// @return true on success else false /// - bool readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError=false); + QPair readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError=false); /// /// @brief read a schema file and resolve $refs @@ -33,7 +35,7 @@ namespace JsonUtils { /// @param[in] log The logger of the caller to print errors /// @return true on success else false /// - bool parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log); + QPair parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log); /// /// @brief parse a json QString and get a QJsonArray. Overloaded function @@ -42,8 +44,8 @@ namespace JsonUtils { /// @param[out] arr Retuns the parsed QJsonArray /// @param[in] log The logger of the caller to print errors /// @return true on success else false - /// - bool parse(const QString& path, const QString& data, QJsonArray& arr, Logger* log); + // + QPair parse(const QString& path, const QString& data, QJsonArray& arr, Logger* log); /// /// @brief parse a json QString and get a QJsonDocument @@ -53,7 +55,7 @@ namespace JsonUtils { /// @param[in] log The logger of the caller to print errors /// @return true on success else false /// - bool parse(const QString& path, const QString& data, QJsonDocument& doc, Logger* log); + QPair parse(const QString& path, const QString& data, QJsonDocument& doc, Logger* log); /// /// @brief Validate json data against a schema @@ -63,7 +65,7 @@ namespace JsonUtils { /// @param[in] log The logger of the caller to print errors /// @return true on success else false /// - bool validate(const QString& file, const QJsonObject& json, const QString& schemaPath, Logger* log); + QPair validate(const QString& file, const QJsonObject& json, const QString& schemaPath, Logger* log); /// /// @brief Validate json data against a schema @@ -73,7 +75,7 @@ namespace JsonUtils { /// @param[in] log The logger of the caller to print errors /// @return true on success else false /// - bool validate(const QString& file, const QJsonObject& json, const QJsonObject& schema, Logger* log); + QPair validate(const QString& file, const QJsonObject& json, const QJsonObject& schema, Logger* log); /// /// @brief Write json data to file diff --git a/include/utils/NetOrigin.h b/include/utils/NetOrigin.h index 75796e66..648d3de3 100644 --- a/include/utils/NetOrigin.h +++ b/include/utils/NetOrigin.h @@ -47,7 +47,9 @@ private slots: private: Logger* _log; /// True when internet access is allowed - bool _internetAccessAllowed; + bool _isInternetAccessAllowed; + /// True when internet access is restricted by a white list + bool _isInternetAccessRestricted; /// Whitelisted ip addresses QList _ipWhitelist; diff --git a/include/utils/jsonschema/QJsonFactory.h b/include/utils/jsonschema/QJsonFactory.h index 2fcf5882..1a9190cd 100644 --- a/include/utils/jsonschema/QJsonFactory.h +++ b/include/utils/jsonschema/QJsonFactory.h @@ -31,7 +31,9 @@ public: if (!schemaChecker.validate(configTree).first) { for (int i = 0; i < messages.size(); ++i) + { std::cout << messages[i].toStdString() << std::endl; + } std::cerr << "Validation failed for configuration file: " << config.toStdString() << std::endl; return -3; @@ -61,9 +63,10 @@ public: if (error.error != QJsonParseError::NoError) { // report to the user the failure and their locations in the document. - int errorLine(0), errorColumn(0); + int errorLine(0); + int errorColumn(0); - for( int i=0, count=qMin( error.offset,config.size()); i // stl includes -#include -#include // Qt includes #include @@ -27,90 +25,91 @@ // ledmapping int <> string transform methods #include -// api includes -#include - using namespace hyperion; +// Constants +namespace { + +const int IMAGE_HEIGHT_MAX = 2000; +const int IMAGE_WIDTH_MAX = 2000; +const int IMAGE_SCALE = 2000; +} + API::API(Logger *log, bool localConnection, QObject *parent) - : QObject(parent) + : QObject(parent) { qRegisterMetaType("int64_t"); qRegisterMetaType("VideoMode"); qRegisterMetaType>("std::map"); - // Init - _log = log; - _authManager = AuthManager::getInstance(); + // Init + _log = log; + _authManager = AuthManager::getInstance(); _instanceManager = HyperionIManager::getInstance(); - _localConnection = localConnection; + _localConnection = localConnection; - _authorized = false; - _adminAuthorized = false; + _authorized = false; + _adminAuthorized = false; - _currInstanceIndex = 0; + _currInstanceIndex = 0; - // connect to possible token responses that has been requested - connect(_authManager, &AuthManager::tokenResponse, [=] (bool success, QObject *caller, const QString &token, const QString &comment, const QString &id, const int &tan) - { - if (this == caller) - emit onTokenResponse(success, token, comment, id, tan); - }); + // connect to possible token responses that has been requested + connect(_authManager, &AuthManager::tokenResponse, this, [=] (bool success, const QObject *caller, const QString &token, const QString &comment, const QString &tokenId, const int &tan) + { + if (this == caller) + { + emit onTokenResponse(success, token, comment, tokenId, tan); + } + }); - // connect to possible startInstance responses that has been requested - connect(_instanceManager, &HyperionIManager::startInstanceResponse, [=] (QObject *caller, const int &tan) - { - if (this == caller) - emit onStartInstanceResponse(tan); - }); + connect(_instanceManager, &HyperionIManager::startInstanceResponse, this, [=] (const QObject *caller, const int &tan) + { + if (this == caller) + { + emit onStartInstanceResponse(tan); + } + }); } void API::init() { _hyperion = _instanceManager->getHyperionInstance(0); + _authorized = false; - bool apiAuthRequired = _authManager->isAuthRequired(); - - // For security we block external connections if default PW is set - if (!_localConnection && API::hasHyperionDefaultPw()) - { - emit forceClose(); - } - // if this is localConnection and network allows unauth locals, set authorized flag - if (apiAuthRequired && _localConnection) + // For security we block external connections, if default PW is set + if (!_localConnection && API::hasHyperionDefaultPw()) { - _authorized = !_authManager->isLocalAuthRequired(); + Warning(_log, "Non local network connect attempt identified, but default Hyperion passwort set! - Reject connection."); + emit forceClose(); } - // admin access is allowed, when the connection is local and the option for local admin isn't set. Con: All local connections get full access - if (_localConnection) - { - _adminAuthorized = !_authManager->isLocalAdminAuthRequired(); - // just in positive direction - if (_adminAuthorized) + // if this is localConnection and network allows unauth locals + if ( _localConnection && !_authManager->isLocalAuthRequired()) + { + _authorized = true; + } + + // // admin access is only allowed after login via user & password or via authorization via token. + _adminAuthorized = false; +} + +void API::setColor(int priority, const std::vector &ledColors, int timeout_ms, const QString &origin, hyperion::Components /*callerComp*/) +{ + if (ledColors.size() % 3 == 0) + { + std::vector fledColors; + for (unsigned i = 0; i < ledColors.size(); i += 3) { - _authorized = true; + fledColors.emplace_back(ColorRgb{ledColors[i], ledColors[i + 1], ledColors[i + 2]}); } - } + QMetaObject::invokeMethod(_hyperion, "setColor", Qt::QueuedConnection, Q_ARG(int, priority), Q_ARG(std::vector, fledColors), Q_ARG(int, timeout_ms), Q_ARG(QString, origin)); + } } -void API::setColor(int priority, const std::vector &ledColors, int timeout_ms, const QString &origin, hyperion::Components callerComp) +bool API::setImage(ImageCmdData &data, hyperion::Components comp, QString &replyMsg, hyperion::Components /*callerComp*/) { - std::vector fledColors; - if (ledColors.size() % 3 == 0) - { - for (unsigned i = 0; i < ledColors.size(); i += 3) - { - fledColors.emplace_back(ColorRgb{ledColors[i], ledColors[i + 1], ledColors[i + 2]}); - } - QMetaObject::invokeMethod(_hyperion, "setColor", Qt::QueuedConnection, Q_ARG(int, priority), Q_ARG(std::vector, fledColors), Q_ARG(int, timeout_ms), Q_ARG(QString, origin)); - } -} - -bool API::setImage(ImageCmdData &data, hyperion::Components comp, QString &replyMsg, hyperion::Components callerComp) -{ - // truncate name length - data.imgName.truncate(16); + // truncate name length + data.imgName.truncate(16); if (!data.format.isEmpty()) { @@ -128,424 +127,475 @@ bool API::setImage(ImageCmdData &data, hyperion::Components comp, QString &reply } QImage img = QImage::fromData(data.data, QSTRING_CSTR(data.format)); - if (img.isNull()) - { + if (img.isNull()) + { replyMsg = "Failed to parse picture, the file might be corrupted or content does not match the given format [" + data.format + "]"; - return false; - } + return false; + } - // check for requested scale - if (data.scale > 24) - { - if (img.height() > data.scale) - { - img = img.scaledToHeight(data.scale); - } - if (img.width() > data.scale) - { - img = img.scaledToWidth(data.scale); - } - } + // check for requested scale + if (data.scale > 24) + { + if (img.height() > data.scale) + { + img = img.scaledToHeight(data.scale); + } + if (img.width() > data.scale) + { + img = img.scaledToWidth(data.scale); + } + } - // check if we need to force a scale - if (img.width() > 2000 || img.height() > 2000) - { - data.scale = 2000; - if (img.height() > data.scale) - { - img = img.scaledToHeight(data.scale); - } - if (img.width() > data.scale) - { - img = img.scaledToWidth(data.scale); - } - } + // check if we need to force a scale + if (img.width() > IMAGE_WIDTH_MAX || img.height() > IMAGE_HEIGHT_MAX) + { + data.scale = IMAGE_SCALE; + if (img.height() > data.scale) + { + img = img.scaledToHeight(data.scale); + } + if (img.width() > data.scale) + { + img = img.scaledToWidth(data.scale); + } + } - data.width = img.width(); - data.height = img.height(); + data.width = img.width(); + data.height = img.height(); - // extract image - img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); - data.data.clear(); - data.data.reserve(img.width() * img.height() * 3); - for (int i = 0; i < img.height(); ++i) - { - const QRgb *scanline = reinterpret_cast(img.scanLine(i)); - for (int j = 0; j < img.width(); ++j) - { - data.data.append((char)qRed(scanline[j])); - data.data.append((char)qGreen(scanline[j])); - data.data.append((char)qBlue(scanline[j])); - } - } - } - else - { - // check consistency of the size of the received data - if (data.data.size() != data.width * data.height * 3) - { - replyMsg = "Size of image data does not match with the width and height"; - return false; - } - } + // extract image + img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); + data.data.clear(); + data.data.reserve(static_cast(img.width() * img.height() * 3)); + for (int i = 0; i < img.height(); ++i) + { + const QRgb *scanline = reinterpret_cast(img.scanLine(i)); + for (int j = 0; j < img.width(); ++j) + { + data.data.append(static_cast(qRed(scanline[j]))); + data.data.append(static_cast(qGreen(scanline[j]))); + data.data.append(static_cast(qBlue(scanline[j]))); + } + } + } + else + { + // check consistency of the size of the received data + if (static_cast(data.data.size()) != static_cast(data.width) * static_cast(data.height) * 3) + { + replyMsg = "Size of image data does not match with the width and height"; + return false; + } + } - // copy image - Image image(data.width, data.height); - memcpy(image.memptr(), data.data.data(), data.data.size()); + // copy image + Image image(data.width, data.height); + memcpy(image.memptr(), data.data.data(), static_cast(data.data.size())); - QMetaObject::invokeMethod(_hyperion, "registerInput", Qt::QueuedConnection, Q_ARG(int, data.priority), Q_ARG(hyperion::Components, comp), Q_ARG(QString, data.origin), Q_ARG(QString, data.imgName)); - QMetaObject::invokeMethod(_hyperion, "setInputImage", Qt::QueuedConnection, Q_ARG(int, data.priority), Q_ARG(Image, image), Q_ARG(int64_t, data.duration)); + QMetaObject::invokeMethod(_hyperion, "registerInput", Qt::QueuedConnection, Q_ARG(int, data.priority), Q_ARG(hyperion::Components, comp), Q_ARG(QString, data.origin), Q_ARG(QString, data.imgName)); + QMetaObject::invokeMethod(_hyperion, "setInputImage", Qt::QueuedConnection, Q_ARG(int, data.priority), Q_ARG(Image, image), Q_ARG(int64_t, data.duration)); - return true; + return true; } -bool API::clearPriority(int priority, QString &replyMsg, hyperion::Components callerComp) +bool API::clearPriority(int priority, QString &replyMsg, hyperion::Components /*callerComp*/) { - if (priority < 0 || (priority > 0 && priority < 254)) - { - QMetaObject::invokeMethod(_hyperion, "clear", Qt::QueuedConnection, Q_ARG(int, priority)); - } - else - { - replyMsg = QString("Priority %1 is not allowed to be cleared").arg(priority); - return false; - } - return true; + if (priority < 0 || (priority > 0 && priority < PriorityMuxer::BG_PRIORITY)) + { + QMetaObject::invokeMethod(_hyperion, "clear", Qt::QueuedConnection, Q_ARG(int, priority)); + } + else + { + replyMsg = QString("Priority %1 is not allowed to be cleared").arg(priority); + return false; + } + return true; } -bool API::setComponentState(const QString &comp, bool &compState, QString &replyMsg, hyperion::Components callerComp) +bool API::setComponentState(const QString &comp, bool &compState, QString &replyMsg, hyperion::Components /*callerComp*/) { - Components component = stringToComponent(comp); + Components component = stringToComponent(comp); - if (component != COMP_INVALID) - { - QMetaObject::invokeMethod(_hyperion, "compStateChangeRequest", Qt::QueuedConnection, Q_ARG(hyperion::Components, component), Q_ARG(bool, compState)); - return true; - } - replyMsg = QString("Unknown component name: %1").arg(comp); - return false; + if (component != COMP_INVALID) + { + QMetaObject::invokeMethod(_hyperion, "compStateChangeRequest", Qt::QueuedConnection, Q_ARG(hyperion::Components, component), Q_ARG(bool, compState)); + return true; + } + replyMsg = QString("Unknown component name: %1").arg(comp); + return false; } -void API::setLedMappingType(int type, hyperion::Components callerComp) +void API::setLedMappingType(int type, hyperion::Components /*callerComp*/) { - QMetaObject::invokeMethod(_hyperion, "setLedMappingType", Qt::QueuedConnection, Q_ARG(int, type)); + QMetaObject::invokeMethod(_hyperion, "setLedMappingType", Qt::QueuedConnection, Q_ARG(int, type)); } -void API::setVideoMode(VideoMode mode, hyperion::Components callerComp) +void API::setVideoMode(VideoMode mode, hyperion::Components /*callerComp*/) { - QMetaObject::invokeMethod(_hyperion, "setVideoMode", Qt::QueuedConnection, Q_ARG(VideoMode, mode)); + QMetaObject::invokeMethod(_hyperion, "setVideoMode", Qt::QueuedConnection, Q_ARG(VideoMode, mode)); } #if defined(ENABLE_EFFECTENGINE) -bool API::setEffect(const EffectCmdData &dat, hyperion::Components callerComp) +bool API::setEffect(const EffectCmdData &dat, hyperion::Components /*callerComp*/) { - int res; - if (!dat.args.isEmpty()) - { - QMetaObject::invokeMethod(_hyperion, "setEffect", Qt::BlockingQueuedConnection, Q_RETURN_ARG(int, res), Q_ARG(QString, dat.effectName), Q_ARG(QJsonObject, dat.args), Q_ARG(int, dat.priority), Q_ARG(int, dat.duration), Q_ARG(QString, dat.pythonScript), Q_ARG(QString, dat.origin), Q_ARG(QString, dat.data)); - } - else - { - QMetaObject::invokeMethod(_hyperion, "setEffect", Qt::BlockingQueuedConnection, Q_RETURN_ARG(int, res), Q_ARG(QString, dat.effectName), Q_ARG(int, dat.priority), Q_ARG(int, dat.duration), Q_ARG(QString, dat.origin)); - } + int isStarted; + if (!dat.args.isEmpty()) + { + QMetaObject::invokeMethod(_hyperion, "setEffect", Qt::BlockingQueuedConnection, Q_RETURN_ARG(int, isStarted), Q_ARG(QString, dat.effectName), Q_ARG(QJsonObject, dat.args), Q_ARG(int, dat.priority), Q_ARG(int, dat.duration), Q_ARG(QString, dat.pythonScript), Q_ARG(QString, dat.origin), Q_ARG(QString, dat.data)); + } + else + { + QMetaObject::invokeMethod(_hyperion, "setEffect", Qt::BlockingQueuedConnection, Q_RETURN_ARG(int, isStarted), Q_ARG(QString, dat.effectName), Q_ARG(int, dat.priority), Q_ARG(int, dat.duration), Q_ARG(QString, dat.origin)); + } - return res >= 0; + return isStarted >= 0; } #endif -void API::setSourceAutoSelect(bool state, hyperion::Components callerComp) +void API::setSourceAutoSelect(bool state, hyperion::Components /*callerComp*/) { - QMetaObject::invokeMethod(_hyperion, "setSourceAutoSelect", Qt::QueuedConnection, Q_ARG(bool, state)); + QMetaObject::invokeMethod(_hyperion, "setSourceAutoSelect", Qt::QueuedConnection, Q_ARG(bool, state)); } -void API::setVisiblePriority(int priority, hyperion::Components callerComp) +void API::setVisiblePriority(int priority, hyperion::Components /*callerComp*/) { - QMetaObject::invokeMethod(_hyperion, "setVisiblePriority", Qt::QueuedConnection, Q_ARG(int, priority)); + QMetaObject::invokeMethod(_hyperion, "setVisiblePriority", Qt::QueuedConnection, Q_ARG(int, priority)); } void API::registerInput(int priority, hyperion::Components component, const QString &origin, const QString &owner, hyperion::Components callerComp) { - if (_activeRegisters.count(priority)) - _activeRegisters.erase(priority); + if (_activeRegisters.count(priority) != 0) + { + _activeRegisters.erase(priority); + } - _activeRegisters.insert({priority, registerData{component, origin, owner, callerComp}}); + _activeRegisters.insert({priority, registerData{component, origin, owner, callerComp}}); - QMetaObject::invokeMethod(_hyperion, "registerInput", Qt::QueuedConnection, Q_ARG(int, priority), Q_ARG(hyperion::Components, component), Q_ARG(QString, origin), Q_ARG(QString, owner)); + QMetaObject::invokeMethod(_hyperion, "registerInput", Qt::QueuedConnection, Q_ARG(int, priority), Q_ARG(hyperion::Components, component), Q_ARG(QString, origin), Q_ARG(QString, owner)); } void API::unregisterInput(int priority) { - if (_activeRegisters.count(priority)) - _activeRegisters.erase(priority); + if (_activeRegisters.count(priority) != 0) + { + _activeRegisters.erase(priority); + } } bool API::setHyperionInstance(quint8 inst) { - if (_currInstanceIndex == inst) - return true; - bool isRunning; - QMetaObject::invokeMethod(_instanceManager, "IsInstanceRunning", Qt::DirectConnection, Q_RETURN_ARG(bool, isRunning), Q_ARG(quint8, inst)); - if (!isRunning) - return false; + if (_currInstanceIndex == inst) + { + return true; + } - disconnect(_hyperion, 0, this, 0); - QMetaObject::invokeMethod(_instanceManager, "getHyperionInstance", Qt::DirectConnection, Q_RETURN_ARG(Hyperion *, _hyperion), Q_ARG(quint8, inst)); - _currInstanceIndex = inst; - return true; + bool isRunning; + QMetaObject::invokeMethod(_instanceManager, "IsInstanceRunning", Qt::DirectConnection, Q_RETURN_ARG(bool, isRunning), Q_ARG(quint8, inst)); + if (!isRunning) + { + return false; + } + + disconnect(_hyperion, nullptr, this, nullptr); + QMetaObject::invokeMethod(_instanceManager, "getHyperionInstance", Qt::DirectConnection, Q_RETURN_ARG(Hyperion *, _hyperion), Q_ARG(quint8, inst)); + _currInstanceIndex = inst; + return true; } bool API::isHyperionEnabled() { - int res; - QMetaObject::invokeMethod(_hyperion, "isComponentEnabled", Qt::BlockingQueuedConnection, Q_RETURN_ARG(int, res), Q_ARG(hyperion::Components, hyperion::COMP_ALL)); - return res > 0; + int isEnabled; + QMetaObject::invokeMethod(_hyperion, "isComponentEnabled", Qt::BlockingQueuedConnection, Q_RETURN_ARG(int, isEnabled), Q_ARG(hyperion::Components, hyperion::COMP_ALL)); + return isEnabled > 0; } -QVector API::getAllInstanceData() +QVector API::getAllInstanceData() const { - QVector vec; - QMetaObject::invokeMethod(_instanceManager, "getInstanceData", Qt::DirectConnection, Q_RETURN_ARG(QVector, vec)); - return vec; + QVector vec; + QMetaObject::invokeMethod(_instanceManager, "getInstanceData", Qt::DirectConnection, Q_RETURN_ARG(QVector, vec)); + return vec; } bool API::startInstance(quint8 index, int tan) { - bool res; - (_instanceManager->thread() != this->thread()) - ? QMetaObject::invokeMethod(_instanceManager, "startInstance", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, res), Q_ARG(quint8, index), Q_ARG(bool, false), Q_ARG(QObject*, this), Q_ARG(int, tan)) - : res = _instanceManager->startInstance(index, false, this, tan); + bool isStarted; + (_instanceManager->thread() != this->thread()) + ? QMetaObject::invokeMethod(_instanceManager, "startInstance", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, isStarted), Q_ARG(quint8, index), Q_ARG(bool, false), Q_ARG(QObject*, this), Q_ARG(int, tan)) + : isStarted = _instanceManager->startInstance(index, false, this, tan); - return res; + return isStarted; } void API::stopInstance(quint8 index) { - QMetaObject::invokeMethod(_instanceManager, "stopInstance", Qt::QueuedConnection, Q_ARG(quint8, index)); + QMetaObject::invokeMethod(_instanceManager, "stopInstance", Qt::QueuedConnection, Q_ARG(quint8, index)); } bool API::deleteInstance(quint8 index, QString &replyMsg) { - if (_adminAuthorized) - { - QMetaObject::invokeMethod(_instanceManager, "deleteInstance", Qt::QueuedConnection, Q_ARG(quint8, index)); - return true; - } - replyMsg = NO_AUTH; - return false; + if (_adminAuthorized) + { + QMetaObject::invokeMethod(_instanceManager, "deleteInstance", Qt::QueuedConnection, Q_ARG(quint8, index)); + return true; + } + replyMsg = NO_AUTHORIZATION; + return false; } QString API::createInstance(const QString &name) { - if (_adminAuthorized) - { - bool success; - QMetaObject::invokeMethod(_instanceManager, "createInstance", Qt::DirectConnection, Q_RETURN_ARG(bool, success), Q_ARG(QString, name)); - if (!success) - return QString("Instance name '%1' is already in use").arg(name); - - return ""; - } - return NO_AUTH; + if (_adminAuthorized) + { + bool success; + QMetaObject::invokeMethod(_instanceManager, "createInstance", Qt::DirectConnection, Q_RETURN_ARG(bool, success), Q_ARG(QString, name)); + if (!success) + { + return QString("Instance name '%1' is already in use").arg(name); + } + return ""; + } + return NO_AUTHORIZATION; } QString API::setInstanceName(quint8 index, const QString &name) { - if (_adminAuthorized) - { - QMetaObject::invokeMethod(_instanceManager, "saveName", Qt::QueuedConnection, Q_ARG(quint8, index), Q_ARG(QString, name)); - return ""; - } - return NO_AUTH; + if (_adminAuthorized) + { + QMetaObject::invokeMethod(_instanceManager, "saveName", Qt::QueuedConnection, Q_ARG(quint8, index), Q_ARG(QString, name)); + return ""; + } + return NO_AUTHORIZATION; } #if defined(ENABLE_EFFECTENGINE) QString API::deleteEffect(const QString &name) { - if (_adminAuthorized) - { - QString res; - QMetaObject::invokeMethod(_hyperion, "deleteEffect", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, res), Q_ARG(QString, name)); - return res; - } - return NO_AUTH; + if (_adminAuthorized) + { + QString res; + QMetaObject::invokeMethod(_hyperion, "deleteEffect", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, res), Q_ARG(QString, name)); + return res; + } + return NO_AUTHORIZATION; } QString API::saveEffect(const QJsonObject &data) { - if (_adminAuthorized) - { - QString res; - QMetaObject::invokeMethod(_hyperion, "saveEffect", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, res), Q_ARG(QJsonObject, data)); - return res; - } - return NO_AUTH; + if (_adminAuthorized) + { + QString res; + QMetaObject::invokeMethod(_hyperion, "saveEffect", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, res), Q_ARG(QJsonObject, data)); + return res; + } + return NO_AUTHORIZATION; } #endif bool API::saveSettings(const QJsonObject &data) { - bool rc = true; - if (!_adminAuthorized) + bool isSaved {true}; + if (!_adminAuthorized) { - rc = false; + isSaved = false; } else { - QMetaObject::invokeMethod(_hyperion, "saveSettings", Qt::DirectConnection, Q_RETURN_ARG(bool, rc), Q_ARG(QJsonObject, data), Q_ARG(bool, true)); + QMetaObject::invokeMethod(_hyperion, "saveSettings", Qt::DirectConnection, Q_RETURN_ARG(bool, isSaved), Q_ARG(QJsonObject, data), Q_ARG(bool, true)); } - return rc; + return isSaved; } bool API::restoreSettings(const QJsonObject &data) { - bool rc = true; + bool isRestored {true}; if (!_adminAuthorized) { - rc = false; + isRestored = false; } else { - QMetaObject::invokeMethod(_hyperion, "restoreSettings", Qt::DirectConnection, Q_RETURN_ARG(bool, rc), Q_ARG(QJsonObject, data), Q_ARG(bool, true)); + QMetaObject::invokeMethod(_hyperion, "restoreSettings", Qt::DirectConnection, Q_RETURN_ARG(bool, isRestored), Q_ARG(QJsonObject, data), Q_ARG(bool, true)); } - return rc; + return isRestored; } bool API::updateHyperionPassword(const QString &password, const QString &newPassword) { - if (!_adminAuthorized) - return false; - bool res; - QMetaObject::invokeMethod(_authManager, "updateUserPassword", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, res), Q_ARG(QString, "Hyperion"), Q_ARG(QString, password), Q_ARG(QString, newPassword)); - return res; + bool isPwUpdated {true}; + if (!_adminAuthorized) + { + isPwUpdated = false; + } + else + { + QMetaObject::invokeMethod(_authManager, "updateUserPassword", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, isPwUpdated), Q_ARG(QString, DEFAULT_USER), Q_ARG(QString, password), Q_ARG(QString, newPassword)); + } + return isPwUpdated; } QString API::createToken(const QString &comment, AuthManager::AuthDefinition &def) { - if (!_adminAuthorized) - return NO_AUTH; - if (comment.isEmpty()) - return "comment is empty"; - QMetaObject::invokeMethod(_authManager, "createToken", Qt::BlockingQueuedConnection, Q_RETURN_ARG(AuthManager::AuthDefinition, def), Q_ARG(QString, comment)); - return ""; + if (!_adminAuthorized) + { + return NO_AUTHORIZATION; + } + + if (comment.isEmpty()) + { + return "Missing token comment"; + } + QMetaObject::invokeMethod(_authManager, "createToken", Qt::BlockingQueuedConnection, Q_RETURN_ARG(AuthManager::AuthDefinition, def), Q_ARG(QString, comment)); + return ""; } -QString API::renameToken(const QString &id, const QString &comment) +QString API::renameToken(const QString &tokenId, const QString &comment) { - if (!_adminAuthorized) - return NO_AUTH; - if (comment.isEmpty() || id.isEmpty()) - return "Empty comment or id"; + if (!_adminAuthorized) + { + return NO_AUTHORIZATION; + } - QMetaObject::invokeMethod(_authManager, "renameToken", Qt::QueuedConnection, Q_ARG(QString, id), Q_ARG(QString, comment)); - return ""; + if (comment.isEmpty()) + { + return "Missing token comment"; + } + + if (tokenId.isEmpty()) { + return "Missing token id"; + } + + bool isTokenRenamed {false}; + QMetaObject::invokeMethod(_authManager, "renameToken", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, isTokenRenamed), Q_ARG(QString, tokenId), Q_ARG(QString, comment)); + + return (!isTokenRenamed) ? "Token does not exist" : ""; } -QString API::deleteToken(const QString &id) +QString API::deleteToken(const QString &tokenId) { - if (!_adminAuthorized) - return NO_AUTH; - if (id.isEmpty()) - return "Empty id"; + if (!_adminAuthorized) + { + return NO_AUTHORIZATION; + } - QMetaObject::invokeMethod(_authManager, "deleteToken", Qt::QueuedConnection, Q_ARG(QString, id)); - return ""; + if (tokenId.isEmpty()) + { + return "Missing token id"; + } + + bool isTokenDeleted {false}; + QMetaObject::invokeMethod(_authManager, "deleteToken", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, isTokenDeleted), Q_ARG(QString, tokenId)); + + return (!isTokenDeleted) ? "Token does not exist" : ""; } -void API::setNewTokenRequest(const QString &comment, const QString &id, const int &tan) +void API::setNewTokenRequest(const QString &comment, const QString &tokenId, const int &tan) { - QMetaObject::invokeMethod(_authManager, "setNewTokenRequest", Qt::QueuedConnection, Q_ARG(QObject *, this), Q_ARG(QString, comment), Q_ARG(QString, id), Q_ARG(int, tan)); + QMetaObject::invokeMethod(_authManager, "setNewTokenRequest", Qt::QueuedConnection, Q_ARG(QObject *, this), Q_ARG(QString, comment), Q_ARG(QString, tokenId), Q_ARG(int, tan)); } -void API::cancelNewTokenRequest(const QString &comment, const QString &id) +void API::cancelNewTokenRequest(const QString &comment, const QString &tokenId) { - QMetaObject::invokeMethod(_authManager, "cancelNewTokenRequest", Qt::QueuedConnection, Q_ARG(QObject *, this), Q_ARG(QString, comment), Q_ARG(QString, id)); + QMetaObject::invokeMethod(_authManager, "cancelNewTokenRequest", Qt::QueuedConnection, Q_ARG(QObject *, this), Q_ARG(QString, comment), Q_ARG(QString, tokenId)); } -bool API::handlePendingTokenRequest(const QString &id, bool accept) +bool API::handlePendingTokenRequest(const QString &tokenId, bool accept) { - if (!_adminAuthorized) - return false; - QMetaObject::invokeMethod(_authManager, "handlePendingTokenRequest", Qt::QueuedConnection, Q_ARG(QString, id), Q_ARG(bool, accept)); - return true; + if (!_adminAuthorized) + { + return false; + } + QMetaObject::invokeMethod(_authManager, "handlePendingTokenRequest", Qt::QueuedConnection, Q_ARG(QString, tokenId), Q_ARG(bool, accept)); + return true; } bool API::getTokenList(QVector &def) { - if (!_adminAuthorized) - return false; - QMetaObject::invokeMethod(_authManager, "getTokenList", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVector, def)); - return true; + if (!_adminAuthorized) + { + return false; + } + QMetaObject::invokeMethod(_authManager, "getTokenList", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVector, def)); + return true; } bool API::getPendingTokenRequests(QVector &map) { - if (!_adminAuthorized) - return false; - QMetaObject::invokeMethod(_authManager, "getPendingRequests", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVector, map)); - return true; + if (!_adminAuthorized) + { + return false; + } + QMetaObject::invokeMethod(_authManager, "getPendingRequests", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVector, map)); + return true; } bool API::isUserTokenAuthorized(const QString &userToken) { - bool res; - QMetaObject::invokeMethod(_authManager, "isUserTokenAuthorized", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, res), Q_ARG(QString, "Hyperion"), Q_ARG(QString, userToken)); - if (res) - { - _authorized = true; - _adminAuthorized = true; - // Listen for ADMIN ACCESS protected signals - connect(_authManager, &AuthManager::newPendingTokenRequest, this, &API::onPendingTokenRequest, Qt::UniqueConnection); - } - return res; + QMetaObject::invokeMethod(_authManager, "isUserTokenAuthorized", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, _authorized), Q_ARG(QString, DEFAULT_USER), Q_ARG(QString, userToken)); + _adminAuthorized = _authorized; + + if (_authorized) + { + // Listen for ADMIN ACCESS protected signals + connect(_authManager, &AuthManager::newPendingTokenRequest, this, &API::onPendingTokenRequest); + } + else + { + disconnect(_authManager, &AuthManager::newPendingTokenRequest, this, &API::onPendingTokenRequest); + } + return _authorized; } bool API::getUserToken(QString &userToken) { - if (!_adminAuthorized) - return false; - QMetaObject::invokeMethod(_authManager, "getUserToken", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, userToken)); - return true; + if (!_adminAuthorized) + { + return false; + } + QMetaObject::invokeMethod(_authManager, "getUserToken", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, userToken)); + return true; } bool API::isTokenAuthorized(const QString &token) { (_authManager->thread() != this->thread()) - ? QMetaObject::invokeMethod(_authManager, "isTokenAuthorized", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, _authorized), Q_ARG(QString, token)) - : _authorized = _authManager->isTokenAuthorized(token); + ? QMetaObject::invokeMethod(_authManager, "isTokenAuthorized", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, _authorized), Q_ARG(QString, token)) + : _authorized = _authManager->isTokenAuthorized(token); + _adminAuthorized = _authorized; - return _authorized; + return _authorized; } bool API::isUserAuthorized(const QString &password) { - bool res; - QMetaObject::invokeMethod(_authManager, "isUserAuthorized", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, res), Q_ARG(QString, "Hyperion"), Q_ARG(QString, password)); - if (res) - { - _authorized = true; - _adminAuthorized = true; - // Listen for ADMIN ACCESS protected signals - connect(_authManager, &AuthManager::newPendingTokenRequest, this, &API::onPendingTokenRequest, Qt::UniqueConnection); - } - return res; + bool isUserAuthorized; + QMetaObject::invokeMethod(_authManager, "isUserAuthorized", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, isUserAuthorized), Q_ARG(QString, DEFAULT_USER), Q_ARG(QString, password)); + if (isUserAuthorized) + { + _authorized = true; + _adminAuthorized = true; + + // Listen for ADMIN ACCESS protected signals + connect(_authManager, &AuthManager::newPendingTokenRequest, this, &API::onPendingTokenRequest); + } + else + { + disconnect(_authManager, &AuthManager::newPendingTokenRequest, this, &API::onPendingTokenRequest); + } + return isUserAuthorized; } bool API::hasHyperionDefaultPw() { - bool res; - QMetaObject::invokeMethod(_authManager, "isUserAuthorized", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, res), Q_ARG(QString, "Hyperion"), Q_ARG(QString, "hyperion")); - return res; + bool isDefaultPassort; + QMetaObject::invokeMethod(_authManager, "isUserAuthorized", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, isDefaultPassort), Q_ARG(QString, DEFAULT_USER), Q_ARG(QString, DEFAULT_PASSWORD)); + return isDefaultPassort; } void API::logout() { - _authorized = false; - _adminAuthorized = false; - // Stop listenig for ADMIN ACCESS protected signals - disconnect(_authManager, &AuthManager::newPendingTokenRequest, this, &API::onPendingTokenRequest); - stopDataConnectionss(); -} - -void API::stopDataConnectionss() -{ + _authorized = false; + _adminAuthorized = false; + // Stop listenig for ADMIN ACCESS protected signals + disconnect(_authManager, &AuthManager::newPendingTokenRequest, this, &API::onPendingTokenRequest); + stopDataConnections(); } diff --git a/libsrc/api/CMakeLists.txt b/libsrc/api/CMakeLists.txt index ac5cdcfd..6818e752 100644 --- a/libsrc/api/CMakeLists.txt +++ b/libsrc/api/CMakeLists.txt @@ -2,10 +2,14 @@ add_library(hyperion-api ${CMAKE_SOURCE_DIR}/include/api/apiStructs.h ${CMAKE_SOURCE_DIR}/include/api/API.h ${CMAKE_SOURCE_DIR}/include/api/JsonAPI.h - ${CMAKE_SOURCE_DIR}/include/api/JsonCB.h + ${CMAKE_SOURCE_DIR}/include/api/JsonCallbacks.h + ${CMAKE_SOURCE_DIR}/include/api/JsonApiCommand.h + ${CMAKE_SOURCE_DIR}/include/api/JsonApiSubscription.h + ${CMAKE_SOURCE_DIR}/include/api/JsonInfo.h ${CMAKE_SOURCE_DIR}/libsrc/api/JsonAPI.cpp ${CMAKE_SOURCE_DIR}/libsrc/api/API.cpp - ${CMAKE_SOURCE_DIR}/libsrc/api/JsonCB.cpp + ${CMAKE_SOURCE_DIR}/libsrc/api/JsonCallbacks.cpp + ${CMAKE_SOURCE_DIR}/libsrc/api/JsonInfo.cpp ${CMAKE_SOURCE_DIR}/libsrc/api/JSONRPC_schemas.qrc ) diff --git a/libsrc/api/JSONRPC_schema/schema-adjustment.json b/libsrc/api/JSONRPC_schema/schema-adjustment.json index b8856ef9..5efed868 100644 --- a/libsrc/api/JSONRPC_schema/schema-adjustment.json +++ b/libsrc/api/JSONRPC_schema/schema-adjustment.json @@ -7,6 +7,12 @@ "required" : true, "enum" : ["adjustment"] }, + "instance" : { + "type": "array", + "required": false, + "items" : {}, + "minItems": 1 + }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-clear.json b/libsrc/api/JSONRPC_schema/schema-clear.json index c63a77f7..b55be0a1 100644 --- a/libsrc/api/JSONRPC_schema/schema-clear.json +++ b/libsrc/api/JSONRPC_schema/schema-clear.json @@ -7,6 +7,12 @@ "required" : true, "enum" : ["clear"] }, + "instance" : { + "type": "array", + "required": false, + "items" : {}, + "minItems": 1 + }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-clearall.json b/libsrc/api/JSONRPC_schema/schema-clearall.json index 8c88cc6c..5d5d2d22 100644 --- a/libsrc/api/JSONRPC_schema/schema-clearall.json +++ b/libsrc/api/JSONRPC_schema/schema-clearall.json @@ -7,6 +7,12 @@ "required" : true, "enum" : ["clearall"] }, + "instance" : { + "type": "array", + "required": false, + "items" : {}, + "minItems": 1 + }, "tan" : { "type" : "integer" } diff --git a/libsrc/api/JSONRPC_schema/schema-color.json b/libsrc/api/JSONRPC_schema/schema-color.json index 754c5ad3..eeeba069 100644 --- a/libsrc/api/JSONRPC_schema/schema-color.json +++ b/libsrc/api/JSONRPC_schema/schema-color.json @@ -7,6 +7,12 @@ "required" : true, "enum" : ["color"] }, + "instance" : { + "type": "array", + "required": false, + "items" : {}, + "minItems": 1 + }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-componentstate.json b/libsrc/api/JSONRPC_schema/schema-componentstate.json index f46324dc..10ca3bb6 100644 --- a/libsrc/api/JSONRPC_schema/schema-componentstate.json +++ b/libsrc/api/JSONRPC_schema/schema-componentstate.json @@ -9,6 +9,12 @@ "required" : true, "enum" : ["componentstate"] }, + "instance" : { + "type": "array", + "required": false, + "items" : {}, + "minItems": 1 + }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-config.json b/libsrc/api/JSONRPC_schema/schema-config.json index 8a134b54..204661cf 100644 --- a/libsrc/api/JSONRPC_schema/schema-config.json +++ b/libsrc/api/JSONRPC_schema/schema-config.json @@ -12,6 +12,11 @@ "required" : true, "enum" : ["getconfig","getschema","setconfig","restoreconfig","reload"] }, + "instance" : { + "type" : "integer", + "minimum": 0, + "maximum": 255 + }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-create-effect.json b/libsrc/api/JSONRPC_schema/schema-create-effect.json index 97b55056..2eceb9a1 100644 --- a/libsrc/api/JSONRPC_schema/schema-create-effect.json +++ b/libsrc/api/JSONRPC_schema/schema-create-effect.json @@ -7,6 +7,11 @@ "required" : true, "enum" : ["create-effect"] }, + "instance" : { + "type" : "integer", + "minimum": 0, + "maximum": 255 + }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-delete-effect.json b/libsrc/api/JSONRPC_schema/schema-delete-effect.json index 8279f854..bdbdee7c 100644 --- a/libsrc/api/JSONRPC_schema/schema-delete-effect.json +++ b/libsrc/api/JSONRPC_schema/schema-delete-effect.json @@ -8,6 +8,11 @@ "required" : true, "enum" : ["delete-effect"] }, + "instance" : { + "type" : "integer", + "minimum": 0, + "maximum": 255 + }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-effect.json b/libsrc/api/JSONRPC_schema/schema-effect.json index 876173f1..5bd0aff6 100644 --- a/libsrc/api/JSONRPC_schema/schema-effect.json +++ b/libsrc/api/JSONRPC_schema/schema-effect.json @@ -7,6 +7,12 @@ "required" : true, "enum" : ["effect"] }, + "instance" : { + "type": "array", + "required": false, + "items" : {}, + "minItems": 1 + }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-image.json b/libsrc/api/JSONRPC_schema/schema-image.json index 3296babb..fbd2ff40 100644 --- a/libsrc/api/JSONRPC_schema/schema-image.json +++ b/libsrc/api/JSONRPC_schema/schema-image.json @@ -7,6 +7,12 @@ "required" : true, "enum" : ["image"] }, + "instance" : { + "type": "array", + "required": false, + "items" : {}, + "minItems": 1 + }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-ledcolors.json b/libsrc/api/JSONRPC_schema/schema-ledcolors.json index b1bda0a3..086914ec 100644 --- a/libsrc/api/JSONRPC_schema/schema-ledcolors.json +++ b/libsrc/api/JSONRPC_schema/schema-ledcolors.json @@ -7,21 +7,18 @@ "required" : true, "enum" : ["ledcolors"] }, + "instance" : { + "type" : "integer", + "minimum": 0, + "maximum": 255 + }, "tan" : { "type" : "integer" }, "subcommand": { "type" : "string", "required" : true, - "enum" : ["ledstream-stop","ledstream-start","testled","imagestream-start","imagestream-stop"] - }, - "oneshot": { - "type" : "bool" - }, - "interval": { - "type" : "integer", - "required" : false, - "minimum": 50 + "enum" : ["ledstream-stop","ledstream-start","imagestream-start","imagestream-stop"] } }, diff --git a/libsrc/api/JSONRPC_schema/schema-leddevice.json b/libsrc/api/JSONRPC_schema/schema-leddevice.json index 5065ea0d..ac74342c 100644 --- a/libsrc/api/JSONRPC_schema/schema-leddevice.json +++ b/libsrc/api/JSONRPC_schema/schema-leddevice.json @@ -7,6 +7,9 @@ "required" : true, "enum" : ["leddevice"] }, + "instance" : { + "type" : "integer" + }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-processing.json b/libsrc/api/JSONRPC_schema/schema-processing.json index d67828f0..0ca7616d 100644 --- a/libsrc/api/JSONRPC_schema/schema-processing.json +++ b/libsrc/api/JSONRPC_schema/schema-processing.json @@ -7,6 +7,12 @@ "required" : true, "enum" : ["processing"] }, + "instance" : { + "type": "array", + "required": false, + "items" : {}, + "minItems": 1 + }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JSONRPC_schema/schema-serverinfo.json b/libsrc/api/JSONRPC_schema/schema-serverinfo.json index 990eec04..74b41453 100644 --- a/libsrc/api/JSONRPC_schema/schema-serverinfo.json +++ b/libsrc/api/JSONRPC_schema/schema-serverinfo.json @@ -7,6 +7,25 @@ "required" : true, "enum" : ["serverinfo"] }, + "subcommand": { + "type": "string", + "enum": ["getInfo", "subscribe", "unsubscribe", "getSubscriptions", "getSubscriptionCommands"] + }, + "instance" : { + "type" : "integer", + "minimum": 0, + "maximum": 255 + }, + "data": { + "type": ["null", "array"], + "properties": { + "subscriptions": { + "type": "array", + "items": {} + } + }, + "additionalProperties": false + }, "subscribe" : { "type" : "array" }, diff --git a/libsrc/api/JSONRPC_schema/schema-sourceselect.json b/libsrc/api/JSONRPC_schema/schema-sourceselect.json index 14f9aaea..8763595c 100644 --- a/libsrc/api/JSONRPC_schema/schema-sourceselect.json +++ b/libsrc/api/JSONRPC_schema/schema-sourceselect.json @@ -7,6 +7,12 @@ "required" : true, "enum" : ["sourceselect"] }, + "instance" : { + "type": "array", + "required": false, + "items" : {}, + "minItems": 1 + }, "tan" : { "type" : "integer" }, diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 6becc6f7..ff4c6841 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -1,5 +1,6 @@ // project includes #include +#include // Qt includes #include @@ -10,8 +11,8 @@ #include #include #include -#include -#include +#include +#include // hyperion includes #include @@ -20,62 +21,11 @@ #include // Required to determine the cmake options -#include -#include - #include #include -#if defined(ENABLE_MF) - #include -#elif defined(ENABLE_V4L2) - #include -#endif - -#if defined(ENABLE_AUDIO) - #include - - #ifdef WIN32 - #include - #endif - - #ifdef __linux__ - #include - #endif -#endif - -#if defined(ENABLE_X11) - #include -#endif - -#if defined(ENABLE_XCB) - #include -#endif - -#if defined(ENABLE_DX) - #include -#endif - -#if defined(ENABLE_FB) - #include -#endif - -#if defined(ENABLE_DISPMANX) - #include -#endif - -#if defined(ENABLE_AMLOGIC) - #include -#endif - -#if defined(ENABLE_OSX) - #include -#endif - #include #include -#include -#include #include #include #include @@ -84,7 +34,7 @@ #include // api includes -#include +#include #include // auth manager @@ -99,23 +49,37 @@ #include #endif +#include +#include + using namespace hyperion; // Constants -namespace { const bool verbose = false; } +namespace { + +constexpr std::chrono::milliseconds NEW_TOKEN_REQUEST_TIMEOUT{ 180000 }; + +const char TOKEN_TAG[] = "token"; +constexpr int TOKEN_TAG_LENGTH = sizeof(TOKEN_TAG) - 1; +const char BEARER_TOKEN_TAG[] = "Bearer"; +constexpr int BEARER_TOKEN_TAG_LENGTH = sizeof(BEARER_TOKEN_TAG) - 1; + +const int MIN_PASSWORD_LENGTH = 8; +const int APP_TOKEN_LENGTH = 36; + +const bool verbose = false; +} JsonAPI::JsonAPI(QString peerAddress, Logger *log, bool localConnection, QObject *parent, bool noListener) : API(log, localConnection, parent) + ,_noListener(noListener) + ,_peerAddress (std::move(peerAddress)) + ,_jsonCB (nullptr) { - _noListener = noListener; - _peerAddress = peerAddress; - _jsonCB = new JsonCB(this); - _streaming_logging_activated = false; - _ledStreamTimer = new QTimer(this); - Q_INIT_RESOURCE(JSONRPC_schemas); qRegisterMetaType("Event"); + _jsonCB = QSharedPointer(new JsonCallbacks( _log, _peerAddress, parent)); } void JsonAPI::initialize() @@ -124,14 +88,14 @@ void JsonAPI::initialize() API::init(); // setup auth interface - connect(this, &API::onPendingTokenRequest, this, &JsonAPI::newPendingTokenRequest); + connect(this, &API::onPendingTokenRequest, this, &JsonAPI::issueNewPendingTokenRequest); connect(this, &API::onTokenResponse, this, &JsonAPI::handleTokenResponse); // listen for killed instances connect(_instanceManager, &HyperionIManager::instanceStateChanged, this, &JsonAPI::handleInstanceStateChange); // pipe callbacks from subscriptions to parent - connect(_jsonCB, &JsonCB::newCallback, this, &JsonAPI::callbackMessage); + connect(_jsonCB.data(), &JsonCallbacks::newCallback, this, &JsonAPI::callbackMessage); // notify hyperion about a jsonMessageForward if (_hyperion != nullptr) @@ -143,8 +107,6 @@ void JsonAPI::initialize() //notify eventhadler on suspend/resume/idle requests connect(this, &JsonAPI::signalEvent, EventHandler::getInstance().data(), &EventHandler::handleEvent); - - connect(_ledStreamTimer, &QTimer::timeout, this, &JsonAPI::streamLedColorsUpdate, Qt::UniqueConnection); } bool JsonAPI::handleInstanceSwitch(quint8 inst, bool /*forced*/) @@ -164,118 +126,273 @@ void JsonAPI::handleMessage(const QString &messageString, const QString &httpAut const QString ident = "JsonRpc@" + _peerAddress; QJsonObject message; - // parse the message - if (!JsonUtils::parse(ident, messageString, message, _log)) + //parse the message + QPair parsingResult = JsonUtils::parse(ident, messageString, message, _log); + if (!parsingResult.first) { - sendErrorReply("Errors during message parsing, please consult the Hyperion Log."); + //Try to find command and tan, even parsing failed + QString command = findCommand(messageString); + int tan = findTan(messageString); + + sendErrorReply("Parse error", parsingResult.second, command, tan); return; } - int tan = 0; - if (message.value("tan") != QJsonValue::Undefined) - tan = message["tan"].toInt(); - - // check basic message - if (!JsonUtils::validate(ident, message, ":schema", _log)) - { - sendErrorReply("Errors during message validation, please consult the Hyperion Log.", "" /*command*/, tan); - return; - } + DebugIf(verbose, _log, "message: [%s]", QJsonDocument(message).toJson(QJsonDocument::Compact).constData() ); // check specific message - const QString command = message["command"].toString(); - if (!JsonUtils::validate(ident, message, QString(":schema-%1").arg(command), _log)) + const QString command = message.value("command").toString(); + const QString subCommand = message.value("subcommand").toString(); + + int tan {0}; + if (message.value("tan") != QJsonValue::Undefined) { - sendErrorReply("Errors during specific message validation, please consult the Hyperion Log", command, tan); + tan = message["tan"].toInt(); + } + + // check basic message + QJsonObject schemaJson = QJsonFactory::readSchema(":schema"); + QPair validationResult = JsonUtils::validate(ident, message, schemaJson, _log); + if (!validationResult.first) + { + sendErrorReply("Invalid command", validationResult.second, command, tan); return; } - // client auth before everything else but not for http - if (!_noListener && command == "authorize") + JsonApiCommand cmd = ApiCommandRegister::getCommandInfo(command, subCommand); + cmd.tan = tan; + + if (cmd.command == Command::Unknown) { - handleAuthorizeCommand(message, command, tan); + const QStringList errorDetails (subCommand.isEmpty() ? "subcommand is missing" : QString("Invalid subcommand: %1").arg(subCommand)); + sendErrorReply("Invalid command", errorDetails, command, tan); return; } - // check auth state - if (!API::isAuthorized()) + if (_noListener) { - // on the fly auth available for http from http Auth header - if (_noListener) + setAuthorization(false); + if(cmd.isNolistenerCmd == NoListenerCmd::No) { - QString cToken = httpAuthHeader.mid(5).trimmed(); - if (API::isTokenAuthorized(cToken)) - goto proceed; + sendErrorReply("Command not supported via single API calls using HTTP/S", cmd); + return; } - sendErrorReply("No Authorization", command, tan); + + // Check authorization for HTTP requests + if (!httpAuthHeader.isEmpty()) + { + int bearTokenLenght {0}; + if (httpAuthHeader.startsWith(BEARER_TOKEN_TAG, Qt::CaseInsensitive)) { + bearTokenLenght = BEARER_TOKEN_TAG_LENGTH; + } + else if (httpAuthHeader.startsWith(TOKEN_TAG, Qt::CaseInsensitive)) { + bearTokenLenght = TOKEN_TAG_LENGTH; + } + + if (bearTokenLenght == 0) + { + sendErrorReply("No bearer token found in Authorization header", cmd); + return; + } + + QString cToken =httpAuthHeader.mid(bearTokenLenght).trimmed(); + API::isTokenAuthorized(cToken); // _authorized && _adminAuthorized are set + } + + if (islocalConnection() && !_authManager->isLocalAuthRequired()) + { + // if the request comes via a local network connection, plus authorization is disabled for local request, + // no token authorization is required for non-admin requests + setAuthorization(true); + } + } + + if (cmd.authorization != Authorization::No ) + { + if (!isAuthorized() || (cmd.authorization == Authorization::Admin && !isAdminAuthorized())) + { + sendNoAuthorization(cmd); + return; + } + } + + schemaJson = QJsonFactory::readSchema(QString(":schema-%1").arg(command)); + validationResult = JsonUtils::validate(ident, message, schemaJson, _log); + if (!validationResult.first) + { + sendErrorReply("Invalid params", validationResult.second, cmd); return; } -proceed: + if (_hyperion == nullptr) { - sendErrorReply("Service Unavailable", command, tan); + sendErrorReply("Service Unavailable", cmd); return; } - // switch over all possible commands and handle them - if (command == "color") - handleColorCommand(message, command, tan); - else if (command == "image") - handleImageCommand(message, command, tan); -#if defined(ENABLE_EFFECTENGINE) - else if (command == "effect") - handleEffectCommand(message, command, tan); - else if (command == "create-effect") - handleCreateEffectCommand(message, command, tan); - else if (command == "delete-effect") - handleDeleteEffectCommand(message, command, tan); -#endif - else if (command == "sysinfo") - handleSysInfoCommand(message, command, tan); - else if (command == "serverinfo") - handleServerInfoCommand(message, command, tan); - else if (command == "clear") - handleClearCommand(message, command, tan); - else if (command == "adjustment") - handleAdjustmentCommand(message, command, tan); - else if (command == "sourceselect") - handleSourceSelectCommand(message, command, tan); - else if (command == "config") - handleConfigCommand(message, command, tan); - else if (command == "componentstate") - handleComponentStateCommand(message, command, tan); - else if (command == "ledcolors") - handleLedColorsCommand(message, command, tan); - else if (command == "logging") - handleLoggingCommand(message, command, tan); - else if (command == "processing") - handleProcessingCommand(message, command, tan); - else if (command == "videomode") - handleVideoModeCommand(message, command, tan); - else if (command == "instance") - handleInstanceCommand(message, command, tan); - else if (command == "leddevice") - handleLedDeviceCommand(message, command, tan); - else if (command == "inputsource") - handleInputSourceCommand(message, command, tan); - else if (command == "service") - handleServiceCommand(message, command, tan); - else if (command == "system") - handleSystemCommand(message, command, tan); - - // BEGIN | The following commands are deprecated but used to ensure backward compatibility with hyperion Classic remote control - else if (command == "clearall") - handleClearallCommand(message, command, tan); - else if (command == "transform" || command == "correction" || command == "temperature") - sendErrorReply("The command " + command + "is deprecated, please use the Hyperion Web Interface to configure", command, tan); - // END - - // handle not implemented commands + if (!message.contains("instance") || cmd.isInstanceCmd == InstanceCmd::No) + { + handleCommand(cmd, message); + } else - handleNotImplemented(command, tan); + { + handleInstanceCommand(cmd, message); + } } -void JsonAPI::handleColorCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleInstanceCommand(const JsonApiCommand& cmd, const QJsonObject &message) +{ + const QJsonValue instanceElement = message.value("instance"); + QJsonArray instances; + if (instanceElement.isDouble()) + { + instances.append(instanceElement); + } else if (instanceElement.isArray()) + { + instances = instanceElement.toArray(); + } + + QList runningInstanceIdxs = _instanceManager->getRunningInstanceIdx(); + + QList instanceIdxList; + QStringList errorDetails; + if (instances.contains("all")) + { + for (const auto& instanceIdx : runningInstanceIdxs) + { + instanceIdxList.append(instanceIdx); + } + } + else + { + for (const auto &instance : std::as_const(instances)) { + + quint8 instanceIdx = static_cast(instance.toInt()); + if (instance.isDouble() && runningInstanceIdxs.contains(instanceIdx)) + { + instanceIdxList.append(instanceIdx); + } + else + { + errorDetails.append("Not a running or valid instance: " + instance.toVariant().toString()); + } + } + } + + if (instanceIdxList.isEmpty() || !errorDetails.isEmpty() ) + { + sendErrorReply("Invalid instance(s) given", errorDetails, cmd); + return; + } + + quint8 currentInstanceIdx = getCurrentInstanceIndex(); + if (instanceIdxList.size() > 1) + { + if (cmd.isInstanceCmd != InstanceCmd::Multi) + { + sendErrorReply("Command does not support multiple instances", cmd); + return; + } + } + + for (const auto &instanceIdx : instanceIdxList) + { + if (setHyperionInstance(instanceIdx)) + { + handleCommand(cmd, message); + } + } + + setHyperionInstance(currentInstanceIdx); +} + +void JsonAPI::handleCommand(const JsonApiCommand& cmd, const QJsonObject &message) +{ + switch (cmd.command) { + case Command::Authorize: + handleAuthorizeCommand(message, cmd); + break; + case Command::Color: + handleColorCommand(message, cmd); + break; + case Command::Image: + handleImageCommand(message, cmd); + break; +#if defined(ENABLE_EFFECTENGINE) + case Command::Effect: + handleEffectCommand(message, cmd); + break; + case Command::CreateEffect: + handleCreateEffectCommand(message, cmd); + break; + case Command::DeleteEffect: + handleDeleteEffectCommand(message, cmd); + break; +#endif + case Command::SysInfo: + handleSysInfoCommand(message, cmd); + break; + case Command::ServerInfo: + handleServerInfoCommand(message, cmd); + break; + case Command::Clear: + handleClearCommand(message, cmd); + break; + case Command::Adjustment: + handleAdjustmentCommand(message, cmd); + break; + case Command::SourceSelect: + handleSourceSelectCommand(message, cmd); + break; + case Command::Config: + handleConfigCommand(message, cmd); + break; + case Command::ComponentState: + handleComponentStateCommand(message, cmd); + break; + case Command::LedColors: + handleLedColorsCommand(message, cmd); + break; + case Command::Logging: + handleLoggingCommand(message, cmd); + break; + case Command::Processing: + handleProcessingCommand(message, cmd); + break; + case Command::VideoMode: + handleVideoModeCommand(message, cmd); + break; + case Command::Instance: + handleInstanceCommand(message, cmd); + break; + case Command::LedDevice: + handleLedDeviceCommand(message, cmd); + break; + case Command::InputSource: + handleInputSourceCommand(message, cmd); + break; + case Command::Service: + handleServiceCommand(message, cmd); + break; + case Command::System: + handleSystemCommand(message, cmd); + break; + case Command::ClearAll: + handleClearallCommand(message, cmd); + break; + // BEGIN | The following commands are deprecated but used to ensure backward compatibility with Hyperion Classic remote control + case Command::Transform: + case Command::Correction: + case Command::Temperature: + sendErrorReply("The command is deprecated, please use the Hyperion Web Interface to configure", cmd); + break; + // END + default: + break; + } +} + +void JsonAPI::handleColorCommand(const QJsonObject &message, const JsonApiCommand& cmd) { emit forwardJsonMessage(message); int priority = message["priority"].toInt(); @@ -284,17 +401,16 @@ void JsonAPI::handleColorCommand(const QJsonObject &message, const QString &comm const QJsonArray &jsonColor = message["color"].toArray(); std::vector colors; - // TODO faster copy - for (const auto &entry : jsonColor) - { - colors.emplace_back(uint8_t(entry.toInt())); - } + colors.reserve(static_cast::size_type>(jsonColor.size())); + // Transform each entry in jsonColor to uint8_t and append to colors + std::transform(jsonColor.begin(), jsonColor.end(), std::back_inserter(colors), + [](const QJsonValue &value) { return static_cast(value.toInt()); }); API::setColor(priority, colors, duration, origin); - sendSuccessReply(command, tan); + sendSuccessReply(cmd); } -void JsonAPI::handleImageCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleImageCommand(const QJsonObject &message, const JsonApiCommand& cmd) { emit forwardJsonMessage(message); @@ -310,16 +426,15 @@ void JsonAPI::handleImageCommand(const QJsonObject &message, const QString &comm idata.data = QByteArray::fromBase64(QByteArray(message["imagedata"].toString().toUtf8())); QString replyMsg; - if (!API::setImage(idata, COMP_IMAGE, replyMsg)) - { - sendErrorReply(replyMsg, command, tan); - return; + if (API::setImage(idata, COMP_IMAGE, replyMsg)) { + sendSuccessReply(cmd); + } else { + sendErrorReply(replyMsg, cmd); } - sendSuccessReply(command, tan); } #if defined(ENABLE_EFFECTENGINE) -void JsonAPI::handleEffectCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleEffectCommand(const QJsonObject &message, const JsonApiCommand& cmd) { emit forwardJsonMessage(message); @@ -332,524 +447,118 @@ void JsonAPI::handleEffectCommand(const QJsonObject &message, const QString &com dat.data = message["imageData"].toString("").toUtf8(); dat.args = message["effect"].toObject()["args"].toObject(); - if (API::setEffect(dat)) - sendSuccessReply(command, tan); - else - sendErrorReply("Effect '" + dat.effectName + "' not found", command, tan); + if (API::setEffect(dat)) { + sendSuccessReply(cmd); + } else { + sendErrorReply("Effect '" + dat.effectName + "' not found", cmd); + } } -void JsonAPI::handleCreateEffectCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleCreateEffectCommand(const QJsonObject &message, const JsonApiCommand& cmd) { const QString resultMsg = API::saveEffect(message); - resultMsg.isEmpty() ? sendSuccessReply(command, tan) : sendErrorReply(resultMsg, command, tan); + resultMsg.isEmpty() ? sendSuccessReply(cmd) : sendErrorReply(resultMsg, cmd); } -void JsonAPI::handleDeleteEffectCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleDeleteEffectCommand(const QJsonObject &message, const JsonApiCommand& cmd) { const QString res = API::deleteEffect(message["name"].toString()); - res.isEmpty() ? sendSuccessReply(command, tan) : sendErrorReply(res, command, tan); + res.isEmpty() ? sendSuccessReply(cmd) : sendErrorReply(res, cmd); } #endif -void JsonAPI::handleSysInfoCommand(const QJsonObject &, const QString &command, int tan) +void JsonAPI::handleSysInfoCommand(const QJsonObject & /*unused*/, const JsonApiCommand& cmd) { - // create result - QJsonObject result; - QJsonObject info; - result["success"] = true; - result["command"] = command; - result["tan"] = tan; - - SysInfo::HyperionSysInfo data = SysInfo::get(); - QJsonObject system; - system["kernelType"] = data.kernelType; - system["kernelVersion"] = data.kernelVersion; - system["architecture"] = data.architecture; - system["cpuModelName"] = data.cpuModelName; - system["cpuModelType"] = data.cpuModelType; - system["cpuHardware"] = data.cpuHardware; - system["cpuRevision"] = data.cpuRevision; - system["wordSize"] = data.wordSize; - system["productType"] = data.productType; - system["productVersion"] = data.productVersion; - system["prettyName"] = data.prettyName; - system["hostName"] = data.hostName; - system["domainName"] = data.domainName; - system["isUserAdmin"] = data.isUserAdmin; - system["qtVersion"] = data.qtVersion; -#if defined(ENABLE_EFFECTENGINE) - system["pyVersion"] = data.pyVersion; -#endif - info["system"] = system; - - QJsonObject hyperion; - hyperion["version"] = QString(HYPERION_VERSION); - hyperion["build"] = QString(HYPERION_BUILD_ID); - hyperion["gitremote"] = QString(HYPERION_GIT_REMOTE); - hyperion["time"] = QString(__DATE__ " " __TIME__); - hyperion["id"] = _authManager->getID(); - hyperion["rootPath"] = _instanceManager->getRootPath(); - hyperion["readOnlyMode"] = _hyperion->getReadOnlyMode(); - - QCoreApplication* app = QCoreApplication::instance(); - hyperion["isGuiMode"] = qobject_cast(app) ? true : false; - - info["hyperion"] = hyperion; - - // send the result - result["info"] = info; - emit callbackMessage(result); + sendSuccessDataReply(JsonInfo::getSystemInfo(_hyperion), cmd); } -void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const JsonApiCommand& cmd) { - QJsonObject info; + QJsonObject info {}; + QStringList errorDetails; - // collect priority information - QJsonArray priorities; - uint64_t now = QDateTime::currentMSecsSinceEpoch(); - QList activePriorities = _hyperion->getActivePriorities(); - activePriorities.removeAll(PriorityMuxer::LOWEST_PRIORITY); - int currentPriority = _hyperion->getCurrentPriority(); - - for(int priority : std::as_const(activePriorities)) - { - const Hyperion::InputInfo &priorityInfo = _hyperion->getPriorityInfo(priority); - - QJsonObject item; - item["priority"] = priority; - - if (priorityInfo.timeoutTime_ms > 0 ) - { - item["duration_ms"] = int(priorityInfo.timeoutTime_ms - now); - } - - // owner has optional informations to the component - if (!priorityInfo.owner.isEmpty()) - { - item["owner"] = priorityInfo.owner; - } - - item["componentId"] = QString(hyperion::componentToIdString(priorityInfo.componentId)); - item["origin"] = priorityInfo.origin; - item["active"] = (priorityInfo.timeoutTime_ms >= -1); - item["visible"] = (priority == currentPriority); - - if (priorityInfo.componentId == hyperion::COMP_COLOR && !priorityInfo.ledColors.empty()) - { - QJsonObject LEDcolor; - - // add RGB Value to Array - QJsonArray RGBValue; - RGBValue.append(priorityInfo.ledColors.begin()->red); - RGBValue.append(priorityInfo.ledColors.begin()->green); - RGBValue.append(priorityInfo.ledColors.begin()->blue); - LEDcolor.insert("RGB", RGBValue); - - uint16_t Hue; - float Saturation; - float Luminace; - - // add HSL Value to Array - QJsonArray HSLValue; - ColorSys::rgb2hsl(priorityInfo.ledColors.begin()->red, - priorityInfo.ledColors.begin()->green, - priorityInfo.ledColors.begin()->blue, - Hue, Saturation, Luminace); - - HSLValue.append(Hue); - HSLValue.append(Saturation); - HSLValue.append(Luminace); - LEDcolor.insert("HSL", HSLValue); - - item["value"] = LEDcolor; - } - - (priority == currentPriority) - ? priorities.prepend(item) - : priorities.append(item); - } - - info["priorities"] = priorities; - info["priorities_autoselect"] = _hyperion->sourceAutoSelectEnabled(); - - // collect adjustment information - QJsonArray adjustmentArray; - for (const QString &adjustmentId : _hyperion->getAdjustmentIds()) - { - const ColorAdjustment *colorAdjustment = _hyperion->getAdjustment(adjustmentId); - if (colorAdjustment == nullptr) - { - Error(_log, "Incorrect color adjustment id: %s", QSTRING_CSTR(adjustmentId)); - continue; - } - - QJsonObject adjustment; - adjustment["id"] = adjustmentId; - - QJsonArray whiteAdjust; - whiteAdjust.append(colorAdjustment->_rgbWhiteAdjustment.getAdjustmentR()); - whiteAdjust.append(colorAdjustment->_rgbWhiteAdjustment.getAdjustmentG()); - whiteAdjust.append(colorAdjustment->_rgbWhiteAdjustment.getAdjustmentB()); - adjustment.insert("white", whiteAdjust); - - QJsonArray redAdjust; - redAdjust.append(colorAdjustment->_rgbRedAdjustment.getAdjustmentR()); - redAdjust.append(colorAdjustment->_rgbRedAdjustment.getAdjustmentG()); - redAdjust.append(colorAdjustment->_rgbRedAdjustment.getAdjustmentB()); - adjustment.insert("red", redAdjust); - - QJsonArray greenAdjust; - greenAdjust.append(colorAdjustment->_rgbGreenAdjustment.getAdjustmentR()); - greenAdjust.append(colorAdjustment->_rgbGreenAdjustment.getAdjustmentG()); - greenAdjust.append(colorAdjustment->_rgbGreenAdjustment.getAdjustmentB()); - adjustment.insert("green", greenAdjust); - - QJsonArray blueAdjust; - blueAdjust.append(colorAdjustment->_rgbBlueAdjustment.getAdjustmentR()); - blueAdjust.append(colorAdjustment->_rgbBlueAdjustment.getAdjustmentG()); - blueAdjust.append(colorAdjustment->_rgbBlueAdjustment.getAdjustmentB()); - adjustment.insert("blue", blueAdjust); - - QJsonArray cyanAdjust; - cyanAdjust.append(colorAdjustment->_rgbCyanAdjustment.getAdjustmentR()); - cyanAdjust.append(colorAdjustment->_rgbCyanAdjustment.getAdjustmentG()); - cyanAdjust.append(colorAdjustment->_rgbCyanAdjustment.getAdjustmentB()); - adjustment.insert("cyan", cyanAdjust); - - QJsonArray magentaAdjust; - magentaAdjust.append(colorAdjustment->_rgbMagentaAdjustment.getAdjustmentR()); - magentaAdjust.append(colorAdjustment->_rgbMagentaAdjustment.getAdjustmentG()); - magentaAdjust.append(colorAdjustment->_rgbMagentaAdjustment.getAdjustmentB()); - adjustment.insert("magenta", magentaAdjust); - - QJsonArray yellowAdjust; - yellowAdjust.append(colorAdjustment->_rgbYellowAdjustment.getAdjustmentR()); - yellowAdjust.append(colorAdjustment->_rgbYellowAdjustment.getAdjustmentG()); - yellowAdjust.append(colorAdjustment->_rgbYellowAdjustment.getAdjustmentB()); - adjustment.insert("yellow", yellowAdjust); - - adjustment["backlightThreshold"] = colorAdjustment->_rgbTransform.getBacklightThreshold(); - adjustment["backlightColored"] = colorAdjustment->_rgbTransform.getBacklightColored(); - adjustment["brightness"] = colorAdjustment->_rgbTransform.getBrightness(); - adjustment["brightnessCompensation"] = colorAdjustment->_rgbTransform.getBrightnessCompensation(); - adjustment["gammaRed"] = colorAdjustment->_rgbTransform.getGammaR(); - adjustment["gammaGreen"] = colorAdjustment->_rgbTransform.getGammaG(); - adjustment["gammaBlue"] = colorAdjustment->_rgbTransform.getGammaB(); - - adjustment["saturationGain"] = colorAdjustment->_okhsvTransform.getSaturationGain(); - adjustment["brightnessGain"] = colorAdjustment->_okhsvTransform.getBrightnessGain(); - - adjustmentArray.append(adjustment); - } - - info["adjustment"] = adjustmentArray; + switch (cmd.getSubCommand()) { + case SubCommand::Empty: + case SubCommand::GetInfo: + info["priorities"] = JsonInfo::getPrioritiestInfo(_hyperion); + info["priorities_autoselect"] = _hyperion->sourceAutoSelectEnabled(); + info["adjustment"] = JsonInfo::getAdjustmentInfo(_hyperion, _log); + info["ledDevices"] = JsonInfo::getAvailableLedDevices(); + info["grabbers"] = JsonInfo::getGrabbers(_hyperion); + info["videomode"] = QString(videoMode2String(_hyperion->getCurrentVideoMode())); + info["cec"] = JsonInfo::getCecInfo(); + info["services"] = JsonInfo::getServices(); + info["components"] = JsonInfo::getComponents(_hyperion); + info["imageToLedMappingType"] = ImageProcessor::mappingTypeToStr(_hyperion->getLedMappingType()); + info["instance"] = JsonInfo::getInstanceInfo(); + info["leds"] = _hyperion->getSetting(settings::LEDS).array(); + info["activeLedColor"] = JsonInfo::getActiveColors(_hyperion); #if defined(ENABLE_EFFECTENGINE) - // collect effect info - QJsonArray effects; - const std::list &effectsDefinitions = _hyperion->getEffects(); - for (const EffectDefinition &effectDefinition : effectsDefinitions) - { - QJsonObject effect; - effect["name"] = effectDefinition.name; - effect["file"] = effectDefinition.file; - effect["script"] = effectDefinition.script; - effect["args"] = effectDefinition.args; - effects.append(effect); - } - - info["effects"] = effects; + info["effects"] = JsonInfo::getEffects(_hyperion); + info["activeEffects"] = JsonInfo::getActiveEffects(_hyperion); #endif - // get available led devices - QJsonObject ledDevices; - QJsonArray availableLedDevices; - for (auto dev : LedDeviceWrapper::getDeviceMap()) - { - availableLedDevices.append(dev.first); - } + // BEGIN | The following entries are deprecated but used to ensure backward compatibility with hyperion Classic or up to Hyperion 2.0.16 + info["hostname"] = QHostInfo::localHostName(); + info["transform"] = JsonInfo::getTransformationInfo(_hyperion); - ledDevices["available"] = availableLedDevices; - info["ledDevices"] = ledDevices; - - QJsonObject grabbers; - // SCREEN - QJsonObject screenGrabbers; - if (GrabberWrapper::getInstance() != nullptr) - { - QStringList activeGrabbers = GrabberWrapper::getInstance()->getActive(_hyperion->getInstanceIndex(), GrabberTypeFilter::SCREEN); - QJsonArray activeGrabberNames; - for (auto grabberName : activeGrabbers) + if (!_noListener && message.contains("subscribe")) { - activeGrabberNames.append(grabberName); - } - - screenGrabbers["active"] = activeGrabberNames; - } - QJsonArray availableScreenGrabbers; - for (auto grabber : GrabberWrapper::availableGrabbers(GrabberTypeFilter::SCREEN)) - { - availableScreenGrabbers.append(grabber); - } - screenGrabbers["available"] = availableScreenGrabbers; - - // VIDEO - QJsonObject videoGrabbers; - if (GrabberWrapper::getInstance() != nullptr) - { - QStringList activeGrabbers = GrabberWrapper::getInstance()->getActive(_hyperion->getInstanceIndex(), GrabberTypeFilter::VIDEO); - QJsonArray activeGrabberNames; - for (auto grabberName : activeGrabbers) - { - activeGrabberNames.append(grabberName); - } - - videoGrabbers["active"] = activeGrabberNames; - } - QJsonArray availableVideoGrabbers; - for (auto grabber : GrabberWrapper::availableGrabbers(GrabberTypeFilter::VIDEO)) - { - availableVideoGrabbers.append(grabber); - } - videoGrabbers["available"] = availableVideoGrabbers; - - // AUDIO - QJsonObject audioGrabbers; - if (GrabberWrapper::getInstance() != nullptr) - { - QStringList activeGrabbers = GrabberWrapper::getInstance()->getActive(_hyperion->getInstanceIndex(), GrabberTypeFilter::AUDIO); - - QJsonArray activeGrabberNames; - for (auto grabberName : activeGrabbers) - { - activeGrabberNames.append(grabberName); - } - - audioGrabbers["active"] = activeGrabberNames; - } - QJsonArray availableAudioGrabbers; - for (auto grabber : GrabberWrapper::availableGrabbers(GrabberTypeFilter::AUDIO)) - { - availableAudioGrabbers.append(grabber); - } - audioGrabbers["available"] = availableAudioGrabbers; - - grabbers.insert("screen", screenGrabbers); - grabbers.insert("video", videoGrabbers); - grabbers.insert("audio", audioGrabbers); - - info["grabbers"] = grabbers; - - info["videomode"] = QString(videoMode2String(_hyperion->getCurrentVideoMode())); - - QJsonObject cecInfo; -#if defined(ENABLE_CEC) - cecInfo["enabled"] = true; -#else - cecInfo["enabled"] = false; -#endif - info["cec"] = cecInfo; - - // get available services - QJsonArray services; - -#if defined(ENABLE_BOBLIGHT_SERVER) - services.append("boblight"); -#endif - -#if defined(ENABLE_CEC) - services.append("cec"); -#endif - -#if defined(ENABLE_EFFECTENGINE) - services.append("effectengine"); -#endif - -#if defined(ENABLE_FORWARDER) - services.append("forwarder"); -#endif - -#if defined(ENABLE_FLATBUF_SERVER) - services.append("flatbuffer"); -#endif - -#if defined(ENABLE_PROTOBUF_SERVER) - services.append("protobuffer"); -#endif - -#if defined(ENABLE_MDNS) - services.append("mDNS"); -#endif - services.append("SSDP"); - - if (!availableScreenGrabbers.isEmpty() || !availableVideoGrabbers.isEmpty() || services.contains("flatbuffer") || services.contains("protobuffer")) - { - services.append("borderdetection"); - } - - info["services"] = services; - - // get available components - QJsonArray component; - std::map components = _hyperion->getComponentRegister()->getRegister(); - for (auto comp : components) - { - QJsonObject item; - item["name"] = QString::fromStdString(hyperion::componentToIdString(comp.first)); - item["enabled"] = comp.second; - - component.append(item); - } - - info["components"] = component; - info["imageToLedMappingType"] = ImageProcessor::mappingTypeToStr(_hyperion->getLedMappingType()); - - // add instance info - QJsonArray instanceInfo; - for (const auto &entry : API::getAllInstanceData()) - { - QJsonObject obj; - obj.insert("friendly_name", entry["friendly_name"].toString()); - obj.insert("instance", entry["instance"].toInt()); - obj.insert("running", entry["running"].toBool()); - instanceInfo.append(obj); - } - info["instance"] = instanceInfo; - - // add leds configs - info["leds"] = _hyperion->getSetting(settings::LEDS).array(); - - // BEGIN | The following entries are deprecated but used to ensure backward compatibility with hyperion Classic remote control - // TODO Output the real transformation information instead of default - - // HOST NAME - info["hostname"] = QHostInfo::localHostName(); - - // TRANSFORM INFORMATION (DEFAULT VALUES) - QJsonArray transformArray; - for (const QString &transformId : _hyperion->getAdjustmentIds()) - { - QJsonObject transform; - QJsonArray blacklevel, whitelevel, gamma, threshold; - - transform["id"] = transformId; - transform["saturationGain"] = 1.0; - transform["brightnessGain"] = 1.0; - transform["saturationLGain"] = 1.0; - transform["luminanceGain"] = 1.0; - transform["luminanceMinimum"] = 0.0; - - for (int i = 0; i < 3; i++) - { - blacklevel.append(0.0); - whitelevel.append(1.0); - gamma.append(2.50); - threshold.append(0.0); - } - - transform.insert("blacklevel", blacklevel); - transform.insert("whitelevel", whitelevel); - transform.insert("gamma", gamma); - transform.insert("threshold", threshold); - - transformArray.append(transform); - } - info["transform"] = transformArray; - -#if defined(ENABLE_EFFECTENGINE) - // ACTIVE EFFECT INFO - QJsonArray activeEffects; - for (const ActiveEffectDefinition &activeEffectDefinition : _hyperion->getActiveEffects()) - { - if (activeEffectDefinition.priority != PriorityMuxer::LOWEST_PRIORITY - 1) - { - QJsonObject activeEffect; - activeEffect["script"] = activeEffectDefinition.script; - activeEffect["name"] = activeEffectDefinition.name; - activeEffect["priority"] = activeEffectDefinition.priority; - activeEffect["timeout"] = activeEffectDefinition.timeout; - activeEffect["args"] = activeEffectDefinition.args; - activeEffects.append(activeEffect); - } - } - info["activeEffects"] = activeEffects; -#endif - - // ACTIVE STATIC LED COLOR - QJsonArray activeLedColors; - const Hyperion::InputInfo &priorityInfo = _hyperion->getPriorityInfo(_hyperion->getCurrentPriority()); - if (priorityInfo.componentId == hyperion::COMP_COLOR && !priorityInfo.ledColors.empty()) - { - // check if LED Color not Black (0,0,0) - if ((priorityInfo.ledColors.begin()->red + - priorityInfo.ledColors.begin()->green + - priorityInfo.ledColors.begin()->blue != - 0)) - { - QJsonObject LEDcolor; - - // add RGB Value to Array - QJsonArray RGBValue; - RGBValue.append(priorityInfo.ledColors.begin()->red); - RGBValue.append(priorityInfo.ledColors.begin()->green); - RGBValue.append(priorityInfo.ledColors.begin()->blue); - LEDcolor.insert("RGB Value", RGBValue); - - uint16_t Hue; - float Saturation, Luminace; - - // add HSL Value to Array - QJsonArray HSLValue; - ColorSys::rgb2hsl(priorityInfo.ledColors.begin()->red, - priorityInfo.ledColors.begin()->green, - priorityInfo.ledColors.begin()->blue, - Hue, Saturation, Luminace); - - HSLValue.append(Hue); - HSLValue.append(Saturation); - HSLValue.append(Luminace); - LEDcolor.insert("HSL Value", HSLValue); - - activeLedColors.append(LEDcolor); - } - } - info["activeLedColor"] = activeLedColors; - - // END - - sendSuccessDataReply(QJsonDocument(info), command, tan); - - // AFTER we send the info, the client might want to subscribe to future updates - if (message.contains("subscribe")) - { - // check if listeners are allowed - if (_noListener) - return; - - QJsonArray subsArr = message["subscribe"].toArray(); - // catch the all keyword and build a list of all cmds - if (subsArr.contains("all")) - { - subsArr = QJsonArray(); - for (const auto& entry : _jsonCB->getCommands()) + const QJsonArray &subscriptions = message["subscribe"].toArray(); + QStringList invaliCommands = _jsonCB->subscribe(subscriptions); + if (!invaliCommands.isEmpty()) { - subsArr.append(entry); + errorDetails.append("subscribe - Invalid commands provided: " + invaliCommands.join(',')); } } + // END - for (const QJsonValueRef entry : subsArr) + break; + + case SubCommand::Subscribe: + case SubCommand::Unsubscribe: + { + const QJsonObject ¶ms = message["data"].toObject(); + const QJsonArray &subscriptions = params["subscriptions"].toArray(); + if (subscriptions.isEmpty()) { + sendErrorReply("Invalid params", {"No subscriptions provided"}, cmd); + return; + } + + QStringList invaliCommands; + if (cmd.subCommand == SubCommand::Subscribe) { - // config callbacks just if auth is set - if ((entry == "settings-update" || entry == "token-update") && !API::isAdminAuthorized()) - continue; - // silent failure if a subscribe type is not found - _jsonCB->subscribeFor(entry.toString()); + invaliCommands = _jsonCB->subscribe(subscriptions); + } + else + { + invaliCommands = _jsonCB->unsubscribe(subscriptions); + } + + if (!invaliCommands.isEmpty()) + { + errorDetails.append("subscriptions - Invalid commands provided: " + invaliCommands.join(',')); } } + break; + + case SubCommand::GetSubscriptions: + info["subscriptions"] = QJsonArray::fromStringList(_jsonCB->getSubscribedCommands()); + break; + + case SubCommand::GetSubscriptionCommands: + info["commands"] = QJsonArray::fromStringList(_jsonCB->getCommands()); + break; + + default: + break; + } + + sendSuccessDataReplyWithError(info, cmd, errorDetails); } -void JsonAPI::handleClearCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleClearCommand(const QJsonObject &message, const JsonApiCommand& cmd) { emit forwardJsonMessage(message); int priority = message["priority"].toInt(); @@ -857,117 +566,113 @@ void JsonAPI::handleClearCommand(const QJsonObject &message, const QString &comm if (!API::clearPriority(priority, replyMsg)) { - sendErrorReply(replyMsg, command, tan); + sendErrorReply(replyMsg, cmd); return; } - sendSuccessReply(command, tan); + sendSuccessReply(cmd); } -void JsonAPI::handleClearallCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleClearallCommand(const QJsonObject &message, const JsonApiCommand& cmd) { emit forwardJsonMessage(message); QString replyMsg; API::clearPriority(-1, replyMsg); - sendSuccessReply(command, tan); + sendSuccessReply(cmd); } -void JsonAPI::handleAdjustmentCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleAdjustmentCommand(const QJsonObject &message, const JsonApiCommand& cmd) { const QJsonObject &adjustment = message["adjustment"].toObject(); - const QString adjustmentId = adjustment["id"].toString(_hyperion->getAdjustmentIds().first()); + const QList adjustmentIds = _hyperion->getAdjustmentIds(); + if (adjustmentIds.isEmpty()) { + sendErrorReply("No adjustment data available", cmd); + return; + } + + const QString adjustmentId = adjustment["id"].toString(adjustmentIds.first()); ColorAdjustment *colorAdjustment = _hyperion->getAdjustment(adjustmentId); - if (colorAdjustment == nullptr) - { + if (colorAdjustment == nullptr) { Warning(_log, "Incorrect adjustment identifier: %s", adjustmentId.toStdString().c_str()); return; } - if (adjustment.contains("red")) - { - const QJsonArray &values = adjustment["red"].toArray(); - colorAdjustment->_rgbRedAdjustment.setAdjustment(values[0u].toInt(), values[1u].toInt(), values[2u].toInt()); - } - - if (adjustment.contains("green")) - { - const QJsonArray &values = adjustment["green"].toArray(); - colorAdjustment->_rgbGreenAdjustment.setAdjustment(values[0u].toInt(), values[1u].toInt(), values[2u].toInt()); - } - - if (adjustment.contains("blue")) - { - const QJsonArray &values = adjustment["blue"].toArray(); - colorAdjustment->_rgbBlueAdjustment.setAdjustment(values[0u].toInt(), values[1u].toInt(), values[2u].toInt()); - } - if (adjustment.contains("cyan")) - { - const QJsonArray &values = adjustment["cyan"].toArray(); - colorAdjustment->_rgbCyanAdjustment.setAdjustment(values[0u].toInt(), values[1u].toInt(), values[2u].toInt()); - } - if (adjustment.contains("magenta")) - { - const QJsonArray &values = adjustment["magenta"].toArray(); - colorAdjustment->_rgbMagentaAdjustment.setAdjustment(values[0u].toInt(), values[1u].toInt(), values[2u].toInt()); - } - if (adjustment.contains("yellow")) - { - const QJsonArray &values = adjustment["yellow"].toArray(); - colorAdjustment->_rgbYellowAdjustment.setAdjustment(values[0u].toInt(), values[1u].toInt(), values[2u].toInt()); - } - if (adjustment.contains("white")) - { - const QJsonArray &values = adjustment["white"].toArray(); - colorAdjustment->_rgbWhiteAdjustment.setAdjustment(values[0u].toInt(), values[1u].toInt(), values[2u].toInt()); - } - - if (adjustment.contains("gammaRed")) - { - colorAdjustment->_rgbTransform.setGamma(adjustment["gammaRed"].toDouble(), colorAdjustment->_rgbTransform.getGammaG(), colorAdjustment->_rgbTransform.getGammaB()); - } - if (adjustment.contains("gammaGreen")) - { - colorAdjustment->_rgbTransform.setGamma(colorAdjustment->_rgbTransform.getGammaR(), adjustment["gammaGreen"].toDouble(), colorAdjustment->_rgbTransform.getGammaB()); - } - if (adjustment.contains("gammaBlue")) - { - colorAdjustment->_rgbTransform.setGamma(colorAdjustment->_rgbTransform.getGammaR(), colorAdjustment->_rgbTransform.getGammaG(), adjustment["gammaBlue"].toDouble()); - } - - if (adjustment.contains("backlightThreshold")) - { - colorAdjustment->_rgbTransform.setBacklightThreshold(adjustment["backlightThreshold"].toDouble()); - } - if (adjustment.contains("backlightColored")) - { - colorAdjustment->_rgbTransform.setBacklightColored(adjustment["backlightColored"].toBool()); - } - if (adjustment.contains("brightness")) - { - colorAdjustment->_rgbTransform.setBrightness(adjustment["brightness"].toInt()); - } - if (adjustment.contains("brightnessCompensation")) - { - colorAdjustment->_rgbTransform.setBrightnessCompensation(adjustment["brightnessCompensation"].toInt()); - } - - if (adjustment.contains("saturationGain")) - { - colorAdjustment->_okhsvTransform.setSaturationGain(adjustment["saturationGain"].toDouble()); - } - - if (adjustment.contains("brightnessGain")) - { - colorAdjustment->_okhsvTransform.setBrightnessGain(adjustment["brightnessGain"].toDouble()); - } - - // commit the changes + applyColorAdjustments(adjustment, colorAdjustment); + applyTransforms(adjustment, colorAdjustment); _hyperion->adjustmentsUpdated(); - - sendSuccessReply(command, tan); + sendSuccessReply(cmd); } -void JsonAPI::handleSourceSelectCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::applyColorAdjustments(const QJsonObject &adjustment, ColorAdjustment *colorAdjustment) +{ + applyColorAdjustment("red", adjustment, colorAdjustment->_rgbRedAdjustment); + applyColorAdjustment("green", adjustment, colorAdjustment->_rgbGreenAdjustment); + applyColorAdjustment("blue", adjustment, colorAdjustment->_rgbBlueAdjustment); + applyColorAdjustment("cyan", adjustment, colorAdjustment->_rgbCyanAdjustment); + applyColorAdjustment("magenta", adjustment, colorAdjustment->_rgbMagentaAdjustment); + applyColorAdjustment("yellow", adjustment, colorAdjustment->_rgbYellowAdjustment); + applyColorAdjustment("white", adjustment, colorAdjustment->_rgbWhiteAdjustment); +} + +void JsonAPI::applyColorAdjustment(const QString &colorName, const QJsonObject &adjustment, RgbChannelAdjustment &rgbAdjustment) +{ + if (adjustment.contains(colorName)) { + const QJsonArray &values = adjustment[colorName].toArray(); + if (values.size() >= 3) { + rgbAdjustment.setAdjustment(static_cast(values[0U].toInt()), + static_cast(values[1U].toInt()), + static_cast(values[2U].toInt())); + } + } +} + +void JsonAPI::applyTransforms(const QJsonObject &adjustment, ColorAdjustment *colorAdjustment) +{ + applyGammaTransform("gammaRed", adjustment, colorAdjustment->_rgbTransform, 'r'); + applyGammaTransform("gammaGreen", adjustment, colorAdjustment->_rgbTransform, 'g'); + applyGammaTransform("gammaBlue", adjustment, colorAdjustment->_rgbTransform, 'b'); + applyTransform("backlightThreshold", adjustment, colorAdjustment->_rgbTransform, &RgbTransform::setBacklightThreshold); + applyTransform("backlightColored", adjustment, colorAdjustment->_rgbTransform, &RgbTransform::setBacklightColored); + applyTransform("brightness", adjustment, colorAdjustment->_rgbTransform, &RgbTransform::setBrightness); + applyTransform("brightnessCompensation", adjustment, colorAdjustment->_rgbTransform, &RgbTransform::setBrightnessCompensation); + applyTransform("saturationGain", adjustment, colorAdjustment->_okhsvTransform, &OkhsvTransform::setSaturationGain); + applyTransform("brightnessGain", adjustment, colorAdjustment->_okhsvTransform, &OkhsvTransform::setBrightnessGain); +} + +void JsonAPI::applyGammaTransform(const QString &transformName, const QJsonObject &adjustment, RgbTransform &rgbTransform, char channel) +{ + if (adjustment.contains(transformName)) { + rgbTransform.setGamma(channel == 'r' ? adjustment[transformName].toDouble() : rgbTransform.getGammaR(), + channel == 'g' ? adjustment[transformName].toDouble() : rgbTransform.getGammaG(), + channel == 'b' ? adjustment[transformName].toDouble() : rgbTransform.getGammaB()); + } +} + +template +void JsonAPI::applyTransform(const QString &transformName, const QJsonObject &adjustment, T &transform, void (T::*setFunction)(bool)) +{ + if (adjustment.contains(transformName)) { + (transform.*setFunction)(adjustment[transformName].toBool()); + } +} + +template +void JsonAPI::applyTransform(const QString &transformName, const QJsonObject &adjustment, T &transform, void (T::*setFunction)(double)) +{ + if (adjustment.contains(transformName)) { + (transform.*setFunction)(adjustment[transformName].toDouble()); + } +} + +template +void JsonAPI::applyTransform(const QString &transformName, const QJsonObject &adjustment, T &transform, void (T::*setFunction)(uint8_t)) +{ + if (adjustment.contains(transformName)) { + (transform.*setFunction)(static_cast(adjustment[transformName].toInt())); + } +} + +void JsonAPI::handleSourceSelectCommand(const QJsonObject &message, const JsonApiCommand& cmd) { if (message.contains("auto")) { @@ -979,84 +684,63 @@ void JsonAPI::handleSourceSelectCommand(const QJsonObject &message, const QStrin } else { - sendErrorReply("Priority request is invalid", command, tan); + sendErrorReply("Priority request is invalid", cmd); return; } - sendSuccessReply(command, tan); + sendSuccessReply(cmd); } -void JsonAPI::handleConfigCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleConfigCommand(const QJsonObject& message, const JsonApiCommand& cmd) { - QString subcommand = message["subcommand"].toString(""); - QString full_command = command + "-" + subcommand; + switch (cmd.subCommand) { + case SubCommand::GetSchema: + handleSchemaGetCommand(message, cmd); + break; - if (subcommand == "getschema") - { - handleSchemaGetCommand(message, full_command, tan); - } - else if (subcommand == "getconfig") - { - if (_adminAuthorized) - sendSuccessDataReply(QJsonDocument(_hyperion->getQJsonConfig()), full_command, tan); - else - sendErrorReply("No Authorization", command, tan); - } - else if (subcommand == "setconfig") - { - if (_adminAuthorized) - handleConfigSetCommand(message, full_command, tan); - else - sendErrorReply("No Authorization", command, tan); - } - else if (subcommand == "restoreconfig") - { - if (_adminAuthorized) - handleConfigRestoreCommand(message, full_command, tan); - else - sendErrorReply("No Authorization", command, tan); - } - else if (subcommand == "reload") - { - if (_adminAuthorized) - { - Debug(_log, "Restarting due to RPC command"); - emit signalEvent(Event::Reload); + case SubCommand::GetConfig: + sendSuccessDataReply(_hyperion->getQJsonConfig(), cmd); + break; - sendSuccessReply(command + "-" + subcommand, tan); - } - else - { - sendErrorReply("No Authorization", command, tan); - } - } - else - { - sendErrorReply("unknown or missing subcommand", full_command, tan); + case SubCommand::SetConfig: + handleConfigSetCommand(message, cmd); + break; + + case SubCommand::RestoreConfig: + handleConfigRestoreCommand(message, cmd); + break; + + case SubCommand::Reload: + Debug(_log, "Restarting due to RPC command"); + emit signalEvent(Event::Reload); + sendSuccessReply(cmd); + break; + + default: + break; } } -void JsonAPI::handleConfigSetCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleConfigSetCommand(const QJsonObject &message, const JsonApiCommand& cmd) { if (message.contains("config")) { QJsonObject config = message["config"].toObject(); if (API::isHyperionEnabled()) { - if ( API::saveSettings(config) ) - { - sendSuccessReply(command, tan); - } - else - { - sendErrorReply("Save settings failed", command, tan); + if ( API::saveSettings(config) ) { + sendSuccessReply(cmd); + } else { + sendErrorReply("Save settings failed", cmd); } } else - sendErrorReply("Saving configuration while Hyperion is disabled isn't possible", command, tan); + { + sendErrorReply("Saving configuration while Hyperion is disabled isn't possible", cmd); + } } } -void JsonAPI::handleConfigRestoreCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleConfigRestoreCommand(const QJsonObject &message, const JsonApiCommand& cmd) { if (message.contains("config")) { @@ -1065,22 +749,26 @@ void JsonAPI::handleConfigRestoreCommand(const QJsonObject &message, const QStri { if ( API::restoreSettings(config) ) { - sendSuccessReply(command, tan); + sendSuccessReply(cmd); } else { - sendErrorReply("Restore settings failed", command, tan); + sendErrorReply("Restore settings failed", cmd); } } else - sendErrorReply("Restoring configuration while Hyperion is disabled isn't possible", command, tan); + { + sendErrorReply("Restoring configuration while Hyperion is disabled is not possible", cmd); + } } } -void JsonAPI::handleSchemaGetCommand(const QJsonObject& /*message*/, const QString &command, int tan) +void JsonAPI::handleSchemaGetCommand(const QJsonObject& /*message*/, const JsonApiCommand& cmd) { // create result - QJsonObject schemaJson, alldevices, properties; + QJsonObject schemaJson; + QJsonObject alldevices; + QJsonObject properties; // make sure the resources are loaded (they may be left out after static linking) Q_INIT_RESOURCE(resource); @@ -1128,687 +816,506 @@ void JsonAPI::handleSchemaGetCommand(const QJsonObject& /*message*/, const QStri schemaJson.insert("properties", properties); // send the result - sendSuccessDataReply(QJsonDocument(schemaJson), command, tan); + sendSuccessDataReply(schemaJson, cmd); } -void JsonAPI::handleComponentStateCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleComponentStateCommand(const QJsonObject &message, const JsonApiCommand& cmd) { const QJsonObject &componentState = message["componentstate"].toObject(); QString comp = componentState["component"].toString("invalid"); bool compState = componentState["state"].toBool(true); QString replyMsg; - if (!API::setComponentState(comp, compState, replyMsg)) - { - sendErrorReply(replyMsg, command, tan); - return; + if (API::setComponentState(comp, compState, replyMsg)) { + sendSuccessReply(cmd); + } else { + sendErrorReply(replyMsg, cmd); } - sendSuccessReply(command, tan); } -void JsonAPI::streamLedColorsUpdate() +void JsonAPI::handleLedColorsCommand(const QJsonObject& /*message*/, const JsonApiCommand& cmd) { - emit streamLedcolorsUpdate(_currentLedValues); -} - -void JsonAPI::handleLedColorsCommand(const QJsonObject &message, const QString &command, int tan) -{ - // create result - QString subcommand = message["subcommand"].toString(""); - - // max 20 Hz (50ms) interval for streaming (default: 10 Hz (100ms)) - qint64 streaming_interval = qMax(message["interval"].toInt(100), 50); - - if (subcommand == "ledstream-start") - { - _streaming_leds_reply["success"] = true; - _streaming_leds_reply["command"] = command + "-ledstream-update"; - _streaming_leds_reply["tan"] = tan; - - connect(_hyperion, &Hyperion::rawLedColors, this, [=](const std::vector &ledValues) { - - if (ledValues != _currentLedValues) - { - _currentLedValues = ledValues; - if (!_ledStreamTimer->isActive() || _ledStreamTimer->interval() != streaming_interval) - { - _ledStreamTimer->start(streaming_interval); - } - } - else - { - _ledStreamTimer->stop(); - } - }); - + switch (cmd.subCommand) { + case SubCommand::LedStreamStart: + _jsonCB->subscribe( Subscription::LedColorsUpdate); // push once _hyperion->update(); - } - else if (subcommand == "ledstream-stop") - { - disconnect(_hyperion, &Hyperion::rawLedColors, this, 0); - _ledStreamTimer->stop(); - disconnect(_ledStreamConnection); - } - else if (subcommand == "imagestream-start") - { - _streaming_image_reply["success"] = true; - _streaming_image_reply["command"] = command + "-imagestream-update"; - _streaming_image_reply["tan"] = tan; + sendSuccessReply(cmd); + break; - connect(_hyperion, &Hyperion::currentImage, this, &JsonAPI::setImage, Qt::UniqueConnection); - } - else if (subcommand == "imagestream-stop") - { - disconnect(_hyperion, &Hyperion::currentImage, this, 0); - } - else - { - return; - } + case SubCommand::LedStreamStop: + _jsonCB->unsubscribe( Subscription::LedColorsUpdate); + sendSuccessReply(cmd); + break; - sendSuccessReply(command + "-" + subcommand, tan); + case SubCommand::ImageStreamStart: + _jsonCB->subscribe(Subscription::ImageUpdate); + sendSuccessReply(cmd); + break; + + case SubCommand::ImageStreamStop: + _jsonCB->unsubscribe(Subscription::ImageUpdate); + sendSuccessReply(cmd); + break; + + default: + break; + } } -void JsonAPI::handleLoggingCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleLoggingCommand(const QJsonObject& /*message*/, const JsonApiCommand& cmd) { - // create result - QString subcommand = message["subcommand"].toString(""); + switch (cmd.subCommand) { + case SubCommand::Start: + _jsonCB->subscribe("logmsg-update"); + sendSuccessReply(cmd); + break; - if (API::isAdminAuthorized()) - { - _streaming_logging_reply["success"] = true; - _streaming_logging_reply["command"] = command; - _streaming_logging_reply["tan"] = tan; - - if (subcommand == "start") - { - if (!_streaming_logging_activated) - { - _streaming_logging_reply["command"] = command + "-update"; - connect(LoggerManager::getInstance().data(), &LoggerManager::newLogMessage, this, &JsonAPI::incommingLogMessage); - - emit incommingLogMessage (Logger::T_LOG_MESSAGE{}); // needed to trigger log sending - Debug(_log, "log streaming activated for client %s", _peerAddress.toStdString().c_str()); - } - } - else if (subcommand == "stop") - { - if (_streaming_logging_activated) - { - disconnect(LoggerManager::getInstance().data(), &LoggerManager::newLogMessage, this, &JsonAPI::incommingLogMessage); - _streaming_logging_activated = false; - Debug(_log, "log streaming deactivated for client %s", _peerAddress.toStdString().c_str()); - } - } - else - { - return; - } - - sendSuccessReply(command + "-" + subcommand, tan); - } - else - { - sendErrorReply("No Authorization", command + "-" + subcommand, tan); + case SubCommand::Stop: + _jsonCB->unsubscribe("logmsg-update"); + sendSuccessReply(cmd); + break; + default: + break; } } -void JsonAPI::handleProcessingCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleProcessingCommand(const QJsonObject &message, const JsonApiCommand& cmd) { API::setLedMappingType(ImageProcessor::mappingTypeToInt(message["mappingType"].toString("multicolor_mean"))); - sendSuccessReply(command, tan); + sendSuccessReply(cmd); } -void JsonAPI::handleVideoModeCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleVideoModeCommand(const QJsonObject &message, const JsonApiCommand& cmd) { API::setVideoMode(parse3DMode(message["videoMode"].toString("2D"))); - sendSuccessReply(command, tan); + sendSuccessReply(cmd); } -void JsonAPI::handleAuthorizeCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleAuthorizeCommand(const QJsonObject &message, const JsonApiCommand& cmd) +{ + switch (cmd.subCommand) { + case SubCommand::TokenRequired: + handleTokenRequired(cmd); + break; + case SubCommand::AdminRequired: + handleAdminRequired(cmd); + break; + case SubCommand::NewPasswordRequired: + handleNewPasswordRequired(cmd); + break; + case SubCommand::Logout: + handleLogout(cmd); + break; + case SubCommand::NewPassword: + handleNewPassword(message, cmd); + break; + case SubCommand::CreateToken: + handleCreateToken(message, cmd); + break; + case SubCommand::RenameToken: + handleRenameToken(message, cmd); + break; + case SubCommand::DeleteToken: + handleDeleteToken(message, cmd); + break; + case SubCommand::RequestToken: + handleRequestToken(message, cmd); + break; + case SubCommand::GetPendingTokenRequests: + handleGetPendingTokenRequests(cmd); + break; + case SubCommand::AnswerRequest: + handleAnswerRequest(message, cmd); + break; + case SubCommand::GetTokenList: + handleGetTokenList(cmd); + break; + case SubCommand::Login: + handleLogin(message, cmd); + break; + default: + return; + } +} + +void JsonAPI::handleTokenRequired(const JsonApiCommand& cmd) +{ + bool isTokenRequired = !islocalConnection() || _authManager->isLocalAuthRequired(); + QJsonObject response { { "required", isTokenRequired} }; + sendSuccessDataReply(response, cmd); +} + +void JsonAPI::handleAdminRequired(const JsonApiCommand& cmd) +{ + bool isAdminAuthRequired = true; + QJsonObject response { { "adminRequired", isAdminAuthRequired} }; + sendSuccessDataReply(response, cmd); +} + +void JsonAPI::handleNewPasswordRequired(const JsonApiCommand& cmd) +{ + QJsonObject response { { "newPasswordRequired", API::hasHyperionDefaultPw() } }; + sendSuccessDataReply(response, cmd); +} + +void JsonAPI::handleLogout(const JsonApiCommand& cmd) +{ + API::logout(); + sendSuccessReply(cmd); +} + +void JsonAPI::handleNewPassword(const QJsonObject &message, const JsonApiCommand& cmd) +{ + const QString password = message["password"].toString().trimmed(); + const QString newPassword = message["newPassword"].toString().trimmed(); + if (API::updateHyperionPassword(password, newPassword)) { + sendSuccessReply(cmd); + } else { + sendErrorReply("Failed to update user password", cmd); + } +} + +void JsonAPI::handleCreateToken(const QJsonObject &message, const JsonApiCommand& cmd) { - const QString &subc = message["subcommand"].toString().trimmed(); - const QString &id = message["id"].toString().trimmed(); - const QString &password = message["password"].toString().trimmed(); - const QString &newPassword = message["newPassword"].toString().trimmed(); const QString &comment = message["comment"].toString().trimmed(); + AuthManager::AuthDefinition def; + const QString createTokenResult = API::createToken(comment, def); + if (createTokenResult.isEmpty()) { + QJsonObject newTok; + newTok["comment"] = def.comment; + newTok["id"] = def.id; + newTok["token"] = def.token; - // catch test if auth is required - if (subc == "tokenRequired") - { - QJsonObject req; - req["required"] = !API::isAuthorized(); - - sendSuccessDataReply(QJsonDocument(req), command + "-" + subc, tan); - return; - } - - // catch test if admin auth is required - if (subc == "adminRequired") - { - QJsonObject req; - req["adminRequired"] = !API::isAdminAuthorized(); - sendSuccessDataReply(QJsonDocument(req), command + "-" + subc, tan); - return; - } - - // default hyperion password is a security risk, replace it asap - if (subc == "newPasswordRequired") - { - QJsonObject req; - req["newPasswordRequired"] = API::hasHyperionDefaultPw(); - sendSuccessDataReply(QJsonDocument(req), command + "-" + subc, tan); - return; - } - - // catch logout - if (subc == "logout") - { - // disconnect all kind of data callbacks - JsonAPI::stopDataConnections(); // TODO move to API - API::logout(); - sendSuccessReply(command + "-" + subc, tan); - return; - } - - // change password - if (subc == "newPassword") - { - // use password, newPassword - if (API::isAdminAuthorized()) - { - if (API::updateHyperionPassword(password, newPassword)) - { - sendSuccessReply(command + "-" + subc, tan); - return; - } - sendErrorReply("Failed to update user password", command + "-" + subc, tan); - return; - } - sendErrorReply("No Authorization", command + "-" + subc, tan); - return; - } - - // token created from ui - if (subc == "createToken") - { - // use comment - // for user authorized sessions - AuthManager::AuthDefinition def; - const QString createTokenResult = API::createToken(comment, def); - if (createTokenResult.isEmpty()) - { - QJsonObject newTok; - newTok["comment"] = def.comment; - newTok["id"] = def.id; - newTok["token"] = def.token; - - sendSuccessDataReply(QJsonDocument(newTok), command + "-" + subc, tan); - return; - } - sendErrorReply(createTokenResult, command + "-" + subc, tan); - return; - } - - // rename Token - if (subc == "renameToken") - { - // use id/comment - const QString renameTokenResult = API::renameToken(id, comment); - if (renameTokenResult.isEmpty()) - { - sendSuccessReply(command + "-" + subc, tan); - return; - } - sendErrorReply(renameTokenResult, command + "-" + subc, tan); - return; - } - - // delete token - if (subc == "deleteToken") - { - // use id - const QString deleteTokenResult = API::deleteToken(id); - if (deleteTokenResult.isEmpty()) - { - sendSuccessReply(command + "-" + subc, tan); - return; - } - sendErrorReply(deleteTokenResult, command + "-" + subc, tan); - return; - } - - // catch token request - if (subc == "requestToken") - { - // use id/comment - const bool &acc = message["accept"].toBool(true); - if (acc) - API::setNewTokenRequest(comment, id, tan); - else - API::cancelNewTokenRequest(comment, id); - // client should wait for answer - return; - } - - // get pending token requests - if (subc == "getPendingTokenRequests") - { - QVector vec; - if (API::getPendingTokenRequests(vec)) - { - QJsonArray arr; - for (const auto &entry : std::as_const(vec)) - { - QJsonObject obj; - obj["comment"] = entry.comment; - obj["id"] = entry.id; - obj["timeout"] = int(entry.timeoutTime); - arr.append(obj); - } - sendSuccessDataReply(QJsonDocument(arr), command + "-" + subc, tan); - } - else - { - sendErrorReply("No Authorization", command + "-" + subc, tan); - } - - return; - } - - // accept/deny token request - if (subc == "answerRequest") - { - // use id - const bool &accept = message["accept"].toBool(false); - if (!API::handlePendingTokenRequest(id, accept)) - sendErrorReply("No Authorization", command + "-" + subc, tan); - return; - } - - // get token list - if (subc == "getTokenList") - { - QVector defVect; - if (API::getTokenList(defVect)) - { - QJsonArray tArr; - for (const auto &entry : defVect) - { - QJsonObject subO; - subO["comment"] = entry.comment; - subO["id"] = entry.id; - subO["last_use"] = entry.lastUse; - - tArr.append(subO); - } - sendSuccessDataReply(QJsonDocument(tArr), command + "-" + subc, tan); - return; - } - sendErrorReply("No Authorization", command + "-" + subc, tan); - return; - } - - // login - if (subc == "login") - { - const QString &token = message["token"].toString().trimmed(); - - // catch token - if (!token.isEmpty()) - { - // userToken is longer - if (token.size() > 36) - { - if (API::isUserTokenAuthorized(token)) - sendSuccessReply(command + "-" + subc, tan); - else - sendErrorReply("No Authorization", command + "-" + subc, tan); - - return; - } - // usual app token is 36 - if (token.size() == 36) - { - if (API::isTokenAuthorized(token)) - { - sendSuccessReply(command + "-" + subc, tan); - } - else - sendErrorReply("No Authorization", command + "-" + subc, tan); - } - return; - } - - // password - // use password - if (password.size() >= 8) - { - QString userTokenRep; - if (API::isUserAuthorized(password) && API::getUserToken(userTokenRep)) - { - // Return the current valid Hyperion user token - QJsonObject obj; - obj["token"] = userTokenRep; - sendSuccessDataReply(QJsonDocument(obj), command + "-" + subc, tan); - } - else - sendErrorReply("No Authorization", command + "-" + subc, tan); - } - else - sendErrorReply("Password too short", command + "-" + subc, tan); + sendSuccessDataReply(newTok, cmd); + } else { + sendErrorReply("Token creation failed", {createTokenResult}, cmd); } } -void JsonAPI::handleInstanceCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleRenameToken(const QJsonObject &message, const JsonApiCommand& cmd) { - const QString &subc = message["subcommand"].toString(); + const QString &identifier = message["id"].toString().trimmed(); + const QString &comment = message["comment"].toString().trimmed(); + const QString renameTokenResult = API::renameToken(identifier, comment); + if (renameTokenResult.isEmpty()) { + sendSuccessReply(cmd); + } else { + sendErrorReply("Token rename failed", {renameTokenResult}, cmd); + } +} + +void JsonAPI::handleDeleteToken(const QJsonObject &message, const JsonApiCommand& cmd) +{ + const QString &identifier = message["id"].toString().trimmed(); + const QString deleteTokenResult = API::deleteToken(identifier); + if (deleteTokenResult.isEmpty()) { + sendSuccessReply(cmd); + } else { + sendErrorReply("Token deletion failed", {deleteTokenResult}, cmd); + } +} + +void JsonAPI::handleRequestToken(const QJsonObject &message, const JsonApiCommand& cmd) +{ + const QString &identifier = message["id"].toString().trimmed(); + const QString &comment = message["comment"].toString().trimmed(); + const bool &acc = message["accept"].toBool(true); + if (acc) { + API::setNewTokenRequest(comment, identifier, cmd.tan); + } else { + API::cancelNewTokenRequest(comment, identifier); + // client should wait for answer + } +} + +void JsonAPI::handleGetPendingTokenRequests(const JsonApiCommand& cmd) +{ + QVector vec; + if (API::getPendingTokenRequests(vec)) { + QJsonArray pendingTokeRequests; + for (const auto &entry : std::as_const(vec)) + { + QJsonObject obj; + obj["comment"] = entry.comment; + obj["id"] = entry.id; + obj["timeout"] = int(entry.timeoutTime); + obj["tan"] = entry.tan; + pendingTokeRequests.append(obj); + } + sendSuccessDataReply(pendingTokeRequests, cmd); + } +} + +void JsonAPI::handleAnswerRequest(const QJsonObject &message, const JsonApiCommand& cmd) +{ + const QString &identifier = message["id"].toString().trimmed(); + const bool &accept = message["accept"].toBool(false); + if (API::handlePendingTokenRequest(identifier, accept)) { + sendSuccessReply(cmd); + } else { + sendErrorReply("Unable to handle token acceptance or denial", cmd); + } +} + +void JsonAPI::handleGetTokenList(const JsonApiCommand& cmd) +{ + QVector defVect; + if (API::getTokenList(defVect)) + { + QJsonArray tokenList; + for (const auto &entry : std::as_const(defVect)) + { + QJsonObject token; + token["comment"] = entry.comment; + token["id"] = entry.id; + token["last_use"] = entry.lastUse; + + tokenList.append(token); + } + sendSuccessDataReply(tokenList, cmd); + } +} + +void JsonAPI::handleLogin(const QJsonObject &message, const JsonApiCommand& cmd) +{ + const QString &token = message["token"].toString().trimmed(); + if (!token.isEmpty()) + { + // userToken is longer than app token + if (token.size() > APP_TOKEN_LENGTH) + { + if (API::isUserTokenAuthorized(token)) { + sendSuccessReply(cmd); + } else { + sendNoAuthorization(cmd); + } + + return; + } + + if (token.size() == APP_TOKEN_LENGTH) + { + if (API::isTokenAuthorized(token)) { + sendSuccessReply(cmd); + } else { + sendNoAuthorization(cmd); + } + } + return; + } + + // password + const QString &password = message["password"].toString().trimmed(); + if (password.size() >= MIN_PASSWORD_LENGTH) + { + QString userTokenRep; + if (API::isUserAuthorized(password) && API::getUserToken(userTokenRep)) + { + // Return the current valid Hyperion user token + QJsonObject response { { "token", userTokenRep } }; + sendSuccessDataReply(response, cmd); + } + else + { + sendNoAuthorization(cmd); + } + } + else + { + sendErrorReply(QString("Password is too short. Minimum length: %1 characters").arg(MIN_PASSWORD_LENGTH), cmd); + } +} + +void JsonAPI::issueNewPendingTokenRequest(const QString &identifier, const QString &comment) +{ + QJsonObject tokenRequest; + tokenRequest["comment"] = comment; + tokenRequest["id"] = identifier; + tokenRequest["timeout"] = static_cast(NEW_TOKEN_REQUEST_TIMEOUT.count()); + + sendNewRequest(tokenRequest, "authorize-tokenRequest"); +} + +void JsonAPI::handleTokenResponse(bool success, const QString &token, const QString &comment, const QString &identifier, const int &tan) +{ + const QString cmd = "authorize-requestToken"; + QJsonObject result; + result["token"] = token; + result["comment"] = comment; + result["id"] = identifier; + + if (success) { + sendSuccessDataReply(result, cmd, tan); + } else { + sendErrorReply("Token request timeout or denied", {}, cmd, tan); + } +} + +void JsonAPI::handleInstanceCommand(const QJsonObject &message, const JsonApiCommand& cmd) +{ + QString replyMsg; + const quint8 &inst = static_cast(message["instance"].toInt()); const QString &name = message["name"].toString(); - if (subc == "switchTo") - { + switch (cmd.subCommand) { + case SubCommand::SwitchTo: if (handleInstanceSwitch(inst)) { - QJsonObject obj; - obj["instance"] = inst; - sendSuccessDataReply(QJsonDocument(obj), command + "-" + subc, tan); + QJsonObject response { { "instance", inst } }; + sendSuccessDataReply(response, cmd); } else - sendErrorReply("Selected Hyperion instance isn't running", command + "-" + subc, tan); - return; - } - - if (subc == "startInstance") - { - //Only send update once - weakConnect(this, &API::onStartInstanceResponse, [this, command, subc] (int tan) { - sendSuccessReply(command + "-" + subc, tan); + sendErrorReply("Selected Hyperion instance is not running", cmd); + } + break; + + case SubCommand::StartInstance: + //Only send update once + weakConnect(this, &API::onStartInstanceResponse, [this, cmd] () + { + sendSuccessReply(cmd); }); - if (!API::startInstance(inst, tan)) - sendErrorReply("Can't start Hyperion instance index " + QString::number(inst), command + "-" + subc, tan); - - return; - } - - if (subc == "stopInstance") - { + if (!API::startInstance(inst, cmd.tan)) + { + sendErrorReply("Cannot start Hyperion instance index " + QString::number(inst), cmd); + } + break; + case SubCommand::StopInstance: // silent fail API::stopInstance(inst); - sendSuccessReply(command + "-" + subc, tan); - return; - } + sendSuccessReply(cmd); + break; - if (subc == "deleteInstance") - { - QString replyMsg; + case SubCommand::DeleteInstance: + handleConfigRestoreCommand(message, cmd); if (API::deleteInstance(inst, replyMsg)) - sendSuccessReply(command + "-" + subc, tan); + { + sendSuccessReply(cmd); + } else - sendErrorReply(replyMsg, command + "-" + subc, tan); - return; - } + { + sendErrorReply(replyMsg, cmd); + } + break; - // create and save name requires name - if (name.isEmpty()) - sendErrorReply("Name string required for this command", command + "-" + subc, tan); + case SubCommand::CreateInstance: + case SubCommand::SaveName: + // create and save name requires name + if (name.isEmpty()) { + sendErrorReply("Name string required for this command", cmd); + return; + } - if (subc == "createInstance") - { - QString replyMsg = API::createInstance(name); - if (replyMsg.isEmpty()) - sendSuccessReply(command + "-" + subc, tan); - else - sendErrorReply(replyMsg, command + "-" + subc, tan); - return; - } + if (cmd.subCommand == SubCommand::CreateInstance) { + replyMsg = API::createInstance(name); + } else { + replyMsg = setInstanceName(inst, name); + } - if (subc == "saveName") - { - QString replyMsg = API::setInstanceName(inst, name); - if (replyMsg.isEmpty()) - sendSuccessReply(command + "-" + subc, tan); - else - sendErrorReply(replyMsg, command + "-" + subc, tan); - return; + if (replyMsg.isEmpty()) { + sendSuccessReply(cmd); + } else { + sendErrorReply(replyMsg, cmd); + } + break; + default: + break; } } -void JsonAPI::handleLedDeviceCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleLedDeviceCommand(const QJsonObject &message, const JsonApiCommand& cmd) { - Debug(_log, "message: [%s]", QString(QJsonDocument(message).toJson(QJsonDocument::Compact)).toUtf8().constData() ); - - const QString &subc = message["subcommand"].toString().trimmed(); const QString &devType = message["ledDeviceType"].toString().trimmed(); + const LedDeviceRegistry& ledDevices = LedDeviceWrapper::getDeviceMap(); - QString full_command = command + "-" + subc; - - // TODO: Validate that device type is a valid one - - { - QJsonObject config; - config.insert("type", devType); - LedDevice* ledDevice = nullptr; - - if (subc == "discover") - { - ledDevice = LedDeviceFactory::construct(config); - const QJsonObject ¶ms = message["params"].toObject(); - const QJsonObject devicesDiscovered = ledDevice->discover(params); - - Debug(_log, "response: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData() ); - - sendSuccessDataReply(QJsonDocument(devicesDiscovered), full_command, tan); - } - else if (subc == "getProperties") - { - ledDevice = LedDeviceFactory::construct(config); - const QJsonObject ¶ms = message["params"].toObject(); - const QJsonObject deviceProperties = ledDevice->getProperties(params); - - Debug(_log, "response: [%s]", QString(QJsonDocument(deviceProperties).toJson(QJsonDocument::Compact)).toUtf8().constData() ); - - sendSuccessDataReply(QJsonDocument(deviceProperties), full_command, tan); - } - else if (subc == "identify") - { - ledDevice = LedDeviceFactory::construct(config); - const QJsonObject ¶ms = message["params"].toObject(); - ledDevice->identify(params); - - sendSuccessReply(full_command, tan); - } - else if (subc == "addAuthorization") - { - ledDevice = LedDeviceFactory::construct(config); - const QJsonObject& params = message["params"].toObject(); - const QJsonObject response = ledDevice->addAuthorization(params); - - Debug(_log, "response: [%s]", QString(QJsonDocument(response).toJson(QJsonDocument::Compact)).toUtf8().constData()); - - sendSuccessDataReply(QJsonDocument(response), full_command, tan); - } - else - { - sendErrorReply("Unknown or missing subcommand", full_command, tan); - } - - delete ledDevice; + if (ledDevices.count(devType) == 0) { + sendErrorReply(QString("Unknown LED-Device type: %1").arg(devType), cmd); + return; } + + QJsonObject config { { "type", devType } }; + LedDevice* ledDevice = LedDeviceFactory::construct(config); + + switch (cmd.subCommand) { + case SubCommand::Discover: + handleLedDeviceDiscover(*ledDevice, message, cmd); + break; + case SubCommand::GetProperties: + handleLedDeviceGetProperties(*ledDevice, message, cmd); + break; + case SubCommand::Identify: + handleLedDeviceIdentify(*ledDevice, message, cmd); + break; + case SubCommand::AddAuthorization: + handleLedDeviceAddAuthorization(*ledDevice, message, cmd); + break; + default: + break; + } + + delete ledDevice; } -void JsonAPI::handleInputSourceCommand(const QJsonObject& message, const QString& command, int tan) +void JsonAPI::handleLedDeviceDiscover(LedDevice& ledDevice, const QJsonObject& message, const JsonApiCommand& cmd) { - DebugIf(verbose, _log, "message: [%s]", QString(QJsonDocument(message).toJson(QJsonDocument::Compact)).toUtf8().constData()); + const QJsonObject ¶ms = message["params"].toObject(); + const QJsonObject devicesDiscovered = ledDevice.discover(params); + Debug(_log, "response: [%s]", QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact).constData() ); + sendSuccessDataReply(devicesDiscovered, cmd); +} - const QString& subc = message["subcommand"].toString().trimmed(); +void JsonAPI::handleLedDeviceGetProperties(LedDevice& ledDevice, const QJsonObject& message, const JsonApiCommand& cmd) +{ + const QJsonObject ¶ms = message["params"].toObject(); + const QJsonObject deviceProperties = ledDevice.getProperties(params); + Debug(_log, "response: [%s]", QJsonDocument(deviceProperties).toJson(QJsonDocument::Compact).constData() ); + sendSuccessDataReply(deviceProperties, cmd); +} + +void JsonAPI::handleLedDeviceIdentify(LedDevice& ledDevice, const QJsonObject& message, const JsonApiCommand& cmd) +{ + const QJsonObject ¶ms = message["params"].toObject(); + ledDevice.identify(params); + sendSuccessReply(cmd); +} + +void JsonAPI::handleLedDeviceAddAuthorization(LedDevice& ledDevice, const QJsonObject& message, const JsonApiCommand& cmd) +{ + const QJsonObject& params = message["params"].toObject(); + const QJsonObject response = ledDevice.addAuthorization(params); + sendSuccessDataReply(response, cmd); +} + +void JsonAPI::handleInputSourceCommand(const QJsonObject& message, const JsonApiCommand& cmd) { const QString& sourceType = message["sourceType"].toString().trimmed(); + const QStringList sourceTypes {"screen", "video", "audio"}; - QString full_command = command + "-" + subc; + if (!sourceTypes.contains(sourceType)) { + sendErrorReply(QString("Unknown input source type: %1").arg(sourceType), cmd); + return; + } - // TODO: Validate that source type is a valid one - { - if (subc == "discover") - { - QJsonObject inputSourcesDiscovered; - inputSourcesDiscovered.insert("sourceType", sourceType); - QJsonArray videoInputs; - QJsonArray audioInputs; + if (cmd.subCommand == SubCommand::Discover) { -#if defined(ENABLE_V4L2) || defined(ENABLE_MF) + const QJsonObject& params = message["params"].toObject(); + QJsonObject inputSourcesDiscovered = JsonInfo().discoverSources(sourceType, params); - if (sourceType == "video" ) - { -#if defined(ENABLE_MF) - MFGrabber* grabber = new MFGrabber(); -#elif defined(ENABLE_V4L2) - V4L2Grabber* grabber = new V4L2Grabber(); -#endif - QJsonObject params; - videoInputs = grabber->discover(params); - delete grabber; - } - else -#endif + DebugIf(verbose, _log, "response: [%s]", QJsonDocument(inputSourcesDiscovered).toJson(QJsonDocument::Compact).constData()); -#if defined(ENABLE_AUDIO) - if (sourceType == "audio") - { - AudioGrabber* grabber; -#ifdef WIN32 - grabber = new AudioGrabberWindows(); -#endif - -#ifdef __linux__ - grabber = new AudioGrabberLinux(); -#endif - QJsonObject params; - audioInputs = grabber->discover(params); - delete grabber; - } - else -#endif - { - DebugIf(verbose, _log, "sourceType: [%s]", QSTRING_CSTR(sourceType)); - - if (sourceType == "screen") - { - QJsonObject params; - - QJsonObject device; - #ifdef ENABLE_QT - QScopedPointer qtgrabber(new QtGrabber()); - device = qtgrabber->discover(params); - if (!device.isEmpty() ) - { - videoInputs.append(device); - } - #endif - - #ifdef ENABLE_DX - QScopedPointer dxgrabber (new DirectXGrabber()); - device = dxgrabber->discover(params); - if (!device.isEmpty() ) - { - videoInputs.append(device); - } - #endif - - #ifdef ENABLE_X11 - QScopedPointer x11Grabber(new X11Grabber()); - device = x11Grabber->discover(params); - if (!device.isEmpty() ) - { - videoInputs.append(device); - } - #endif - - #ifdef ENABLE_XCB - QScopedPointer xcbGrabber (new XcbGrabber()); - device = xcbGrabber->discover(params); - if (!device.isEmpty() ) - { - videoInputs.append(device); - } - #endif - - //Ignore FB for Amlogic, as it is embedded in the Amlogic grabber itself - #if defined(ENABLE_FB) && !defined(ENABLE_AMLOGIC) - - QScopedPointer fbGrabber(new FramebufferFrameGrabber()); - device = fbGrabber->discover(params); - if (!device.isEmpty() ) - { - videoInputs.append(device); - } - #endif - - #if defined(ENABLE_DISPMANX) - QScopedPointer dispmanx(new DispmanxFrameGrabber()); - if (dispmanx->isAvailable()) - { - device = dispmanx->discover(params); - if (!device.isEmpty() ) - { - videoInputs.append(device); - } - } - #endif - - #if defined(ENABLE_AMLOGIC) - QScopedPointer amlGrabber(new AmlogicGrabber()); - device = amlGrabber->discover(params); - if (!device.isEmpty() ) - { - videoInputs.append(device); - } - #endif - - #if defined(ENABLE_OSX) - QScopedPointer osxGrabber(new OsxFrameGrabber()); - device = osxGrabber->discover(params); - if (!device.isEmpty() ) - { - videoInputs.append(device); - } - #endif - } - - } - inputSourcesDiscovered["video_sources"] = videoInputs; - inputSourcesDiscovered["audio_sources"] = audioInputs; - - DebugIf(verbose, _log, "response: [%s]", QString(QJsonDocument(inputSourcesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); - - sendSuccessDataReply(QJsonDocument(inputSourcesDiscovered), full_command, tan); - } - else - { - sendErrorReply("Unknown or missing subcommand", full_command, tan); - } + sendSuccessDataReply(inputSourcesDiscovered, cmd); } } -void JsonAPI::handleServiceCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleServiceCommand(const QJsonObject &message, const JsonApiCommand& cmd) { - DebugIf(verbose, _log, "message: [%s]", QString(QJsonDocument(message).toJson(QJsonDocument::Compact)).toUtf8().constData()); - - const QString &subc = message["subcommand"].toString().trimmed(); - const QString type = message["serviceType"].toString().trimmed(); - - QString full_command = command + "-" + subc; - - if (subc == "discover") + if (cmd.subCommand == SubCommand::Discover) { QByteArray serviceType; - - QJsonObject servicesDiscovered; - QJsonObject servicesOfType; - QJsonArray serviceList; - + const QString type = message["serviceType"].toString().trimmed(); #ifdef ENABLE_MDNS QString discoveryMethod("mDNS"); serviceType = MdnsServiceRegister::getServiceType(type); @@ -1817,212 +1324,168 @@ void JsonAPI::handleServiceCommand(const QJsonObject &message, const QString &co #endif if (!serviceType.isEmpty()) { + QJsonArray serviceList; #ifdef ENABLE_MDNS QMetaObject::invokeMethod(MdnsBrowser::getInstance().data(), "browseForServiceType", - Qt::QueuedConnection, Q_ARG(QByteArray, serviceType)); + Qt::QueuedConnection, Q_ARG(QByteArray, serviceType)); serviceList = MdnsBrowser::getInstance().data()->getServicesDiscoveredJson(serviceType, MdnsServiceRegister::getServiceNameFilter(type), DEFAULT_DISCOVER_TIMEOUT); #endif + QJsonObject servicesDiscovered; + QJsonObject servicesOfType; + servicesOfType.insert(type, serviceList); servicesDiscovered.insert("discoveryMethod", discoveryMethod); servicesDiscovered.insert("services", servicesOfType); - sendSuccessDataReply(QJsonDocument(servicesDiscovered), full_command, tan); + sendSuccessDataReply(servicesDiscovered, cmd); } else { - sendErrorReply(QString("Discovery of service type [%1] via %2 not supported").arg(type, discoveryMethod), full_command, tan); + sendErrorReply(QString("Discovery of service type [%1] via %2 not supported").arg(type, discoveryMethod), cmd); } } - else - { - sendErrorReply("Unknown or missing subcommand", full_command, tan); - } } -void JsonAPI::handleSystemCommand(const QJsonObject &message, const QString &command, int tan) +void JsonAPI::handleSystemCommand(const QJsonObject& /*message*/, const JsonApiCommand& cmd) { - DebugIf(verbose, _log, "message: [%s]", QString(QJsonDocument(message).toJson(QJsonDocument::Compact)).toUtf8().constData()); - - const QString &subc = message["subcommand"].toString().trimmed(); - - if (subc == "suspend") - { + switch (cmd.subCommand) { + case SubCommand::Suspend: emit signalEvent(Event::Suspend); - sendSuccessReply(command + "-" + subc, tan); - } - else if (subc == "resume") - { + break; + case SubCommand::Resume: emit signalEvent(Event::Resume); - sendSuccessReply(command + "-" + subc, tan); - } - else if (subc == "restart") - { + break; + case SubCommand::Restart: emit signalEvent(Event::Restart); - sendSuccessReply(command + "-" + subc, tan); - } - else if (subc == "toggleSuspend") - { + break; + case SubCommand::ToggleSuspend: emit signalEvent(Event::ToggleSuspend); - sendSuccessReply(command + "-" + subc, tan); - } - else if (subc == "idle") - { + break; + case SubCommand::Idle: emit signalEvent(Event::Idle); - sendSuccessReply(command + "-" + subc, tan); - } - else if (subc == "resumeIdle") - { - emit signalEvent(Event::ResumeIdle); - sendSuccessReply(command + "-" + subc, tan); - } - else if (subc == "toggleIdle") - { + break; + case SubCommand::ToggleIdle: emit signalEvent(Event::ToggleIdle); - sendSuccessReply(command + "-" + subc, tan); + break; + default: + return; } - else + sendSuccessReply(cmd); +} + +QJsonObject JsonAPI::getBasicCommandReply(bool success, const QString &command, int tan, InstanceCmd::Type isInstanceCmd) const +{ + QJsonObject reply; + reply["success"] = success; + reply["command"] = command; + reply["tan"] = tan; + + if (isInstanceCmd == InstanceCmd::Yes || ( isInstanceCmd == InstanceCmd::Multi && !_noListener)) { - QString full_command = command + "-" + subc; - sendErrorReply("Unknown or missing subcommand", full_command, tan); + reply["instance"] = _hyperion->getInstanceIndex(); } + return reply; } -void JsonAPI::handleNotImplemented(const QString &command, int tan) +void JsonAPI::sendSuccessReply(const JsonApiCommand& cmd) { - sendErrorReply("Command not implemented", command, tan); + sendSuccessReply(cmd.toString(), cmd.tan, cmd.isInstanceCmd); } -void JsonAPI::sendSuccessReply(const QString &command, int tan) +void JsonAPI::sendSuccessReply(const QString &command, int tan, InstanceCmd::Type isInstanceCmd) { - // create reply - QJsonObject reply; - reply["instance"] = _hyperion->getInstanceIndex(); - reply["success"] = true; - reply["command"] = command; - reply["tan"] = tan; - - // send reply - emit callbackMessage(reply); + emit callbackMessage(getBasicCommandReply(true, command, tan , isInstanceCmd)); } -void JsonAPI::sendSuccessDataReply(const QJsonDocument &doc, const QString &command, int tan) +void JsonAPI::sendSuccessDataReply(const QJsonValue &infoData, const JsonApiCommand& cmd) { - QJsonObject reply; - reply["instance"] = _hyperion->getInstanceIndex(); - reply["success"] = true; - reply["command"] = command; - reply["tan"] = tan; - if (doc.isArray()) - reply["info"] = doc.array(); - else - reply["info"] = doc.object(); + sendSuccessDataReplyWithError(infoData, cmd.toString(), cmd.tan, {}, cmd.isInstanceCmd); +} + +void JsonAPI::sendSuccessDataReply(const QJsonValue &infoData, const QString &command, int tan, InstanceCmd::Type isInstanceCmd) +{ + sendSuccessDataReplyWithError(infoData, command, tan, {}, isInstanceCmd); +} + +void JsonAPI::sendSuccessDataReplyWithError(const QJsonValue &infoData, const JsonApiCommand& cmd, const QStringList& errorDetails) +{ + sendSuccessDataReplyWithError(infoData, cmd.toString(), cmd.tan, errorDetails, cmd.isInstanceCmd); +} + +void JsonAPI::sendSuccessDataReplyWithError(const QJsonValue &infoData, const QString &command, int tan, const QStringList& errorDetails, InstanceCmd::Type isInstanceCmd) +{ + QJsonObject reply {getBasicCommandReply(true, command, tan , isInstanceCmd)}; + reply["info"] = infoData; + + if (!errorDetails.isEmpty()) + { + QJsonArray errorsArray; + for (const QString& errorString : errorDetails) + { + QJsonObject errorObject; + errorObject["description"] = errorString; + errorsArray.append(errorObject); + } + reply["errorData"] = errorsArray; + } emit callbackMessage(reply); } -void JsonAPI::sendErrorReply(const QString &error, const QString &command, int tan) +void JsonAPI::sendErrorReply(const QString &error, const JsonApiCommand& cmd) { - // create reply - QJsonObject reply; - reply["instance"] = _hyperion->getInstanceIndex(); - reply["success"] = false; + sendErrorReply(error, {}, cmd.toString(), cmd.tan, cmd.isInstanceCmd); +} + +void JsonAPI::sendErrorReply(const QString &error, const QStringList& errorDetails, const JsonApiCommand& cmd) +{ + sendErrorReply(error, errorDetails, cmd.toString(), cmd.tan, cmd.isInstanceCmd); +} + +void JsonAPI::sendErrorReply(const QString &error, const QStringList& errorDetails, const QString &command, int tan, InstanceCmd::Type isInstanceCmd) +{ + QJsonObject reply {getBasicCommandReply(false, command, tan , isInstanceCmd)}; reply["error"] = error; - reply["command"] = command; - reply["tan"] = tan; + if (!errorDetails.isEmpty()) + { + QJsonArray errorsArray; + for (const QString& errorString : errorDetails) + { + QJsonObject errorObject; + errorObject["description"] = errorString; + errorsArray.append(errorObject); + } + reply["errorData"] = errorsArray; + } - // send reply emit callbackMessage(reply); } -void JsonAPI::streamLedcolorsUpdate(const std::vector &ledColors) +void JsonAPI::sendNewRequest(const QJsonValue &infoData, const JsonApiCommand& cmd) { - QJsonObject result; - QJsonArray leds; + sendSuccessDataReplyWithError(infoData, cmd.toString(), cmd.isInstanceCmd); +} - for (const auto &color : ledColors) +void JsonAPI::sendNewRequest(const QJsonValue &infoData, const QString &command, InstanceCmd::Type isInstanceCmd) +{ + QJsonObject request; + request["command"] = command; + + if (isInstanceCmd != InstanceCmd::No) { - leds << QJsonValue(color.red) << QJsonValue(color.green) << QJsonValue(color.blue); + request["instance"] = _hyperion->getInstanceIndex(); } - result["leds"] = leds; - _streaming_leds_reply["result"] = result; + request["info"] = infoData; - // send the result - emit callbackMessage(_streaming_leds_reply); + emit callbackMessage(request); } -void JsonAPI::setImage(const Image &image) +void JsonAPI::sendNoAuthorization(const JsonApiCommand& cmd) { - QImage jpgImage((const uint8_t *)image.memptr(), image.width(), image.height(), 3 * image.width(), QImage::Format_RGB888); - QByteArray ba; - QBuffer buffer(&ba); - buffer.open(QIODevice::WriteOnly); - jpgImage.save(&buffer, "jpg"); - - QJsonObject result; - result["image"] = "data:image/jpg;base64," + QString(ba.toBase64()); - _streaming_image_reply["result"] = result; - emit callbackMessage(_streaming_image_reply); -} - -void JsonAPI::incommingLogMessage(const Logger::T_LOG_MESSAGE &msg) -{ - QJsonObject result, message; - QJsonArray messageArray; - - if (!_streaming_logging_activated) - { - _streaming_logging_activated = true; - QMetaObject::invokeMethod(LoggerManager::getInstance().data(), "getLogMessageBuffer", - Qt::DirectConnection, - Q_RETURN_ARG(QJsonArray, messageArray), - Q_ARG(Logger::LogLevel, _log->getLogLevel())); - } - else - { - message["loggerName"] = msg.loggerName; - message["loggerSubName"] = msg.loggerSubName; - message["function"] = msg.function; - message["line"] = QString::number(msg.line); - message["fileName"] = msg.fileName; - message["message"] = msg.message; - message["levelString"] = msg.levelString; - message["utime"] = QString::number(msg.utime); - - messageArray.append(message); - } - - result.insert("messages", messageArray); - _streaming_logging_reply["result"] = result; - - // send the result - emit callbackMessage(_streaming_logging_reply); -} - -void JsonAPI::newPendingTokenRequest(const QString &id, const QString &comment) -{ - QJsonObject obj; - obj["comment"] = comment; - obj["id"] = id; - obj["timeout"] = 180000; - - sendSuccessDataReply(QJsonDocument(obj), "authorize-tokenRequest", 1); -} - -void JsonAPI::handleTokenResponse(bool success, const QString &token, const QString &comment, const QString &id, const int &tan) -{ - const QString cmd = "authorize-requestToken"; - QJsonObject result; - result["token"] = token; - result["comment"] = comment; - result["id"] = id; - - if (success) - sendSuccessDataReply(QJsonDocument(result), cmd, tan); - else - sendErrorReply("Token request timeout or denied", cmd, tan); + sendErrorReply(NO_AUTHORIZATION, cmd); } void JsonAPI::handleInstanceStateChange(InstanceState state, quint8 instance, const QString& /*name */) @@ -2034,24 +1497,45 @@ void JsonAPI::handleInstanceStateChange(InstanceState state, quint8 instance, co { handleInstanceSwitch(); } - break; + break; case InstanceState::H_STARTED: case InstanceState::H_STOPPED: case InstanceState::H_CREATED: case InstanceState::H_DELETED: - default: - break; + break; } } void JsonAPI::stopDataConnections() { - LoggerManager::getInstance()->disconnect(); - _streaming_logging_activated = false; _jsonCB->resetSubscriptions(); - // led stream colors - disconnect(_hyperion, &Hyperion::rawLedColors, this, 0); - _ledStreamTimer->stop(); - disconnect(_ledStreamConnection); + LoggerManager::getInstance()->disconnect(); +} + +QString JsonAPI::findCommand (const QString& jsonString) +{ + QString commandValue {"unknown"}; + + // Define a regular expression pattern to match the value associated with the key "command" + static QRegularExpression regex("\"command\"\\s*:\\s*\"([^\"]+)\""); + QRegularExpressionMatch match = regex.match(jsonString); + + if (match.hasMatch()) { + commandValue = match.captured(1); + } + return commandValue; +} + +int JsonAPI::findTan (const QString& jsonString) +{ + int tanValue {0}; + static QRegularExpression regex("\"tan\"\\s*:\\s*(\\d+)"); + QRegularExpressionMatch match = regex.match(jsonString); + + if (match.hasMatch()) { + QString valueStr = match.captured(1); + tanValue = valueStr.toInt(); + } + return tanValue; } diff --git a/libsrc/api/JsonCB.cpp b/libsrc/api/JsonCB.cpp deleted file mode 100644 index e3c4b32a..00000000 --- a/libsrc/api/JsonCB.cpp +++ /dev/null @@ -1,416 +0,0 @@ -// proj incl -#include - -// hyperion -#include - -// HyperionIManager -#include -// components - -#include -// priorityMuxer - -#include - -// utils -#include - -// qt -#include -#include - -// Image to led map helper -#include - -using namespace hyperion; - -JsonCB::JsonCB(QObject* parent) - : QObject(parent) - , _hyperion(nullptr) - , _componentRegister(nullptr) - , _prioMuxer(nullptr) -{ - _availableCommands << "components-update" << "priorities-update" << "imageToLedMapping-update" - << "adjustment-update" << "videomode-update" << "settings-update" << "leds-update" << "instance-update" << "token-update"; - - #if defined(ENABLE_EFFECTENGINE) - _availableCommands << "effects-update"; - #endif - - qRegisterMetaType("InputsMap"); -} - -bool JsonCB::subscribeFor(const QString& type, bool unsubscribe) -{ - if(!_availableCommands.contains(type)) - return false; - - if(unsubscribe) - _subscribedCommands.removeAll(type); - else - _subscribedCommands << type; - - if(type == "components-update") - { - if(unsubscribe) - disconnect(_componentRegister, &ComponentRegister::updatedComponentState, this, &JsonCB::handleComponentState); - else - connect(_componentRegister, &ComponentRegister::updatedComponentState, this, &JsonCB::handleComponentState, Qt::UniqueConnection); - } - - if(type == "priorities-update") - { - if (unsubscribe) - disconnect(_prioMuxer, &PriorityMuxer::prioritiesChanged, this, &JsonCB::handlePriorityUpdate); - else - connect(_prioMuxer, &PriorityMuxer::prioritiesChanged, this, &JsonCB::handlePriorityUpdate, Qt::UniqueConnection); - } - - if(type == "imageToLedMapping-update") - { - if(unsubscribe) - disconnect(_hyperion, &Hyperion::imageToLedsMappingChanged, this, &JsonCB::handleImageToLedsMappingChange); - else - connect(_hyperion, &Hyperion::imageToLedsMappingChanged, this, &JsonCB::handleImageToLedsMappingChange, Qt::UniqueConnection); - } - - if(type == "adjustment-update") - { - if(unsubscribe) - disconnect(_hyperion, &Hyperion::adjustmentChanged, this, &JsonCB::handleAdjustmentChange); - else - connect(_hyperion, &Hyperion::adjustmentChanged, this, &JsonCB::handleAdjustmentChange, Qt::UniqueConnection); - } - - if(type == "videomode-update") - { - if(unsubscribe) - disconnect(_hyperion, &Hyperion::newVideoMode, this, &JsonCB::handleVideoModeChange); - else - connect(_hyperion, &Hyperion::newVideoMode, this, &JsonCB::handleVideoModeChange, Qt::UniqueConnection); - } - -#if defined(ENABLE_EFFECTENGINE) - if(type == "effects-update") - { - if(unsubscribe) - disconnect(_hyperion, &Hyperion::effectListUpdated, this, &JsonCB::handleEffectListChange); - else - connect(_hyperion, &Hyperion::effectListUpdated, this, &JsonCB::handleEffectListChange, Qt::UniqueConnection); - } -#endif - - if(type == "settings-update") - { - if(unsubscribe) - disconnect(_hyperion, &Hyperion::settingsChanged, this, &JsonCB::handleSettingsChange); - else - connect(_hyperion, &Hyperion::settingsChanged, this, &JsonCB::handleSettingsChange, Qt::UniqueConnection); - } - - if(type == "leds-update") - { - if(unsubscribe) - disconnect(_hyperion, &Hyperion::settingsChanged, this, &JsonCB::handleLedsConfigChange); - else - connect(_hyperion, &Hyperion::settingsChanged, this, &JsonCB::handleLedsConfigChange, Qt::UniqueConnection); - } - - - if(type == "instance-update") - { - if(unsubscribe) - disconnect(HyperionIManager::getInstance(), &HyperionIManager::change, this, &JsonCB::handleInstanceChange); - else - connect(HyperionIManager::getInstance(), &HyperionIManager::change, this, &JsonCB::handleInstanceChange, Qt::UniqueConnection); - } - - if (type == "token-update") - { - if (unsubscribe) - disconnect(AuthManager::getInstance(), &AuthManager::tokenChange, this, &JsonCB::handleTokenChange); - else - connect(AuthManager::getInstance(), &AuthManager::tokenChange, this, &JsonCB::handleTokenChange, Qt::UniqueConnection); - } - - return true; -} - -void JsonCB::resetSubscriptions() -{ - for(const auto & entry : getSubscribedCommands()) - { - subscribeFor(entry, true); - } -} - -void JsonCB::setSubscriptionsTo(Hyperion* hyperion) -{ - assert(hyperion); - - // get current subs - QStringList currSubs(getSubscribedCommands()); - - // stop subs - resetSubscriptions(); - - // update pointer - _hyperion = hyperion; - _componentRegister = _hyperion->getComponentRegister(); - _prioMuxer = _hyperion->getMuxerInstance(); - - // re-apply subs - for(const auto & entry : currSubs) - { - subscribeFor(entry); - } -} - -void JsonCB::doCallback(const QString& cmd, const QVariant& data) -{ - QJsonObject obj; - obj["instance"] = _hyperion->getInstanceIndex(); - obj["command"] = cmd; - - if (data.userType() == QMetaType::QJsonArray) - obj["data"] = data.toJsonArray(); - else - obj["data"] = data.toJsonObject(); - - emit newCallback(obj); -} - -void JsonCB::handleComponentState(hyperion::Components comp, bool state) -{ - QJsonObject data; - data["name"] = componentToIdString(comp); - data["enabled"] = state; - - doCallback("components-update", QVariant(data)); -} - -void JsonCB::handlePriorityUpdate(int currentPriority, const PriorityMuxer::InputsMap& activeInputs) -{ - QJsonObject data; - QJsonArray priorities; - uint64_t now = QDateTime::currentMSecsSinceEpoch(); - QList activePriorities = activeInputs.keys(); - - activePriorities.removeAll(PriorityMuxer::LOWEST_PRIORITY); - - for (int priority : std::as_const(activePriorities)) { - - const Hyperion::InputInfo& priorityInfo = activeInputs[priority]; - - QJsonObject item; - item["priority"] = priority; - - if (priorityInfo.timeoutTime_ms > 0 ) - { - item["duration_ms"] = int(priorityInfo.timeoutTime_ms - now); - } - - // owner has optional informations to the component - if(!priorityInfo.owner.isEmpty()) - { - item["owner"] = priorityInfo.owner; - } - - item["componentId"] = QString(hyperion::componentToIdString(priorityInfo.componentId)); - item["origin"] = priorityInfo.origin; - item["active"] = (priorityInfo.timeoutTime_ms >= -1); - item["visible"] = (priority == currentPriority); - - if(priorityInfo.componentId == hyperion::COMP_COLOR && !priorityInfo.ledColors.empty()) - { - QJsonObject LEDcolor; - - // add RGB Value to Array - QJsonArray RGBValue; - RGBValue.append(priorityInfo.ledColors.begin()->red); - RGBValue.append(priorityInfo.ledColors.begin()->green); - RGBValue.append(priorityInfo.ledColors.begin()->blue); - LEDcolor.insert("RGB", RGBValue); - - uint16_t Hue; - float Saturation; - float Luminace; - - // add HSL Value to Array - QJsonArray HSLValue; - ColorSys::rgb2hsl(priorityInfo.ledColors.begin()->red, - priorityInfo.ledColors.begin()->green, - priorityInfo.ledColors.begin()->blue, - Hue, Saturation, Luminace); - - HSLValue.append(Hue); - HSLValue.append(Saturation); - HSLValue.append(Luminace); - LEDcolor.insert("HSL", HSLValue); - - item["value"] = LEDcolor; - } - priorities.append(item); - } - - data["priorities"] = priorities; - data["priorities_autoselect"] = _hyperion->sourceAutoSelectEnabled(); - - doCallback("priorities-update", QVariant(data)); -} - -void JsonCB::handleImageToLedsMappingChange(int mappingType) -{ - QJsonObject data; - data["imageToLedMappingType"] = ImageProcessor::mappingTypeToStr(mappingType); - - doCallback("imageToLedMapping-update", QVariant(data)); -} - -void JsonCB::handleAdjustmentChange() -{ - QJsonArray adjustmentArray; - for (const QString& adjustmentId : _hyperion->getAdjustmentIds()) - { - const ColorAdjustment * colorAdjustment = _hyperion->getAdjustment(adjustmentId); - if (colorAdjustment == nullptr) - { - continue; - } - - QJsonObject adjustment; - adjustment["id"] = adjustmentId; - - QJsonArray whiteAdjust; - whiteAdjust.append(colorAdjustment->_rgbWhiteAdjustment.getAdjustmentR()); - whiteAdjust.append(colorAdjustment->_rgbWhiteAdjustment.getAdjustmentG()); - whiteAdjust.append(colorAdjustment->_rgbWhiteAdjustment.getAdjustmentB()); - adjustment.insert("white", whiteAdjust); - - QJsonArray redAdjust; - redAdjust.append(colorAdjustment->_rgbRedAdjustment.getAdjustmentR()); - redAdjust.append(colorAdjustment->_rgbRedAdjustment.getAdjustmentG()); - redAdjust.append(colorAdjustment->_rgbRedAdjustment.getAdjustmentB()); - adjustment.insert("red", redAdjust); - - QJsonArray greenAdjust; - greenAdjust.append(colorAdjustment->_rgbGreenAdjustment.getAdjustmentR()); - greenAdjust.append(colorAdjustment->_rgbGreenAdjustment.getAdjustmentG()); - greenAdjust.append(colorAdjustment->_rgbGreenAdjustment.getAdjustmentB()); - adjustment.insert("green", greenAdjust); - - QJsonArray blueAdjust; - blueAdjust.append(colorAdjustment->_rgbBlueAdjustment.getAdjustmentR()); - blueAdjust.append(colorAdjustment->_rgbBlueAdjustment.getAdjustmentG()); - blueAdjust.append(colorAdjustment->_rgbBlueAdjustment.getAdjustmentB()); - adjustment.insert("blue", blueAdjust); - - QJsonArray cyanAdjust; - cyanAdjust.append(colorAdjustment->_rgbCyanAdjustment.getAdjustmentR()); - cyanAdjust.append(colorAdjustment->_rgbCyanAdjustment.getAdjustmentG()); - cyanAdjust.append(colorAdjustment->_rgbCyanAdjustment.getAdjustmentB()); - adjustment.insert("cyan", cyanAdjust); - - QJsonArray magentaAdjust; - magentaAdjust.append(colorAdjustment->_rgbMagentaAdjustment.getAdjustmentR()); - magentaAdjust.append(colorAdjustment->_rgbMagentaAdjustment.getAdjustmentG()); - magentaAdjust.append(colorAdjustment->_rgbMagentaAdjustment.getAdjustmentB()); - adjustment.insert("magenta", magentaAdjust); - - QJsonArray yellowAdjust; - yellowAdjust.append(colorAdjustment->_rgbYellowAdjustment.getAdjustmentR()); - yellowAdjust.append(colorAdjustment->_rgbYellowAdjustment.getAdjustmentG()); - yellowAdjust.append(colorAdjustment->_rgbYellowAdjustment.getAdjustmentB()); - adjustment.insert("yellow", yellowAdjust); - - adjustment["backlightThreshold"] = colorAdjustment->_rgbTransform.getBacklightThreshold(); - adjustment["backlightColored"] = colorAdjustment->_rgbTransform.getBacklightColored(); - adjustment["brightness"] = colorAdjustment->_rgbTransform.getBrightness(); - adjustment["brightnessCompensation"] = colorAdjustment->_rgbTransform.getBrightnessCompensation(); - adjustment["gammaRed"] = colorAdjustment->_rgbTransform.getGammaR(); - adjustment["gammaGreen"] = colorAdjustment->_rgbTransform.getGammaG(); - adjustment["gammaBlue"] = colorAdjustment->_rgbTransform.getGammaB(); - - adjustmentArray.append(adjustment); - } - - doCallback("adjustment-update", QVariant(adjustmentArray)); -} - -void JsonCB::handleVideoModeChange(VideoMode mode) -{ - QJsonObject data; - data["videomode"] = QString(videoMode2String(mode)); - doCallback("videomode-update", QVariant(data)); -} - -#if defined(ENABLE_EFFECTENGINE) -void JsonCB::handleEffectListChange() -{ - QJsonArray effectList; - QJsonObject effects; - const std::list & effectsDefinitions = _hyperion->getEffects(); - for (const EffectDefinition & effectDefinition : effectsDefinitions) - { - QJsonObject effect; - effect["name"] = effectDefinition.name; - effect["file"] = effectDefinition.file; - effect["script"] = effectDefinition.script; - effect["args"] = effectDefinition.args; - effectList.append(effect); - }; - effects["effects"] = effectList; - doCallback("effects-update", QVariant(effects)); -} -#endif - -void JsonCB::handleSettingsChange(settings::type type, const QJsonDocument& data) -{ - QJsonObject dat; - if(data.isObject()) - dat[typeToString(type)] = data.object(); - else - dat[typeToString(type)] = data.array(); - - doCallback("settings-update", QVariant(dat)); -} - -void JsonCB::handleLedsConfigChange(settings::type type, const QJsonDocument& data) -{ - if(type == settings::LEDS) - { - QJsonObject dat; - dat[typeToString(type)] = data.array(); - doCallback("leds-update", QVariant(dat)); - } -} - -void JsonCB::handleInstanceChange() -{ - QJsonArray arr; - - for(const auto & entry : HyperionIManager::getInstance()->getInstanceData()) - { - QJsonObject obj; - obj.insert("friendly_name", entry["friendly_name"].toString()); - obj.insert("instance", entry["instance"].toInt()); - obj.insert("running", entry["running"].toBool()); - arr.append(obj); - } - doCallback("instance-update", QVariant(arr)); -} - -void JsonCB::handleTokenChange(const QVector &def) -{ - QJsonArray arr; - for (const auto &entry : def) - { - QJsonObject sub; - sub["comment"] = entry.comment; - sub["id"] = entry.id; - sub["last_use"] = entry.lastUse; - arr.push_back(sub); - } - doCallback("token-update", QVariant(arr)); -} diff --git a/libsrc/api/JsonCallbacks.cpp b/libsrc/api/JsonCallbacks.cpp new file mode 100644 index 00000000..0e156727 --- /dev/null +++ b/libsrc/api/JsonCallbacks.cpp @@ -0,0 +1,459 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace hyperion; + +JsonCallbacks::JsonCallbacks(Logger *log, const QString& peerAddress, QObject* parent) + : QObject(parent) + , _log (log) + , _hyperion(nullptr) + , _peerAddress (peerAddress) + , _componentRegister(nullptr) + , _prioMuxer(nullptr) + , _islogMsgStreamingActive(false) +{ + qRegisterMetaType("InputsMap"); +} + +bool JsonCallbacks::subscribe(const Subscription::Type cmd) +{ + switch (cmd) { + case Subscription::AdjustmentUpdate: + connect(_hyperion, &Hyperion::adjustmentChanged, this, &JsonCallbacks::handleAdjustmentChange); + break; + case Subscription::ComponentsUpdate: + connect(_componentRegister, &ComponentRegister::updatedComponentState, this, &JsonCallbacks::handleComponentState); + break; +#if defined(ENABLE_EFFECTENGINE) + case Subscription::EffectsUpdate: + connect(_hyperion, &Hyperion::effectListUpdated, this, &JsonCallbacks::handleEffectListChange); + break; +#endif + case Subscription::EventUpdate: + connect(EventHandler::getInstance().data(), &EventHandler::signalEvent, this, &JsonCallbacks::handleEventUpdate); + break; + case Subscription::ImageToLedMappingUpdate: + connect(_hyperion, &Hyperion::imageToLedsMappingChanged, this, &JsonCallbacks::handleImageToLedsMappingChange); + break; + case Subscription::ImageUpdate: + connect(_hyperion, &Hyperion::currentImage, this, &JsonCallbacks::handleImageUpdate); + break; + case Subscription::InstanceUpdate: + connect(HyperionIManager::getInstance(), &HyperionIManager::change, this, &JsonCallbacks::handleInstanceChange); + break; + case Subscription::LedColorsUpdate: + connect(_hyperion, &Hyperion::rawLedColors, this, &JsonCallbacks::handleLedColorUpdate); + break; + case Subscription::LedsUpdate: + connect(_hyperion, &Hyperion::settingsChanged, this, &JsonCallbacks::handleLedsConfigChange); + break; + case Subscription::LogMsgUpdate: + if (!_islogMsgStreamingActive) + { + handleLogMessageUpdate (Logger::T_LOG_MESSAGE{}); // needed to trigger log sending + _islogMsgStreamingActive = true; + Debug(_log, "log streaming activated for client %s", _peerAddress.toStdString().c_str()); + } + connect(LoggerManager::getInstance().data(), &LoggerManager::newLogMessage, this, &JsonCallbacks::handleLogMessageUpdate); + break; + case Subscription::PrioritiesUpdate: + connect(_prioMuxer, &PriorityMuxer::prioritiesChanged, this, &JsonCallbacks::handlePriorityUpdate); + break; + case Subscription::SettingsUpdate: + connect(_hyperion, &Hyperion::settingsChanged, this, &JsonCallbacks::handleSettingsChange); + break; + case Subscription::TokenUpdate: + connect(AuthManager::getInstance(), &AuthManager::tokenChange, this, &JsonCallbacks::handleTokenChange, Qt::AutoConnection); + break; + case Subscription::VideomodeUpdate: + connect(_hyperion, &Hyperion::newVideoMode, this, &JsonCallbacks::handleVideoModeChange); + break; + + default: + return false; + } + + _subscribedCommands.insert(cmd); + + return true; +} + +bool JsonCallbacks::subscribe(const QString& cmd) +{ + JsonApiSubscription subscription = ApiSubscriptionRegister::getSubscriptionInfo(cmd); + if (subscription.cmd == Subscription::Unknown) + { + return false; + } + return subscribe(subscription.cmd); +} + +QStringList JsonCallbacks::subscribe(const QJsonArray& subscriptions) +{ + QJsonArray subsArr; + if (subscriptions.contains("all")) + { + for (const auto& entry : getCommands(false)) + { + subsArr.append(entry); + } + } + else + { + subsArr = subscriptions; + } + + QStringList invalidSubscriptions; + for (auto it = subsArr.begin(); it != subsArr.end(); ++it) + { + const QJsonValue& entry = *it; + if (!subscribe(entry.toString())) + { + invalidSubscriptions.append(entry.toString()); + } + } + return invalidSubscriptions; +} + +bool JsonCallbacks::unsubscribe(const Subscription::Type cmd) +{ + _subscribedCommands.remove(cmd); + + switch (cmd) { + case Subscription::AdjustmentUpdate: + disconnect(_hyperion, &Hyperion::adjustmentChanged, this, &JsonCallbacks::handleAdjustmentChange); + break; + case Subscription::ComponentsUpdate: + disconnect(_componentRegister, &ComponentRegister::updatedComponentState, this, &JsonCallbacks::handleComponentState); + break; +#if defined(ENABLE_EFFECTENGINE) + case Subscription::EffectsUpdate: + disconnect(_hyperion, &Hyperion::effectListUpdated, this, &JsonCallbacks::handleEffectListChange); + break; +#endif + case Subscription::EventUpdate: + disconnect(EventHandler::getInstance().data(), &EventHandler::signalEvent, this, &JsonCallbacks::handleEventUpdate); + break; + case Subscription::ImageToLedMappingUpdate: + disconnect(_hyperion, &Hyperion::imageToLedsMappingChanged, this, &JsonCallbacks::handleImageToLedsMappingChange); + break; + case Subscription::ImageUpdate: + disconnect(_hyperion, &Hyperion::currentImage, this, &JsonCallbacks::handleImageUpdate); + break; + case Subscription::InstanceUpdate: + disconnect(HyperionIManager::getInstance(), &HyperionIManager::change, this, &JsonCallbacks::handleInstanceChange); + break; + case Subscription::LedColorsUpdate: + disconnect(_hyperion, &Hyperion::rawLedColors, this, &JsonCallbacks::handleLedColorUpdate); + break; + case Subscription::LedsUpdate: + disconnect(_hyperion, &Hyperion::settingsChanged, this, &JsonCallbacks::handleLedsConfigChange); + break; + case Subscription::LogMsgUpdate: + disconnect(LoggerManager::getInstance().data(), &LoggerManager::newLogMessage, this, &JsonCallbacks::handleLogMessageUpdate); + if (_islogMsgStreamingActive) + { + _islogMsgStreamingActive = false; + Debug(_log, "log streaming deactivated for client %s", _peerAddress.toStdString().c_str()); + } + break; + case Subscription::PrioritiesUpdate: + disconnect(_prioMuxer, &PriorityMuxer::prioritiesChanged, this, &JsonCallbacks::handlePriorityUpdate); + break; + case Subscription::SettingsUpdate: + disconnect(_hyperion, &Hyperion::settingsChanged, this, &JsonCallbacks::handleSettingsChange); + break; + case Subscription::TokenUpdate: + disconnect(AuthManager::getInstance(), &AuthManager::tokenChange, this, &JsonCallbacks::handleTokenChange); + break; + case Subscription::VideomodeUpdate: + disconnect(_hyperion, &Hyperion::newVideoMode, this, &JsonCallbacks::handleVideoModeChange); + break; + + default: + return false; + } + return true; +} + +bool JsonCallbacks::unsubscribe(const QString& cmd) +{ + JsonApiSubscription subscription = ApiSubscriptionRegister::getSubscriptionInfo(cmd); + if (subscription.cmd == Subscription::Unknown) + { + return false; + } + return unsubscribe(subscription.cmd); +} + +QStringList JsonCallbacks::unsubscribe(const QJsonArray& subscriptions) +{ + QJsonArray subsArr; + if (subscriptions.contains("all")) + { + for (const auto& entry : getCommands(false)) + { + subsArr.append(entry); + } + } + else + { + subsArr = subscriptions; + } + + QStringList invalidSubscriptions; + for (auto it = subsArr.begin(); it != subsArr.end(); ++it) + { + const QJsonValue& entry = *it; + if (!unsubscribe(entry.toString())) + { + invalidSubscriptions.append(entry.toString()); + } + } + return invalidSubscriptions; +} + +void JsonCallbacks::resetSubscriptions() +{ + const QSet currentSubscriptions = _subscribedCommands; + for (QSet::const_iterator it = currentSubscriptions.constBegin(); it != currentSubscriptions.constEnd(); ++it) + { + unsubscribe(*it); + } +} + +void JsonCallbacks::setSubscriptionsTo(Hyperion* hyperion) +{ + assert(hyperion); + + // get current subs + const QSet currSubs(_subscribedCommands); + + // stop subs + resetSubscriptions(); + + // update pointer + _hyperion = hyperion; + _componentRegister = _hyperion->getComponentRegister(); + _prioMuxer = _hyperion->getMuxerInstance(); + + // re-apply subs + for(const auto & entry : currSubs) + { + subscribe(entry); + } +} + +QStringList JsonCallbacks::getCommands(bool fullList) const +{ + QStringList commands; + for (JsonApiSubscription subscription : ApiSubscriptionRegister::getSubscriptionLookup()) + { + if (fullList || subscription.isAll) + { + commands << Subscription::toString(subscription.cmd); + } + } + return commands; +} + +QStringList JsonCallbacks::getSubscribedCommands() const +{ + QStringList commands; + for (Subscription::Type cmd : _subscribedCommands) + { + commands << Subscription::toString(cmd); + } + return commands; +} + +void JsonCallbacks::doCallback(Subscription::Type cmd, const QVariant& data) +{ + QJsonObject obj; + obj["command"] = Subscription::toString(cmd); + + if (Subscription::isInstanceSpecific(cmd)) + { + obj["instance"] = _hyperion->getInstanceIndex(); + } + + if (data.userType() == QMetaType::QJsonArray) { + obj["data"] = data.toJsonArray(); + } else { + obj["data"] = data.toJsonObject(); + } + + emit newCallback(obj); +} + +void JsonCallbacks::handleComponentState(hyperion::Components comp, bool state) +{ + QJsonObject data; + data["name"] = componentToIdString(comp); + data["enabled"] = state; + + doCallback(Subscription::ComponentsUpdate, QVariant(data)); +} + +void JsonCallbacks::handlePriorityUpdate(int currentPriority, const PriorityMuxer::InputsMap& activeInputs) +{ + QJsonObject data; + data["priorities"] = JsonInfo::getPrioritiestInfo(currentPriority, activeInputs); + data["priorities_autoselect"] = _hyperion->sourceAutoSelectEnabled(); + + doCallback(Subscription::PrioritiesUpdate, QVariant(data)); +} + +void JsonCallbacks::handleImageToLedsMappingChange(int mappingType) +{ + QJsonObject data; + data["imageToLedMappingType"] = ImageProcessor::mappingTypeToStr(mappingType); + + doCallback(Subscription::ImageToLedMappingUpdate, QVariant(data)); +} + +void JsonCallbacks::handleAdjustmentChange() +{ + doCallback(Subscription::AdjustmentUpdate, JsonInfo::getAdjustmentInfo(_hyperion,_log)); +} + +void JsonCallbacks::handleVideoModeChange(VideoMode mode) +{ + QJsonObject data; + data["videomode"] = QString(videoMode2String(mode)); + doCallback(Subscription::VideomodeUpdate, QVariant(data)); +} + +#if defined(ENABLE_EFFECTENGINE) +void JsonCallbacks::handleEffectListChange() +{ + QJsonObject effects; + effects["effects"] = JsonInfo::getEffects(_hyperion); + doCallback(Subscription::EffectsUpdate, QVariant(effects)); +} +#endif + +void JsonCallbacks::handleSettingsChange(settings::type type, const QJsonDocument& data) +{ + QJsonObject dat; + if(data.isObject()) { + dat[typeToString(type)] = data.object(); + } else { + dat[typeToString(type)] = data.array(); + } + + doCallback(Subscription::SettingsUpdate, QVariant(dat)); +} + +void JsonCallbacks::handleLedsConfigChange(settings::type type, const QJsonDocument& data) +{ + if(type == settings::LEDS) + { + QJsonObject dat; + dat[typeToString(type)] = data.array(); + doCallback(Subscription::LedsUpdate, QVariant(dat)); + } +} + +void JsonCallbacks::handleInstanceChange() +{ + doCallback(Subscription::InstanceUpdate, JsonInfo::getInstanceInfo()); +} + +void JsonCallbacks::handleTokenChange(const QVector &def) +{ + QJsonArray arr; + for (const auto &entry : def) + { + QJsonObject sub; + sub["comment"] = entry.comment; + sub["id"] = entry.id; + sub["last_use"] = entry.lastUse; + arr.push_back(sub); + } + doCallback(Subscription::TokenUpdate, QVariant(arr)); +} + +void JsonCallbacks::handleLedColorUpdate(const std::vector &ledColors) +{ + QJsonObject result; + QJsonArray leds; + + for (const auto &color : ledColors) + { + leds << QJsonValue(color.red) << QJsonValue(color.green) << QJsonValue(color.blue); + } + result["leds"] = leds; + + doCallback(Subscription::LedColorsUpdate, QVariant(result)); +} + +void JsonCallbacks::handleImageUpdate(const Image &image) +{ + QImage jpgImage(reinterpret_cast(image.memptr()), image.width(), image.height(), qsizetype(3) * image.width(), QImage::Format_RGB888); + QByteArray byteArray; + QBuffer buffer(&byteArray); + buffer.open(QIODevice::WriteOnly); + jpgImage.save(&buffer, "jpg"); + + QJsonObject result; + result["image"] = "data:image/jpg;base64," + QString(byteArray.toBase64()); + + doCallback(Subscription::ImageUpdate, QVariant(result)); +} + +void JsonCallbacks::handleLogMessageUpdate(const Logger::T_LOG_MESSAGE &msg) +{ + QJsonObject result; + QJsonObject message; + QJsonArray messageArray; + + if (!_islogMsgStreamingActive) + { + _islogMsgStreamingActive = true; + QMetaObject::invokeMethod(LoggerManager::getInstance().data(), "getLogMessageBuffer", + Qt::DirectConnection, + Q_RETURN_ARG(QJsonArray, messageArray), + Q_ARG(Logger::LogLevel, _log->getLogLevel())); + } + else + { + message["loggerName"] = msg.loggerName; + message["loggerSubName"] = msg.loggerSubName; + message["function"] = msg.function; + message["line"] = QString::number(msg.line); + message["fileName"] = msg.fileName; + message["message"] = msg.message; + message["levelString"] = msg.levelString; + message["utime"] = QString::number(msg.utime); + + messageArray.append(message); + } + result.insert("messages", messageArray); + + doCallback(Subscription::LogMsgUpdate, QVariant(result)); +} + +void JsonCallbacks::handleEventUpdate(const Event &event) +{ + QJsonObject result; + + result["event"] = eventToString(event); + + doCallback(Subscription::EventUpdate, QVariant(result)); +} + diff --git a/libsrc/api/JsonInfo.cpp b/libsrc/api/JsonInfo.cpp new file mode 100644 index 00000000..e2a73ffe --- /dev/null +++ b/libsrc/api/JsonInfo.cpp @@ -0,0 +1,620 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include // Required to determine the cmake options + +#include +#include + + +QJsonArray JsonInfo::getAdjustmentInfo(const Hyperion* hyperion, Logger* log) +{ + QJsonArray adjustmentArray; + for (const QString &adjustmentId : hyperion->getAdjustmentIds()) + { + const ColorAdjustment *colorAdjustment = hyperion->getAdjustment(adjustmentId); + if (colorAdjustment == nullptr) + { + Error(log, "Incorrect color adjustment id: %s", QSTRING_CSTR(adjustmentId)); + continue; + } + + QJsonObject adjustment; + adjustment["id"] = adjustmentId; + + QJsonArray whiteAdjust; + whiteAdjust.append(colorAdjustment->_rgbWhiteAdjustment.getAdjustmentR()); + whiteAdjust.append(colorAdjustment->_rgbWhiteAdjustment.getAdjustmentG()); + whiteAdjust.append(colorAdjustment->_rgbWhiteAdjustment.getAdjustmentB()); + adjustment.insert("white", whiteAdjust); + + QJsonArray redAdjust; + redAdjust.append(colorAdjustment->_rgbRedAdjustment.getAdjustmentR()); + redAdjust.append(colorAdjustment->_rgbRedAdjustment.getAdjustmentG()); + redAdjust.append(colorAdjustment->_rgbRedAdjustment.getAdjustmentB()); + adjustment.insert("red", redAdjust); + + QJsonArray greenAdjust; + greenAdjust.append(colorAdjustment->_rgbGreenAdjustment.getAdjustmentR()); + greenAdjust.append(colorAdjustment->_rgbGreenAdjustment.getAdjustmentG()); + greenAdjust.append(colorAdjustment->_rgbGreenAdjustment.getAdjustmentB()); + adjustment.insert("green", greenAdjust); + + QJsonArray blueAdjust; + blueAdjust.append(colorAdjustment->_rgbBlueAdjustment.getAdjustmentR()); + blueAdjust.append(colorAdjustment->_rgbBlueAdjustment.getAdjustmentG()); + blueAdjust.append(colorAdjustment->_rgbBlueAdjustment.getAdjustmentB()); + adjustment.insert("blue", blueAdjust); + + QJsonArray cyanAdjust; + cyanAdjust.append(colorAdjustment->_rgbCyanAdjustment.getAdjustmentR()); + cyanAdjust.append(colorAdjustment->_rgbCyanAdjustment.getAdjustmentG()); + cyanAdjust.append(colorAdjustment->_rgbCyanAdjustment.getAdjustmentB()); + adjustment.insert("cyan", cyanAdjust); + + QJsonArray magentaAdjust; + magentaAdjust.append(colorAdjustment->_rgbMagentaAdjustment.getAdjustmentR()); + magentaAdjust.append(colorAdjustment->_rgbMagentaAdjustment.getAdjustmentG()); + magentaAdjust.append(colorAdjustment->_rgbMagentaAdjustment.getAdjustmentB()); + adjustment.insert("magenta", magentaAdjust); + + QJsonArray yellowAdjust; + yellowAdjust.append(colorAdjustment->_rgbYellowAdjustment.getAdjustmentR()); + yellowAdjust.append(colorAdjustment->_rgbYellowAdjustment.getAdjustmentG()); + yellowAdjust.append(colorAdjustment->_rgbYellowAdjustment.getAdjustmentB()); + adjustment.insert("yellow", yellowAdjust); + + adjustment["backlightThreshold"] = colorAdjustment->_rgbTransform.getBacklightThreshold(); + adjustment["backlightColored"] = colorAdjustment->_rgbTransform.getBacklightColored(); + adjustment["brightness"] = colorAdjustment->_rgbTransform.getBrightness(); + adjustment["brightnessCompensation"] = colorAdjustment->_rgbTransform.getBrightnessCompensation(); + adjustment["gammaRed"] = colorAdjustment->_rgbTransform.getGammaR(); + adjustment["gammaGreen"] = colorAdjustment->_rgbTransform.getGammaG(); + adjustment["gammaBlue"] = colorAdjustment->_rgbTransform.getGammaB(); + + adjustment["saturationGain"] = colorAdjustment->_okhsvTransform.getSaturationGain(); + adjustment["brightnessGain"] = colorAdjustment->_okhsvTransform.getBrightnessGain(); + + adjustmentArray.append(adjustment); + } + return adjustmentArray; +} + +QJsonArray JsonInfo::getPrioritiestInfo(const Hyperion* hyperion) +{ + return getPrioritiestInfo(hyperion->getCurrentPriority(), hyperion->getPriorityInfo()); +} + +QJsonArray JsonInfo::getPrioritiestInfo(int currentPriority, const PriorityMuxer::InputsMap& activeInputs) +{ + QJsonArray priorities; + int64_t now = QDateTime::currentMSecsSinceEpoch(); + + QList activePriorities = activeInputs.keys(); + activePriorities.removeAll(PriorityMuxer::LOWEST_PRIORITY); + + for(int priority : std::as_const(activePriorities)) + { + const PriorityMuxer::InputInfo priorityInfo = activeInputs.value(priority); + + QJsonObject item; + item["priority"] = priority; + + if (priorityInfo.timeoutTime_ms > 0 ) + { + item["duration_ms"] = int(priorityInfo.timeoutTime_ms - now); + } + + // owner has optional informations to the component + if (!priorityInfo.owner.isEmpty()) + { + item["owner"] = priorityInfo.owner; + } + + item["componentId"] = QString(hyperion::componentToIdString(priorityInfo.componentId)); + item["origin"] = priorityInfo.origin; + item["active"] = (priorityInfo.timeoutTime_ms >= -1); + item["visible"] = (priority == currentPriority); + + if (priorityInfo.componentId == hyperion::COMP_COLOR && !priorityInfo.ledColors.empty()) + { + QJsonObject LEDcolor; + + // add RGB Value to Array + QJsonArray RGBValue; + RGBValue.append(priorityInfo.ledColors.begin()->red); + RGBValue.append(priorityInfo.ledColors.begin()->green); + RGBValue.append(priorityInfo.ledColors.begin()->blue); + LEDcolor.insert("RGB", RGBValue); + + uint16_t Hue; + float Saturation; + float Luminace; + + // add HSL Value to Array + QJsonArray HSLValue; + ColorSys::rgb2hsl(priorityInfo.ledColors.begin()->red, + priorityInfo.ledColors.begin()->green, + priorityInfo.ledColors.begin()->blue, + Hue, Saturation, Luminace); + + HSLValue.append(static_cast(Hue)); + HSLValue.append(static_cast(Saturation)); + HSLValue.append(static_cast(Luminace)); + LEDcolor.insert("HSL", HSLValue); + + item["value"] = LEDcolor; + } + + (priority == currentPriority) + ? priorities.prepend(item) + : priorities.append(item); + } + return priorities; +} + +QJsonArray JsonInfo::getEffects(const Hyperion* hyperion) +{ + QJsonArray effects; +#if defined(ENABLE_EFFECTENGINE) + // collect effect info + + const std::list &effectsDefinitions = hyperion->getEffects(); + for (const EffectDefinition &effectDefinition : effectsDefinitions) + { + QJsonObject effect; + effect["name"] = effectDefinition.name; + effect["file"] = effectDefinition.file; + effect["script"] = effectDefinition.script; + effect["args"] = effectDefinition.args; + effects.append(effect); + } +#endif + return effects; +} + +QJsonObject JsonInfo::getAvailableLedDevices() +{ + // get available led devices + QJsonObject ledDevices; + QJsonArray availableLedDevices; + for (const auto& dev : LedDeviceWrapper::getDeviceMap()) + { + availableLedDevices.append(dev.first); + } + + ledDevices["available"] = availableLedDevices; + + return ledDevices; +} + +QJsonArray JsonInfo::getAvailableScreenGrabbers() +{ + QJsonArray availableScreenGrabbers; + for (const auto& grabber : GrabberWrapper::availableGrabbers(GrabberTypeFilter::SCREEN)) + { + availableScreenGrabbers.append(grabber); + } + return availableScreenGrabbers; +} + +QJsonArray JsonInfo::getAvailableVideoGrabbers() +{ + QJsonArray availableVideoGrabbers; + for (const auto& grabber : GrabberWrapper::availableGrabbers(GrabberTypeFilter::VIDEO)) + { + availableVideoGrabbers.append(grabber); + } + return availableVideoGrabbers; +} +QJsonArray JsonInfo::getAvailableAudioGrabbers() +{ + QJsonArray availableAudioGrabbers; + for (const auto& grabber : GrabberWrapper::availableGrabbers(GrabberTypeFilter::AUDIO)) + { + availableAudioGrabbers.append(grabber); + } + return availableAudioGrabbers; +} + +QJsonObject JsonInfo::getGrabbers(const Hyperion* hyperion) +{ + QJsonObject grabbers; + // SCREEN + QJsonObject screenGrabbers; + if (GrabberWrapper::getInstance() != nullptr) + { + const QStringList activeGrabbers = GrabberWrapper::getInstance()->getActive(hyperion->getInstanceIndex(), GrabberTypeFilter::SCREEN); + QJsonArray activeGrabberNames; + for (const auto& grabberName : activeGrabbers) + { + activeGrabberNames.append(grabberName); + } + + screenGrabbers["active"] = activeGrabberNames; + } + screenGrabbers["available"] = getAvailableScreenGrabbers(); + + // VIDEO + QJsonObject videoGrabbers; + if (GrabberWrapper::getInstance() != nullptr) + { + const QStringList activeGrabbers = GrabberWrapper::getInstance()->getActive(hyperion->getInstanceIndex(), GrabberTypeFilter::VIDEO); + QJsonArray activeGrabberNames; + for (const auto& grabberName : activeGrabbers) + { + activeGrabberNames.append(grabberName); + } + + videoGrabbers["active"] = activeGrabberNames; + } + videoGrabbers["available"] = getAvailableVideoGrabbers(); + + // AUDIO + QJsonObject audioGrabbers; + if (GrabberWrapper::getInstance() != nullptr) + { + const QStringList activeGrabbers = GrabberWrapper::getInstance()->getActive(hyperion->getInstanceIndex(), GrabberTypeFilter::AUDIO); + + QJsonArray activeGrabberNames; + for (const auto& grabberName : activeGrabbers) + { + activeGrabberNames.append(grabberName); + } + + audioGrabbers["active"] = activeGrabberNames; + } + audioGrabbers["available"] = getAvailableAudioGrabbers() ; + + grabbers.insert("screen", screenGrabbers); + grabbers.insert("video", videoGrabbers); + grabbers.insert("audio", audioGrabbers); + + return grabbers; +} + +QJsonObject JsonInfo::getCecInfo() +{ + QJsonObject cecInfo; +#if defined(ENABLE_CEC) + cecInfo["enabled"] = true; +#else + cecInfo["enabled"] = false; +#endif + return cecInfo; +} + +QJsonArray JsonInfo::getServices() +{ + // get available services + QJsonArray services; + +#if defined(ENABLE_BOBLIGHT_SERVER) + services.append("boblight"); +#endif + +#if defined(ENABLE_CEC) + services.append("cec"); +#endif + +#if defined(ENABLE_EFFECTENGINE) + services.append("effectengine"); +#endif + +#if defined(ENABLE_FORWARDER) + services.append("forwarder"); +#endif + +#if defined(ENABLE_FLATBUF_SERVER) + services.append("flatbuffer"); +#endif + +#if defined(ENABLE_PROTOBUF_SERVER) + services.append("protobuffer"); +#endif + +#if defined(ENABLE_MDNS) + services.append("mDNS"); +#endif + services.append("SSDP"); + + if (!getAvailableScreenGrabbers().isEmpty() || !getAvailableVideoGrabbers().isEmpty() || services.contains("flatbuffer") || services.contains("protobuffer")) + { + services.append("borderdetection"); + } + return services; +} + +QJsonArray JsonInfo::getComponents(const Hyperion* hyperion) +{ + // get available components + QJsonArray component; + std::map components = hyperion->getComponentRegister()->getRegister(); + for (auto comp : components) + { + QJsonObject item; + item["name"] = QString::fromStdString(hyperion::componentToIdString(comp.first)); + item["enabled"] = comp.second; + + component.append(item); + } + return component; +} + +QJsonArray JsonInfo::getInstanceInfo() +{ + QJsonArray instanceInfo; + for (const auto &entry : HyperionIManager::getInstance()->getInstanceData()) + { + QJsonObject obj; + obj.insert("friendly_name", entry["friendly_name"].toString()); + obj.insert("instance", entry["instance"].toInt()); + obj.insert("running", entry["running"].toBool()); + instanceInfo.append(obj); + } + return instanceInfo; +} + +QJsonArray JsonInfo::getTransformationInfo(const Hyperion* hyperion) +{ + // TRANSFORM INFORMATION (DEFAULT VALUES) + QJsonArray transformArray; + for (const QString &transformId : hyperion->getAdjustmentIds()) + { + QJsonObject transform; + QJsonArray blacklevel; + QJsonArray whitelevel; + QJsonArray gamma; + QJsonArray threshold; + + transform["id"] = transformId; + transform["saturationGain"] = 1.0; + transform["brightnessGain"] = 1.0; + transform["saturationLGain"] = 1.0; + transform["luminanceGain"] = 1.0; + transform["luminanceMinimum"] = 0.0; + + for (int i = 0; i < 3; i++) + { + blacklevel.append(0.0); + whitelevel.append(1.0); + gamma.append(2.50); + threshold.append(0.0); + } + + transform.insert("blacklevel", blacklevel); + transform.insert("whitelevel", whitelevel); + transform.insert("gamma", gamma); + transform.insert("threshold", threshold); + + transformArray.append(transform); + } + return transformArray; +} + +QJsonArray JsonInfo::getActiveEffects(const Hyperion* hyperion) +{ + // ACTIVE EFFECT INFO + QJsonArray activeEffects; +#if defined(ENABLE_EFFECTENGINE) + for (const ActiveEffectDefinition &activeEffectDefinition : hyperion->getActiveEffects()) + { + if (activeEffectDefinition.priority != PriorityMuxer::LOWEST_PRIORITY - 1) + { + QJsonObject activeEffect; + activeEffect["script"] = activeEffectDefinition.script; + activeEffect["name"] = activeEffectDefinition.name; + activeEffect["priority"] = activeEffectDefinition.priority; + activeEffect["timeout"] = activeEffectDefinition.timeout; + activeEffect["args"] = activeEffectDefinition.args; + activeEffects.append(activeEffect); + } + } +#endif + return activeEffects; +} + +QJsonArray JsonInfo::getActiveColors(const Hyperion* hyperion) +{ + // ACTIVE STATIC LED COLOR + QJsonArray activeLedColors; + const Hyperion::InputInfo &priorityInfo = hyperion->getPriorityInfo(hyperion->getCurrentPriority()); + if (priorityInfo.componentId == hyperion::COMP_COLOR && !priorityInfo.ledColors.empty()) + { + // check if LED Color not Black (0,0,0) + if ((priorityInfo.ledColors.begin()->red + + priorityInfo.ledColors.begin()->green + + priorityInfo.ledColors.begin()->blue != + 0)) + { + QJsonObject LEDcolor; + + // add RGB Value to Array + QJsonArray RGBValue; + RGBValue.append(priorityInfo.ledColors.begin()->red); + RGBValue.append(priorityInfo.ledColors.begin()->green); + RGBValue.append(priorityInfo.ledColors.begin()->blue); + LEDcolor.insert("RGB Value", RGBValue); + + uint16_t Hue; + float Saturation; + float Luminace; + + // add HSL Value to Array + QJsonArray HSLValue; + ColorSys::rgb2hsl(priorityInfo.ledColors.begin()->red, + priorityInfo.ledColors.begin()->green, + priorityInfo.ledColors.begin()->blue, + Hue, Saturation, Luminace); + + HSLValue.append(static_cast(Hue)); + HSLValue.append(static_cast(Saturation)); + HSLValue.append(static_cast(Luminace)); + LEDcolor.insert("HSL Value", HSLValue); + + activeLedColors.append(LEDcolor); + } + } + return activeLedColors; +} + +QJsonObject JsonInfo::getSystemInfo(const Hyperion* hyperion) +{ + QJsonObject info; + + SysInfo::HyperionSysInfo data = SysInfo::get(); + QJsonObject systemInfo; + systemInfo["kernelType"] = data.kernelType; + systemInfo["kernelVersion"] = data.kernelVersion; + systemInfo["architecture"] = data.architecture; + systemInfo["cpuModelName"] = data.cpuModelName; + systemInfo["cpuModelType"] = data.cpuModelType; + systemInfo["cpuHardware"] = data.cpuHardware; + systemInfo["cpuRevision"] = data.cpuRevision; + systemInfo["wordSize"] = data.wordSize; + systemInfo["productType"] = data.productType; + systemInfo["productVersion"] = data.productVersion; + systemInfo["prettyName"] = data.prettyName; + systemInfo["hostName"] = data.hostName; + systemInfo["domainName"] = data.domainName; + systemInfo["isUserAdmin"] = data.isUserAdmin; + systemInfo["qtVersion"] = data.qtVersion; +#if defined(ENABLE_EFFECTENGINE) + systemInfo["pyVersion"] = data.pyVersion; +#endif + info["system"] = systemInfo; + + QJsonObject hyperionInfo; + hyperionInfo["version"] = QString(HYPERION_VERSION); + hyperionInfo["build"] = QString(HYPERION_BUILD_ID); + hyperionInfo["gitremote"] = QString(HYPERION_GIT_REMOTE); + hyperionInfo["time"] = QString(__DATE__ " " __TIME__); + hyperionInfo["id"] = AuthManager::getInstance()->getID(); + hyperionInfo["rootPath"] = HyperionIManager::getInstance()->getRootPath(); + hyperionInfo["readOnlyMode"] = hyperion->getReadOnlyMode(); + + QCoreApplication* app = QCoreApplication::instance(); + hyperionInfo["isGuiMode"] = qobject_cast(app) != nullptr; + + info["hyperion"] = hyperionInfo; + + return info; +} + +QJsonObject JsonInfo::discoverSources(const QString& sourceType, const QJsonObject& params) +{ + QJsonObject inputSourcesDiscovered; + inputSourcesDiscovered.insert("sourceType", sourceType); + + if (sourceType == "video") { + QJsonArray videoInputs = discoverVideoInputs(params); + inputSourcesDiscovered["video_sources"] = videoInputs; + } else if (sourceType == "audio") { + QJsonArray audioInputs = discoverAudioInputs(params); + inputSourcesDiscovered["audio_sources"] = audioInputs; + } else if (sourceType == "screen") { + QJsonArray screenInputs = discoverScreenInputs(params); + inputSourcesDiscovered["video_sources"] = screenInputs; + } + + return inputSourcesDiscovered; +} + +template +void JsonInfo::discoverGrabber(QJsonArray& inputs, const QJsonObject& params) const +{ + GrabberType grabber; + QJsonValue discoveryResult = grabber.discover(params); + + if (discoveryResult.isArray()) + { + inputs = discoveryResult.toArray(); + } + else + { + if (!discoveryResult.toObject().isEmpty()) + { + inputs.append(discoveryResult); + } + } +} + +QJsonArray JsonInfo::discoverVideoInputs(const QJsonObject& params) const +{ + QJsonArray videoInputs; + +#ifdef ENABLE_V4L2 + discoverGrabber(videoInputs, params); +#endif + +#ifdef ENABLE_MF + discoverGrabber(videoInputs, params); +#endif + + return videoInputs; +} + +QJsonArray JsonInfo::discoverAudioInputs(const QJsonObject& params) const +{ + QJsonArray audioInputs; + +#ifdef ENABLE_AUDIO +#ifdef WIN32 + discoverGrabber(audioInputs, params); +#endif + +#ifdef __linux__audioInputs + discoverGrabber(audioInputs, params); +#endif + +#endif + + return audioInputs; +} + +QJsonArray JsonInfo::discoverScreenInputs(const QJsonObject& params) const +{ + QJsonArray screenInputs; + +#ifdef ENABLE_QT + discoverGrabber(screenInputs, params); +#endif + +#ifdef ENABLE_DX + discoverGrabber(screenInputs, params); +#endif + +#ifdef ENABLE_X11 + discoverGrabber(screenInputs, params); +#endif + +#ifdef ENABLE_XCB + discoverGrabber(screenInputs, params); +#endif + +#if defined(ENABLE_FB) && !defined(ENABLE_AMLOGIC) + discoverGrabber(screenInputs, params); +#endif + +#ifdef ENABLE_DISPMANX + discoverGrabber(screenInputs, params); +#endif + +#ifdef ENABLE_AMLOGIC + discoverGrabber(screenInputs, params); +#endif + +#ifdef ENABLE_OSX + discoverGrabber(screenInputs, params); +#endif + + return screenInputs; +} diff --git a/libsrc/effectengine/EffectFileHandler.cpp b/libsrc/effectengine/EffectFileHandler.cpp index 3d1c77eb..734c4da2 100644 --- a/libsrc/effectengine/EffectFileHandler.cpp +++ b/libsrc/effectengine/EffectFileHandler.cpp @@ -103,7 +103,7 @@ QString EffectFileHandler::saveEffect(const QJsonObject& message) if (it != effectsSchemas.end()) { - if (!JsonUtils::validate("EffectFileHandler", message["args"].toObject(), it->schemaFile, _log)) + if (!JsonUtils::validate("EffectFileHandler", message["args"].toObject(), it->schemaFile, _log).first) { return "Error during arg validation against schema, please consult the Hyperion Log"; } @@ -298,12 +298,12 @@ bool EffectFileHandler::loadEffectDefinition(const QString& path, const QString& // Read and parse the effect json config file QJsonObject configEffect; - if (!JsonUtils::readFile(fileName, configEffect, _log)) { + if (!JsonUtils::readFile(fileName, configEffect, _log).first) { return false; } // validate effect config with effect schema(path) - if (!JsonUtils::validate(fileName, configEffect, ":effect-schema", _log)) { + if (!JsonUtils::validate(fileName, configEffect, ":effect-schema", _log).first) { return false; } @@ -335,7 +335,7 @@ bool EffectFileHandler::loadEffectSchema(const QString& path, const QString& sch { // Read and parse the effect schema file QJsonObject schemaEffect; - if (!JsonUtils::readFile(schemaFilePath, schemaEffect, _log)) + if (!JsonUtils::readFile(schemaFilePath, schemaEffect, _log).first) { return false; } diff --git a/libsrc/events/EventHandler.cpp b/libsrc/events/EventHandler.cpp index a76ed462..f0d70a48 100644 --- a/libsrc/events/EventHandler.cpp +++ b/libsrc/events/EventHandler.cpp @@ -144,7 +144,7 @@ void EventHandler::handleEvent(Event event) { QObject *senderObj = QObject::sender(); QString senderObjectClass; - if (senderObj) + if (senderObj != nullptr) { senderObjectClass = senderObj->metaObject()->className(); } else @@ -179,13 +179,19 @@ void EventHandler::handleEvent(Event event) break; case Event::Reload: + emit signalEvent(Event::Reload); Process::restartHyperion(10); break; case Event::Restart: + emit signalEvent(Event::Restart); Process::restartHyperion(11); break; + case Event::Quit: + emit signalEvent(Event::Quit); + break; + default: Error(_log,"Unkonwn Event '%d' received", event); break; diff --git a/libsrc/events/OsEventHandler.cpp b/libsrc/events/OsEventHandler.cpp index 24026532..188ac184 100644 --- a/libsrc/events/OsEventHandler.cpp +++ b/libsrc/events/OsEventHandler.cpp @@ -35,7 +35,7 @@ OsEventHandlerBase::OsEventHandlerBase() _log = Logger::getInstance("EVENTS-OS"); QCoreApplication* app = QCoreApplication::instance(); - if (!qobject_cast(app)) + if (qobject_cast(app) == nullptr) { _isService = true; } @@ -46,6 +46,7 @@ OsEventHandlerBase::OsEventHandlerBase() OsEventHandlerBase::~OsEventHandlerBase() { + quit(); QObject::disconnect(this, &OsEventHandlerBase::signalEvent, EventHandler::getInstance().data(), &EventHandler::handleEvent); OsEventHandlerBase::unregisterLockHandler(); @@ -130,6 +131,11 @@ void OsEventHandlerBase::lock(bool isLocked) } } +void OsEventHandlerBase::quit() +{ + emit signalEvent(Event::Quit); +} + #if defined(_WIN32) OsEventHandlerWindows* OsEventHandlerWindows::getInstance() diff --git a/libsrc/hyperion/AuthManager.cpp b/libsrc/hyperion/AuthManager.cpp index 931443a7..4e60807e 100644 --- a/libsrc/hyperion/AuthManager.cpp +++ b/libsrc/hyperion/AuthManager.cpp @@ -15,7 +15,6 @@ AuthManager::AuthManager(QObject *parent, bool readonlyMode) , _authTable(new AuthTable("", this, readonlyMode)) , _metaTable(new MetaTable(this, readonlyMode)) , _pendingRequests() - , _authRequired(true) , _timer(new QTimer(this)) , _authBlockTimer(new QTimer(this)) { @@ -36,13 +35,13 @@ AuthManager::AuthManager(QObject *parent, bool readonlyMode) connect(_authBlockTimer, &QTimer::timeout, this, &AuthManager::checkAuthBlockTimeout); // init with default user and password - if (!_authTable->userExist("Hyperion")) + if (!_authTable->userExist(hyperion::DEFAULT_USER)) { - _authTable->createUser("Hyperion", "hyperion"); + _authTable->createUser(hyperion::DEFAULT_USER, hyperion::DEFAULT_PASSWORD); } // update Hyperion user token on startup - _authTable->setUserToken("Hyperion"); + _authTable->setUserToken(hyperion::DEFAULT_USER); } AuthManager::AuthDefinition AuthManager::createToken(const QString &comment) @@ -201,6 +200,8 @@ QVector AuthManager::getPendingRequests() const def.comment = entry.comment; def.id = entry.id; def.timeoutTime = entry.timeoutTime - QDateTime::currentMSecsSinceEpoch(); + def.tan = entry.tan; + def.caller = nullptr; finalVec.append(def); } return finalVec; @@ -208,20 +209,26 @@ QVector AuthManager::getPendingRequests() const bool AuthManager::renameToken(const QString &id, const QString &comment) { - if (_authTable->renameToken(id, comment)) + if (_authTable->idExist(id)) { - emit tokenChange(getTokenList()); - return true; + if (_authTable->renameToken(id, comment)) + { + emit tokenChange(getTokenList()); + return true; + } } return false; } bool AuthManager::deleteToken(const QString &id) { - if (_authTable->deleteToken(id)) + if (_authTable->idExist(id)) { - emit tokenChange(getTokenList()); - return true; + if (_authTable->deleteToken(id)) + { + emit tokenChange(getTokenList()); + return true; + } } return false; } @@ -231,9 +238,7 @@ void AuthManager::handleSettingsUpdate(settings::type type, const QJsonDocument if (type == settings::NETWORK) { const QJsonObject &obj = config.object(); - _authRequired = obj["apiAuth"].toBool(true); _localAuthRequired = obj["localApiAuth"].toBool(false); - _localAdminAuthRequired = obj["localAdminAuth"].toBool(true); } } diff --git a/libsrc/hyperion/Hyperion.cpp b/libsrc/hyperion/Hyperion.cpp index 2a2b50d6..de719578 100644 --- a/libsrc/hyperion/Hyperion.cpp +++ b/libsrc/hyperion/Hyperion.cpp @@ -550,6 +550,11 @@ QList Hyperion::getActivePriorities() const return _muxer->getPriorities(); } +Hyperion::InputsMap Hyperion::getPriorityInfo() const +{ + return _muxer->getInputInfo(); +} + Hyperion::InputInfo Hyperion::getPriorityInfo(int priority) const { return _muxer->getInputInfo(priority); diff --git a/libsrc/hyperion/HyperionIManager.cpp b/libsrc/hyperion/HyperionIManager.cpp index b49d3ddf..268cbf75 100644 --- a/libsrc/hyperion/HyperionIManager.cpp +++ b/libsrc/hyperion/HyperionIManager.cpp @@ -45,6 +45,11 @@ QVector HyperionIManager::getInstanceData() const return instances; } +QList HyperionIManager::getRunningInstanceIdx() const +{ + return _runningInstances.keys(); +} + void HyperionIManager::startAll() { for(const auto & entry : _instanceTable->getAllInstances(true)) diff --git a/libsrc/hyperion/PriorityMuxer.cpp b/libsrc/hyperion/PriorityMuxer.cpp index ae4c64e8..91024744 100644 --- a/libsrc/hyperion/PriorityMuxer.cpp +++ b/libsrc/hyperion/PriorityMuxer.cpp @@ -128,6 +128,11 @@ bool PriorityMuxer::hasPriority(int priority) const return (priority == PriorityMuxer::LOWEST_PRIORITY) ? true : _activeInputs.contains(priority); } +PriorityMuxer::InputsMap PriorityMuxer::getInputInfo() const +{ + return _activeInputs; +} + PriorityMuxer::InputInfo PriorityMuxer::getInputInfo(int priority) const { auto elemIt = _activeInputs.constFind(priority); diff --git a/libsrc/hyperion/SettingsManager.cpp b/libsrc/hyperion/SettingsManager.cpp index 2c1201b1..cd5e8727 100644 --- a/libsrc/hyperion/SettingsManager.cpp +++ b/libsrc/hyperion/SettingsManager.cpp @@ -52,7 +52,7 @@ SettingsManager::SettingsManager(quint8 instance, QObject* parent, bool readonly // get default config QJsonObject defaultConfig; - if (!JsonUtils::readFile(":/hyperion_default.config", defaultConfig, _log)) + if (!JsonUtils::readFile(":/hyperion_default.config", defaultConfig, _log).first) { throw std::runtime_error("Failed to read default config"); } diff --git a/libsrc/hyperion/schema/schema-network.json b/libsrc/hyperion/schema/schema-network.json index e159c95f..b5bca5ae 100644 --- a/libsrc/hyperion/schema/schema-network.json +++ b/libsrc/hyperion/schema/schema-network.json @@ -4,26 +4,13 @@ "required" : true, "properties" : { - "apiAuth" : - { - "type" : "boolean", - "title" : "edt_conf_net_apiAuth_title", - "required" : true, - "default" : true, - "propertyOrder" : 1 - }, "internetAccessAPI" : { "type" : "boolean", "title" : "edt_conf_net_internetAccessAPI_title", "required" : true, "default" : false, - "options": { - "dependencies": { - "apiAuth": true - } - }, - "propertyOrder" : 2 + "propertyOrder" : 1 }, "restirctedInternetAccessAPI" : { @@ -36,7 +23,7 @@ "internetAccessAPI": true } }, - "propertyOrder" : 3 + "propertyOrder" : 2 }, "ipWhitelist" : { @@ -53,7 +40,7 @@ "restirctedInternetAccessAPI": true } }, - "propertyOrder" : 4 + "propertyOrder" : 3 }, "localApiAuth" : { @@ -66,15 +53,7 @@ "apiAuth": true } }, - "propertyOrder" : 5 - }, - "localAdminAuth" : - { - "type" : "boolean", - "title" : "edt_conf_net_localAdminAuth_title", - "required" : true, - "default" : true, - "propertyOrder" : 5 + "propertyOrder" : 4 } }, "additionalProperties" : false diff --git a/libsrc/leddevice/LedDeviceWrapper.cpp b/libsrc/leddevice/LedDeviceWrapper.cpp index cc8177fb..838a5b37 100644 --- a/libsrc/leddevice/LedDeviceWrapper.cpp +++ b/libsrc/leddevice/LedDeviceWrapper.cpp @@ -193,11 +193,17 @@ QJsonObject LedDeviceWrapper::getLedDeviceSchemas() } QJsonObject schema; - if(!JsonUtils::parse(schemaPath, data, schema, Logger::getInstance("LEDDEVICE"))) + QPair parsingResult = JsonUtils::parse(schemaPath, data, schema, Logger::getInstance("LEDDEVICE")); + if (!parsingResult.first) { - throw std::runtime_error("ERROR: JSON schema wrong of file: " + item.toStdString()); + QStringList errorList = parsingResult.second; + for (const auto& errorMessage : errorList) { + Debug(Logger::getInstance("LEDDEVICE"), "JSON parse error: %s ", QSTRING_CSTR(errorMessage)); + } + throw std::runtime_error("ERROR: JSON schema is wrong for file: " + item.toStdString()); } + schemaJson = schema; schemaJson["title"] = QString("edt_dev_spec_header_title"); diff --git a/libsrc/utils/JsonUtils.cpp b/libsrc/utils/JsonUtils.cpp index d43c5bba..1fd44f68 100644 --- a/libsrc/utils/JsonUtils.cpp +++ b/libsrc/utils/JsonUtils.cpp @@ -8,25 +8,26 @@ #include #include #include +#include namespace JsonUtils { - bool readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError) + QPair readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError) { QString data; if(!FileUtils::readFile(path, data, log, ignError)) - return false; + { + return qMakePair(false, QStringList(QString("Error reading file: %1").arg(path))); + } - if(!parse(path, data, obj, log)) - return false; - - return true; + QPair parsingResult = JsonUtils::parse(path, data, obj, log); + return parsingResult; } bool readSchema(const QString& path, QJsonObject& obj, Logger* log) { QJsonObject schema; - if(!readFile(path, schema, log)) + if(!readFile(path, schema, log).first) return false; if(!resolveRefs(schema, obj, log)) @@ -35,80 +36,89 @@ namespace JsonUtils { return true; } - bool parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log) + QPair parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log) { QJsonDocument doc; - if(!parse(path, data, doc, log)) - return false; - + QPair parsingResult = JsonUtils::parse(path, data, doc, log); obj = doc.object(); - return true; + return parsingResult; } - bool parse(const QString& path, const QString& data, QJsonArray& arr, Logger* log) + QPair parse(const QString& path, const QString& data, QJsonArray& arr, Logger* log) { QJsonDocument doc; - if(!parse(path, data, doc, log)) - return false; + QPair parsingResult = JsonUtils::parse(path, data, doc, log); arr = doc.array(); - return true; + return parsingResult; } - bool parse(const QString& path, const QString& data, QJsonDocument& doc, Logger* log) + QPair parse(const QString& path, const QString& data, QJsonDocument& doc, Logger* log) { - //remove Comments in data - QString cleanData = data; + QStringList errorList; + QJsonParseError error; - doc = QJsonDocument::fromJson(cleanData.toUtf8(), &error); + doc = QJsonDocument::fromJson(data.toUtf8(), &error); if (error.error != QJsonParseError::NoError) { - // report to the user the failure and their locations in the document. - int errorLine(0), errorColumn(0); + int errorLine = 1; + int errorColumn = 1; - for( int i=0, count=qMin( error.offset,cleanData.size()); i validate(const QString& file, const QJsonObject& json, const QString& schemaPath, Logger* log) { // get the schema data QJsonObject schema; - if(!readFile(schemaPath, schema, log)) - return false; - if(!validate(file, json, schema, log)) - return false; - return true; + QPair readResult = readFile(schemaPath, schema, log); + if(!readResult.first) + { + return readResult; + } + QPair validationResult = validate(file, json, schema, log); + return validationResult; } - bool validate(const QString& file, const QJsonObject& json, const QJsonObject& schema, Logger* log) + QPair validate(const QString& file, const QJsonObject& json, const QJsonObject& schema, Logger* log) { + QStringList errorList; + QJsonSchemaChecker schemaChecker; schemaChecker.setSchema(schema); if (!schemaChecker.validate(json).first) { - const QStringList & errors = schemaChecker.getMessages(); - for (auto & error : errors) + const QStringList &errors = schemaChecker.getMessages(); + for (const auto& error : errors) { - Error(log, "While validating schema against json data of '%s':%s", QSTRING_CSTR(file), QSTRING_CSTR(error)); + QString errorMessage = QString("JSON parse error: %1") + .arg(error); + errorList.push_back(errorMessage); + Error(log, "%s", QSTRING_CSTR(errorMessage)); } - return false; + return qMakePair(false, errorList); } - return true; + return qMakePair(true, errorList); } bool write(const QString& filename, const QJsonObject& json, Logger* log) diff --git a/libsrc/utils/NetOrigin.cpp b/libsrc/utils/NetOrigin.cpp index d5a486e6..42693491 100644 --- a/libsrc/utils/NetOrigin.cpp +++ b/libsrc/utils/NetOrigin.cpp @@ -1,13 +1,15 @@ #include #include +#include NetOrigin* NetOrigin::instance = nullptr; NetOrigin::NetOrigin(QObject* parent, Logger* log) : QObject(parent) , _log(log) - , _internetAccessAllowed(false) + , _isInternetAccessAllowed(false) + , _isInternetAccessRestricted(false) , _ipWhitelist() { NetOrigin::instance = this; @@ -15,37 +17,73 @@ NetOrigin::NetOrigin(QObject* parent, Logger* log) bool NetOrigin::accessAllowed(const QHostAddress& address, const QHostAddress& local) const { - if(_internetAccessAllowed) - return true; + bool isAllowed {false}; - if(_ipWhitelist.contains(address)) // v4 and v6 - return true; - - if(!isLocalAddress(address, local)) + if(isLocalAddress(address, local)) { - Warning(_log,"Client connection with IP address '%s' has been rejected! It's not whitelisted, access denied.",QSTRING_CSTR(address.toString())); - return false; + isAllowed = true; } - return true; + else + { + if(_isInternetAccessAllowed) + { + if (!_isInternetAccessRestricted) + { + isAllowed = true; + } + else + { + for (const QHostAddress &listAddress : _ipWhitelist) + { + if (address.isEqual(listAddress)) + { + isAllowed = true; + break; + } + } + WarningIf(!isAllowed, _log,"Client connection from IP address '%s' has been rejected! It's not whitelisted.",QSTRING_CSTR(address.toString())); + } + } + } + return isAllowed; } -bool NetOrigin::isLocalAddress(const QHostAddress& address, const QHostAddress& local) const + +bool NetOrigin::isLocalAddress(const QHostAddress& ipAddress, const QHostAddress& /*local*/) const { - if(address.protocol() == QAbstractSocket::IPv4Protocol) + QHostAddress address = ipAddress; + + if (address.isLoopback() || address.isLinkLocal()) { - if(!address.isInSubnet(local, 24)) // 255.255.255.xxx; IPv4 0-32 - { - return false; + return true; + } + + //Convert to IPv4 to check, if an IPv6 address is an IPv4 mapped address + QHostAddress ipv4Address(address.toIPv4Address()); + if (ipv4Address != QHostAddress::AnyIPv4) // ipv4Address is not "0.0.0.0" + { + address = ipv4Address; + } + + QList allInterfaces = QNetworkInterface::allInterfaces(); + for (const QNetworkInterface &networkInterface : allInterfaces) { + QList addressEntries = networkInterface.addressEntries(); + for (const QNetworkAddressEntry &localNetworkAddressEntry : addressEntries) { + QHostAddress localIP = localNetworkAddressEntry.ip(); + + if(localIP.protocol() != QAbstractSocket::NetworkLayerProtocol::IPv4Protocol) + { + continue; + } + + bool isInSubnet = address.isInSubnet(localIP, localNetworkAddressEntry.prefixLength()); + if (isInSubnet) + { + return true; + } } } - else if(address.protocol() == QAbstractSocket::IPv6Protocol) - { - if(!address.isInSubnet(local, 64)) // 2001:db8:abcd:0012:XXXX:XXXX:XXXX:XXXX; IPv6 0-128 - { - return false; - } - } - return true; + return false; } void NetOrigin::handleSettingsUpdate(settings::type type, const QJsonDocument& config) @@ -53,16 +91,19 @@ void NetOrigin::handleSettingsUpdate(settings::type type, const QJsonDocument& c if(type == settings::NETWORK) { const QJsonObject& obj = config.object(); - _internetAccessAllowed = obj["internetAccessAPI"].toBool(false); + _isInternetAccessAllowed = obj["internetAccessAPI"].toBool(false); + _isInternetAccessRestricted = obj["restirctedInternetAccessAPI"].toBool(false); + const QJsonArray arr = obj["ipWhitelist"].toArray(); - const QJsonArray& arr = obj["ipWhitelist"].toArray(); - _ipWhitelist.clear(); + _ipWhitelist.clear(); - for(const auto& e : arr) + for(const auto& item : std::as_const(arr)) { - const QString& entry = e.toString(""); + const QString& entry = item.toString(""); if(entry.isEmpty()) + { continue; + } QHostAddress host(entry); if(host.isNull()) diff --git a/src/hyperion-remote/JsonConnection.cpp b/src/hyperion-remote/JsonConnection.cpp index 669a5bf4..618f30be 100644 --- a/src/hyperion-remote/JsonConnection.cpp +++ b/src/hyperion-remote/JsonConnection.cpp @@ -125,7 +125,7 @@ void JsonConnection::setEffect(const QString &effectName, const QString & effect if (effectArgs.size() > 0) { QJsonObject effObj; - if(!JsonUtils::parse("hyperion-remote-args", effectArgs, effObj, _log)) + if(!JsonUtils::parse("hyperion-remote-args", effectArgs, effObj, _log).first) { throw std::runtime_error("Error in effect arguments, abort"); } @@ -160,7 +160,7 @@ void JsonConnection::createEffect(const QString &effectName, const QString &effe if (effectArgs.size() > 0) { QJsonObject effObj; - if(!JsonUtils::parse("hyperion-remote-args", effectScript, effObj, _log)) + if(!JsonUtils::parse("hyperion-remote-args", effectScript, effObj, _log).first) { throw std::runtime_error("Error in effect arguments, abort"); } @@ -440,7 +440,7 @@ void JsonConnection::setConfig(const QString &jsonString) if (jsonString.size() > 0) { QJsonObject configObj; - if(!JsonUtils::parse("hyperion-remote-args", jsonString, configObj, _log)) + if(!JsonUtils::parse("hyperion-remote-args", jsonString, configObj, _log).first) { throw std::runtime_error("Error in configSet arguments, abort"); } diff --git a/src/hyperion-v4l2/hyperion-v4l2.cpp b/src/hyperion-v4l2/hyperion-v4l2.cpp index ecc6ed58..920be15c 100644 --- a/src/hyperion-v4l2/hyperion-v4l2.cpp +++ b/src/hyperion-v4l2/hyperion-v4l2.cpp @@ -231,7 +231,7 @@ int main(int argc, char** argv) ScreenshotHandler handler("screenshot.png", signalDetectionOffset); QObject::connect(&grabber, SIGNAL(newFrame(Image)), &handler, SLOT(receiveImage(Image))); - grabber.prepare(); + grabber.prepare(); grabber.start(); QCoreApplication::exec(); grabber.stop(); diff --git a/src/hyperiond/systray.cpp b/src/hyperiond/systray.cpp index c0bac537..939e852c 100644 --- a/src/hyperiond/systray.cpp +++ b/src/hyperiond/systray.cpp @@ -84,10 +84,8 @@ void SysTray::createTrayIcon() restartAction = new QAction(tr("&Restart"), this); restartAction->setIcon(QPixmap(":/restart.svg")); - connect(restartAction, &QAction::triggered, this , [=](){ Process::restartHyperion(12); }); + connect(restartAction, &QAction::triggered, this , [=](){ emit signalEvent(Event::Restart); }); - - // TODO: Check if can be done with SystemEvents suspendAction = new QAction(tr("&Suspend"), this); suspendAction->setIcon(QPixmap(":/suspend.svg")); connect(suspendAction, &QAction::triggered, this, [this]() { emit signalEvent(Event::Suspend); }); @@ -129,7 +127,9 @@ void SysTray::createTrayIcon() // add seperator if custom effects exists if (!_trayIconEfxMenu->isEmpty()) + { _trayIconEfxMenu->addSeparator(); + } // build in effects for (const auto &efx : efxs) From 0a93af95c7c2d23fb55e2c302f7194220cead1d8 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Thu, 9 May 2024 11:51:00 +0200 Subject: [PATCH 106/117] Disable Identify for same serial device (#1737) --- assets/webconfig/js/content_leds.js | 139 ++++++++++++++-------------- 1 file changed, 70 insertions(+), 69 deletions(-) diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index f058450a..a0f216a0 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -1053,28 +1053,28 @@ $(document).ready(function () { }); $("#leddevices").off().on("change", function () { - var generalOptions = window.serverSchema.properties.device; + const generalOptions = window.serverSchema.properties.device; - var ledType = $(this).val(); - var specificOptions = window.serverSchema.properties.alldevices[ledType]; + const ledType = $(this).val(); + const specificOptions = window.serverSchema.properties.alldevices[ledType]; conf_editor = createJsonEditor('editor_container_leddevice', { specificOptions: specificOptions, generalOptions: generalOptions, }); - var values_general = {}; - var values_specific = {}; - var isCurrentDevice = (window.serverConfig.device.type == ledType); + let values_general = {}; + let values_specific = {}; + const isCurrentDevice = (window.serverConfig.device.type == ledType); - for (var key in window.serverConfig.device) { + for (const key in window.serverConfig.device) { if (key != "type" && key in generalOptions.properties) values_general[key] = window.serverConfig.device[key]; }; conf_editor.getEditor("root.generalOptions").setValue(values_general); if (isCurrentDevice) { - var specificOptions_val = conf_editor.getEditor("root.specificOptions").getValue(); - for (var key in specificOptions_val) { + const specificOptions_val = conf_editor.getEditor("root.specificOptions").getValue(); + for (const key in specificOptions_val) { values_specific[key] = (key in window.serverConfig.device) ? window.serverConfig.device[key] : specificOptions_val[key]; }; conf_editor.getEditor("root.specificOptions").setValue(values_specific); @@ -1089,9 +1089,9 @@ $(document).ready(function () { createLedDeviceWizards(ledType); conf_editor.on('ready', function () { - var hwLedCountDefault = 1; - var colorOrderDefault = "rgb"; - var filter = {}; + let hwLedCountDefault = 1; + let colorOrderDefault = "rgb"; + let filter = {}; $('#btn_layout_controller').hide(); $('#btn_test_controller').hide(); @@ -1139,58 +1139,55 @@ $(document).ready(function () { .catch(error => { showNotification('danger', "Device discovery for " + ledType + " failed with error:" + error); }); - - hwLedCountDefault = 1; - colorOrderDefault = "rgb"; break; - case "philipshue": + case "philipshue": { disableAutoResolvedGeneralOptions(); - var lights = conf_editor.getEditor("root.specificOptions.lightIds").getValue(); + const lights = conf_editor.getEditor("root.specificOptions.lightIds").getValue(); hwLedCountDefault = lights.length; - colorOrderDefault = "rgb"; + } break; - case "yeelight": + case "yeelight": { disableAutoResolvedGeneralOptions(); - var lights = conf_editor.getEditor("root.specificOptions.lights").getValue(); + const lights = conf_editor.getEditor("root.specificOptions.lights").getValue(); hwLedCountDefault = lights.length; - colorOrderDefault = "rgb"; + } break; - case "atmoorb": + case "atmoorb": { disableAutoResolvedGeneralOptions(); - var configruedOrbIds = conf_editor.getEditor("root.specificOptions.orbIds").getValue().trim(); + const configruedOrbIds = conf_editor.getEditor("root.specificOptions.orbIds").getValue().trim(); if (configruedOrbIds.length !== 0) { hwLedCountDefault = configruedOrbIds.split(",").map(Number).length; } else { hwLedCountDefault = 0; } - colorOrderDefault = "rgb"; + } break; - case "razer": + case "razer": { disableAutoResolvedGeneralOptions(); - hwLedCountDefault = 1; colorOrderDefault = "bgr"; - var subType = conf_editor.getEditor("root.specificOptions.subType").getValue(); - let params = { subType: subType }; + const subType = conf_editor.getEditor("root.specificOptions.subType").getValue(); + const params = { subType }; getProperties_device(ledType, subType, params); + } break; default: } if (ledType !== window.serverConfig.device.type) { - var hwLedCount = conf_editor.getEditor("root.generalOptions.hardwareLedCount"); + let hwLedCount = conf_editor.getEditor("root.generalOptions.hardwareLedCount"); if (hwLedCount) { hwLedCount.setValue(hwLedCountDefault); } - var colorOrder = conf_editor.getEditor("root.generalOptions.colorOrder"); + let colorOrder = conf_editor.getEditor("root.generalOptions.colorOrder"); if (colorOrder) { colorOrder.setValue(colorOrderDefault); } @@ -1199,8 +1196,8 @@ $(document).ready(function () { conf_editor.on('change', function () { // //Check, if device can be identified/tested and/or saved - var canIdentify = false; - var canSave = false; + let canIdentify = false; + let canSave = false; switch (ledType) { @@ -1212,11 +1209,12 @@ $(document).ready(function () { case "udpartnet": case "udpddp": case "udph801": - case "udpraw": - var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + case "udpraw": { + const host = conf_editor.getEditor("root.specificOptions.host").getValue(); if (host !== "") { canSave = true; } + } break; case "adalight": @@ -1224,50 +1222,63 @@ $(document).ready(function () { case "karate": case "dmx": case "sedu": - case "tpm2": - var rate = conf_editor.getEditor("root.specificOptions.rate").getValue(); + case "tpm2": { + let currentDeviceType = window.serverConfig.device.type; + if ($.inArray(currentDeviceType, devSerial) === -1) { + canIdentify = true; + } else { + let output = conf_editor.getEditor("root.specificOptions.output").getValue(); + if (window.serverConfig.device.output !== output) { + canIdentify = true; + } + } + + const rate = conf_editor.getEditor("root.specificOptions.rate").getValue(); if (rate > 0) { canSave = true; } + } break; - case "philipshue": - var host = conf_editor.getEditor("root.specificOptions.host").getValue(); - var username = conf_editor.getEditor("root.specificOptions.username").getValue(); + case "philipshue": { + const host = conf_editor.getEditor("root.specificOptions.host").getValue(); + const username = conf_editor.getEditor("root.specificOptions.username").getValue(); if (host !== "" && username != "") { - var useEntertainmentAPI = conf_editor.getEditor("root.specificOptions.useEntertainmentAPI").getValue(); - var clientkey = conf_editor.getEditor("root.specificOptions.clientkey").getValue(); + const useEntertainmentAPI = conf_editor.getEditor("root.specificOptions.useEntertainmentAPI").getValue(); + const clientkey = conf_editor.getEditor("root.specificOptions.clientkey").getValue(); if (!useEntertainmentAPI || clientkey !== "") { canSave = true; } } + } break; case "wled": - case "cololight": - var hostList = conf_editor.getEditor("root.specificOptions.hostList").getValue(); + case "cololight": { + const hostList = conf_editor.getEditor("root.specificOptions.hostList").getValue(); if (hostList !== "SELECT") { - var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + const host = conf_editor.getEditor("root.specificOptions.host").getValue(); if (host !== "") { canIdentify = true; canSave = true; } } + } break; - case "nanoleaf": - var hostList = conf_editor.getEditor("root.specificOptions.hostList").getValue(); + case "nanoleaf": { + const hostList = conf_editor.getEditor("root.specificOptions.hostList").getValue(); if (hostList !== "SELECT") { - var host = conf_editor.getEditor("root.specificOptions.host").getValue(); - var token = conf_editor.getEditor("root.specificOptions.token").getValue(); + const host = conf_editor.getEditor("root.specificOptions.host").getValue(); + const token = conf_editor.getEditor("root.specificOptions.token").getValue(); if (host !== "" && token !== "") { canIdentify = true; canSave = true; } } + } break; default: - canIdentify = false; canSave = true; } @@ -1395,30 +1406,27 @@ $(document).ready(function () { } }); + conf_editor.watch('root.specificOptions.output', () => { - var output = conf_editor.getEditor("root.specificOptions.output").getValue(); + const output = conf_editor.getEditor("root.specificOptions.output").getValue(); if (output === "NONE" || output === "SELECT" || output === "") { $('#btn_submit_controller').prop('disabled', true); $('#btn_test_controller').prop('disabled', true); $('#btn_test_controller').hide(); - conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(1); showAllDeviceInputOptions("output", false); } else { showAllDeviceInputOptions("output", true); let params = {}; - var canIdentify = false; switch (ledType) { - case "adalight": - canIdentify = true; - break; case "atmo": case "karate": params = { serialPort: output }; getProperties_device(ledType, output, params); break; + case "adalight": case "dmx": case "sedu": case "tpm2": @@ -1437,8 +1445,8 @@ $(document).ready(function () { } if ($.inArray(ledType, devSerial) != -1) { - var rateList = conf_editor.getEditor("root.specificOptions.rateList").getValue(); - var showRate = false; + const rateList = conf_editor.getEditor("root.specificOptions.rateList").getValue(); + let showRate = false; if (rateList == "CUSTOM") { showRate = true; } @@ -1446,13 +1454,6 @@ $(document).ready(function () { } if (!conf_editor.validate().length) { - if (canIdentify) { - $("#btn_test_controller").show(); - $('#btn_test_controller').prop('disabled', false); - } else { - $('#btn_test_controller').hide(); - $('#btn_test_controller').prop('disabled', true); - } if (!window.readOnlyMode) { $('#btn_submit_controller').prop('disabled', false); } @@ -1504,12 +1505,12 @@ $(document).ready(function () { }); conf_editor.watch('root.specificOptions.rateList', () => { - var specOptPath = 'root.specificOptions.'; - var rateList = conf_editor.getEditor("root.specificOptions.rateList"); - if (rateList) { - var val = rateList.getValue(); - var rate = conf_editor.getEditor("root.specificOptions.rate"); + const specOptPath = 'root.specificOptions.'; + const rateList = conf_editor.getEditor("root.specificOptions.rateList"); + let rate = conf_editor.getEditor("root.specificOptions.rate"); + if (rateList) { + const val = rateList.getValue(); switch (val) { case 'CUSTOM': case '': From f6cc92606c5de2f1d2750d37d48ee69883a63c3f Mon Sep 17 00:00:00 2001 From: Thinner77 <59623671+Thinner77@users.noreply.github.com> Date: Mon, 13 May 2024 19:31:51 +0200 Subject: [PATCH 107/117] Change confusing logic (#1731) * Change confusing logic * Change confusing logic #2 * revert indent --------- Co-authored-by: Thinner77 --- include/grabber/video/mediafoundation/MFGrabber.h | 2 +- include/grabber/video/v4l2/V4L2Grabber.h | 2 +- libsrc/grabber/video/mediafoundation/MFGrabber.cpp | 6 +++--- libsrc/grabber/video/v4l2/V4L2Grabber.cpp | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/include/grabber/video/mediafoundation/MFGrabber.h b/include/grabber/video/mediafoundation/MFGrabber.h index 30142069..47c8cc62 100644 --- a/include/grabber/video/mediafoundation/MFGrabber.h +++ b/include/grabber/video/mediafoundation/MFGrabber.h @@ -119,7 +119,7 @@ private: QAtomicInt _currentFrame; ColorRgb _noSignalThresholdColor; bool _signalDetectionEnabled, - _noSignalDetected, + _signalDetected, _initialized, _reload; double _x_frac_min, diff --git a/include/grabber/video/v4l2/V4L2Grabber.h b/include/grabber/video/v4l2/V4L2Grabber.h index 75b375df..5ac00738 100644 --- a/include/grabber/video/v4l2/V4L2Grabber.h +++ b/include/grabber/video/v4l2/V4L2Grabber.h @@ -158,7 +158,7 @@ private: // signal detection int _noSignalCounterThreshold; ColorRgb _noSignalThresholdColor; - bool _standbyActivated, _signalDetectionEnabled, _noSignalDetected; + bool _standbyActivated, _signalDetectionEnabled, _signalDetected; int _noSignalCounter; int _brightness, _contrast, _saturation, _hue; double _x_frac_min; diff --git a/libsrc/grabber/video/mediafoundation/MFGrabber.cpp b/libsrc/grabber/video/mediafoundation/MFGrabber.cpp index ca2b6a35..1cacf4aa 100644 --- a/libsrc/grabber/video/mediafoundation/MFGrabber.cpp +++ b/libsrc/grabber/video/mediafoundation/MFGrabber.cpp @@ -42,7 +42,7 @@ MFGrabber::MFGrabber() , _currentFrame(0) , _noSignalThresholdColor(ColorRgb{0,0,0}) , _signalDetectionEnabled(true) - , _noSignalDetected(false) + , _signalDetected(false) , _initialized(false) , _reload(false) , _x_frac_min(0.25) @@ -580,7 +580,7 @@ void MFGrabber::newThreadFrame(Image image) { if (_noSignalCounter >= _noSignalCounterThreshold) { - _noSignalDetected = true; + _signalDetected = true; Info(_log, "Signal detected"); } @@ -593,7 +593,7 @@ void MFGrabber::newThreadFrame(Image image) } else if (_noSignalCounter == _noSignalCounterThreshold) { - _noSignalDetected = false; + _signalDetected = false; Info(_log, "Signal lost"); } } diff --git a/libsrc/grabber/video/v4l2/V4L2Grabber.cpp b/libsrc/grabber/video/v4l2/V4L2Grabber.cpp index 9ac2972d..d4a73ab9 100644 --- a/libsrc/grabber/video/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/video/v4l2/V4L2Grabber.cpp @@ -81,7 +81,7 @@ V4L2Grabber::V4L2Grabber() , _noSignalThresholdColor(ColorRgb{0,0,0}) , _standbyActivated(false) , _signalDetectionEnabled(true) - , _noSignalDetected(false) + , _signalDetected(false) , _noSignalCounter(0) , _brightness(0) , _contrast(0) @@ -1060,7 +1060,7 @@ void V4L2Grabber::newThreadFrame(Image image) { if (_noSignalCounter >= _noSignalCounterThreshold) { - _noSignalDetected = true; + _signalDetected = true; Info(_log, "Signal detected"); } @@ -1073,7 +1073,7 @@ void V4L2Grabber::newThreadFrame(Image image) } else if (_noSignalCounter == _noSignalCounterThreshold) { - _noSignalDetected = false; + _signalDetected = false; Info(_log, "Signal lost"); } } From 2eb5c87e0eb8238f168756da03dc0ddd0728b226 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Mon, 13 May 2024 20:14:26 +0200 Subject: [PATCH 108/117] New Language + Fixes (#1738) * Fix typos in texts * New language Bulgarian --- assets/webconfig/i18n/bg.json | 93 ++++++++++++++++++++++++++++++++ assets/webconfig/i18n/en.json | 26 ++++----- assets/webconfig/js/languages.js | 4 +- 3 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 assets/webconfig/i18n/bg.json diff --git a/assets/webconfig/i18n/bg.json b/assets/webconfig/i18n/bg.json new file mode 100644 index 00000000..15fa2b7b --- /dev/null +++ b/assets/webconfig/i18n/bg.json @@ -0,0 +1,93 @@ +{ + "conf_effect_path_intro": "Заредете ефекти от дефинираните пътища. Освен това можете да деактивирате отделни ефекти по име, за да ги скриете от всички списъци с ефекти.", + "conf_general_impexp_expbtn": "Експортиране", + "conf_general_impexp_impbtn": "Импортиране", + "conf_general_impexp_l1": "Импортирайте конфигурация, като изберете конфигурационен файл по-долу и щракнете върху „Импортиране“.", + "conf_general_impexp_l2": "Експортирайте конфигурация, като щракнете върху „Експортиране“. Вашият браузър започва изтегляне.", + "conf_general_impexp_title": "Импортиране/Експортиране на Конфигурация", + "conf_general_intro": "Основни настройки около Hyperion и WebUI, които не се вписват в друга категория.", + "conf_general_label_title": "Общи настройки", + "conf_helptable_expl": "Обяснение", + "conf_helptable_option": "Опции", + "conf_leds_layout_checkp1": "Черният светодиод е вашият първи светодиод, първият светодиод е точката, в която въвеждате вашия сигнал с данни.", + "conf_leds_layout_checkp2": "Оформлението винаги е изгледът отпред на вашия телевизор, никога отзад.", + "conf_leds_layout_checkp3": "Уверете се, че посоката е правилна. Сивите светодиоди показват светодиод номер 2 и 3 за визуализиране на посоката на данните.", + "conf_leds_optgroup_network": "Мрежа", + "dashboard_alert_message_confedit": "Вашата конфигурация на Hyperion е променена. За да го приложите, рестартирайте Hyperion.", + "dashboard_alert_message_confedit_t": "Конфигурацията е променена", + "dashboard_alert_message_confsave_success": "Вашата конфигурация на Hyperion е запазена успешно. Вашите промени вече са активни.", + "dashboard_alert_message_confsave_success_t": "Конфигурацията е запаметена", + "dashboard_alert_message_disabled": "Тази инстанция в момента е деактивирана! За да я използвате отново, активирайте я на таблото за управление.", + "dashboard_alert_message_disabled_t": "ЛЕД хардуерна инстанция е деактивирана", + "dashboard_componentbox_label_comp": "Компонент", + "dashboard_componentbox_label_status": "Статус", + "dashboard_componentbox_label_title": "Статус на компонентите", + "dashboard_infobox_label_currenthyp": "Вашата версия на Hyperion:", + "dashboard_infobox_label_instance": "Инстанция:", + "dashboard_infobox_label_latesthyp": "Последната версия на Hyperion:", + "dashboard_infobox_label_platform": "Платформа:", + "dashboard_infobox_label_ports": "Портове", + "dashboard_infobox_label_statush": "Hyperion статус:", + "dashboard_infobox_label_title": "Информация", + "dashboard_infobox_message_updatesuccess": "Вие използвате най-новата версия на Hyperion.", + "dashboard_infobox_message_updatewarning": "Нова версия на Hyperion е налична! ($1)", + "dashboard_newsbox_label_title": "Hyperion-Блог", + "dashboard_newsbox_noconn": "Не можете да се свържете с Hyperion Server, за да извлечете най-новите публикации, вашата интернет връзка работи ли?", + "dashboard_newsbox_readmore": "Прочети повече", + "dashboard_newsbox_visitblog": "Посети Hyperion-Блог", + "edt_conf_webc_port_title": "HTTP порт", + "general_access_default": "По подразбиране", + "general_btn_back": "Назад", + "general_btn_cancel": "Откажи", + "general_btn_continue": "Продължи", + "general_btn_next": "Следващ", + "general_btn_off": "Изключване", + "general_btn_ok": "ОК", + "general_btn_on": "Включване", + "general_btn_restarthyperion": "Рестартирай Hyperion", + "general_btn_save": "Запази", + "general_btn_saveandreload": "Запази и презареди", + "general_btn_yes": "Да", + "general_button_savesettings": "Запази настройките", + "general_col_blue": "синьо", + "general_col_green": "зелено", + "general_col_red": "червено", + "general_comp_LEDDEVICE": "ЛЕД Изход", + "general_country_de": "Германия", + "general_country_es": "Испания", + "general_country_fr": "Франция", + "general_country_it": "Италия", + "general_country_nl": "Холандия", + "general_country_uk": "Англия", + "general_country_us": "САЩ", + "general_speech_cs": "Чешки", + "general_speech_de": "Немски", + "general_speech_en": "Английски", + "general_speech_es": "Испански", + "general_speech_he": "Иврит", + "general_speech_id": "Индонезийски", + "general_speech_it": "Италиански", + "general_speech_uk": "Украински", + "general_webui_title": "Hyperion - Уеб Конфигурация", + "main_ledsim_btn_togglelednumber": "Брой ЛЕД", + "main_ledsim_btn_toggleleds": "Показване на светодиоди", + "main_ledsim_btn_togglelivevideo": "Видео на живо", + "main_ledsim_text": "Визуализация на живо на LED цветове и по избор текущия видео поток на вашето устройство за прихващане.", + "main_ledsim_title": "ЛЕД Визуализация", + "main_menu_about_token": "Относно Hyperion", + "main_menu_colors_conf_token": "Обработка на изображение", + "main_menu_configuration_token": "ЛЕД Инстанции", + "main_menu_dashboard_token": "Табло за управление", + "main_menu_effect_conf_token": "Ефекти", + "main_menu_effectsconfigurator_token": "Конфигуратор на ефекти", + "main_menu_general_conf_token": "Общи", + "main_menu_input_selection_token": "Ибор на вход", + "main_menu_leds_conf_token": "ЛЕД Изход", + "main_menu_logging_token": "Дневник", + "main_menu_network_conf_token": "Мрежови услуги", + "main_menu_remotecontrol_token": "Дистанционно управление", + "main_menu_support_token": "Поддръжка", + "main_menu_system_token": "Система", + "main_menu_update_token": "Актуализация", + "main_menu_webconfig_token": "Уеб конфигурация" +} \ No newline at end of file diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 0d638c53..81f748ef 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -455,13 +455,13 @@ "edt_conf_fw_flat_expl": "One flatbuffer target per configuration item", "edt_conf_fw_flat_itemtitle": "flatbuffer target", "edt_conf_fw_flat_services_discovered_expl": "Hyperion servers discovered providing flatbuffer services", - "edt_conf_fw_flat_services_discovered_title": "Flatbuffer targets discoverded", + "edt_conf_fw_flat_services_discovered_title": "Flatbuffer targets discovered", "edt_conf_fw_flat_title": "List of flatbuffer targets", "edt_conf_fw_heading_title": "Forwarder", "edt_conf_fw_json_expl": "One JSON target per configuration item", "edt_conf_fw_json_itemtitle": "JSON target", "edt_conf_fw_json_services_discovered_expl": "Hyperion servers discovered providing JSON-API services", - "edt_conf_fw_json_services_discovered_title": "JSON targets discoverded", + "edt_conf_fw_json_services_discovered_title": "JSON targets discovered", "edt_conf_fw_json_title": "List of JSON targets", "edt_conf_fw_remote_service_discovered_none": "No remote services discovered", "edt_conf_fw_service_name_expl": "Name of the service provider", @@ -757,17 +757,17 @@ "edt_eff_collision_header": "color collision", "edt_eff_collision_header_desc": "Two color projectiles are sent from random positions and collide with each other", "edt_eff_color": "Color", - "edt_eff_colorHour": "Color hour", + "edt_eff_colorHour": "Color hours", "edt_eff_colorMarker": "Marker color", - "edt_eff_colorMinute": "Color minute", - "edt_eff_colorSecond": "Color second", + "edt_eff_colorMinute": "Color minutes", + "edt_eff_colorSecond": "Color seconds", "edt_eff_colorcount": "Color length", "edt_eff_colorend": "Color end", "edt_eff_colorendtime": "Time to hold start color", "edt_eff_colorevel": "Color level", "edt_eff_colorone": "Color one", "edt_eff_colorrandom": "Random color", - "edt_eff_colorshift": "Color Shift", + "edt_eff_colorshift": "Color shift", "edt_eff_colorstart": "Color start", "edt_eff_colorstarttime": "Time to hold end color", "edt_eff_colortwo": "Color two", @@ -839,7 +839,7 @@ "edt_eff_reversedirection": "Reverse direction", "edt_eff_rotationtime": "Rotation time", "edt_eff_saturation": "Saturation", - "edt_eff_set_post_color": "Set post color after alam", + "edt_eff_set_post_color": "Set post color after alarm", "edt_eff_showseconds": "Show seconds", "edt_eff_sleeptime": "Sleep time", "edt_eff_smooth_custom": "Enable smoothing", @@ -967,6 +967,7 @@ "general_country_us": "United States", "general_disabled": "disabled", "general_enabled": "enabled", + "general_speech_bg": "Bulgarian", "general_speech_ca": "Catalan", "general_speech_cs": "Czech", "general_speech_da": "Danish", @@ -1094,7 +1095,6 @@ "support_label_fbtext": "Share our Hyperion Facebook page and get a notice when new updates are released", "support_label_forumtext": "Showcases, discussions, help and more", "support_label_forumtitle": "Forum", - "support_label_ggtext": "Circle us on Google +!", "support_label_ghtext": "Visit us on GitHub", "support_label_igtext": "Visit us on Instagram to watch the latest Hyperion pictures!", "support_label_intro": "Hyperion is a free, non-profit software. A small team is working on it and this is why we need your steady support.", @@ -1116,7 +1116,7 @@ "update_no_updates_for_branch": "No updates for selected version channel.", "update_versreminder": "Your version: $1", "wiz_atmoorb_desc2": "Now choose which Orbs should be added. The position assigns the lamp to a specific position on your \"picture\". Disabled lamps won't be added. To identify single lamps press the button on the right.", - "wiz_atmoorb_intro1": "This wizards configures Hyperion for AtmoOrbs. Features are the AtmoOrb auto detection, setting each light to a specific position on your picture or disable it and optimise the Hyperion settings automatically! So in short: All you need are some clicks and you are done!", + "wiz_atmoorb_intro1": "This wizard configures Hyperion for AtmoOrbs. Features are the AtmoOrb auto detection, setting each light to a specific position on your picture or disable it and optimise the Hyperion settings automatically! So in short: All you need are some clicks and you are done!", "wiz_atmoorb_title": "AtmoOrb Wizard", "wiz_cc_adjustgamma": "Gamma: What you have to do is, adjust gamma levels of each channel until you have the same perceived amount of each channel. Hint: Neutral is 1.0! For example, if your Grey is a bit reddish it means that you have to increase red gamma to reduce the amount of red (the more gamma, the less amount of color).", "wiz_cc_adjustit": "Adjust your \"$1\", until your are fine with it. Take notice: The more you adjust away from the default value the color spectrum will be limited (Also for all colors in between). Depending on TV/LED color spectrum the results will vary.", @@ -1141,7 +1141,7 @@ "wiz_cc_testintrowok": "Check out the following link to download test videos:", "wiz_cc_title": "Colour calibration wizard", "wiz_cololight_desc2": "Now choose which Cololights should be added. To identify single lights, press the button on the right.", - "wiz_cololight_intro1": "This wizards configures Hyperion for the Cololight system. Features are the Cololight auto detection and tune the Hyperion settings automatically! In short: All you need are some clicks and you are done!
    Note: In case of Cololight Strip, you might need to manually correct the LED count and layout.", + "wiz_cololight_intro1": "This wizard configures Hyperion for the Cololight system. Features are the Cololight auto detection and tune the Hyperion settings automatically! In short: All you need are some clicks and you are done!
    Note: In case of Cololight Strip, you might need to manually correct the LED count and layout.", "wiz_cololight_noprops": "Not able to get device properties - Define Hardware LED count manually", "wiz_cololight_title": "Cololight Wizard", "wiz_guideyou": "The $1 will guide you through the settings. Just press the button!", @@ -1155,7 +1155,7 @@ "wiz_hue_e_desc1": "1. Hyperion searches automatically for a Hue-Bridge, in case it cannot find one you need to provide the hostname or IP-address and push the reload button.
    2. Provide a user id and the clientkey, if you do not have both, create new ones.", "wiz_hue_e_desc2": "3. Choose your entertainment group, which has all your lights inside for use with Hyperion.", "wiz_hue_e_desc3": "4. Choose in which position the respective lamp should be \"in the picture\". A preselection of the position was made based on the configured positions of the lights in the entertainment group. This is just a recommendation and can be customized as desired. You can therefore highlight them briefly by clicking on the right button to improve the selection.", - "wiz_hue_e_intro1": "This wizards configures Hyperion for the well known Philips Hue Entertainment system. Features are: Hue Bridge auto detection, user and clientkey creation, entertainment group selection, setting group lights to a specific position on your picture and optimise the Hyperion settings automatically! So in short: All you need are some clicks and you are done!", + "wiz_hue_e_intro1": "This wizard configures Hyperion for the well known Philips Hue Entertainment system. Features are: Hue Bridge auto detection, user and clientkey creation, entertainment group selection, setting group lights to a specific position on your picture and optimise the Hyperion settings automatically! So in short: All you need are some clicks and you are done!", "wiz_hue_e_noapisupport": "The Wizard has disabled entertainment API support and will continue in classic mode.", "wiz_hue_e_noapisupport_hint": "The option \"Use Hue Entertainment API\" was unchecked.", "wiz_hue_e_noegrpids": "No entertainment groups in this Hue bridge defined.", @@ -1166,7 +1166,7 @@ "wiz_hue_failure_connection": "Timeout: Please press the bridge button within the period of 30 seconds", "wiz_hue_failure_ip": "No Bridge found, please provide a valid hostname or IP-address", "wiz_hue_failure_user": "User not found, create a new one with the button below or input a valid user id and press the \"reload\" symbol.", - "wiz_hue_intro1": "This wizards configures Hyperion for the well known Philips Hue system. Features are Hue Bridge auto detection, user creation, set each hue light to a specific position on your picture or disable it and tune the Hyperion settings automatically! So in short: All you need are some clicks and you are done!", + "wiz_hue_intro1": "This wizard configures Hyperion for the well known Philips Hue system. Features are Hue Bridge auto detection, user creation, set each hue light to a specific position on your picture or disable it and tune the Hyperion settings automatically! So in short: All you need are some clicks and you are done!", "wiz_hue_ip": "Hostname or IP", "wiz_hue_noids": "This Hue bridge has no bulbs/stripes, please pair them before with the Hue Apps", "wiz_hue_press_link": "Please press link button on the Hue Bridge.", @@ -1198,7 +1198,7 @@ "wiz_cc_try_connect": "Connecting...", "wiz_wizavail": "Wizard available", "wiz_yeelight_desc2": "Now choose which lamps should be added. The position assigns the lamp to a specific position on your \"picture\". Disabled lamps won't be added. To identify single lamps press the button on the right.", - "wiz_yeelight_intro1": "This wizards configures Hyperion for the Yeelight system. Features are the Yeelighs' auto detection, setting each light to a specific position on your picture or disable it and tune the Hyperion settings automatically! So in short: All you need are some clicks and you are done!", + "wiz_yeelight_intro1": "This wizard configures Hyperion for the Yeelight system. Features are the Yeelights' auto detection, setting each light to a specific position on your picture or disable it and tune the Hyperion settings automatically! So in short: All you need are some clicks and you are done!", "wiz_yeelight_title": "Yeelight Wizard", "wiz_yeelight_unsupported": "Unsupported" } diff --git a/assets/webconfig/js/languages.js b/assets/webconfig/js/languages.js index bbc5078a..28f9e8c2 100644 --- a/assets/webconfig/js/languages.js +++ b/assets/webconfig/js/languages.js @@ -1,6 +1,6 @@ var storedLang; -var availLang = ['ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'fr', 'he', 'hu', 'id', 'it', 'ja', 'nl', 'nb', 'pl', 'pt', 'ro', 'ru', 'sv', 'tr', 'uk', 'vi', 'zh-CN']; -var availLangText = ['Català', 'Čeština', 'Dansk', 'Deutsch', 'Ελληνική', 'English', 'Español', 'Français', 'עִברִית' ,'Magyar', 'Indonesia', 'Italiano', '日本語', 'Nederlands', 'Norsk Bokmål', 'Polski', 'Português', 'Română', 'русский', 'Svenska', 'Türkçe', 'Українська', 'Tiếng Việt', '汉语']; +var availLang = ['bg', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'fr', 'he', 'hu', 'id', 'it', 'ja', 'nl', 'nb', 'pl', 'pt', 'ro', 'ru', 'sv', 'tr', 'uk', 'vi', 'zh-CN']; +var availLangText = ['Български', 'Català', 'Čeština', 'Dansk', 'Deutsch', 'Ελληνική', 'English', 'Español', 'Français', 'עִברִית' ,'Magyar', 'Indonesia', 'Italiano', '日本語', 'Nederlands', 'Norsk Bokmål', 'Polski', 'Português', 'Română', 'русский', 'Svenska', 'Türkçe', 'Українська', 'Tiếng Việt', '汉语']; //$.i18n.debug = true; From be0eb23b6ffc6f002f74bc65401cb6d2dc1b83d2 Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Mon, 13 May 2024 20:16:59 +0200 Subject: [PATCH 109/117] Update de.json (POEditor.com) --- assets/webconfig/i18n/de.json | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/assets/webconfig/i18n/de.json b/assets/webconfig/i18n/de.json index 56848b0a..a41167a6 100644 --- a/assets/webconfig/i18n/de.json +++ b/assets/webconfig/i18n/de.json @@ -360,7 +360,7 @@ "edt_conf_enum_NTSC": "NTSC", "edt_conf_enum_PAL": "PAL", "edt_conf_enum_SECAM": "SECAM", - "edt_conf_enum_VERTICAL": "Horizontal", + "edt_conf_enum_VERTICAL": "Vertikal", "edt_conf_enum_action_idle": "Leerlauf", "edt_conf_enum_action_restart": "Neustart", "edt_conf_enum_action_resume": "Aktivieren", @@ -496,15 +496,12 @@ "edt_conf_log_level_expl": "Abhängig der Stufe sind weniger oder mehr Meldungen sichtbar.", "edt_conf_log_level_title": "Protokollstufe", "edt_conf_net_apiAuth_expl": "Zwinge alle Anwendungen, welche die Hyperion API nutzen sich zu authentifizieren. Aktivieren für höhere Sicherheit, da nun jede neue Anwendung einmalig von dir bestätigt werden muss.", - "edt_conf_net_apiAuth_title": "API-Authentifizierung", "edt_conf_net_heading_title": "Network", "edt_conf_net_internetAccessAPI_expl": "Erlaube Zugriff auf das Hyperion API/Webinterface über das Internet. Deaktiviere den Zugriff für höhere Sicherheit.", "edt_conf_net_internetAccessAPI_title": "Internet API-Zugriff", "edt_conf_net_ipWhitelist_expl": "Anstatt den Zugriff für alle Verbindungen aus dem Internet zu erlauben kannst du hier Ausnahmen für zugelassene IP-Adressen hinzufügen.", "edt_conf_net_ipWhitelist_title": "Erlaubte IP-Adressen", "edt_conf_net_ip_itemtitle": "IP", - "edt_conf_net_localAdminAuth_expl": "Wenn aktiviert, muss der Administrationszugriff aus dem Heimnetzwerk mit einem Passwort authentifiziert werden.", - "edt_conf_net_localAdminAuth_title": "Lokale Admin Authentifizierung", "edt_conf_net_localApiAuth_expl": "Wenn aktiviert, müssen Verbindungen aus dem Heimnetzwerk mit einem Token authentifiziert werden.", "edt_conf_net_localApiAuth_title": "Lokale API-Authentifizierung", "edt_conf_net_restirctedInternetAccessAPI_expl": "Den Zugriff auf die API über das Internet auf bestimmte IP-Adressen beschränken", @@ -513,7 +510,7 @@ "edt_conf_os_events_lockEnable_title": "Reagiere auf Bildschirmsperre", "edt_conf_os_events_suspendEnable_expl": "Reagiere auf Ereignisse, die das Betriebssystem aussetzen/fortsetzen", "edt_conf_os_events_suspendEnable_title": "Reagiere auf Ruhezusstand", - "edt_conf_os_events_suspendOnLockEnable_expl": "Wechsel in den Ruhezustand, wenn der Bildschirm gesperrt ist; andernfalls in den Leerlaufmodus wechseln", + "edt_conf_os_events_suspendOnLockEnable_expl": "Wechsel in den Ruhezustand, wenn der Bildschirm gesperrt ist, andernfalls in den Leerlaufmodus wechseln", "edt_conf_os_events_suspendOnLockEnable_title": "Leerlauf, bei Bildschirmsperre", "edt_conf_pbs_heading_title": "Protocol Buffers Server", "edt_conf_pbs_timeout_expl": "Wenn für die angegebene Zeit keine Daten empfangen werden, wird die Komponente (vorübergehend) deaktiviert", @@ -560,7 +557,7 @@ "edt_conf_v4l2_encoding_title": "Videokodierungsformat", "edt_conf_v4l2_flip_expl": "Hiermit kannst du das Bild in horizontaler, vertikaler oder in beide Richtungen spiegeln.", "edt_conf_v4l2_flip_title": "Spiegelung", - "edt_conf_v4l2_fpsSoftwareDecimation_expl": "Jeder n-te Frame wird übersprungen um Ressourcen zu sparen.\nBeispiel: Ein Wert von 5 resultiert bei einem Aufnahmegerät mit 30fps in einer neuen Framerate von 6 fps.", + "edt_conf_v4l2_fpsSoftwareDecimation_expl": "Um Ressourcen zu sparen, wird nur jedes n-te Bild verarbeitet. Wenn z.B. der Grabber auf 30fps eingestellt ist und diese Option auf 5 gesetzt ist, wird das Endergebnis ca. 6fps sein.", "edt_conf_v4l2_fpsSoftwareDecimation_title": "Überspringen von Frames", "edt_conf_v4l2_framerate_expl": "Die unterstützten Bilder pro Sekunde des aktiven Gerätes. Auf 'Automatisch' wird der gewählte Modus vom v4l interface beibehalten.", "edt_conf_v4l2_framerate_title": "Bilder pro Sekunde", @@ -758,7 +755,7 @@ "edt_eff_colorHour": "Farbe Stunde", "edt_eff_colorMarker": "Marker Farbe", "edt_eff_colorMinute": "Farbe Minute", - "edt_eff_colorSecond": "Farbe Sekunde", + "edt_eff_colorSecond": "Farbe der Sekunden", "edt_eff_colorcount": "Farblänge", "edt_eff_colorend": "Farbe Ende", "edt_eff_colorendtime": "Zeit für Start-Farbe", @@ -1071,7 +1068,7 @@ "remote_input_sourceactiv_btn": "Quelle aktiv", "remote_input_status": "Status/Aktion", "remote_losthint": "Hinweis: Alle Änderungen gehen nach einem Neustart verloren.", - "remote_maptype_intro": "Für gewöhnlich entscheidet dein LED-Layout welcher Bildbereich welche LED zugewiesen bekommt, dies kann hier geändert werden. $1", + "remote_maptype_intro": "Für gewöhnlich entscheidet dein LED-Layout welcher Bildbereich welche LED zugewiesen bekommt, dies kann hier geändert werden: $1", "remote_maptype_label": "LED-Bereich Zuordnung", "remote_maptype_label_dominant_color": "Dominante Farbe", "remote_maptype_label_dominant_color_advanced": "Dominante Farbe fortgeschritten", @@ -1096,7 +1093,6 @@ "support_label_fbtext": "Teile Inhalte in Facebook und halte dich und andere auf dem Laufenden", "support_label_forumtext": "Diskussion und Hilfestellung von der Community", "support_label_forumtitle": "Forum", - "support_label_ggtext": "Platziere uns in deinen Kreisen auf Google+", "support_label_ghtext": "Besuche uns auf GitHub", "support_label_igtext": "Schau doch mal bei Instagram vorbei!", "support_label_intro": "Hyperion ist ein kostenloses Open Source Projekt und ein kleines Team arbeitet an seiner Weiterentwicklung. Darum benötigen wir DEINE Unterstützung, um weiter in bessere Infrastruktur und Weiterentwicklung investieren zu können.", From 4645b671c72a99af92472fa803d712934b977f90 Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Mon, 13 May 2024 20:17:01 +0200 Subject: [PATCH 110/117] Update es.json (POEditor.com) --- assets/webconfig/i18n/es.json | 74 +++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/assets/webconfig/i18n/es.json b/assets/webconfig/i18n/es.json index be9ec081..a6c18c11 100644 --- a/assets/webconfig/i18n/es.json +++ b/assets/webconfig/i18n/es.json @@ -22,6 +22,8 @@ "about_resources": "$1 librerías", "about_translations": "Traducciones", "about_version": "Versión", + "conf_cec_events_heading_title": "Eventos CEC", + "conf_cec_events_intro": "Ajustes relacionados con los diferentes eventos del protocolo CEC (Consumer Electronics Control) que Hyperion puede gestionar", "conf_colors_blackborder_intro": "Omite bordes negros dondequiera que estén. Cada modo usa otro algoritmo de detección que está ajustado para situaciones especiales. Sube el umbral si no percibes funcionamiento.", "conf_colors_color_intro": "Crea uno o más perfiles de calibración, ajusta cada color, brillo, linealización y más.", "conf_colors_smoothing_intro": "El suavizado aplana los cambios de color/brillo para reducir la distracción molesta.", @@ -82,6 +84,8 @@ "conf_leds_layout_cl_bottomright": "Inferior Derecha (Esquina)", "conf_leds_layout_cl_cornergap": "Hueco de esquina", "conf_leds_layout_cl_edgegap": "Hueco de borde", + "conf_leds_layout_cl_entertainment": "Área de Entretenimiento", + "conf_leds_layout_cl_entertainment_center": "Centro del Área de Entretenimiento", "conf_leds_layout_cl_gaglength": "Longitud de hueco", "conf_leds_layout_cl_gappos": "Posición del hueco", "conf_leds_layout_cl_hleddepth": "Profundidad LED Horizontal", @@ -111,7 +115,13 @@ "conf_leds_layout_cl_topright": "Superior Derecha (Esquina)", "conf_leds_layout_cl_vleddepth": "Profundidad LED vertical", "conf_leds_layout_frame": "Disposición Clásica (Marco LED)", + "conf_leds_layout_gapbottom": "Hueco Inferior", + "conf_leds_layout_gapleft": "Hueco izquierdo", + "conf_leds_layout_gapright": "Hueco derecho", + "conf_leds_layout_gaptop": "Hueco superior", "conf_leds_layout_generatedconf": "Configuración LED Generada/Actual", + "conf_leds_layout_generation_error": "Trazado de LED no generado", + "conf_leds_layout_generation_success": "Trazado de LED generado correctamente", "conf_leds_layout_intro": "Necesitas también un diseño led, que refleje tus posiciones led. La disposición clásica es el marco generalmente usado de la TV, pero también apoyamos la creación de matriz led (paredes led). La vista en esta disposición es SIEMPRE del FRENTE de tu TV.", "conf_leds_layout_ma_cabling": "Cableado", "conf_leds_layout_ma_direction": "Dirección", @@ -184,6 +194,10 @@ "conf_network_tok_intro": "Aquí puedes crear y eliminar Tokens para la autenticación de la API. Los Tokens creados sólo se mostrarán una vez.", "conf_network_tok_lastuse": "Último uso", "conf_network_tok_title": "Gestión de Tokens", + "conf_os_events_heading_title": "Eventos del sistema operativo", + "conf_os_events_intro": "Ajustes relacionados con diferentes eventos del sistema operativo que Hyperion puede gestionar", + "conf_sched_events_heading_title": "Eventos programados", + "conf_sched_events_intro": "Ajustes relacionados con eventos programados, es decir, basados en el tiempo, que Hyperion gestionará", "conf_webconfig_label_intro": "Ajustes de configuración web. Editar sabiamente.", "dashboard_active_instance": "Instalación seleccionada", "dashboard_alert_message_confedit": "Se ha modificado la configuración de Hyperion. Para aplicarlo, reinicia Hyperion.", @@ -235,6 +249,9 @@ "edt_append_pixel": "Píxel", "edt_append_s": "s", "edt_append_sdegree": "s/grado", + "edt_conf_action_expl": "Acción a ser aplicada", + "edt_conf_action_record_validation_error": "Un mismo evento sólo puede desencadenar una acción. Limpiar Acciones $1", + "edt_conf_action_title": "Acción", "edt_conf_audio_device_expl": "Dispositivo de entrada de audio seleccionado", "edt_conf_audio_device_title": "Dispositivo de Audio", "edt_conf_audio_effect_enum_vumeter": "Medidor-UV", @@ -271,6 +288,17 @@ "edt_conf_bb_unknownFrameCnt_title": "Fotogramas desconocidos", "edt_conf_bge_heading_title": "Efecto/color de fondo", "edt_conf_bobls_heading_title": "Servidor Boblight", + "edt_conf_cec_actions_header_expl": "Definir qué acción debe llevarse a cabo en un acontecimiento CEC reconocido", + "edt_conf_cec_actions_header_item_title": "Acción", + "edt_conf_cec_actions_header_title": "Acciones", + "edt_conf_cec_button_release_delay_ms_expl": "Tiempo de liberación del botón remoto", + "edt_conf_cec_button_release_delay_ms_title": "Tiempo de liberación del botón", + "edt_conf_cec_button_repeat_rate_ms_expl": "Tasa de repetición de botones remotos", + "edt_conf_cec_button_repeat_rate_ms_title": "Tasa de repetición de botones", + "edt_conf_cec_double_tap_timeout_ms_expl": "Retardo de pulsación de botón remoto antes de repetir", + "edt_conf_cec_double_tap_timeout_ms_title": "Retardo del botón antes de repetir", + "edt_conf_cec_event_expl": "Evento CEC que desencadenará una acción", + "edt_conf_cec_event_title": "Evento CEC", "edt_conf_color_accuracyLevel_expl": "Nivel de precisión con el que se evalúan los colores dominantes. Un nivel más alto crea resultados más precisos, pero también requiere más potencia de procesamiento. Debe combinarse con un procesamiento de píxeles reducido.", "edt_conf_color_accuracyLevel_title": "Nivel de precisión", "edt_conf_color_backlightColored_expl": "Añade un poco de color a tu retroiluminación.", @@ -333,6 +361,13 @@ "edt_conf_enum_PAL": "PAL", "edt_conf_enum_SECAM": "SECAM", "edt_conf_enum_VERTICAL": "Vertical", + "edt_conf_enum_action_idle": "Inactivo", + "edt_conf_enum_action_restart": "Reiniciar", + "edt_conf_enum_action_resume": "Reanudar", + "edt_conf_enum_action_resumeIdle": "Reanudar Inactividad", + "edt_conf_enum_action_suspend": "Suspender", + "edt_conf_enum_action_toggleIdle": "Alternar Inactividad", + "edt_conf_enum_action_toggleSuspend": "Alternar Suspension", "edt_conf_enum_automatic": "Automático", "edt_conf_enum_bbclassic": "Clásico", "edt_conf_enum_bbdefault": "Predeterminado", @@ -341,6 +376,12 @@ "edt_conf_enum_bgr": "BGR", "edt_conf_enum_bottom_up": "De abajo a arriba", "edt_conf_enum_brg": "BRG", + "edt_conf_enum_cec_key_f1_blue": "Botón azul pulsado", + "edt_conf_enum_cec_key_f2_red": "Botón rojo pulsado", + "edt_conf_enum_cec_key_f3_green": "Botón verde pulsado", + "edt_conf_enum_cec_key_f4_yellow": "Botón amarillo pulsado", + "edt_conf_enum_cec_opcode_set stream path": "TV encendida", + "edt_conf_enum_cec_opcode_standby": "TV apagada", "edt_conf_enum_color": "Color", "edt_conf_enum_custom": "Personalizado", "edt_conf_enum_decay": "Degradación", @@ -455,22 +496,28 @@ "edt_conf_log_level_expl": "Dependiendo del nivel de registro verás menos o más mensajes en tu registro.", "edt_conf_log_level_title": "Nivel de registro", "edt_conf_net_apiAuth_expl": "Imponer a todas las aplicaciones que utilizan la API de Hyperion a autenticarse contra Hyperion (Excepción: \"Autenticación de la API local\"). Mayor seguridad, ya que se controla el acceso y se revoca en cualquier momento.", - "edt_conf_net_apiAuth_title": "Autenticación de API", "edt_conf_net_heading_title": "Red", "edt_conf_net_internetAccessAPI_expl": "Permite el acceso a la API/interfaz web de Hyperion desde Internet, desactivado para mayor seguridad.", "edt_conf_net_internetAccessAPI_title": "Acceso a la API de Internet", "edt_conf_net_ipWhitelist_expl": "Puedes hacer una lista blanca de direcciones IP en vez de permitir que todas las conexiones de internet se conecten a la API/Webinterface de Hyperion.", "edt_conf_net_ipWhitelist_title": "IPs de la lista blanca", "edt_conf_net_ip_itemtitle": "IP", - "edt_conf_net_localAdminAuth_expl": "Cuando está habilitado, el acceso de administración desde tu red local necesita una contraseña.", - "edt_conf_net_localAdminAuth_title": "Autenticación de la API de administración local", "edt_conf_net_localApiAuth_expl": "Cuando está habilitado, las conexiones de tu red doméstica también necesitan autenticarse contra Hyperion.", "edt_conf_net_localApiAuth_title": "Autenticación de API local", "edt_conf_net_restirctedInternetAccessAPI_expl": "Puedes restringir el acceso a la API a través de Internet a determinadas IP.", "edt_conf_net_restirctedInternetAccessAPI_title": "Restringir a las IP", + "edt_conf_os_events_lockEnable_expl": "Escuchar eventos de bloqueo/desbloqueo", + "edt_conf_os_events_lockEnable_title": "Escuchar eventos de bloqueo", + "edt_conf_os_events_suspendEnable_expl": "Escuchar eventos de suspension/resumen del sistema operativo", + "edt_conf_os_events_suspendEnable_title": "Escuchar eventos de suspensión", + "edt_conf_os_events_suspendOnLockEnable_expl": "Suspender cuando la pantalla está bloqueada, de lo contrario pasa al modo inactivo", + "edt_conf_os_events_suspendOnLockEnable_title": "Suspender cuando esté bloqueado", "edt_conf_pbs_heading_title": "Servidor de Buffers de Protocolo", "edt_conf_pbs_timeout_expl": "Si no se reciben datos para el período dado, el componente se desactivará (suavemente).", "edt_conf_pbs_timeout_title": "Tiempo de espera", + "edt_conf_sched_actions_header_expl": "Defina qué acción debe tener lugar en un momento determinado. La acción se programará diariamente.", + "edt_conf_sched_actions_header_item_title": "Acción", + "edt_conf_sched_actions_header_title": "Acciones", "edt_conf_smooth_continuousOutput_expl": "Actualizar los LED incluso si no hay cambio de imagen.", "edt_conf_smooth_continuousOutput_title": "Salida continua", "edt_conf_smooth_decay_expl": "La velocidad de degradación. 1 es lineal, los valores mayores tienen un efecto más fuerte.", @@ -488,6 +535,8 @@ "edt_conf_smooth_updateDelay_title": "Retardo de actualización", "edt_conf_smooth_updateFrequency_expl": "La velocidad de salida a tu controlador led.", "edt_conf_smooth_updateFrequency_title": "Frecuencia de actualización", + "edt_conf_time_event_expl": "Momento que desencadenará una acción", + "edt_conf_time_event_title": "Tiempo", "edt_conf_v4l2_blueSignalThreshold_expl": "Oscurece los valores bajos de color azul (reconocidos como negros)", "edt_conf_v4l2_blueSignalThreshold_title": "Umbral de señal azul", "edt_conf_v4l2_cecDetection_expl": "Si está activado, la captura USB se desactivará temporalmente cuando el evento de espera de CEC se reciba desde el bus HDMI.", @@ -560,6 +609,8 @@ "edt_conf_webc_port_title": "Puerto HTTP", "edt_conf_webc_sslport_expl": "Puerto del servidor web HTTPS", "edt_conf_webc_sslport_title": "Puerto HTTPS", + "edt_dev_auth_key_title": "Token de Autorización", + "edt_dev_auth_key_title_info": "Token de Autorización requerido para acceder al dispositivo", "edt_dev_enum_sub_min_cool_adjust": "Min. Ajuste fresco", "edt_dev_enum_sub_min_warm_adjust": "Min. Ajuste caliente", "edt_dev_enum_subtract_minimum": "Restar el mínimo", @@ -677,6 +728,7 @@ "edt_dev_spec_transistionTime_title": "Tiempo de transición", "edt_dev_spec_uid_title": "UID", "edt_dev_spec_universe_title": "Universo", + "edt_dev_spec_useAPIv2_title": "Usar API v2", "edt_dev_spec_useEntertainmentAPI_title": "Usar la API de entretenimiento de Hue", "edt_dev_spec_useOrbSmoothing_title": "Utilizar suavizado de orbe", "edt_dev_spec_useRgbwProtocol_title": "Utilizar el protocolo RGBW", @@ -748,6 +800,8 @@ "edt_eff_ledlist": "Lista Led", "edt_eff_ledtest_header": "Prueba de Led", "edt_eff_ledtest_header_desc": "Salida giratoria: Rojo, Azul, Verde, Blanco, Negro", + "edt_eff_ledtest_seq_header": "Test LED - Secuencia", + "edt_eff_ledtest_seq_header_desc": "Encender los LED en secuencia", "edt_eff_length": "Longitud", "edt_eff_lightclock_header": "Reloj de luz", "edt_eff_lightclock_header_desc": "¡Un verdadero reloj como la luz! Ajustar los colores de las horas, los minutos, los segundos. También hay disponible un marcador opcional de 3/6/9/12 en punto. En caso de que el reloj esté equivocado, debes revisar el reloj de tu sistema.", @@ -916,7 +970,9 @@ "general_speech_en": "Inglés", "general_speech_es": "Español", "general_speech_fr": "Francés", + "general_speech_he": "Hebreo", "general_speech_hu": "Húngaro", + "general_speech_id": "Indonesio", "general_speech_it": "Italiano", "general_speech_ja": "Japonés", "general_speech_nb": "Noruego (Bokmål)", @@ -927,6 +983,7 @@ "general_speech_ru": "Ruso", "general_speech_sv": "Sueco", "general_speech_tr": "Turco", + "general_speech_uk": "Ucraniano", "general_speech_vi": "Vietnamita", "general_speech_zh-CN": "Chino (simplificado)", "general_webui_title": "Hyperion - Configuración Web", @@ -974,6 +1031,8 @@ "main_menu_dashboard_token": "Cuadro de mandos", "main_menu_effect_conf_token": "Efectos", "main_menu_effectsconfigurator_token": "Configurador de Efectos", + "main_menu_event_services_token": "Servicios de Evento", + "main_menu_events": "Servicios de Evento", "main_menu_general_conf_token": "General", "main_menu_grabber_conf_token": "Hardware de Captura", "main_menu_input_selection_token": "Selección de entrada", @@ -1034,7 +1093,6 @@ "support_label_fbtext": "Comparte nuestra página de Hyperion en Facebook y obten un aviso cuando se publiquen nuevas actualizaciones", "support_label_forumtext": "Casos de ejemplo, discusiones, ayuda y mucho más", "support_label_forumtitle": "Foro", - "support_label_ggtext": "¡Haznos un círculo en Google+!", "support_label_ghtext": "Visitanos en Github", "support_label_igtext": "¡Visítanos en Instagram para ver las últimas imágenes de Hyperion!", "support_label_intro": "Hyperion es un software libre sin fines de lucro. Un pequeño equipo está trabajando en ello y es por eso que necesitamos tu apoyo constante.", @@ -1086,6 +1144,7 @@ "wiz_cololight_noprops": "Imposible obtener las propiedades del dispositivo - Define el conteo de LEDs de hardware manualmente", "wiz_cololight_title": "Asistente Cololight", "wiz_guideyou": "El $1 te guiará a través de los ajustes. Simplemente ¡presiona el botón!", + "wiz_hue_blinkblue": "Deja que se ilumine", "wiz_hue_clientkey": "Llave de cliente:", "wiz_hue_create_user": "Crear Usuario", "wiz_hue_desc1": "1. Busca automáticamente un puente Hue, en caso de que no encuentre uno necesitas proporcionar la dirección IP y pulsar el botón de recarga a la derecha. Ahora necesitas una identificación de usuario, si no tienes una, crea una nueva.", @@ -1118,6 +1177,13 @@ "wiz_identify_tip": "Identificar el dispositivo configurado iluminándolo", "wiz_ids_disabled": "Desactivado", "wiz_ids_entire": "Toda la imagen", + "wiz_layout": "Generar Trazado", + "wiz_layout_tip": "Generar un diseño para el dispositivo configurado", + "wiz_nanoleaf_failure_auth_token": "Pulse el botón de encendido/apagado de Nanoleaf antes de 30 segundos.", + "wiz_nanoleaf_failure_auth_token_t": "Tiempo de espera de generación de token de autorización de usuario", + "wiz_nanoleaf_press_onoff_button": "Pulsa el botón de encendido/apagado de su dispositivo Nanoleaf durante 5-7 segundos", + "wiz_nanoleaf_user_auth_intro": "El asistente ayuda a generar un token de autorización de usuario necesario para que Hyperion pueda acceder al dispositivo.", + "wiz_nanoleaf_user_auth_title": "Asistente para generar tokens de autorización", "wiz_noLights": "¡No se encontró $1! Por favor, conecta las luces a la red o configúralas manualmente.", "wiz_pos": "Posición/Estado", "wiz_rgb_expl": "El punto de color cambia cada x segundos el color (rojo, verde), al mismo tiempo que tus leds cambian el color también. Responde las preguntas en la parte inferior para verificar/corregir tu orden de bytes.", From 38b00029168fcc3248fd9490c53c21dedcfaf2a7 Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Mon, 13 May 2024 20:17:02 +0200 Subject: [PATCH 111/117] Update sv.json (POEditor.com) --- assets/webconfig/i18n/sv.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/assets/webconfig/i18n/sv.json b/assets/webconfig/i18n/sv.json index 2bf66b95..b5ad8f86 100644 --- a/assets/webconfig/i18n/sv.json +++ b/assets/webconfig/i18n/sv.json @@ -191,6 +191,7 @@ "conf_network_tok_diaTitle": "Ny nyckel skapad!", "conf_network_tok_grantMsg": "En app begär åtkomst till Hyperion API via en nyckel. Vill du tillåta detta? Vänligen kontrollera informationen!", "conf_network_tok_grantT": "App-nyckel begärd", + "conf_network_tok_idhead": "ID", "conf_network_tok_intro": "Här kan du skapa eller ta bort nycklar för API-autentisering. Nyskapade nycklar visas en gång.", "conf_network_tok_lastuse": "Senast använd", "conf_network_tok_title": "Nyckelhantering", @@ -496,15 +497,12 @@ "edt_conf_log_level_expl": "Beroende på nivå är färre eller fler meddelanden synliga.", "edt_conf_log_level_title": "Loggnivå", "edt_conf_net_apiAuth_expl": "Tvinga alla applikationer som använder Hyperion API att autentisera sig själva. Aktivera för högre säkerhet, eftersom varje ny ansökan nu måste bekräftas av dig en gång.", - "edt_conf_net_apiAuth_title": "API-autentisering", "edt_conf_net_heading_title": "Nätverk", "edt_conf_net_internetAccessAPI_expl": "Tillåt åtkomst till Hyperion API/webbgränssnitt över Internet. Inaktivera åtkomst för ökad säkerhet.", "edt_conf_net_internetAccessAPI_title": "Internet API-åtkomst", "edt_conf_net_ipWhitelist_expl": "Istället för att tillåta åtkomst för alla anslutningar från internet kan du lägga till undantag för tillåtna IP-adresser här.", "edt_conf_net_ipWhitelist_title": "Tillåtna IP-adresser", "edt_conf_net_ip_itemtitle": "IP", - "edt_conf_net_localAdminAuth_expl": "Om den är aktiverad måste administrationsåtkomst från hemnätverket autentiseras med ett lösenord.", - "edt_conf_net_localAdminAuth_title": "Lokal administratörsautentisering", "edt_conf_net_localApiAuth_expl": "Om den är aktiverad måste anslutningar från hemnätverket autentiseras med en nyckel.", "edt_conf_net_localApiAuth_title": "Lokal API-autentisering", "edt_conf_net_restirctedInternetAccessAPI_expl": "Begränsa åtkomsten till API:t över internet till specifika IP-adresser", @@ -1096,7 +1094,6 @@ "support_label_fbtext": "Dela innehåll på Facebook och håll dig själv och andra uppdaterade", "support_label_forumtext": "Diskussion och hjälp från samhället", "support_label_forumtitle": "Forum", - "support_label_ggtext": "Placera oss i dina cirklar på Google+", "support_label_ghtext": "Besök oss på GitHub", "support_label_igtext": "Ta en titt på Instagram!", "support_label_intro": "Hyperion är ett gratis projekt med öppen källkod och ett litet team arbetar med vidareutvecklingen. Det är därför vi behöver DITT stöd för att fortsätta investera i bättre infrastruktur och vidareutveckling.", From 051072ee46517e355f8650547e3c947f352abfe0 Mon Sep 17 00:00:00 2001 From: Hyperion-Bot <20935312+Hyperion-Bot@users.noreply.github.com> Date: Mon, 13 May 2024 20:17:03 +0200 Subject: [PATCH 112/117] Update bg.json (POEditor.com) From 4a5b0b6bf22c2ac1968d0b0f258a3bf432d851f0 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sat, 18 May 2024 09:14:30 +0200 Subject: [PATCH 113/117] Fix #1688 - Fragmented http-Headers (#1741) --- libsrc/webserver/QtHttpClientWrapper.cpp | 253 ++++++++++++----------- libsrc/webserver/QtHttpClientWrapper.h | 1 + 2 files changed, 130 insertions(+), 124 deletions(-) diff --git a/libsrc/webserver/QtHttpClientWrapper.cpp b/libsrc/webserver/QtHttpClientWrapper.cpp index 4ea3bd83..ea9aeb0d 100644 --- a/libsrc/webserver/QtHttpClientWrapper.cpp +++ b/libsrc/webserver/QtHttpClientWrapper.cpp @@ -37,7 +37,7 @@ QString QtHttpClientWrapper::getGuid (void) { m_guid = QString::fromLocal8Bit ( QCryptographicHash::hash ( - QByteArray::number ((quint64) (this)), + QByteArray::number (reinterpret_cast(this)), QCryptographicHash::Md5 ).toHex () ); @@ -50,66 +50,70 @@ void QtHttpClientWrapper::onClientDataReceived (void) { if (m_sockClient != Q_NULLPTR) { - while (m_sockClient->bytesAvailable ()) + while (m_sockClient->bytesAvailable () != 0) { QByteArray line = m_sockClient->readLine (); switch (m_parsingStatus) // handle parsing steps { - case AwaitingRequest: // "command url version" × 1 + case AwaitingRequest: // "command url version" × 1 + { + QString str = QString::fromUtf8 (line).trimmed (); + QStringList parts = QStringUtils::split(str,SPACE, QStringUtils::SplitBehavior::SkipEmptyParts); + if (parts.size () == 3) { - QString str = QString::fromUtf8 (line).trimmed (); - QStringList parts = QStringUtils::split(str,SPACE, QStringUtils::SplitBehavior::SkipEmptyParts); - if (parts.size () == 3) - { - QString command = parts.at (0); - QString url = parts.at (1); - QString version = parts.at (2); + const QString& command = parts.at (0); + const QString& url = parts.at (1); + const QString& version = parts.at (2); - if (version == QtHttpServer::HTTP_VERSION) - { - m_currentRequest = new QtHttpRequest (this, m_serverHandle); - m_currentRequest->setClientInfo(m_sockClient->localAddress(), m_sockClient->peerAddress()); - m_currentRequest->setUrl (QUrl (url)); - m_currentRequest->setCommand (command); - m_parsingStatus = AwaitingHeaders; - } - else - { - m_parsingStatus = ParsingError; - // Error : unhandled HTTP version - } + if (version == QtHttpServer::HTTP_VERSION) + { + m_currentRequest = new QtHttpRequest (this, m_serverHandle); + m_currentRequest->setClientInfo(m_sockClient->localAddress(), m_sockClient->peerAddress()); + m_currentRequest->setUrl (QUrl (url)); + m_currentRequest->setCommand (command); + m_parsingStatus = AwaitingHeaders; } else { m_parsingStatus = ParsingError; - // Error : incorrect HTTP command line + // Error : unhandled HTTP version } - - break; } - case AwaitingHeaders: // "header: value" × N (until empty line) + else { - QByteArray raw = line.trimmed (); + m_parsingStatus = ParsingError; + // Error : incorrect HTTP command line + } + m_fragment.clear(); + break; + } + case AwaitingHeaders: // "header: value" × N (until empty line) + { + m_fragment.append(line); + + if ( m_fragment.endsWith(CRLF)) + { + QByteArray raw = m_fragment.trimmed (); if (!raw.isEmpty ()) // parse headers { int pos = raw.indexOf (COLON); - if (pos > 0) { QByteArray header = raw.left (pos).trimmed(); QByteArray value = raw.mid (pos +1).trimmed(); m_currentRequest->addHeader (header, value); - #if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)) if (header.compare(QtHttpHeader::ContentLength, Qt::CaseInsensitive) == 0) - #else +#else if (header.toLower() == QtHttpHeader::ContentLength.toLower()) - #endif +#endif { - bool ok = false; - const int len = value.toInt (&ok, 10); - if (ok) + bool isConversionOk = false; + const int len = value.toInt (&isConversionOk, 10); + if (isConversionOk) { m_currentRequest->addHeader (QtHttpHeader::ContentLength, QByteArray::number (len)); } @@ -132,107 +136,109 @@ void QtHttpClientWrapper::onClientDataReceived (void) m_parsingStatus = RequestParsed; } } - - break; + m_fragment.clear(); } - case AwaitingContent: // raw data × N (until EOF ??) + + break; + } + case AwaitingContent: // raw data × N (until EOF ??) + { + m_currentRequest->appendRawData (line); + + if (m_currentRequest->getRawDataSize () == m_currentRequest->getHeader (QtHttpHeader::ContentLength).toInt ()) { - m_currentRequest->appendRawData (line); - - if (m_currentRequest->getRawDataSize () == m_currentRequest->getHeader (QtHttpHeader::ContentLength).toInt ()) - { - m_parsingStatus = RequestParsed; - } - - break; - } - default: - { - break; + m_parsingStatus = RequestParsed; } + + break; + } + default: + { + break; + } } switch (m_parsingStatus) // handle parsing status end/error { - case RequestParsed: // a valid request has ben fully parsed + case RequestParsed: // a valid request has ben fully parsed + { + // Catch websocket header "Upgrade" + if(m_currentRequest->getHeader(QtHttpHeader::Upgrade).toLower() == "websocket") { - // Catch websocket header "Upgrade" - if(m_currentRequest->getHeader(QtHttpHeader::Upgrade) == "websocket") + if(m_websocketClient == Q_NULLPTR) { - if(m_websocketClient == Q_NULLPTR) + // disconnect this slot from socket for further requests + disconnect(m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived); + // disabling packet bunching + m_sockClient->setSocketOption(QAbstractSocket::LowDelayOption, 1); + m_sockClient->setSocketOption(QAbstractSocket::KeepAliveOption, 1); + m_websocketClient = new WebSocketClient(m_currentRequest, m_sockClient, m_localConnection, this); + } + + break; + } + + // add post data to request and catch /jsonrpc subroute url + if ( m_currentRequest->getCommand() == "POST") + { + QtHttpPostData postData; + QByteArray data = m_currentRequest->getRawData(); + QList parts = data.split('&'); + + for (int i = 0; i < parts.size(); ++i) + { + QList keyValue = parts.at(i).split('='); + QByteArray value; + + if (keyValue.size()>1) { - // disconnect this slot from socket for further requests - disconnect(m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived); - // disabling packet bunching - m_sockClient->setSocketOption(QAbstractSocket::LowDelayOption, 1); - m_sockClient->setSocketOption(QAbstractSocket::KeepAliveOption, 1); - m_websocketClient = new WebSocketClient(m_currentRequest, m_sockClient, m_localConnection, this); + value = QByteArray::fromPercentEncoding(keyValue.at(1)); } + postData.insert(QString::fromUtf8(keyValue.at(0)),value); + } + + m_currentRequest->setPostData(postData); + + // catch /jsonrpc in url, we need async callback, StaticFileServing is sync + QString path = m_currentRequest->getUrl ().path (); + + QStringList uri_parts = QStringUtils::split(path,'/', QStringUtils::SplitBehavior::SkipEmptyParts); + if ( ! uri_parts.empty() && uri_parts.at(0) == "json-rpc" ) + { + if(m_webJsonRpc == Q_NULLPTR) + { + m_webJsonRpc = new WebJsonRpc(m_currentRequest, m_serverHandle, m_localConnection, this); + } + + m_webJsonRpc->handleMessage(m_currentRequest); break; } - - // add post data to request and catch /jsonrpc subroute url - if ( m_currentRequest->getCommand() == "POST") - { - QtHttpPostData postData; - QByteArray data = m_currentRequest->getRawData(); - QList parts = data.split('&'); - - for (int i = 0; i < parts.size(); ++i) - { - QList keyValue = parts.at(i).split('='); - QByteArray value; - - if (keyValue.size()>1) - { - value = QByteArray::fromPercentEncoding(keyValue.at(1)); - } - - postData.insert(QString::fromUtf8(keyValue.at(0)),value); - } - - m_currentRequest->setPostData(postData); - - // catch /jsonrpc in url, we need async callback, StaticFileServing is sync - QString path = m_currentRequest->getUrl ().path (); - - QStringList uri_parts = QStringUtils::split(path,'/', QStringUtils::SplitBehavior::SkipEmptyParts); - if ( ! uri_parts.empty() && uri_parts.at(0) == "json-rpc" ) - { - if(m_webJsonRpc == Q_NULLPTR) - { - m_webJsonRpc = new WebJsonRpc(m_currentRequest, m_serverHandle, m_localConnection, this); - } - - m_webJsonRpc->handleMessage(m_currentRequest); - break; - } - } - - QtHttpReply reply (m_serverHandle); - connect (&reply, &QtHttpReply::requestSendHeaders, this, &QtHttpClientWrapper::onReplySendHeadersRequested); - connect (&reply, &QtHttpReply::requestSendData, this, &QtHttpClientWrapper::onReplySendDataRequested); - emit m_serverHandle->requestNeedsReply (m_currentRequest, &reply); // allow app to handle request - m_parsingStatus = sendReplyToClient (&reply); - - break; } - case ParsingError: // there was an error durin one of parsing steps - { - m_sockClient->readAll (); // clear remaining buffer to ignore content - QtHttpReply reply (m_serverHandle); - reply.setStatusCode (QtHttpReply::BadRequest); - reply.appendRawData (QByteArrayLiteral ("

    Bad Request (HTTP parsing error) !

    ")); - reply.appendRawData (CRLF); - m_parsingStatus = sendReplyToClient (&reply); - break; - } - default: - { - break; - } + QtHttpReply reply (m_serverHandle); + connect (&reply, &QtHttpReply::requestSendHeaders, this, &QtHttpClientWrapper::onReplySendHeadersRequested); + connect (&reply, &QtHttpReply::requestSendData, this, &QtHttpClientWrapper::onReplySendDataRequested); + emit m_serverHandle->requestNeedsReply (m_currentRequest, &reply); // allow app to handle request + m_parsingStatus = sendReplyToClient (&reply); + + break; + } + case ParsingError: // there was an error durin one of parsing steps + { + m_sockClient->readAll (); // clear remaining buffer to ignore content + QtHttpReply reply (m_serverHandle); + reply.setStatusCode (QtHttpReply::BadRequest); + reply.appendRawData (QByteArrayLiteral ("

    Bad Request (HTTP parsing error) !

    ")); + reply.appendRawData (CRLF); + m_parsingStatus = sendReplyToClient (&reply); + + break; + } + default: + { + break; + } } } } @@ -315,10 +321,9 @@ QtHttpClientWrapper::ParsingStatus QtHttpClientWrapper::sendReplyToClient (QtHtt { if (!reply->useChunked ()) { - //reply->appendRawData (CRLF); // send all headers and all data in one shot - reply->requestSendHeaders (); - reply->requestSendData (); + emit reply->requestSendHeaders (); + emit reply->requestSendData (); } else { @@ -331,7 +336,7 @@ QtHttpClientWrapper::ParsingStatus QtHttpClientWrapper::sendReplyToClient (QtHtt { static const QByteArray & CLOSE = QByteArrayLiteral ("close"); - if (m_currentRequest->getHeader(QtHttpHeader::Connection) == CLOSE) + if (m_currentRequest->getHeader(QtHttpHeader::Connection).toLower() == CLOSE) { // must close connection after this request m_sockClient->close (); diff --git a/libsrc/webserver/QtHttpClientWrapper.h b/libsrc/webserver/QtHttpClientWrapper.h index 5f3f31c6..fd2d9f64 100644 --- a/libsrc/webserver/QtHttpClientWrapper.h +++ b/libsrc/webserver/QtHttpClientWrapper.h @@ -58,6 +58,7 @@ private: const bool m_localConnection; WebSocketClient * m_websocketClient; WebJsonRpc * m_webJsonRpc; + QByteArray m_fragment; }; #endif // QTHTTPCLIENTWRAPPER_H From e6714b21f92650b484666b36c124b54d0d321a57 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Tue, 21 May 2024 19:37:43 +0200 Subject: [PATCH 114/117] Fix Philip Hue APIv2 support without Entertainment group defined (#1743) --- CHANGELOG.md | 1 + .../js/wizards/LedDevice_philipshue.js | 20 +++++++++----- libsrc/leddevice/LedDeviceWrapper.cpp | 2 +- .../leddevice/dev_net/LedDevicePhilipsHue.cpp | 26 +++++++++---------- libsrc/leddevice/dev_net/ProviderRestApi.cpp | 12 ++++++++- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75577551..4ea84fc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Workaround to address Web UI keeps forcing browser to download the html instead (#1692) - Fixed: Kodi Color Calibration, Refactor Wizards (#1674) - Fixed: Token Dialog not closing +- Fixed: Philip Hue APIv2 support without Entertainment group defined (#1742) **JSON-API** - Refactored JSON-API to ensure consistent authorization behaviour across sessions and single requests with token authorization. diff --git a/assets/webconfig/js/wizards/LedDevice_philipshue.js b/assets/webconfig/js/wizards/LedDevice_philipshue.js index 92f1c173..bfc33bd8 100644 --- a/assets/webconfig/js/wizards/LedDevice_philipshue.js +++ b/assets/webconfig/js/wizards/LedDevice_philipshue.js @@ -502,7 +502,9 @@ const philipshueWizard = (() => { let serviceID; if (isAPIv2Ready) { - serviceID = lightLocation.service.rid; + if (lightLocation) { + serviceID = lightLocation.service.rid; + } } if (position.startsWith("entertainment")) { @@ -531,7 +533,7 @@ const philipshueWizard = (() => { // Layout per manual settings let maxSegments = 1; - if (isAPIv2Ready) { + if (isAPIv2Ready && serviceID) { const service = hueEntertainmentServices.find(service => service.id === serviceID); maxSegments = service.segments.max_segments; } @@ -593,10 +595,10 @@ const philipshueWizard = (() => { d.enableAttempts = parseInt(conf_editor.getEditor("root.generalOptions.enableAttempts").getValue()); d.enableAttemptsInterval = parseInt(conf_editor.getEditor("root.generalOptions.enableAttemptsInterval").getValue()); - d.useEntertainmentAPI = isEntertainmentReady; + d.useEntertainmentAPI = isEntertainmentReady && (d.groupId !== ""); d.useAPIv2 = isAPIv2Ready; - if (isEntertainmentReady) { + if (d.useEntertainmentAPI) { d.hardwareLedCount = channelNumber; if (window.serverConfig.device.type !== d.type) { //smoothing on, if new device @@ -803,12 +805,18 @@ const philipshueWizard = (() => { "lightPosBottomLeft112", "lightPosBottomLeftNewMid", "lightPosBottomLeft121" ]; - if (isEntertainmentReady) { + if (isEntertainmentReady && hueEntertainmentConfigs.length > 0) { lightOptions.unshift("entertainment_center"); lightOptions.unshift("entertainment"); } else { lightOptions.unshift("disabled"); - groupLights = Object.keys(hueLights); + if (isAPIv2Ready) { + for (const light in hueLights) { + groupLights.push(hueLights[light].id); + } + } else { + groupLights = Object.keys(hueLights); + } } $('.lidsb').html(""); diff --git a/libsrc/leddevice/LedDeviceWrapper.cpp b/libsrc/leddevice/LedDeviceWrapper.cpp index 838a5b37..53532074 100644 --- a/libsrc/leddevice/LedDeviceWrapper.cpp +++ b/libsrc/leddevice/LedDeviceWrapper.cpp @@ -65,7 +65,7 @@ void LedDeviceWrapper::createLedDevice(const QJsonObject& config) connect(thread, &QThread::started, _ledDevice, &LedDevice::start); // further signals - connect(this, &LedDeviceWrapper::updateLeds, _ledDevice, &LedDevice::updateLeds, Qt::QueuedConnection); + connect(this, &LedDeviceWrapper::updateLeds, _ledDevice, &LedDevice::updateLeds, Qt::BlockingQueuedConnection); connect(this, &LedDeviceWrapper::switchOn, _ledDevice, &LedDevice::switchOn, Qt::BlockingQueuedConnection); connect(this, &LedDeviceWrapper::switchOff, _ledDevice, &LedDevice::switchOff, Qt::BlockingQueuedConnection); diff --git a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp index f5f8d24b..e3df5c7d 100644 --- a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp @@ -895,6 +895,7 @@ void LedDevicePhilipsHueBridge::setBridgeDetails(const QJsonDocument &doc, bool log( "API-Version", "%u.%u.%u", _api_major, _api_minor, _api_patch ); log( "API v2 ready", "%s", _isAPIv2Ready ? "Yes" : "No" ); log( "Entertainment ready", "%s", _isHueEntertainmentReady ? "Yes" : "No" ); + log( "Use Entertainment API", "%s", _useEntertainmentAPI ? "Yes" : "No" ); log( "DIYHue", "%s", _isDiyHue ? "Yes" : "No" ); } } @@ -1799,11 +1800,11 @@ bool LedDevicePhilipsHue::init(const QJsonObject &deviceConfig) if (LedDevicePhilipsHueBridge::init(_devConfig)) { - log( "Off on Black", "%s", _switchOffOnBlack ? "Yes" : "No" ); - log( "Brightness Factor", "%f", _brightnessFactor ); - log( "Transition Time", "%d", _transitionTime ); - log( "Restore Original State", "%s", _isRestoreOrigState ? "Yes" : "No" ); - log( "Use Hue Entertainment API", "%s", _useEntertainmentAPI ? "Yes" : "No" ); + log("Off on Black", "%s", _switchOffOnBlack ? "Yes" : "No" ); + log("Brightness Factor", "%f", _brightnessFactor ); + log("Transition Time", "%d", _transitionTime ); + log("Restore Original State", "%s", _isRestoreOrigState ? "Yes" : "No" ); + log("Use Hue Entertainment API", "%s", _useEntertainmentAPI ? "Yes" : "No" ); log("Brightness Threshold", "%f", _blackLevel); log("CandyGamma", "%s", _candyGamma ? "Yes" : "No" ); log("Time powering off when black", "%s", _onBlackTimeToPowerOff ? "Yes" : "No" ); @@ -1864,7 +1865,7 @@ bool LedDevicePhilipsHue::setLights() Debug(_log, "Lights configured: %d", configuredLightsCount ); if (updateLights( getLightMap())) { - if (_useApiV2) + if (_useApiV2 && _useEntertainmentAPI) { _channelsCount = getGroupChannelsCount (_groupId); @@ -2208,15 +2209,14 @@ int LedDevicePhilipsHue::write(const std::vector & ledValues) int rc {0}; if (_isOn) { - if (!_useApiV2) - { - rc = writeSingleLights( ledValues ); - } - if (_useEntertainmentAPI && _isInitLeds) { rc= writeStreamData(ledValues); } + else + { + rc = writeSingleLights( ledValues ); + } } return rc; } @@ -2482,7 +2482,7 @@ void LedDevicePhilipsHue::setColor(PhilipsHueLight& light, CiColor& color) QJsonObject colorXY; colorXY[API_X_COORDINATE] = color.x; colorXY[API_Y_COORDINATE] = color.y; - cmd.insert(API_COLOR, QJsonObject {{API_DURATION, colorXY }}); + cmd.insert(API_COLOR, QJsonObject {{API_XY_COORDINATES, colorXY }}); cmd.insert(API_DIMMING, QJsonObject {{API_BRIGHTNESS, bri }}); } else @@ -2556,7 +2556,7 @@ void LedDevicePhilipsHue::setState(PhilipsHueLight& light, bool on, const CiColo QJsonObject colorXY; colorXY[API_X_COORDINATE] = color.x; colorXY[API_Y_COORDINATE] = color.y; - cmd.insert(API_COLOR, QJsonObject {{API_DURATION, colorXY }}); + cmd.insert(API_COLOR, QJsonObject {{API_XY_COORDINATES, colorXY }}); cmd.insert(API_DIMMING, QJsonObject {{API_BRIGHTNESS, bri }}); } else diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.cpp b/libsrc/leddevice/dev_net/ProviderRestApi.cpp index 7321810f..e981d00a 100644 --- a/libsrc/leddevice/dev_net/ProviderRestApi.cpp +++ b/libsrc/leddevice/dev_net/ProviderRestApi.cpp @@ -30,7 +30,8 @@ enum HttpStatusCode { BadRequest = 400, UnAuthorized = 401, Forbidden = 403, - NotFound = 404 + NotFound = 404, + TooManyRequests = 429 }; } //End of constants @@ -336,6 +337,15 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply) case HttpStatusCode::NotFound: advise = "Check Resource given"; break; + case HttpStatusCode::TooManyRequests: + { + QString retryAfterTime = response.getHeader("Retry-After"); + if (!retryAfterTime.isEmpty()) + { + advise = "Retry-After: " + response.getHeader("Retry-After"); + } + } + break; default: advise = httpReason; break; From bde5d156a99fd0f6342fb1d462e8a635ba913895 Mon Sep 17 00:00:00 2001 From: Thinner77 <59623671+Thinner77@users.noreply.github.com> Date: Fri, 24 May 2024 09:13:33 +0200 Subject: [PATCH 115/117] faster imageresampler (#1744) --- include/grabber/video/v4l2/V4L2Grabber.h | 9 + libsrc/grabber/video/v4l2/V4L2Grabber.cpp | 16 ++ libsrc/utils/ImageResampler.cpp | 190 ++++++++++++++-------- 3 files changed, 147 insertions(+), 68 deletions(-) diff --git a/include/grabber/video/v4l2/V4L2Grabber.h b/include/grabber/video/v4l2/V4L2Grabber.h index 5ac00738..a9c1e687 100644 --- a/include/grabber/video/v4l2/V4L2Grabber.h +++ b/include/grabber/video/v4l2/V4L2Grabber.h @@ -1,5 +1,11 @@ #pragma once +#define FRAME_BENCH + +#ifdef FRAME_BENCH + #include +#endif + // stl includes #include #include @@ -166,6 +172,9 @@ private: double _x_frac_max; double _y_frac_max; +#ifdef FRAME_BENCH + QElapsedTimer _frameTimer; +#endif QSocketNotifier *_streamNotifier; bool _initialized, _reload; diff --git a/libsrc/grabber/video/v4l2/V4L2Grabber.cpp b/libsrc/grabber/video/v4l2/V4L2Grabber.cpp index d4a73ab9..081b978b 100644 --- a/libsrc/grabber/video/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/video/v4l2/V4L2Grabber.cpp @@ -1079,6 +1079,22 @@ void V4L2Grabber::newThreadFrame(Image image) } else emit newFrame(image); + +#ifdef FRAME_BENCH + // calculate average frametime + if (_currentFrame > 1) + { + if (_currentFrame % 100 == 0) + { + Debug(_log, "%d: avg. frametime=%.02fms / %.02fms", int(_currentFrame), _frameTimer.restart()/100.0, 1000.0/_fps); + } + } + else + { + Debug(_log, "%d: frametimer started", int(_currentFrame)); + _frameTimer.start(); + } +#endif } int V4L2Grabber::xioctl(int request, void *arg) diff --git a/libsrc/utils/ImageResampler.cpp b/libsrc/utils/ImageResampler.cpp index 94b606da..483e35a9 100644 --- a/libsrc/utils/ImageResampler.cpp +++ b/libsrc/utils/ImageResampler.cpp @@ -29,9 +29,6 @@ void ImageResampler::processImage(const uint8_t * data, int width, int height, i int cropTop = _cropTop; int cropBottom = _cropBottom; - int xDestFlip = 0, yDestFlip = 0; - int uOffset = 0, vOffset = 0; - // handle 3D mode switch (_videoMode) { @@ -53,118 +50,175 @@ void ImageResampler::processImage(const uint8_t * data, int width, int height, i outputImage.resize(outputWidth, outputHeight); - for (int yDest = 0, ySource = cropTop + (_verticalDecimation >> 1); yDest < outputHeight; ySource += _verticalDecimation, ++yDest) + int xDestStart, xDestEnd; + int yDestStart, yDestEnd; + + switch (_flipMode) { - int yOffset = lineLength * ySource; - if (pixelFormat == PixelFormat::NV12) - { - uOffset = (height + ySource / 2) * lineLength; - } - else if (pixelFormat == PixelFormat::I420) - { - uOffset = width * height + (ySource/2) * width/2; - vOffset = width * height * 1.25 + (ySource/2) * width/2; - } + case FlipMode::NO_CHANGE: + xDestStart = 0; + xDestEnd = outputWidth-1; + yDestStart = 0; + yDestEnd = outputHeight-1; + break; + case FlipMode::HORIZONTAL: + xDestStart = 0; + xDestEnd = outputWidth-1; + yDestStart = -(outputHeight-1); + yDestEnd = 0; + break; + case FlipMode::VERTICAL: + xDestStart = -(outputWidth-1); + xDestEnd = 0; + yDestStart = 0; + yDestEnd = outputHeight-1; + break; + case FlipMode::BOTH: + xDestStart = -(outputWidth-1); + xDestEnd = 0; + yDestStart = -(outputHeight-1); + yDestEnd = 0; + break; + } - for (int xDest = 0, xSource = cropLeft + (_horizontalDecimation >> 1); xDest < outputWidth; xSource += _horizontalDecimation, ++xDest) + switch (pixelFormat) + { + case PixelFormat::UYVY: { - switch (_flipMode) + for (int yDest = yDestStart, ySource = cropTop + (_verticalDecimation >> 1); yDest <= yDestEnd; ySource += _verticalDecimation, ++yDest) { - case FlipMode::HORIZONTAL: - - xDestFlip = xDest; - yDestFlip = outputHeight-yDest-1; - break; - case FlipMode::VERTICAL: - xDestFlip = outputWidth-xDest-1; - yDestFlip = yDest; - break; - case FlipMode::BOTH: - xDestFlip = outputWidth-xDest-1; - yDestFlip = outputHeight-yDest-1; - break; - case FlipMode::NO_CHANGE: - xDestFlip = xDest; - yDestFlip = yDest; - break; - } - - ColorRgb &rgb = outputImage(xDestFlip, yDestFlip); - switch (pixelFormat) - { - case PixelFormat::UYVY: + for (int xDest = xDestStart, xSource = cropLeft + (_horizontalDecimation >> 1); xDest <= xDestEnd; xSource += _horizontalDecimation, ++xDest) { - int index = yOffset + (xSource << 1); + ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); + int index = lineLength * ySource + (xSource << 1); uint8_t y = data[index+1]; uint8_t u = ((xSource&1) == 0) ? data[index ] : data[index-2]; uint8_t v = ((xSource&1) == 0) ? data[index+2] : data[index ]; ColorSys::yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); } - break; - case PixelFormat::YUYV: + } + break; + } + + case PixelFormat::YUYV: + { + 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) { - int index = yOffset + (xSource << 1); + ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); + int index = lineLength * ySource + (xSource << 1); uint8_t y = data[index]; uint8_t u = ((xSource&1) == 0) ? data[index+1] : data[index-1]; uint8_t v = ((xSource&1) == 0) ? data[index+3] : data[index+1]; ColorSys::yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); } - break; - case PixelFormat::BGR16: + } + break; + } + + case PixelFormat::BGR16: + { + 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) { - int index = yOffset + (xSource << 1); + ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); + int index = lineLength * ySource + (xSource << 1); rgb.blue = (data[index] & 0x1f) << 3; rgb.green = (((data[index+1] & 0x7) << 3) | (data[index] & 0xE0) >> 5) << 2; rgb.red = (data[index+1] & 0xF8); } - break; - case PixelFormat::BGR24: + } + break; + } + + case PixelFormat::BGR24: + { + 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) { - int index = yOffset + (xSource << 1) + xSource; + ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); + int index = lineLength * ySource + (xSource << 1) + xSource; rgb.blue = data[index ]; rgb.green = data[index+1]; rgb.red = data[index+2]; } - break; - case PixelFormat::RGB32: + } + break; + } + + case PixelFormat::RGB32: + { + 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) { - int index = yOffset + (xSource << 2); + ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); + int index = lineLength * ySource + (xSource << 2); rgb.red = data[index ]; rgb.green = data[index+1]; rgb.blue = data[index+2]; } - break; - case PixelFormat::BGR32: + } + break; + } + + case PixelFormat::BGR32: + { + 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) { - int index = yOffset + (xSource << 2); + ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); + int index = lineLength * ySource + (xSource << 2); rgb.blue = data[index ]; rgb.green = data[index+1]; rgb.red = data[index+2]; } - break; - case PixelFormat::NV12: + } + break; + } + + case PixelFormat::NV12: + { + for (int yDest = yDestStart, ySource = cropTop + (_verticalDecimation >> 1); yDest <= yDestEnd; ySource += _verticalDecimation, ++yDest) + { + int uOffset = (height + ySource / 2) * lineLength; + for (int xDest = xDestStart, xSource = cropLeft + (_horizontalDecimation >> 1); xDest <= xDestEnd; xSource += _horizontalDecimation, ++xDest) { - uint8_t y = data[yOffset + xSource]; + ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); + uint8_t y = data[lineLength * ySource + xSource]; uint8_t u = data[uOffset + ((xSource >> 1) << 1)]; uint8_t v = data[uOffset + ((xSource >> 1) << 1) + 1]; ColorSys::yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); } - break; - case PixelFormat::I420: + } + break; + } + + case PixelFormat::I420: + { + for (int yDest = yDestStart, ySource = cropTop + (_verticalDecimation >> 1); yDest <= yDestEnd; ySource += _verticalDecimation, ++yDest) + { + int uOffset = width * height + (ySource/2) * width/2; + int vOffset = width * height * 1.25 + (ySource/2) * width/2; + for (int xDest = xDestStart, xSource = cropLeft + (_horizontalDecimation >> 1); xDest <= xDestEnd; xSource += _horizontalDecimation, ++xDest) { - int y = data[yOffset + xSource]; + ColorRgb & rgb = outputImage(abs(xDest), abs(yDest)); + int y = data[lineLength * ySource + xSource]; int u = data[uOffset + (xSource >> 1)]; int v = data[vOffset + (xSource >> 1)]; ColorSys::yuv2rgb(y, u, v, rgb.red, rgb.green, rgb.blue); - break; } - break; - case PixelFormat::MJPEG: - break; - case PixelFormat::NO_CHANGE: - Error(Logger::getInstance("ImageResampler"), "Invalid pixel format given"); - break; } + break; } + case PixelFormat::MJPEG: + break; + case PixelFormat::NO_CHANGE: + Error(Logger::getInstance("ImageResampler"), "Invalid pixel format given"); + break; } } From 5bcbe599d3c8b564c6fd4336d06ad6698bb0c369 Mon Sep 17 00:00:00 2001 From: Thinner77 <59623671+Thinner77@users.noreply.github.com> Date: Fri, 24 May 2024 17:57:10 +0200 Subject: [PATCH 116/117] Fix flipmode for BGR24 special handling (#1739) --- libsrc/grabber/video/EncoderThread.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/libsrc/grabber/video/EncoderThread.cpp b/libsrc/grabber/video/EncoderThread.cpp index 254472c0..e891c821 100644 --- a/libsrc/grabber/video/EncoderThread.cpp +++ b/libsrc/grabber/video/EncoderThread.cpp @@ -122,18 +122,6 @@ void EncoderThread::process() else #endif { - if (_pixelFormat == PixelFormat::BGR24) - { - if (_flipMode == FlipMode::NO_CHANGE) - _imageResampler.setFlipMode(FlipMode::HORIZONTAL); - else if (_flipMode == FlipMode::HORIZONTAL) - _imageResampler.setFlipMode(FlipMode::NO_CHANGE); - else if (_flipMode == FlipMode::VERTICAL) - _imageResampler.setFlipMode(FlipMode::BOTH); - else if (_flipMode == FlipMode::BOTH) - _imageResampler.setFlipMode(FlipMode::VERTICAL); - } - Image image = Image(); _imageResampler.processImage( _localData, From b390d16b0c47ea96a15a18335b73332bb673e564 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Fri, 24 May 2024 18:24:26 +0200 Subject: [PATCH 117/117] Disable V42L Benchmark Code --- include/grabber/video/v4l2/V4L2Grabber.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/grabber/video/v4l2/V4L2Grabber.h b/include/grabber/video/v4l2/V4L2Grabber.h index a9c1e687..3db42711 100644 --- a/include/grabber/video/v4l2/V4L2Grabber.h +++ b/include/grabber/video/v4l2/V4L2Grabber.h @@ -1,6 +1,6 @@ #pragma once -#define FRAME_BENCH +#define NOFRAME_BENCH #ifdef FRAME_BENCH #include